skip to content

In CREATE TABLE, when must a constraint be written at table level instead of inline on a column?

level: juniorimportance: must knowfreq 70%

answer

  1. two positions inside the CREATE TABLE body
  2. count the columns the rule touches
  3. a column list needs somewhere to live
  4. composite keys have no inline form
  5. NOT NULL only has one home

basics

~20 s

Any rule covering more than one column — a composite PRIMARY KEY, UNIQUE or FOREIGN KEY, or a CHECK comparing two columns — must be written as a table constraint after the column list. Single-column rules may be written either way.

solid answer

~50 s

A `CREATE TABLE` body is a list of elements, and each element is either a column definition or a table constraint. A **column constraint** is written inline after the column's data type and applies only to that column: `price NUMERIC(10,2) NOT NULL CHECK (price > 0)`. A **table constraint** is its own element and names the columns it covers in parentheses: `CONSTRAINT pk_order_item PRIMARY KEY (order_id, sku)`. The forcing rule is mechanical — an inline constraint has nowhere to list columns, so anything touching two or more columns has to go at table level: composite primary keys, composite unique keys, composite foreign keys, and a CHECK comparing columns such as `CHECK (ends_at > starts_at)`. Writing `PRIMARY KEY` inline on two columns declares two primary keys, not one composite key, and the statement fails. `NOT NULL` is the exception in the other direction: the standard defines it only as a column constraint. For a single-column rule the two positions produce the same constraint, so the choice is house style.

go deeper

for a junior

Be ready to write a CREATE TABLE from a description and place each rule correctly: NOT NULL inline, and any key or check spanning two columns after the column list.

for a middle

Explain why the split exists — an inline constraint has no column list — and know that NOT NULL is column-only while everything else can be written either way.

for a senior

Show a schema style you would enforce in review: inline nullability, named table-level keys and checks, foreign keys at table level so inline-REFERENCES portability gaps never bite.

for a principal

Own the convention across many teams and migrations: a predictable constraint layout is what makes schema diffs reviewable and generated migrations comparable between environments.

## The two syntactic positions The body of `CREATE TABLE` is a comma-separated list of *elements*. Each element is either a **column definition** — a name, a data type, and optionally some constraints written inline after the type — or a **table constraint**, which stands on its own and names in parentheses the columns it applies to. ```sql -- column constraint: attaches to the column it follows price NUMERIC(10,2) NOT NULL CHECK (price > 0) -- table constraint: a list element in its own right CONSTRAINT ck_item_price CHECK (price > 0) ``` For a rule that touches exactly one column these two are equivalent. The engine records a constraint on the table either way; where you typed it does not survive into behaviour, does not change when the rule is evaluated, and does not change the error you get when a write violates it. ## What forces the table-level form The rule is purely mechanical: **a constraint that mentions more than one column cannot be written inline**, because inline syntax has no column list. That covers four common cases. - A composite primary key: `PRIMARY KEY (order_id, sku)`. - A composite unique key: `UNIQUE (team_id, jersey_no)`. - A composite foreign key: `FOREIGN KEY (dept_code, course_no) REFERENCES course (dept_code, course_no)`. - A CHECK relating columns: `CHECK (ends_at > starts_at)`. The composite primary key is where beginners actually get burned. Writing `PRIMARY KEY` inline on two different columns does not declare one key over the pair — it declares two separate primary keys, and since a table may have at most one, the statement is rejected: ```sql -- rejected: two PRIMARY KEY declarations, not one composite key CREATE TABLE enrollment ( student_id INTEGER PRIMARY KEY, course_id INTEGER PRIMARY KEY ); ``` The fix is a single table constraint: `PRIMARY KEY (student_id, course_id)`. ## NOT NULL is the exception in the other direction `NOT NULL` is defined by the standard as a *column* constraint only; there is no `NOT NULL (col)` table-constraint form. The nearest table-level equivalent is `CHECK (col IS NOT NULL)`, which expresses the same rule but is recorded as a check constraint — catalogs, information-schema views and schema-diff tools will report the column as nullable with a check attached rather than as a non-nullable column, and code generators keyed on nullability will get it wrong. Write `NOT NULL` inline. ## Naming works in either position The optional `CONSTRAINT <name>` prefix is legal in both positions, immediately before the constraint type: ```sql email VARCHAR(320) CONSTRAINT uq_users_email UNIQUE, ... CONSTRAINT uq_users_email UNIQUE (email) ``` That matters because a name is the handle you need later to drop or alter the constraint. ## A worked pair The same table, written both ways as far as each allows: ```sql -- mostly inline CREATE TABLE order_item ( order_id INTEGER NOT NULL REFERENCES orders (id), sku VARCHAR(32) NOT NULL, quantity INTEGER NOT NULL CHECK (quantity > 0), PRIMARY KEY (order_id, sku) -- two columns: must be table-level ); -- keys and rules gathered at table level, all named CREATE TABLE order_item ( order_id INTEGER NOT NULL, sku VARCHAR(32) NOT NULL, quantity INTEGER NOT NULL, CONSTRAINT pk_order_item PRIMARY KEY (order_id, sku), CONSTRAINT fk_order_item_ord FOREIGN KEY (order_id) REFERENCES orders (id), CONSTRAINT ck_order_item_qty CHECK (quantity > 0) ); ``` ## Choosing a style A common convention: keep `NOT NULL` and `DEFAULT` inline, since they read as properties of the column, and gather keys and multi-column rules at the bottom of the table body with explicit names. The bottom block then reads as a summary of the table's identity and relationships, and adding or removing a key shows up as a clean one-line diff instead of an edit buried in a column definition. ## Portability notes Two details differ between engines. First, an inline `REFERENCES parent (col)` is standard shorthand for a single-column foreign key, but engines differ in whether they record it as a real foreign key or merely parse it — writing foreign keys at table level avoids the question. Second, the standard says a column constraint may reference only the column it is attached to; some engines accept an inline CHECK that mentions other columns anyway. Put multi-column checks at table level and the code is unambiguous everywhere.

  • Can a table-level CHECK reference a column defined later in the column list?
    Yes. A table constraint sees the whole table, so column order in the body does not matter — that is part of why multi-column rules belong there. The standard says a column-level CHECK may reference only its own column; some engines are laxer, so write multi-column checks at table level for portability.
  • Does the standard let you write NOT NULL in the table-level constraint list?
    No. NOT NULL is defined only as a column constraint. The table-level rule with the same meaning is `CHECK (col IS NOT NULL)`, but catalogs and tools then report the column as nullable with a check attached rather than as a non-nullable column, so inline NOT NULL is the right form.
  • Does it change anything at runtime whether a single-column CHECK was written inline or at table level?
    No. Both produce the same constraint on the table. The difference is only in what you can express — a column constraint has no column list — and in readability. Naming is available in both positions.

A column constraint is a note in the margin next to one line; a table constraint is a footnote at the bottom of the page that can point at several lines at once.

saying these in an interview costs you the question

  • Claims a table can declare several PRIMARY KEY constraints
  • Writes PRIMARY KEY inline on both columns of a composite key
  • Thinks inline and table-level constraints behave differently at runtime
  • Believes NOT NULL can be written in the table-level constraint list
  • Puts a two-column CHECK inline on one of the columns

context