This error occurs when an INSERT or UPDATE statement attempts to set a NULL value in a column declared with a NOT NULL constraint. PostgreSQL enforces data integrity by preventing null values in columns defined as NOT NULL. To fix this, provide a non-null value for the affected column or adjust the query to include all required fields.
PostgreSQL error 23502 (not_null_violation) is an integrity constraint violation that occurs when you try to insert or update a row with a NULL value in a column that is explicitly defined as NOT NULL. A NOT NULL constraint ensures that a column must always contain a value - empty or undefined values are not permitted. This is a fundamental data integrity rule to prevent incomplete or invalid data in critical fields like IDs, timestamps, or user-provided information. The error occurs at the moment PostgreSQL attempts to execute the INSERT or UPDATE statement, preventing the problematic data from being written to the database. The error message typically includes the table name and the specific column that violated the constraint.
The error message explicitly tells you the column name. Look for output like:
ERROR: 23502: null value in column "user_id" of relation "orders" violates not-null constraint
DETAIL: Failing row contains (null, 100.00, 2024-01-15).In this example, the user_id column in the orders table cannot be NULL.
If your error message is truncated, query the table schema:
SELECT column_name, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'orders'
ORDER BY ordinal_position;Look for columns with is_nullable = 'NO' - these are your NOT NULL columns.
WRONG (missing required column):
INSERT INTO orders (order_total, order_date)
VALUES (99.99, '2024-01-15');
-- Fails if user_id is NOT NULLCORRECT (include all NOT NULL columns):
INSERT INTO orders (user_id, order_total, order_date)
VALUES (42, 99.99, '2024-01-15');If you have multiple rows:
INSERT INTO orders (user_id, order_total, order_date) VALUES
(42, 99.99, '2024-01-15'),
(43, 150.00, '2024-01-15'),
(44, 75.50, '2024-01-15');Make sure every NOT NULL column gets a value in every row.
WRONG (sets NOT NULL column to NULL):
UPDATE orders
SET user_id = NULL
WHERE order_id = 123;
-- Fails because user_id is NOT NULLCORRECT (use a valid value instead):
UPDATE orders
SET user_id = 1 -- system default user
WHERE order_id = 123;Or if you're trying to clear optional data, use a different column:
UPDATE orders
SET notes = NULL, -- notes is nullable
updated_at = NOW()
WHERE order_id = 123;To find which columns ARE nullable:
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'orders' AND is_nullable = 'YES';If the NOT NULL column is a PRIMARY KEY or SERIAL auto-increment:
WRONG (explicitly passing NULL to SERIAL):
INSERT INTO users (id, username, email)
VALUES (NULL, 'alice', '[email protected]');
-- Fails even though SERIAL auto-generates idCORRECT (omit the SERIAL column to auto-generate):
INSERT INTO users (username, email)
VALUES ('alice', '[email protected]');
-- id is auto-generated as next sequence valueIf you need to specify a particular id:
INSERT INTO users (id, username, email)
VALUES (100, 'alice', '[email protected]');
-- Explicitly provide non-NULL idCheck your column definition:
SELECT column_name, data_type, column_default
FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'id';If column_default contains 'nextval', it's SERIAL and should be omitted from INSERT.
Most modern ORMs can validate NOT NULL constraints before executing SQL:
JavaScript/TypeScript (Prisma):
const order = await prisma.orders.create({
data: {
userId: 42, // Required - NOT NULL
orderTotal: 99.99,
orderDate: new Date(),
},
});
// Prisma's type system enforces userId is requiredPython (SQLAlchemy):
order = Order(
user_id=42, # Required
order_total=99.99,
order_date=datetime.now()
)
session.add(order)
session.commit() # Validates before commitRuby (ActiveRecord):
order = Order.new(
user_id: 42, # Required
order_total: 99.99,
order_date: Time.now
)
order.save! # Raises if validation failsAdd application-level validation:
function validateOrder(data: any) {
if (!data.userId) {
throw new Error('user_id is required');
}
if (!data.orderTotal) {
throw new Error('order_total is required');
}
return true;
}
validateOrder(orderData);
const result = await insertOrder(orderData);If a column should always have a value but is sometimes missing, define a DEFAULT:
Add a DEFAULT to an existing column:
ALTER TABLE orders
ALTER COLUMN status SET DEFAULT 'pending';Define DEFAULT when creating a table:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
order_total DECIMAL(10, 2) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);Now INSERT without specifying status or created_at:
INSERT INTO orders (user_id, order_total)
VALUES (42, 99.99);
-- status defaults to 'pending'
-- created_at defaults to current timestampWhen copying data from other tables, NULL values may propagate:
WRONG (NULL from JOIN):
INSERT INTO archive (user_id, amount)
SELECT u.id, o.amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'inactive';
-- Fails if u.id is NULL (unlikely) or o.amount is NULLCORRECT (use COALESCE to provide defaults):
INSERT INTO archive (user_id, amount)
SELECT u.id, COALESCE(o.amount, 0)
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'inactive'
AND u.id IS NOT NULL;Or filter out rows with missing required data:
INSERT INTO archive (user_id, amount)
SELECT u.id, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.status = 'inactive'
AND o.amount IS NOT NULL;COALESCE returns the first non-NULL value:
COALESCE(column, default_value)
COALESCE(column1, column2, 'fallback')If you're trying to add a NOT NULL constraint to an existing column that contains NULLs:
WRONG (fails immediately):
ALTER TABLE orders
ALTER COLUMN user_id SET NOT NULL;
-- Fails if any rows have user_id = NULLCORRECT (update NULLs first):
-- Step 1: Check how many NULLs exist
SELECT COUNT(*) FROM orders WHERE user_id IS NULL;
-- Step 2: Update NULLs to a valid value
UPDATE orders
SET user_id = 1 -- default/fallback user id
WHERE user_id IS NULL;
-- Step 3: Verify all rows now have values
SELECT COUNT(*) FROM orders WHERE user_id IS NULL;
-- Step 4: Now add the constraint
ALTER TABLE orders
ALTER COLUMN user_id SET NOT NULL;If you're adding a new NOT NULL column to an existing table:
-- Add column with temporary DEFAULT
ALTER TABLE orders
ADD COLUMN status VARCHAR(50) NOT NULL DEFAULT 'pending';
-- PostgreSQL applies DEFAULT to existing rows
-- No need for UPDATE if DEFAULT is sufficient
-- Later, you can remove DEFAULT if desired
ALTER TABLE orders
ALTER COLUMN status DROP DEFAULT;Understanding NOT NULL Constraints:
NOT NULL is one of PostgreSQL's most basic integrity constraints. Unlike most constraints, it applies at the row level for every INSERT/UPDATE, making it a first-line defense against incomplete data.
Constraint vs Default:
A NOT NULL constraint (enforces rule) is different from a DEFAULT (provides automatic value):
-- NOT NULL alone: value REQUIRED
CREATE TABLE users (
id INT NOT NULL
);
-- INSERT requires explicit value for id
-- NOT NULL with DEFAULT: value optional (uses default if omitted)
CREATE TABLE users (
id SERIAL NOT NULL DEFAULT nextval('users_id_seq'),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- INSERT can omit both; defaults are used
-- Nullable with DEFAULT: value optional
CREATE TABLE users (
deleted_at TIMESTAMP DEFAULT NULL
);
-- INSERT can omit; NULL becomes defaultComposite Keys and NOT NULL:
All columns in a composite PRIMARY KEY are implicitly NOT NULL:
CREATE TABLE order_items (
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id)
);
-- All three columns are requiredPerformance Note:
NOT NULL constraints have no performance penalty. In fact, they enable better query optimization because PostgreSQL knows the column always has a value. Nullable columns require extra checking in some operations.
Common NOT NULL Pattern:
Most production schemas use this pattern:
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
amount DECIMAL(10, 2) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
notes TEXT, -- nullable for optional data
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);Tools to Check Constraints:
-- List all constraints on a table
SELECT constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_name = 'orders';
-- Check specific column nullability
SELECT column_name, is_nullable
FROM information_schema.columns
WHERE table_name = 'orders'
ORDER BY ordinal_position;Related Errors:
- 23503 (foreign_key_violation): Referenced row doesn't exist
- 23505 (unique_violation): Duplicate value in unique column
- 23514 (check_violation): Value fails CHECK constraint
insufficient columns in unique constraint for partition key
How to fix "insufficient columns in unique constraint for partition key" in PostgreSQL
ERROR 42501: must be owner of table
How to fix "must be owner of table" in PostgreSQL
trigger cannot change partition destination
How to fix "Trigger cannot change partition destination" in PostgreSQL
SSL error: certificate does not match host name
SSL error: certificate does not match host name in PostgreSQL
No SSL connection
No SSL connection to PostgreSQL