skip to content

Declaring Constraints

The syntax for stating data rules in DDL: primary and foreign keys, UNIQUE, CHECK and NOT NULL, at column or table level, with referential actions. Interviewers expect you to write these fluently and to name constraints so they can be altered later.

part ofSQLoverview, primer and where to startread it →
on this pageshow

questions

5

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

open as a page

How do you declare a composite FOREIGN KEY with ON DELETE CASCADE in CREATE TABLE?

level: middleimportance: must knowfreq 62%

basics

~20 s

As a table constraint: FOREIGN KEY (a, b) REFERENCES parent (x, y) ON DELETE CASCADE. The two column lists correspond by position, the referenced columns must carry a PRIMARY KEY or UNIQUE declaration, and ON UPDATE is a separate clause.

open as a page

How does UNIQUE (team_id, jersey_no) differ from separate UNIQUE (team_id) and UNIQUE (jersey_no) declarations?

level: juniorimportance: should knowfreq 52%

basics

~20 s

UNIQUE (team_id, jersey_no) requires the pair to be unique, so each value may repeat on its own. Two single-column constraints are far stricter: no team_id could ever appear twice, and neither could any jersey number.

open as a page

Why write CONSTRAINT uq_users_email before UNIQUE (email) instead of leaving the constraint unnamed?

level: middleimportance: should knowfreq 50%

basics

~20 s

The name is the handle you need later: dropping or altering a constraint takes its name, and violation errors quote it. Without one, the engine generates a name that is unpredictable and can differ between engines and environments.

open as a page

In a composite FOREIGN KEY, what does MATCH FULL change compared with the default MATCH SIMPLE?

level: seniorimportance: nice to knowfreq 20%

basics

~10 s

MATCH SIMPLE, the default, treats the constraint as satisfied as soon as any referencing column is NULL, skipping the parent lookup. MATCH FULL allows only all-NULL or all-non-NULL: a half-filled composite key is rejected.

open as a page