Skip to content
Discord Get Started

DDL — Data Definition

Data Definition Language statements for creating and managing database objects.

SQL
CREATE TABLE [IF NOT EXISTS] table_name (
column_name type [constraints],
...
[table_constraints]
);
-- Create from query result
CREATE TABLE new_table AS SELECT ... FROM source;
SELECT ... INTO new_table FROM source;

CREATE TABLE ... AS does not accept PostgreSQL’s trailing WITH DATA or WITH NO DATA: either fails with 42601 (syntax error at or near "WITH"). For an empty table with the query’s columns, add WHERE false to the query instead; the table still gets the _rowid column described below.

Column constraints: NOT NULL, DEFAULT, PRIMARY KEY, UNIQUE, REFERENCES (foreign key), CHECK

Table constraints: PRIMARY KEY (col, ...), UNIQUE (col, ...), FOREIGN KEY (col) REFERENCES table(col) with ON DELETE/ON UPDATE actions, CHECK (expr)

SERIAL and BIGSERIAL columns are supported for auto-incrementing IDs. Self-referential foreign keys are supported.

Identity columns:

SQL
-- Alternatives: ALWAYS refuses a user-supplied id, BY DEFAULT accepts one (see the caution below)
CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY);
CREATE TABLE t (id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY);

Generated (computed) columns:

SQL
CREATE TABLE products (
price NUMERIC,
tax NUMERIC,
total NUMERIC GENERATED ALWAYS AS (price + tax) STORED
);
SQL
-- B-tree index (default)
CREATE INDEX idx_name ON table (column);
-- Unique index
CREATE UNIQUE INDEX idx_name ON table (column);
-- GIN index (for JSONB, full-text search, arrays)
CREATE INDEX idx_name ON table USING GIN (column);
-- Hash index (equality lookups on integer and OID columns)
CREATE INDEX idx_name ON table USING HASH (column);
-- Expression index
CREATE INDEX idx_name ON table (lower(column));
-- Partial index
CREATE INDEX idx_name ON table (column) WHERE active = true;
-- Non-blocking index creation
CREATE INDEX CONCURRENTLY idx_name ON table (column);

btree (default), hash, gin, and hnsw are the recognized index access methods, and all of them can be created — subject to the column-type restrictions noted below for hash and gin. See the pgvector page for HNSW. IF NOT EXISTS is supported.

GIN index example (JSONB containment):

SQL
CREATE TABLE documents (id SERIAL PRIMARY KEY, metadata JSONB);
CREATE INDEX idx_metadata ON documents USING GIN (metadata);
-- Use the @> containment operator (accelerated by GIN)
SELECT * FROM documents WHERE metadata @> '{"type": "pdf"}';

GIN indexes support JSONB containment (@>) and full-text search (@@) operators. See Full-Text Search for tsvector usage.

GIN has a column-type restriction of its own: it builds only over JSONB, TSVECTOR, and array types. On any other column — including JSON, TEXT, and INTEGER — CREATE INDEX fails with the same error PostgreSQL gives, data type text has no default operator class for access method "gin" (42704). To index text, index the to_tsvector(...) expression rather than the column. See the compatibility matrix for the full list of index restrictions.

SQL
CREATE VIEW view_name AS SELECT ...;
CREATE OR REPLACE VIEW view_name AS SELECT ...;

CREATE OR REPLACE VIEW cannot drop or rename existing columns; only appending new columns is allowed.

SQL
CREATE MATERIALIZED VIEW mv_name AS SELECT ...;
REFRESH MATERIALIZED VIEW mv_name;

A materialized view is always populated when it is created. PostgreSQL’s trailing WITH NO DATA, which creates the view empty for a later REFRESH to fill, is not parsed, and neither is WITH DATA: both fail with 42601 (syntax error at or near "WITH"), so leave the clause out.

OperationSyntax
Add columnALTER TABLE t ADD COLUMN col type
Drop columnALTER TABLE t DROP COLUMN col
Rename columnALTER TABLE t RENAME COLUMN old TO new
Rename tableALTER TABLE t RENAME TO new_name
Set defaultALTER TABLE t ALTER COLUMN col SET DEFAULT val
Drop defaultALTER TABLE t ALTER COLUMN col DROP DEFAULT
Set not nullALTER TABLE t ALTER COLUMN col SET NOT NULL
Drop not nullALTER TABLE t ALTER COLUMN col DROP NOT NULL
Change typeALTER TABLE t ALTER COLUMN col SET DATA TYPE type [USING expr] — not on primary-key columns, and on a foreign key’s referencing column only INTEGER ↔ BIGINT and VARCHAR(n) → TEXT; see below
Add constraintALTER TABLE t ADD CONSTRAINT name ...
Drop constraintALTER TABLE t DROP CONSTRAINT name
Rename constraintALTER TABLE t RENAME CONSTRAINT old TO new
Disable triggerALTER TABLE t DISABLE TRIGGER {name | USER | ALL} — prefer USER; ALL is refused on any table a foreign key touches, see disabling triggers
Enable triggerALTER TABLE t ENABLE TRIGGER {name | USER | ALL} — prefer USER; ALL is refused on any table a foreign key touches, see disabling triggers
Enable as replica/alwaysALTER TABLE t ENABLE {REPLICA | ALWAYS} TRIGGER name — a name is required; USER/ALL give 42601 REPLICA and ALWAYS require a trigger name

Every form above also accepts the optional ONLY keyword (ALTER TABLE ONLY t …), with the same effect, as do the row-level security forms. A pg_dump that writes ALTER TABLE ONLY therefore restores as-is.

SQL
DROP TABLE [IF EXISTS] table_name [CASCADE];
DROP INDEX [IF EXISTS] index_name;
DROP VIEW [IF EXISTS] view_name [CASCADE];
DROP MATERIALIZED VIEW [IF EXISTS] mv_name [CASCADE];
TRUNCATE [TABLE] table_name [RESTART IDENTITY | CONTINUE IDENTITY] [CASCADE | RESTRICT];

CASCADE transitively resolves and drops all dependent views and materialized views.

TRUNCATE t and TRUNCATE TABLE t are the accepted spellings, each optionally followed by RESTART IDENTITY or CONTINUE IDENTITY, and by CASCADE or RESTRICT. Both identity clauses take effect:

SQL
CREATE TABLE t (id SERIAL PRIMARY KEY, v TEXT);
INSERT INTO t (v) VALUES ('a'), ('b'), ('c');
TRUNCATE t RESTART IDENTITY; -- the next INSERT gets id 1
TRUNCATE t CONTINUE IDENTITY; -- the next INSERT continues from where the sequence stood

CONTINUE IDENTITY is PostgreSQL’s default, so it is only ever written explicitly. CASCADE and RESTRICT work as in PostgreSQL: TRUNCATE parent CASCADE also empties every table whose foreign key references parent, transitively, and TRUNCATE parent RESTART IDENTITY CASCADE resets the sequences of all of them. Unlike PostgreSQL, DB9 prints no NOTICE: truncate cascades to table … line for the extra tables it empties. The multi-table form TRUNCATE a, b and ONLY are rejected at parse time (42601).

TRUNCATE without CASCADE on a table that another table’s foreign key references is refused, as in PostgreSQL, with 0A000 (cannot truncate a table referenced in a foreign key constraint) — even when the referencing table is empty, and with RESTRICT as well. Use TRUNCATE parent CASCADE, or empty the referencing table first and remove the parent’s rows with DELETE — see Compatibility matrix.

StatementSupported
CREATE SCHEMAYes
ALTER SCHEMAOWNER TO only — RENAME TO is rejected (42601)
CREATE SEQUENCE / DROP SEQUENCEYes
ALTER SEQUENCEYes, except SET SCHEMA, SET LOGGED and SET UNLOGGED — see below
CREATE TYPE (enum, composite)Yes — but enum values sort by label text, and composite values cannot be read field-wise; see Custom Types and the composite type caution
ALTER TYPEEnum values, RENAME TO, RENAME ATTRIBUTE and OWNER TO only — see below
DROP TYPEYes — CASCADE clears a function dependency but not a column one, and a RETURNS TABLE (...) column is not checked; see below
CREATE FUNCTION / DROP FUNCTIONYes
CREATE TRIGGER / DROP TRIGGERYes
CREATE COLLATION / DROP COLLATIONYes
CREATE DATABASE / DROP DATABASEYes
ALTER DATABASERENAME TO, OWNER TO, SET and RESET — see below
CREATE EXTENSION / DROP EXTENSIONYes
ALTER VIEWOWNER TO only — RENAME TO and SET SCHEMA are rejected (42601), as is every ALTER MATERIALIZED VIEW form
COMMENT ON (table, column, function, procedure, extension, index, schema)Yes — indexes only when standalone, see below

Sequence options: START, INCREMENT, MINVALUE, MAXVALUE, CACHE and CYCLE take effect in CREATE SEQUENCE, as in PostgreSQL — see Sequence Behavior for what CACHE does to the order and gaps of values across sessions. An identity column definition takes the same options, plus SEQUENCE NAME — see Identity columns above.

SQL
-- Enable / disable RLS on a table
ALTER TABLE t ENABLE ROW LEVEL SECURITY;
ALTER TABLE t DISABLE ROW LEVEL SECURITY;
-- Force RLS for table owner
ALTER TABLE t FORCE ROW LEVEL SECURITY;
ALTER TABLE t NO FORCE ROW LEVEL SECURITY;
-- Create, alter, and drop policies
CREATE POLICY name ON t FOR SELECT USING (expr);
CREATE POLICY name ON t FOR INSERT WITH CHECK (expr);
CREATE POLICY name ON t AS RESTRICTIVE FOR SELECT USING (expr);
ALTER POLICY name ON t USING (new_expr);
DROP POLICY [IF EXISTS] name ON t;

ALTER TABLE ONLY t ... ROW LEVEL SECURITY is accepted for all four forms and does the same thing as the plain spelling — pg_class.relrowsecurity and relforcerowsecurity change as expected — so a pg_dump, which writes ALTER TABLE ONLY, restores unchanged. See Row-Level Security for complete policy semantics and examples.