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 with CREATE 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. A
double 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 for
decimal(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 and 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, so
current_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. The constraints 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. Use
CREATE 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 answers
Cannot 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 rejects
STORED. See
generated columns for what the
expression may use and how each database spells it.
Checking what you built
describe order_imports DESCRIBE lists columns, types, and defaults.
duckdb_constraints() lists the rules, and
information_schema.columns is the portable equivalent of the
first. See
SHOW TABLES and DESCRIBE for
the rest of the catalog.
Type names are the least portable part of SQL. PostgreSQL accepts
varchar but idiom favors text, and BigQuery has
only string; 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
Wrap risky changes so you can undo them.
The column computes itself. Leave it out of the list.
Create derived values in a SELECT list.
DROP TABLE will not remove a view, and IF EXISTS will not save it.
NOT NULL, DEFAULT, CHECK, UNIQUE, and keys.
Add, rename, retype, and drop columns on a live table.