SQL CREATE TABLE
Declaring a table up front is how you say what the data is allowed to look like, before any of it arrives.
CREATE TABLE declares a table by listing its columns and their types. Everything else — defaults, keys, checks — hangs off that list.
create table order_imports (
order_id integer,
customer_email varchar,
order_date date,
quantity integer,
unit_price decimal(10, 2)
)Compare that withCREATE TABLE AS SELECT, which infers everything from a query. Declaring the table yourself is more typing and buys you two things: the types are what you chose rather than what a query happened to produce, and you get somewhere to put the rules.
Types worth knowing
| Type | Use it for |
|---|---|
integer, bigint | Counts, IDs, quantities. |
decimal(p, s) | Money. Exact to the digit you specify. |
double | Measurements and ratios where tiny rounding error is acceptable. |
varchar | Any text. No length needed. |
date, timestamp | Calendar days, and points in time. |
boolean | True/false flags. |
Use decimal for money, not double. Adouble stores 0.1 as the nearest binary approximation, so totals drift by fractions of a cent and a sum that should be 100.00 compares as unequal to 100.00. Reach fordecimal(10, 2) and the arithmetic stays exact.
Defaults
A DEFAULT fills a column when the insert does not mention it. It applies on insert only — it never rewrites rows that are already there.
create or replace table order_imports (
order_id integer,
status varchar default 'pending',
loaded_at timestamp default current_timestamp,
quantity integer default 1
)A default expression is evaluated per row at insert time, socurrent_timestamp records when each row arrived rather than when the table was created.
Rules on the table
Constraints move a rule out of every query that touches the data and into the table itself, where it cannot be forgotten. Theconstraints reference covers each one and the error it raises; the short version:
create or replace table order_imports (
order_id integer primary key,
customer_email varchar not null,
status varchar default 'pending',
quantity integer check (quantity > 0),
coupon_code varchar unique
)Rerunning the statement
CREATE TABLE on a name that already exists is an error. Two ways around it, and they are not interchangeable.
-- leaves the existing table exactly as it is
create table if not exists order_imports (
order_id integer
);
-- throws the existing table away, rows included
create or replace table order_imports (
order_id integer,
customer_email varchar
);IF NOT EXISTS is silent about the table it skipped. If you edit the column list and rerun a script written that way, nothing changes and nothing complains — you are still on the old shape. UseCREATE OR REPLACE TABLE when the definition is the thing you changed, and remember it discards the rows.
Generated columns
A generated column is defined by an expression over the other columns. DuckDB computes it on read, so it never falls out of step with the values it is derived from.
create table line_items (
order_id integer,
quantity integer,
unit_price decimal(10, 2),
line_total as (quantity * unit_price)
)You cannot insert into a generated column — DuckDB answersCannot insert into a generated column — which is the point. Other engines let you add STORED to keep the computed value on disk; DuckDB supports virtual generated columns only and rejectsSTORED.
Checking what you built
describe order_importsDESCRIBE lists columns, types, and defaults.duckdb_constraints() lists the rules, andinformation_schema.columns is the portable equivalent of the first.
Type names are the least portable part of SQL. varchar istext in PostgreSQL and string in BigQuery; auto-incrementing IDs are serial,auto_increment, or identity depending on the engine. DuckDB uses a sequence for that. The structure of CREATE TABLE ports cleanly; the type names rarely do.
Habits worth keeping
- Name tables for what one row is —
orders,line_items— so a join reads like a sentence. - Pick
decimalfor anything anyone will add up as money. - Declare the table when it will receive writes; use CTAS when you are storing the answer to a query.
- Put the rule in the table, not in a comment about the table. A
CHECKoutlives the person who knew about it.
Related
NOT NULL, DEFAULT, CHECK, UNIQUE, and keys.
Add, rename, retype, and drop columns on a live table.
Remove tables, views, schemas, and more without breakage.
Generate ID values with nextval and column defaults.