PostgreSQL
NOT NULL, DEFAULT & CHECK
Lesson 11 introduced all six constraint types. This lesson goes deeper on three of the most frequently used ones — NOT NULL, DEFAULT, and CHECK. These three work together to control exactly what values are allowed into your columns. You will see how to use them individually, how they interact with each other, how to apply them to existing tables, and how they behave with real data from the Dataplexa Store.
NOT NULL — Guaranteeing a Value Always Exists
When you mark a column NOT NULL you are telling PostgreSQL — this piece of information is mandatory. Every row must provide it. The moment an INSERT or UPDATE tries to leave that column empty, PostgreSQL stops the operation immediately with a clear error. This protects you from silent data quality problems that would otherwise only surface later when your application tries to use the missing value.
The decision of whether to mark a column NOT NULL comes down to one question — can this piece of information ever legitimately not exist? A customer's email address? Always required. Their middle name? Truly optional — leave it nullable. Their order date? Always required — an order without a date is meaningless.
-- Applying NOT NULL when creating a table
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
first_name VARCHAR(50) NOT NULL, -- required — every customer has a first name
last_name VARCHAR(50) NOT NULL, -- required
email VARCHAR(100) NOT NULL, -- required — used for login and contact
middle_name VARCHAR(50), -- optional — can be NULL
phone VARCHAR(20) -- optional — not every customer provides this
);
-- What happens when you try to skip a NOT NULL column
INSERT INTO customers (first_name, email)
VALUES ('Alice', 'alice@example.com'); -- last_name is missing — fails
violates not-null constraint
DETAIL: Failing row contains (1, Alice, null, alice@example.com, null, null).
Adding and Removing NOT NULL on Existing Tables
You can add or remove NOT NULL from any column at any time using ALTER TABLE. When adding NOT NULL to a column that already has rows, PostgreSQL checks that no existing rows have a NULL in that column first. If any do, the operation fails — you need to fill those in before enforcing the constraint.
-- Add NOT NULL to an existing column
ALTER TABLE customers ALTER COLUMN phone SET NOT NULL;
-- Remove NOT NULL from a column (make it optional again)
ALTER TABLE customers ALTER COLUMN phone DROP NOT NULL;
-- Safe approach: fill in NULLs first, then add the constraint
UPDATE customers SET phone = 'N/A' WHERE phone IS NULL;
ALTER TABLE customers ALTER COLUMN phone SET NOT NULL;
ALTER TABLE
UPDATE 12
ALTER TABLE
DEFAULT — Smart Fallback Values
A DEFAULT gives a column a fallback value that PostgreSQL uses automatically when no value is provided during an INSERT. This keeps your INSERT statements shorter and ensures every row starts with a sensible value — no manual effort required from the application.
Defaults are not just for simple values like 0 or TRUE. You can use any expression that PostgreSQL can evaluate at insert time — current timestamp, a generated string, or a function call. This makes defaults very powerful for audit columns, status tracking, and anything that should initialise automatically.
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- new orders start as pending
total_amount NUMERIC(10,2) DEFAULT 0.00, -- starts at zero
is_priority BOOLEAN DEFAULT FALSE, -- not priority by default
created_at TIMESTAMPTZ DEFAULT NOW(), -- auto timestamp
notes TEXT DEFAULT '' -- empty string instead of NULL
);
-- Insert without specifying defaults
INSERT INTO orders (customer_id) VALUES (1);
-- See what defaults filled in
SELECT id, customer_id, status, total_amount, is_priority, created_at
FROM orders WHERE customer_id = 1;
id | customer_id | status | total_amount | is_priority | created_at
----+-------------+---------+--------------+-------------+-------------------------------
1 | 1 | pending | 0.00 | f | 2024-06-01 14:22:05.123456+00
Overriding a Default
A default is just a fallback — you can always override it by providing a value explicitly. You can also use the keyword DEFAULT in an INSERT statement to explicitly request the default value, which is useful when you want to be clear in your code that you are intentionally using the default.
-- Override the default status for a rush order
INSERT INTO orders (customer_id, status, is_priority)
VALUES (2, 'processing', TRUE);
-- Use DEFAULT keyword explicitly in INSERT
INSERT INTO orders (customer_id, status, total_amount)
VALUES (3, DEFAULT, 125.50); -- status uses its default 'pending'
SELECT id, customer_id, status, is_priority FROM orders ORDER BY id;
----+-------------+------------+-------------
1 | 1 | pending | f
2 | 2 | processing | t
3 | 3 | pending | f
Changing and Removing a Default
-- Change the default value of an existing column
ALTER TABLE orders ALTER COLUMN status SET DEFAULT 'received';
-- Remove the default entirely — column becomes nullable with no fallback
ALTER TABLE orders ALTER COLUMN status DROP DEFAULT;
-- Verify the change
SELECT column_name, column_default
FROM information_schema.columns
WHERE table_name = 'orders' AND column_name = 'status';
ALTER TABLE
column_name | column_default
-------------+----------------
status |
CHECK — Enforcing Business Rules at the Database Level
A CHECK constraint is a custom validation rule written as a SQL boolean expression. PostgreSQL evaluates it on every INSERT and UPDATE. If the expression returns false for a row, the operation is rejected. This is how you bake business rules directly into the database — so they are enforced no matter which application, script, or user modifies the data.
Any valid SQL boolean expression works in a CHECK — comparisons, ranges, IN lists, LIKE patterns, and even combinations using AND and OR.
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price NUMERIC(10,2) NOT NULL CHECK (price > 0),
stock_qty INTEGER DEFAULT 0 CHECK (stock_qty >= 0),
rating NUMERIC(2,1) CHECK (rating BETWEEN 1.0 AND 5.0),
category VARCHAR(50) CHECK (category IN (
'Electronics', 'Office', 'Furniture', 'Accessories'
)),
discount NUMERIC(5,2) CHECK (discount >= 0 AND discount <= 100)
);
-- Try inserting invalid data
INSERT INTO products (name, price, stock_qty)
VALUES ('Test Item', -10.00, 5);
DETAIL: Failing row contains (1, Test Item, -10.00, 5, null, null, null).
Naming CHECK Constraints
When a CHECK fails, PostgreSQL shows the constraint name in the error message. If your constraint is unnamed, the auto-generated name is cryptic. If you name it yourself, the error message is immediately meaningful — both for debugging and for showing useful messages to your users.
CREATE TABLE employees (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
salary NUMERIC(10,2) NOT NULL,
age INTEGER NOT NULL,
CONSTRAINT salary_must_be_positive CHECK (salary > 0),
CONSTRAINT salary_within_budget CHECK (salary <= 250000),
CONSTRAINT age_must_be_adult CHECK (age >= 18),
CONSTRAINT age_realistic CHECK (age <= 80)
);
-- Try inserting an underage employee
INSERT INTO employees (name, salary, age) VALUES ('Alex', 55000, 16);
DETAIL: Failing row contains (1, Alex, 55000.00, 16).
Much more useful than a cryptic auto-generated constraint name. The error tells you exactly which rule was broken.
CHECK Across Multiple Columns
A CHECK constraint can reference more than one column in the same table. This lets you enforce rules that involve a relationship between columns — for example, the end date of a promotion must always be after the start date.
CREATE TABLE promotions (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
discount_pct NUMERIC(5,2) NOT NULL CHECK (discount_pct > 0 AND discount_pct <= 90),
start_date DATE NOT NULL,
end_date DATE NOT NULL,
CONSTRAINT valid_date_range CHECK (end_date > start_date) -- end must be after start
);
-- Try inserting a promotion where end is before start
INSERT INTO promotions (name, discount_pct, start_date, end_date)
VALUES ('Summer Sale', 20.00, '2024-08-01', '2024-07-01'); -- fails
DETAIL: Failing row contains (1, Summer Sale, 20.00, 2024-08-01, 2024-07-01).
How NOT NULL, DEFAULT, and CHECK Work Together
These three constraints complement each other perfectly. NOT NULL ensures a value is always provided. DEFAULT ensures a sensible value is used when none is given. CHECK ensures the value — whether provided by the user or filled by a default — meets your business rules. Together they form a tight validation layer right at the database level.
-- All three working together on the same column
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL DEFAULT 1 CHECK (quantity > 0), -- must exist, defaults to 1, must be positive
unit_price NUMERIC(10,2) NOT NULL CHECK (unit_price > 0), -- must exist, must be positive
discount NUMERIC(5,2) DEFAULT 0 CHECK (discount >= 0 AND discount <= 100) -- optional, defaults to 0, must be a valid percentage
);
Lesson Summary
| Constraint | Purpose | Key Behaviour |
|---|---|---|
| NOT NULL | Column must always have a value | Rejects INSERT/UPDATE that leaves column empty |
| DEFAULT | Fallback value when none is supplied | Auto-fills column — can use expressions like NOW() |
| CHECK | Custom rule every row must pass | Rejects rows where expression is false |
| Adding to existing table | ALTER TABLE ... ALTER COLUMN / ADD CONSTRAINT | Validates all existing rows before applying |
| Named constraints | Use CONSTRAINT keyword | Clearer errors, easy to drop by name later |
🧪 Practice Questions
Answer based on what you learned in this lesson.
1. What ALTER TABLE syntax removes a NOT NULL constraint from an existing column?
2. Which PostgreSQL function is commonly used as a DEFAULT value to automatically record when a row was created?
3. Which SQL keyword is used inside a CHECK constraint to validate that a value falls within a range — for example a rating from 1 to 5?
4. Which keyword lets you give a custom name to a CHECK constraint?
5. Which keyword can you use inside an INSERT statement to explicitly request that a column uses its defined default value?
🎯 Quiz — Test Your Understanding
Q1. You try to add NOT NULL to a column that already has NULL values in existing rows. What must you do first?
Q2. A column has DEFAULT 'pending'. What happens when you UPDATE that column to NULL?
Q3. You want to ensure that a promotion's end date is always after its start date. Which constraint achieves this?
Q4. What is the main benefit of naming a CHECK constraint using the CONSTRAINT keyword?
Q5. Which combination gives you a column that always has a value, starts with a sensible default, and must pass a business rule?
Next up: Modifying tables that already exist — adding columns, renaming, and restructuring with ALTER TABLE.