Try Opteryx
Learning path · Data engineer

Build tables that maintain themselves

You own the tables other people query: loading them, shaping them, keeping them fresh and controlling who sees what.

16 steps·150 min of reading·About 3 hours with the exercise

What you'll be able to do

  • Load files into catalog tables from the UI, the API or the command line
  • Create, change and compact tables, and read any earlier version of them
  • Replace a refresh pipeline with a materialized view, or with a task and a trigger
  • Grant access with roles and know what each role permits
  • Point a workspace at your own Iceberg catalog

Before you start

  • The Analyst path, or comfort with SQL DDL and DML.
  • An account on opteryx.app.

The path

Work through these in order. Each one is an existing docs page; the note under it says why it is on this path. Tick a step when you are done with it. Progress is remembered by this browser only.

0 of 16 steps done
  1. Where it fits and where it does not, before you design around it.

    Introduction·5 min
  2. The upload session, part and commit flow that every ingestion path uses.

    Getting started·10 min
  3. Get files in from a shell or a scheduled job.

    Guide·10 min
  4. A full round trip: CTAS, UPDATE, DELETE, snapshots, OPTIMIZE and DROP.

    Getting started·15 min
  5. Append rows from literal values or from a query.

    Reference·5 min
  6. Upserts: the statement behind most incremental loads.

    Reference·10 min
  7. Read a table as it was at a snapshot or at a point in time.

    Reference·10 min
  8. One statement instead of a scheduler, a job and a swap. And when that is the wrong trade.

    Guide·10 min
  9. The general form: a stored statement fired by a commit and handed exactly what that commit changed. A blueprint and two worked examples.

    Guide·15 min
  10. The general form: any statement, run on demand or when a table commits.

    Reference·10 min
  11. Wire a task to a table, and whose identity an unattended run carries.

    Reference·10 min
  12. Workspaces as the boundary, and what reader, writer and owner each allow.

    Concept·10 min
  13. Grant a role on a workspace, a collection or a single dataset.

    Reference·5 min
  14. The platform identity, and why a long-lived view should not depend on one person's account.

    Concept·5 min
  15. Keep your tables in your own Iceberg REST catalog instead of Opteryx storage.

    Guide·15 min
  16. How usage is measured, so refresh frequency and scan width are deliberate choices.

    Concept·5 min

Hands-on exercise: a summary that keeps itself fresh

You will build a table you own, put a materialized view on top of it, watch the view refresh itself when the table changes, run a task by hand, then clean everything up. It takes about 40 minutes in Studio. Everything is created under your personal workspace, so nothing here is visible to anyone else.

Find your username first. Every statement below writes personal.<you>, and you substitute the value this returns:

sql
SELECT USER();

1. A table of your own

sql
CREATE TABLE personal.<you>.planet_log AS
SELECT name,
       mass,
       number_of_moons,
       'initial' AS load_batch
  FROM public.astronomy.planets;

One row per planet in the sample, committed as a single version. load_batch is a plain string column you will use to tell loads apart.

2. A view that maintains itself

sql
CREATE MATERIALIZED VIEW personal.<you>.batch_totals AS
SELECT load_batch,
       COUNT(*)             AS planets,
       SUM(number_of_moons) AS moons
  FROM personal.<you>.planet_log
 GROUP BY load_batch;
sql
SELECT * FROM personal.<you>.batch_totals;

One row, initial. Creating the view also registered a refresh trigger on planet_log. Look at it:

sql
SHOW TRIGGERS FOR personal.<you>.planet_log;

3. Change the source, watch the view follow

sql
INSERT INTO personal.<you>.planet_log (name, mass, number_of_moons, load_batch)
VALUES ('Ceres', 0.00094, 0, 'dwarf');

Commits within about a minute coalesce into one refresh, so wait a minute, then:

sql
SELECT * FROM personal.<you>.batch_totals ORDER BY load_batch;

Two rows now. If there is still one, check how the last refresh went:

sql
SELECT trigger_name, target, last_fired_at, last_fired_status
  FROM personal.information_schema.triggers;

4. Read the history

sql
SHOW SNAPSHOTS FOR personal.<you>.planet_log;

Two commits: the CREATE and the INSERT. The refresh does not appear here because it was a commit to batch_totals, not to planet_log.

Read the table as it was before the insert:

sql
SELECT load_batch, COUNT(*) AS planets
  FROM personal.<you>.planet_log VERSION AS OF PREVIOUS
 GROUP BY load_batch;

Only initial.

5. A task you run by hand

A materialized view re-runs one SELECT. A task runs any statement, with values supplied at run time:

sql
CREATE TASK personal.<you>.add_body AS
    INSERT INTO personal.<you>.planet_log (name, mass, number_of_moons, load_batch)
    VALUES (:name, :mass, :moons, 'manual');
sql
EXECUTE personal.<you>.add_body
    USING 'Eris' AS name,
          0.0166 AS mass,
          1 AS moons;

The task ran as you, gated by your permissions at that moment. Query batch_totals again after a minute: a manual row should have appeared, because the task's INSERT committed to a table the view's trigger watches.

6. Who can see it

sql
SHOW GRANTS;

This lists the roles you hold and what each one permits. Your personal workspace cannot be shared with anyone else, so there is nothing to grant here. Read GRANT for the statement you would use on a shared workspace, then work out which role each of steps 1 to 5 needed. Creating the view and its trigger needed writer on planet_log, not just reader.

7. Clean up

sql
DROP TASK personal.<you>.add_body;
sql
DROP MATERIALIZED VIEW personal.<you>.batch_totals;
sql
DROP TABLE personal.<you>.planet_log;

Drop the view before the table so nothing is left pointing at a table that has gone. DROP TABLE also removes the history you read in step 4.

Check your understanding

Why can a second materialized view not be built on top of batch_totals?

Materialized views do not stack. The outer view would always be one refresh behind the inner one, and a failed inner refresh would silently freeze everything above it. Model the whole transform as one query over the base table instead. See CREATE MATERIALIZED VIEW.

What would change if step 5 had defined the task with ON personal.<you>.planet_log?

A trigger would be created alongside the task, and the task would run unattended as its owner on every commit to planet_log, including the commits the task itself makes. Think carefully before wiring a task to the table it writes to. See CREATE TRIGGER.

The view in step 2 refreshes with your permissions. Why is that a problem for a view that should outlive your account?

If your access is removed, refreshes are denied and the view goes stale. Hand a long-lived view to a service identity with ALTER MATERIALIZED VIEW ... OWNER TO. See Federator.

Where next