Build tables that maintain themselves
You own the tables other people query: loading them, shaping them, keeping them fresh and controlling who sees what.
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.
Where it fits and where it does not, before you design around it.
The upload session, part and commit flow that every ingestion path uses.
Get files in from a shell or a scheduled job.
A full round trip: CTAS, UPDATE, DELETE, snapshots, OPTIMIZE and DROP.
Append rows from literal values or from a query.
Upserts: the statement behind most incremental loads.
Read a table as it was at a snapshot or at a point in time.
One statement instead of a scheduler, a job and a swap. And when that is the wrong trade.
The general form: a stored statement fired by a commit and handed exactly what that commit changed. A blueprint and two worked examples.
The general form: any statement, run on demand or when a table commits.
Wire a task to a table, and whose identity an unattended run carries.
Workspaces as the boundary, and what reader, writer and owner each allow.
Grant a role on a workspace, a collection or a single dataset.
The platform identity, and why a long-lived view should not depend on one person's account.
Keep your tables in your own Iceberg REST catalog instead of Opteryx storage.
How usage is measured, so refresh frequency and scan width are deliberate choices.
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:
SELECT USER();1. A table of your own
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
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;SELECT * FROM personal.<you>.batch_totals;One row, initial. Creating the view also registered a refresh trigger on planet_log. Look at it:
SHOW TRIGGERS FOR personal.<you>.planet_log;3. Change the source, watch the view follow
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:
SELECT * FROM personal.<you>.batch_totals ORDER BY load_batch;Two rows now. If there is still one, check how the last refresh went:
SELECT trigger_name, target, last_fired_at, last_fired_status
FROM personal.information_schema.triggers;4. Read the history
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:
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:
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');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
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
DROP TASK personal.<you>.add_body;DROP MATERIALIZED VIEW personal.<you>.batch_totals;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.