keys and constraints
Keys and Constraints
Learn how primary, candidate, alternate, composite, surrogate, natural, and foreign keys identify and connect relational data, while NOT NULL, UNIQUE, CHECK, DEFAULT, and referential constraints protect database integrity.
Introduction
A relational database should do more than store values. It should reject data that violates the domain rules represented by its schema.
Keys identify rows and connect related tables. Constraints define rules that every valid database state must satisfy.
Keys and constraints help ensure that:
- Every important row can be identified
- Duplicate business identifiers are rejected
- Required values cannot be omitted
- Relationships point to valid parent records
- Numeric and date values remain within valid ranges
- Invalid lifecycle states cannot be stored
- One-to-one and many-to-many relationships remain valid
- Application defects do not silently corrupt stored data
Core idea: Application validation improves usability, but database constraints provide the final integrity boundary for every application, migration, background job, script, and administrative tool that writes to the database.
In your System Design curriculum, Keys and Constraints is Topic 5.3 under Relational Data Modeling & SQL. It follows normalization and denormalization and precedes ACID, transactions, isolation, B-tree and composite indexes, query plans, and connection pools.
Prerequisites
| # | Prerequisite | Why It Is Needed |
|---|---|---|
| 1 | Entities and relationships | Keys identify entities and foreign keys implement relationships. |
| 2 | Normalization | Normalized tables are connected through primary and foreign keys. |
| 3 | SQL data types | Key and constraint behaviour depends on appropriate column types. |
| 4 | CREATE TABLE | Most constraints are declared as part of the database schema. |
| 5 | INSERT, UPDATE, and DELETE | Constraints are evaluated when stored data changes. |
| 6 | Business requirements | Constraints must represent genuine domain rules rather than assumptions. |
What Is a Key?
A key is an attribute or set of attributes used to identify rows, enforce uniqueness, or establish relationships between tables.
Common key categories include:
- Super key
- Candidate key
- Primary key
- Alternate key
- Composite key
- Natural key
- Surrogate key
- Foreign key
- Partial key
Key Terminology
| Key Type | Meaning | Example |
|---|---|---|
| Super key | Any attribute set that uniquely identifies a row | Customer ID plus email |
| Candidate key | A minimal super key containing no unnecessary attribute | Customer number |
| Primary key | The candidate key selected as the main row identifier | Customer ID |
| Alternate key | A candidate key not selected as the primary key | Customer number or email |
| Composite key | A key containing two or more columns | Order ID plus line number |
| Natural key | A key derived from meaningful domain information | Country code |
| Surrogate key | A system-generated identifier without business meaning | Numeric identity or UUID |
| Foreign key | A column set referencing a key in another or the same table | Orders.customer_id |
Super Keys
A super key is any set of attributes that uniquely identifies a row. It can contain attributes that are unnecessary for uniqueness.
CUSTOMER attributes:
customer_id
customer_number
email_address
display_name
Possible super keys:
{customer_id}
{customer_number}
{email_address}
{customer_id, display_name}
{customer_number, email_address}
If customer_id uniquely identifies a customer, adding
display_name does not improve identification. The resulting
attribute set remains a super key but is not minimal.
Candidate Keys
A candidate key is a minimal super key. Removing any attribute from a candidate key causes it to lose its uniqueness property.
Candidate keys for CUSTOMER:
customer_id
customer_number
email_address
Each candidate key:
- Uniquely identifies a row
- Contains no unnecessary attribute
Candidate keys come from actual business rules. An email address is a candidate key only when the system guarantees that every relevant customer has one unique, stable email under the defined scope.
Primary Keys
A primary key is the candidate key selected as the table's principal row identifier.
CREATE TABLE customers
(
customer_id BIGINT PRIMARY KEY,
customer_number VARCHAR(30) NOT NULL,
display_name VARCHAR(200) NOT NULL,
email_address VARCHAR(254) NOT NULL
);
A suitable primary key should normally be:
- Unique
- Non-null
- Stable
- Minimal
- Available for every row
- Appropriate for references
Identity rule: Choose a primary key whose value does not need to change when ordinary business attributes such as names, email addresses, telephone numbers, or product descriptions change.
Alternate Keys
Candidate keys that are not selected as the primary key are alternate keys. They should normally be protected through UNIQUE and NOT NULL constraints when the business rules require a value for every row.
CREATE TABLE customers
(
customer_id BIGINT PRIMARY KEY,
customer_number VARCHAR(30) NOT NULL,
email_address VARCHAR(254) NOT NULL,
display_name VARCHAR(200) NOT NULL,
CONSTRAINT uq_customers_number
UNIQUE (customer_number),
CONSTRAINT uq_customers_email
UNIQUE (email_address)
);
The unique constraints preserve the candidate-key rules even though
customer_id is the selected primary key.
Composite Keys
A composite key contains more than one column. The combination must be unique even when individual components repeat.
Order-line Example
CREATE TABLE order_lines
(
order_id BIGINT NOT NULL,
line_number INT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(18, 2) NOT NULL,
CONSTRAINT pk_order_lines
PRIMARY KEY
(
order_id,
line_number
)
);
| Order ID | Line Number | Valid Combination? |
|---|---|---|
| 1001 | 1 | Yes |
| 1001 | 2 | Yes |
| 1002 | 1 | Yes |
| 1001 | 1 | No, duplicate composite key |
Composite keys are common in weak entities, junction tables, historical tables, and relationships identified by participating entity keys.
Natural Keys
A natural key is based on meaningful domain information.
Examples can include:
- An ISO country code
- A formally assigned product code
- A tenant-scoped employee number
- A course code within an institution
CREATE TABLE countries
(
country_code CHAR(2) PRIMARY KEY,
country_name VARCHAR(100) NOT NULL,
CONSTRAINT uq_countries_name
UNIQUE (country_name)
);
Natural-key Benefits
- Can be meaningful to users and integrations
- Can prevent duplicate domain records naturally
- Can reduce the need for a second business identifier
Natural-key Risks
- The business value can change
- The value can be long
- Uniqueness can be scoped rather than global
- The value can expose sensitive information
- Business rules can change after deployment
Surrogate Keys
A surrogate key is generated by the system and has no inherent domain meaning.
CREATE TABLE products
(
product_id BIGINT GENERATED ALWAYS AS IDENTITY
PRIMARY KEY,
product_code VARCHAR(50) NOT NULL,
product_name VARCHAR(200) NOT NULL,
CONSTRAINT uq_products_code
UNIQUE (product_code)
);
Database-specific identity syntax varies. Other implementations can use sequences, UUIDs, or application-generated identifiers.
Surrogate-key rule: Adding a generated primary key does
not remove the need to constrain the real business key. If product codes
must be unique, keep a UNIQUE constraint on product_code.
Natural vs Surrogate Keys
| Area | Natural Key | Surrogate Key |
|---|---|---|
| Meaning | Has domain meaning | Has no business meaning |
| Stability | Depends on business policy | Normally immutable |
| Size | Can contain several or long columns | Can be compact or fixed-format |
| Uniqueness rule | Directly represents a business rule | Needs separate constraints for business uniqueness |
| References | Carry domain values into child tables | Keep relationships independent of mutable attributes |
Foreign Keys
A foreign key is a column or set of columns that references a candidate key in another table or the same table.
Customer and Order Relationship
CREATE TABLE customers
(
customer_id BIGINT PRIMARY KEY,
display_name VARCHAR(200) NOT NULL
);
CREATE TABLE orders
(
order_id BIGINT PRIMARY KEY,
customer_id BIGINT NOT NULL,
order_status VARCHAR(30) NOT NULL,
CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id)
REFERENCES customers (customer_id)
);
The foreign key prevents an order from referring to a customer that does not exist.
Valid:
Customer 42 exists.
Order 9001 references Customer 42.
Invalid:
Customer 99 does not exist.
Order 9002 references Customer 99.
Result:
Database rejects the invalid relationship.
Referential Integrity
Referential integrity ensures that a foreign-key value references a valid parent key or is null when the relationship is optional.
It helps prevent:
- Orders without valid customers
- Order lines without valid orders
- Payments without valid invoices or orders
- Enrollments without valid learners or courses
- Memberships without valid users or projects
- Orphaned dependent records
Self-referencing Foreign Keys
A foreign key can reference another row in the same table.
CREATE TABLE employees
(
employee_id BIGINT PRIMARY KEY,
employee_name VARCHAR(200) NOT NULL,
manager_employee_id BIGINT NULL,
CONSTRAINT fk_employees_manager
FOREIGN KEY (manager_employee_id)
REFERENCES employees (employee_id),
CONSTRAINT ck_employee_not_own_manager
CHECK
(
manager_employee_id IS NULL
OR manager_employee_id <> employee_id
)
);
The check prevents direct self-management. Preventing longer management cycles can require transaction-level application logic or database-specific recursive validation.
Composite Foreign Keys
A composite foreign key references a composite parent key. Column order and compatible data types must align with the referenced key.
CREATE TABLE tenant_products
(
tenant_id BIGINT NOT NULL,
product_code VARCHAR(50) NOT NULL,
product_name VARCHAR(200) NOT NULL,
PRIMARY KEY
(
tenant_id,
product_code
)
);
CREATE TABLE tenant_order_lines
(
order_id BIGINT NOT NULL,
line_number INT NOT NULL,
tenant_id BIGINT NOT NULL,
product_code VARCHAR(50) NOT NULL,
PRIMARY KEY
(
order_id,
line_number
),
CONSTRAINT fk_order_line_product
FOREIGN KEY
(
tenant_id,
product_code
)
REFERENCES tenant_products
(
tenant_id,
product_code
)
);
Including the tenant in the key can also help enforce that a child relationship remains within the intended tenant boundary.
NOT NULL Constraint
NOT NULL requires a column to contain a value for every row.
CREATE TABLE courses
(
course_id BIGINT PRIMARY KEY,
course_title VARCHAR(200) NOT NULL,
course_status VARCHAR(30) NOT NULL,
published_at TIMESTAMP NULL
);
The title and status are mandatory. Publication time is optional because an unpublished course can legitimately have no publication timestamp.
Nullability rule: Make a column nullable only when the absence of the value has a clear domain meaning. Do not use nullability merely to avoid correcting existing invalid data.
UNIQUE Constraint
A UNIQUE constraint prevents duplicate values or duplicate column combinations according to the database platform's null semantics.
Single-column Uniqueness
CREATE TABLE users
(
user_id BIGINT PRIMARY KEY,
email_address VARCHAR(254) NOT NULL,
CONSTRAINT uq_users_email
UNIQUE (email_address)
);
Composite Uniqueness
CREATE TABLE employees
(
employee_id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
employee_number VARCHAR(30) NOT NULL,
employee_name VARCHAR(200) NOT NULL,
CONSTRAINT uq_employee_number_per_tenant
UNIQUE
(
tenant_id,
employee_number
)
);
The same employee number can exist in different tenants, but it cannot occur twice within one tenant.
PRIMARY KEY vs UNIQUE
| Area | PRIMARY KEY | UNIQUE |
|---|---|---|
| Main purpose | Principal identifier for each row | Protects another candidate or business uniqueness rule |
| Number per table | One primary-key constraint | Several unique constraints can exist |
| Nullability | Primary-key columns are non-null | Null treatment varies by database platform |
| Foreign-key target | Can be referenced | A qualifying unique candidate key can also be referenced |
CHECK Constraint
A CHECK constraint requires an inserted or updated row to satisfy a Boolean condition.
Positive Quantity
CREATE TABLE order_lines
(
order_id BIGINT NOT NULL,
line_number INT NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(18, 2) NOT NULL,
PRIMARY KEY
(
order_id,
line_number
),
CONSTRAINT ck_order_lines_quantity
CHECK (quantity > 0),
CONSTRAINT ck_order_lines_price
CHECK (unit_price >= 0)
);
Date Range
CREATE TABLE promotions
(
promotion_id BIGINT PRIMARY KEY,
starts_at TIMESTAMP NOT NULL,
ends_at TIMESTAMP NOT NULL,
CONSTRAINT ck_promotion_dates
CHECK (ends_at > starts_at)
);
Allowed Values
CREATE TABLE orders
(
order_id BIGINT PRIMARY KEY,
order_status VARCHAR(30) NOT NULL,
CONSTRAINT ck_orders_status
CHECK
(
order_status IN
(
'pending',
'confirmed',
'processing',
'completed',
'cancelled'
)
)
);
When allowed values have attributes, relationships, translations, or independent lifecycles, a lookup table can be more suitable than a long CHECK list.
DEFAULT Values
A DEFAULT clause supplies a value when an INSERT does not provide one.
CREATE TABLE customers
(
customer_id BIGINT PRIMARY KEY,
display_name VARCHAR(200) NOT NULL,
customer_status VARCHAR(30)
NOT NULL
DEFAULT 'active',
created_at TIMESTAMP
NOT NULL
DEFAULT CURRENT_TIMESTAMP
);
A default is not a substitute for validation. The generated value must still satisfy the column's data type and constraints.
Default rule: A default is used when the caller omits a value. It does not necessarily replace a value explicitly supplied as NULL, and exact behaviour should be verified for the selected database.
Foreign-key Delete Actions
A foreign key can define what happens when a referenced parent row is deleted.
| Action | Behaviour | Typical Consideration |
|---|---|---|
| RESTRICT | Rejects deletion when dependent rows exist | Parent must not be removed while referenced |
| NO ACTION | Requires referential integrity to remain valid according to database timing | Exact timing can vary by platform |
| CASCADE | Deletes dependent child rows | Children have no valid existence without the parent |
| SET NULL | Sets the child reference to null | The relationship is genuinely optional |
| SET DEFAULT | Applies the child column's valid default relationship | Requires platform support and a meaningful default |
Restricted Deletion
CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id)
REFERENCES customers (customer_id)
ON DELETE RESTRICT
Cascading Deletion
CONSTRAINT fk_order_lines_order
FOREIGN KEY (order_id)
REFERENCES orders (order_id)
ON DELETE CASCADE
Optional Relationship
CONSTRAINT fk_employees_manager
FOREIGN KEY (manager_employee_id)
REFERENCES employees (employee_id)
ON DELETE SET NULL
Deletion warning: Use cascading deletion only when automatically deleting every dependent row is correct for business, historical, audit, legal, and recovery requirements.
Foreign-key Update Actions
A foreign key can also define what happens when a referenced key value changes.
CONSTRAINT fk_order_lines_order
FOREIGN KEY (order_id)
REFERENCES orders (order_id)
ON UPDATE CASCADE
Stable surrogate primary keys normally do not need business-driven updates. Mutable natural keys can create broader update propagation.
Junction-table Constraints
A many-to-many relationship should prevent duplicate associations and invalid participants.
CREATE TABLE course_instructors
(
course_id BIGINT NOT NULL,
instructor_id BIGINT NOT NULL,
assigned_at TIMESTAMP NOT NULL
DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_course_instructors
PRIMARY KEY
(
course_id,
instructor_id
),
CONSTRAINT fk_course_instructors_course
FOREIGN KEY (course_id)
REFERENCES courses (course_id)
ON DELETE CASCADE,
CONSTRAINT fk_course_instructors_instructor
FOREIGN KEY (instructor_id)
REFERENCES instructors (instructor_id)
ON DELETE RESTRICT
);
The composite primary key prevents the same instructor from being assigned to the same course twice.
One-to-one Constraints
A unique foreign key can enforce a one-to-one relationship.
CREATE TABLE user_profiles
(
profile_id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
display_name VARCHAR(200) NOT NULL,
CONSTRAINT uq_user_profiles_user
UNIQUE (user_id),
CONSTRAINT fk_user_profiles_user
FOREIGN KEY (user_id)
REFERENCES users (user_id)
ON DELETE CASCADE
);
The foreign key ensures that the user exists. The unique constraint ensures that each user can have at most one profile.
Tenant-aware Constraints
Multi-tenant schemas can include the tenant key in unique and foreign-key constraints to enforce tenant boundaries structurally.
CREATE TABLE tenant_customers
(
tenant_id BIGINT NOT NULL,
customer_id BIGINT NOT NULL,
customer_number VARCHAR(30) NOT NULL,
PRIMARY KEY
(
tenant_id,
customer_id
),
CONSTRAINT uq_customer_number_per_tenant
UNIQUE
(
tenant_id,
customer_number
)
);
CREATE TABLE tenant_orders
(
tenant_id BIGINT NOT NULL,
order_id BIGINT NOT NULL,
customer_id BIGINT NOT NULL,
PRIMARY KEY
(
tenant_id,
order_id
),
CONSTRAINT fk_tenant_orders_customer
FOREIGN KEY
(
tenant_id,
customer_id
)
REFERENCES tenant_customers
(
tenant_id,
customer_id
)
);
This prevents an order in one tenant from referencing a customer belonging to another tenant through the constrained relationship.
Case-insensitive Business Uniqueness
Text uniqueness depends on collation, case rules, whitespace, Unicode normalization, and business requirements.
Potentially equivalent email inputs:
Customer@Example.com
customer@example.com
customer@example.com followed by whitespace
If the business treats these as equivalent, normalize the value according to an approved rule and enforce uniqueness on the normalized representation.
CREATE TABLE users
(
user_id BIGINT PRIMARY KEY,
email_address VARCHAR(254) NOT NULL,
normalized_email VARCHAR(254) NOT NULL,
CONSTRAINT uq_users_normalized_email
UNIQUE (normalized_email)
);
Email handling requirements can be domain-specific. Do not invent normalization rules without confirming the identity and communication contract.
NULL and UNIQUE Semantics
Database systems can differ in how UNIQUE constraints treat null values. Verify behaviour for the selected platform.
Business requirement:
A username is optional.
When present:
It must be unique.
Design questions:
- Can several rows contain NULL?
- Is an empty string allowed?
- Is matching case-sensitive?
- Should whitespace be normalized?
- Can a filtered or partial unique index be used?
Constraints must implement the actual business rule, not rely on an unverified assumption about database-specific null behaviour.
Cross-column Constraints
CHECK constraints can compare values within the same row.
CREATE TABLE subscriptions
(
subscription_id BIGINT PRIMARY KEY,
starts_on DATE NOT NULL,
ends_on DATE NULL,
cancellation_date DATE NULL,
CONSTRAINT ck_subscription_period
CHECK
(
ends_on IS NULL
OR ends_on >= starts_on
),
CONSTRAINT ck_cancellation_date
CHECK
(
cancellation_date IS NULL
OR cancellation_date >= starts_on
)
);
Cross-row and cross-table rules usually require other mechanisms because a simple row CHECK normally evaluates only the row being changed.
What Constraints Cannot Express Easily
Examples of complex rules include:
- An order must always have at least one line after completion
- A manager hierarchy must contain no indirect cycles
- Reservation date ranges must not overlap
- Total payment must not exceed the order balance
- Exactly one address must be primary for each customer
- One active assignment can exist for a time range
- A shipment can be created only for an eligible order state
Depending on the database and consistency requirements, these can require:
- Transactions
- Additional unique or exclusion constraints
- Deferred constraints
- Triggers
- Stored procedures
- Application services
- Serialized operations
Application Validation vs Database Constraints
| Layer | Primary Responsibility |
|---|---|
| User interface | Early feedback and input guidance |
| API validation | Request schema, format, and safe client errors |
| Business service | Workflow, authorization, and domain-state rules |
| Database constraints | Final enforcement of representable storage invariants |
1. Application checks whether email exists.
2. Email is absent.
3. Another request performs the same check.
4. Both requests insert the email.
5. Duplicate users are created.
1. Application validates the request.
2. Database UNIQUE constraint arbitrates concurrent inserts.
3. One insert succeeds.
4. The duplicate insert is rejected.
5. Application maps the violation to a safe conflict response.
Constraints and Concurrency
A check-then-insert sequence does not safely enforce uniqueness under concurrency.
SELECT
user_id
FROM users
WHERE
normalized_email = :email;
Two transactions can both see no row.
Both then attempt to insert.
The UNIQUE constraint must provide
the final atomic uniqueness decision.
Adding Constraints to Existing Tables
Before adding a new constraint, inspect and correct existing violations.
Detect Duplicates
SELECT
normalized_email,
COUNT(*) AS duplicate_count
FROM users
GROUP BY
normalized_email
HAVING
COUNT(*) > 1;
Detect Orphaned Foreign Keys
SELECT
o.order_id,
o.customer_id
FROM orders AS o
LEFT JOIN customers AS c
ON c.customer_id = o.customer_id
WHERE
c.customer_id IS NULL;
Detect Invalid Values
SELECT
order_id,
order_status
FROM orders
WHERE
order_status NOT IN
(
'pending',
'confirmed',
'processing',
'completed',
'cancelled'
);
Add a Constraint
ALTER TABLE users
ADD CONSTRAINT uq_users_normalized_email
UNIQUE (normalized_email);
ALTER TABLE syntax, validation timing, and online-schema-change capabilities differ between database platforms.
Naming Constraints
Meaningful constraint names improve migrations, diagnostics, monitoring, and error handling.
| Prefix | Constraint Type | Example |
|---|---|---|
pk_ |
Primary key | pk_orders |
fk_ |
Foreign key | fk_orders_customer |
uq_ |
Unique constraint | uq_customers_email |
ck_ |
Check constraint | ck_order_lines_quantity |
Follow the naming convention established for the database project rather than introducing a different convention table by table.
Complete Constrained Order Schema
CREATE TABLE customers
(
customer_id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
customer_number VARCHAR(30) NOT NULL,
display_name VARCHAR(200) NOT NULL,
email_address VARCHAR(254) NOT NULL,
customer_status VARCHAR(30)
NOT NULL
DEFAULT 'active',
created_at TIMESTAMP
NOT NULL
DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_customer_number_per_tenant
UNIQUE
(
tenant_id,
customer_number
),
CONSTRAINT uq_customer_email_per_tenant
UNIQUE
(
tenant_id,
email_address
),
CONSTRAINT ck_customer_status
CHECK
(
customer_status IN
(
'active',
'inactive'
)
)
);
CREATE TABLE products
(
product_id BIGINT PRIMARY KEY,
product_code VARCHAR(50) NOT NULL,
product_name VARCHAR(200) NOT NULL,
current_price DECIMAL(18, 2) NOT NULL,
product_status VARCHAR(30)
NOT NULL
DEFAULT 'active',
CONSTRAINT uq_products_code
UNIQUE (product_code),
CONSTRAINT ck_products_price
CHECK (current_price >= 0),
CONSTRAINT ck_products_status
CHECK
(
product_status IN
(
'active',
'inactive',
'discontinued'
)
)
);
CREATE TABLE orders
(
order_id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
order_number VARCHAR(30) NOT NULL,
customer_id BIGINT NOT NULL,
order_status VARCHAR(30)
NOT NULL
DEFAULT 'pending',
ordered_at TIMESTAMP
NOT NULL
DEFAULT CURRENT_TIMESTAMP,
total_amount DECIMAL(18, 2) NOT NULL,
currency CHAR(3) NOT NULL,
CONSTRAINT uq_order_number_per_tenant
UNIQUE
(
tenant_id,
order_number
),
CONSTRAINT fk_orders_customer
FOREIGN KEY
(
tenant_id,
customer_id
)
REFERENCES customers
(
tenant_id,
customer_id
),
CONSTRAINT ck_orders_status
CHECK
(
order_status IN
(
'pending',
'confirmed',
'processing',
'completed',
'cancelled'
)
),
CONSTRAINT ck_orders_total
CHECK (total_amount >= 0)
);
CREATE TABLE order_lines
(
order_id BIGINT NOT NULL,
line_number INT NOT NULL,
product_id BIGINT NOT NULL,
product_description_snapshot VARCHAR(200) NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(18, 2) NOT NULL,
CONSTRAINT pk_order_lines
PRIMARY KEY
(
order_id,
line_number
),
CONSTRAINT fk_order_lines_order
FOREIGN KEY (order_id)
REFERENCES orders (order_id)
ON DELETE CASCADE,
CONSTRAINT fk_order_lines_product
FOREIGN KEY (product_id)
REFERENCES products (product_id)
ON DELETE RESTRICT,
CONSTRAINT ck_order_lines_number
CHECK (line_number > 0),
CONSTRAINT ck_order_lines_quantity
CHECK (quantity > 0),
CONSTRAINT ck_order_lines_price
CHECK (unit_price >= 0)
);
This schema is illustrative. Data types, identity generation, tenant-key strategy, decimal precision, default syntax, and constraint capabilities should be adapted to the selected database platform.
Keys, Constraints, and Indexes
Constraints and indexes have related but different purposes.
| Object | Primary Purpose |
|---|---|
| PRIMARY KEY | Enforces principal entity identity |
| UNIQUE constraint | Enforces candidate-key or business uniqueness |
| FOREIGN KEY | Enforces referential integrity |
| CHECK constraint | Enforces a Boolean data rule |
| Index | Provides a physical access structure for selected queries |
A database can create an index automatically for primary or unique constraints. Foreign-key indexing behaviour varies. Index foreign-key columns when the query and modification workload justifies it, and verify the resulting execution plans.
Constraints and Transactions
Constraints are evaluated as data changes. Exact timing can be immediate or deferred depending on the database and constraint type.
Begin transaction
1. Insert parent row.
2. Insert related child rows.
3. Update aggregate state.
4. Validate applicable constraints.
Commit transaction
Multi-row business invariants should be designed together with transaction boundaries and isolation behaviour.
Handling Constraint Violations
Database errors should be translated into safe application responses rather than exposing raw SQL, table names, or internal stack traces.
| Constraint Violation | Possible API Meaning |
|---|---|
| Duplicate candidate key | Resource conflict |
| Missing parent foreign key | Invalid related resource |
| NOT NULL violation | Required value missing |
| CHECK violation | Value violates a domain rule |
| Restricted parent deletion | Resource has dependent records |
{
"type": "resource-conflict",
"title": "The customer number already exists",
"status": 409,
"errors": [
{
"field": "customerNumber",
"code": "duplicate"
}
]
}
Constraint Observability
Useful database-integrity metrics include:
- Primary-key violation count
- Unique-constraint violation count
- Foreign-key violation count
- Check-constraint violation count
- NOT NULL violation count
- Restricted-deletion failure count
- Constraint name and affected operation
- Schema-migration validation duration
- Orphan-reconciliation result count
Logs should include safe constraint identifiers and trace information without exposing sensitive values or raw database details to clients.
Constraint-design Workflow
- Identify each table's entity or relationship meaning.
- List all candidate keys.
- Select a stable primary key.
- Preserve alternate keys with UNIQUE constraints.
- Define required and optional columns.
- Identify parent-child relationships.
- Add foreign keys for referential integrity.
- Define delete and update actions explicitly.
- Add CHECK constraints for row-level domain rules.
- Add meaningful defaults where omission has a valid interpretation.
- Review tenant and ownership boundaries.
- Test constraints under concurrent writes.
- Map violations to safe application errors.
- Deploy through repeatable schema migrations.
Common Key and Constraint Mistakes
Using a Mutable Field as the Primary Key
Changing an email, telephone number, username, or product code can require updates across many relationships.
Adding a Surrogate Key without Business Uniqueness
A generated ID does not prevent duplicate customer numbers, email addresses, memberships, or other domain identities.
Relying Only on Application Validation
Concurrent requests, scripts, migrations, and other applications can bypass or race application checks.
Leaving Relationships without Foreign Keys
Invalid references and orphaned records can accumulate silently.
Making Every Column Nullable
Missing required data spreads validation complexity and weakens the stored model.
Using Empty Strings instead of Meaningful Nullability
Empty, unknown, not applicable, and omitted values can represent different domain states.
Using Cascade Delete without Impact Analysis
Cascading actions can remove historical, financial, audit, or dependent records unexpectedly.
Forgetting Composite Business Scope
An employee number or order number can be unique within a tenant rather than globally.
Assuming UNIQUE Null Behaviour Is Universal
Verify how the selected database handles nulls, collation, case, and filtered uniqueness.
Using CHECK for Cross-table Rules
Simple CHECK constraints generally cannot enforce complex rules involving several rows or tables.
Adding Constraints without Cleaning Existing Data
Existing duplicates, nulls, invalid values, or orphaned references can cause the migration to fail.
Exposing Raw Database Errors
Constraint violations should become safe domain errors rather than reveal SQL, schema names, or internal implementation details.
Recommended Test Cases
| Test | Expected Evidence |
|---|---|
| Duplicate primary key | The second row is rejected |
| Null primary key | The row is rejected |
| Duplicate alternate key | The UNIQUE constraint rejects the duplicate |
| Composite uniqueness | The duplicate combination is rejected |
| Missing required value | The NOT NULL constraint rejects the row |
| Invalid numeric value | The CHECK constraint rejects the value |
| Invalid lifecycle status | The unsupported status is rejected |
| Missing foreign-key parent | The child insert is rejected |
| Restricted parent deletion | The parent remains while dependent records exist |
| Cascading child deletion | Only the intended dependent rows are removed |
| Concurrent duplicate inserts | Only one transaction establishes the unique value |
| Cross-tenant relationship | The composite foreign key rejects the invalid relationship |
Keys and Constraints Best Practices
Recommended Practices
- Identify all candidate keys before selecting a primary key.
- Choose a stable, minimal, non-null primary key.
- Protect alternate business keys with UNIQUE constraints.
- Do not assume that a surrogate key enforces business uniqueness.
- Use composite keys when identity genuinely depends on several attributes.
- Use foreign keys to preserve referential integrity.
- Include tenant scope in keys when uniqueness is tenant-specific.
- Use NOT NULL for genuinely required values.
- Use CHECK constraints for representable row-level rules.
- Use meaningful defaults only when omission has defined semantics.
- Define delete and update actions explicitly.
- Use cascading actions only after impact analysis.
- Enforce important rules in the database as well as the application.
- Test uniqueness under concurrent writes.
- Verify database-specific null, collation, and constraint behaviour.
- Name constraints consistently.
- Clean existing data before applying stricter constraints.
- Deploy schema rules through repeatable migrations.
- Translate violations into safe domain responses.
- Monitor integrity violations and migration failures.
Practice Exercise
Add keys and constraints to the normalized course-platform model from the previous lesson.
Requirements
- Create stable primary keys for learners, courses, chapters, lessons, quizzes, and attempts.
- Make learner email unique within the approved identity scope.
- Make course code unique.
- Require every course to reference a valid category.
- Require every chapter to reference a valid course.
- Require every lesson to reference a valid chapter.
- Prevent duplicate learner-course enrollments.
- Prevent duplicate course-instructor assignments.
- Require completion percentage to remain from 0 through 100.
- Require quiz scores to remain within their valid range.
- Require submission time to be after attempt start time.
- Define safe parent-deletion behaviour.
- Use tenant-aware uniqueness when the platform supports several organizations.
- Test concurrent enrollment attempts.
- Map database violations to safe API errors.
Suggested Enrollment Table
CREATE TABLE enrollments
(
enrollment_id BIGINT PRIMARY KEY,
learner_id BIGINT NOT NULL,
course_id BIGINT NOT NULL,
enrollment_status VARCHAR(30)
NOT NULL
DEFAULT 'active',
completion_percentage DECIMAL(5, 2)
NOT NULL
DEFAULT 0,
enrolled_at TIMESTAMP
NOT NULL
DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP NULL,
CONSTRAINT uq_enrollment_learner_course
UNIQUE
(
learner_id,
course_id
),
CONSTRAINT fk_enrollments_learner
FOREIGN KEY (learner_id)
REFERENCES learners (learner_id)
ON DELETE RESTRICT,
CONSTRAINT fk_enrollments_course
FOREIGN KEY (course_id)
REFERENCES courses (course_id)
ON DELETE RESTRICT,
CONSTRAINT ck_enrollment_status
CHECK
(
enrollment_status IN
(
'active',
'completed',
'cancelled'
)
),
CONSTRAINT ck_completion_percentage
CHECK
(
completion_percentage
BETWEEN 0 AND 100
),
CONSTRAINT ck_enrollment_completion
CHECK
(
completed_at IS NULL
OR completion_percentage = 100
)
);
Constraint Inventory Template
| Business Rule | Constraint | Test |
|---|---|---|
| Each learner has a stable identity | PRIMARY KEY | Attempt duplicate learner ID |
| Course code cannot repeat | UNIQUE and NOT NULL | Attempt duplicate course code |
| Course must have a valid category | FOREIGN KEY and NOT NULL | Reference nonexistent category |
| Learner enrolls once per course | Composite UNIQUE | Attempt duplicate enrollment |
| Completion stays within range | CHECK | Attempt values below 0 and above 100 |
| Enrollment has a default state | DEFAULT and CHECK | Omit status and verify resulting value |
Frequently Asked Questions
What is a database key?
A key is an attribute or attribute set used to identify rows, enforce uniqueness, or connect related tables.
What is a candidate key?
A candidate key is a minimal attribute set that uniquely identifies each row.
What is a primary key?
A primary key is the candidate key selected as the table's main, non-null row identifier.
What is an alternate key?
An alternate key is a candidate key that was not selected as the primary key.
What is a composite key?
A composite key uses two or more columns whose combined values identify or constrain a row.
What is a foreign key?
A foreign key references a candidate key in another or the same table and preserves referential integrity.
What is the difference between PRIMARY KEY and UNIQUE?
A primary key is the table's principal non-null identifier. UNIQUE protects additional candidate keys or business uniqueness rules.
Does a surrogate primary key prevent business duplicates?
No. Business identifiers still need suitable UNIQUE constraints.
What does NOT NULL enforce?
NOT NULL requires a stored value for the column in every row.
What does a CHECK constraint do?
A CHECK constraint rejects rows that do not satisfy its defined Boolean condition.
Should validation exist in both the application and database?
Yes. Application validation provides useful feedback, while database constraints protect integrity across every write path and concurrent operation.
What comes after keys and constraints?
The next topic is ACID, followed by transactions and isolation.
Key Takeaway
Keys provide identity, uniqueness, and relationships. Select a stable candidate key as the primary key, preserve alternate business keys with UNIQUE constraints, and use composite keys when identity or uniqueness genuinely depends on several attributes. Foreign keys preserve referential integrity, while NOT NULL, CHECK, and DEFAULT rules define valid row states. Use application validation for clear feedback and database constraints for final atomic enforcement. Define cascading actions carefully, test concurrent writes, include tenant scope where required, and translate constraint violations into safe domain-level responses.