SQL CREATE TABLE AS SELECT
CTAS runs a query once and keeps the rows. Use it when a result is expensive to recompute or needs to hold still.
CREATE TABLE AS SELECT — CTAS for short — runs a query and stores its rows in a new table. The table is ordinary from then on: query it, join it, index it, drop it. Nothing links it back to the query that filled it.
That last sentence is the whole point. Aview re-runs its query every time and always matches the current data. A CTAS table is a photograph: it shows what the query returned at the moment you ran it, and it keeps showing that until you rebuild it.
create table category_revenue as
select p.category_id,
sum(oi.quantity * oi.unit_price) as revenue
from order_items oi
join products p on p.product_id = oi.product_id
group by p.category_idYou do not declare column names or types. CTAS takes both from the query, socategory_id arrives as BIGINT andrevenue as DOUBLE without you saying so. Check withDESCRIBE category_revenue when the types matter.
The column names come from the query too, and an unaliased expression keeps its full text as a name. select sum(quantity * unit_price) from order_items produces a column literally calledsum((quantity * unit_price)), which every later query then has to quote. Alias every computed column before you store it.
Rebuilding it
A snapshot is only useful if refreshing it is easy. Two spellings, and they do different things.
create or replace table category_revenue as
select p.category_id,
sum(oi.quantity * oi.unit_price) as revenue
from order_items oi
join products p on p.product_id = oi.product_id
group by p.category_idinsert into category_revenue
select p.category_id,
sum(oi.quantity * oi.unit_price) as revenue
from order_items oi
join products p on p.product_id = oi.product_id
where p.discontinued = true
group by p.category_idCREATE OR REPLACE TABLE throws away the old rows and the old shape, so it is the safe choice when the query changed.INSERT INTO keeps the existing rows and appends, which is what you want for an accumulating log and exactly what you do not want when you meant to refresh — run it twice and every row is in there twice.
CREATE TABLE IF NOT EXISTS ... AS SELECT does nothing when the table already exists, and says nothing about it. A daily refresh script written that way silently serves the first day's numbers forever. If you mean "rebuild this", write CREATE OR REPLACE TABLE.
What CTAS does not carry over
CTAS copies rows and column types. It does not copy constraints. Snapshot a table with a primary key and the copy has no primary key, noNOT NULL, no defaults — DuckDB will happily accept a duplicate key into the copy that the original would have rejected.
create or replace table product_keys (
product_id integer primary key,
product_name varchar not null
);
create or replace table product_keys_copy as
select * from product_keys;
select table_name, constraint_type
from duckdb_constraints()
where table_name in ('product_keys', 'product_keys_copy')
order by table_name, constraint_type;Only product_keys comes back. The copy has the same two columns holding the same values, and none of the rules.
This is usually fine — a reporting snapshot does not need to enforce anything. It stops being fine when someone treats the copy as the real table and starts writing to it. If the copy needs the rules, declare the table first and INSERT INTO it.
Snapshot vs view, side by side
| View | CTAS table | |
|---|---|---|
| Stores rows | No, stores the query | Yes |
| Cost to read | Re-runs the query every time | Reads stored rows |
| When source data changes | Reflects it immediately | Unchanged until rebuilt |
| Breaks if a base table is dropped | Yes, at query time | No, the rows are already copied |
| Good for | Shared definitions that must stay current | Expensive results, and numbers that must not move |
When to reach for it
- A report that must not move. Month-end figures should be the same tomorrow as they were when you signed off on them. A view would quietly restate them as late data arrives.
- An expensive step you read many times. A view re-runs its query on every reference; a snapshot pays once.
- Staging a load. Land the raw rows, clean them into a second table, and check each stage separately.
- Freezing a "before" picture. Snapshot a table before a bulk
UPDATE, so you can compare or restore afterwards.
And when not to: a dashboard that needs current numbers wants a view. A snapshot there is a bug that looks like working software, because the query still runs and still returns rows — just old ones.
Useful variants
create table orders_backup as
select *
from orders
where 1 = 0A WHERE that matches nothing still creates the table with the right columns and types, giving you an empty table shaped like the original to insert into.
create or replace view customer_revenue as
select o.customer_id,
sum(oi.quantity * oi.unit_price) as revenue
from orders o
join order_items oi on oi.order_id = o.order_id
group by o.customer_id;
create or replace table customer_revenue_snapshot as
select *
from customer_revenue;Materializing a view is just CTAS over it. That is also DuckDB's answer to the materialized-view question, below.
CREATE TABLE AS SELECT is widely supported — PostgreSQL, SQL Server (as SELECT ... INTO), Snowflake, BigQuery, and SQLite all have a form of it. CREATE OR REPLACE TABLE is not as portable: PostgreSQL has no such statement, so the equivalent there isDROP TABLE IF EXISTS followed by the CTAS, ideally inside a transaction.
A materialized view is a snapshot the database maintains for you, with aREFRESH command and a record of how it was built. DuckDB has noCREATE MATERIALIZED VIEW. CTAS plus a rerun is the working equivalent, with the difference that nothing in the database remembers the table is derived — that lives in your script, or in nobody's head.
Habits worth keeping
- Name snapshots so nobody mistakes them for live tables:
daily_revenue_snapshot, notdaily_revenue_2. - Write the rebuild as
CREATE OR REPLACE TABLEso rerunning the script is safe. - Alias every computed column before it becomes a stored column name.
- Store the "as of" moment in the table itself — a
current_date as snapshot_datecolumn costs nothing and answers the only question anyone asks about a snapshot. - If the snapshot needs constraints, create the table explicitly and insert into it. CTAS will not give you them.
Open the DuckDB playground with the matching dataset and query already filled in.
Related
Save a query under a name and reuse it like a table.
Scratch tables and views that clean up after themselves.
Name a reusable expression or a parameterized query.
The WITH clause, chained steps, and CTE vs subquery.