- SQLSTATE
- 23503
- Condition name
foreign_key_violation- Product
- PostgreSQL
- Versions
- PostgreSQL 13 to 18 (examples run on PostgreSQL 18.6 in PGlite)
- Category
- Integrity Constraints
- Last reviewed
- 24 October 2026
On this page
What it means
SQLSTATE 23503 means a row would point at a parent row that does not exist, or a parent row that other rows still point at would be deleted or changed, so PostgreSQL rejected the statement to keep the foreign key valid.
A foreign key says that every non-null value in the child columns must exist in the referenced (parent) key. PostgreSQL checks this from both sides and raises 23503 (foreign_key_violation) in two forms. The child side, "insert or update on table ... violates foreign key constraint", means the new or changed child row names a parent that is not there; DETAIL shows the value: Key (customer_id)=(99) is not present in table "customers". The parent side, "update or delete on table ... violates foreign key constraint ... on table ...", means you tried to delete a parent row, or change its key, while child rows still reference it; DETAIL ends with is still referenced from table "orders".
The two forms need opposite fixes, so read the first word of the message: "insert or update" is a missing parent, "update or delete" is a parent with children. The same error appears when ALTER TABLE ... ADD FOREIGN KEY finds existing orphan rows, and at COMMIT when the constraint is DEFERRABLE INITIALLY DEFERRED. Inside a transaction it aborts the transaction, so later statements fail with 25P02 until you ROLLBACK.
Common causes
- Child rows are inserted before their parent rows, for example an import that loads orders before customers, or two inserts run in the wrong order.
- The application sends an id that does not exist: a typo, a stale id from a cache, an id from another environment, or 0 used where NULL was meant.
- A DELETE removes a parent row that still has child rows, and the foreign key uses the default ON DELETE NO ACTION (or RESTRICT).
- An UPDATE changes a referenced primary key value, leaving its child rows pointing at the old value.
- A foreign key is added to a table that already contains orphan rows, so ALTER TABLE ... ADD FOREIGN KEY fails while checking them.
Diagnose it
Read-only These queries only read the system catalogs (pg_catalog, information_schema) and statistics views such as pg_stat_activity and pg_locks. They do not change data, roles or settings, so they are safe to run first.
Which foreign keys point at this table?
Use the table and constraint names from the message. For the parent side form, this lists every child table that can block a delete.
SELECT conname,
conrelid::regclass AS child_table,
confrelid::regclass AS parent_table,
pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE contype = 'f'
AND confrelid = 'customers'::regclass
ORDER BY conname; One row per foreign key, for example FOREIGN KEY (customer_id) REFERENCES customers(id). The definition also shows ON DELETE / ON UPDATE actions and DEFERRABLE when they are set; no action shown means the default NO ACTION.
Does the parent row exist?
SELECT *
FROM customers
WHERE id = 99; -- the value from the DETAIL line No row means the parent really is missing: insert it first or correct the child value. If a row comes back, check that you are connected to the database and schema you expect.
Which child rows still reference the parent?
SELECT id, customer_id
FROM orders
WHERE customer_id = 1; -- the key from "is still referenced" The rows that block the delete. Decide whether they should be deleted, moved to another parent or kept (in which case the parent must stay).
Find orphan rows before adding a foreign key
SELECT o.id, o.customer_id
FROM orders AS o
WHERE o.customer_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM customers AS c WHERE c.id = o.customer_id); Every child row whose parent is missing. ADD FOREIGN KEY fails while any row is returned.
How to fix it
Each fix is labelled with what it changes. Try read-only checks and the least invasive fix first, and test changes outside production.
Insert the parent first, in the same transaction
Changes dataFor the "insert or update" form, create the missing parent before the child, or correct the child value to an existing key. Doing both in one transaction keeps them together if anything fails. When a load cannot be ordered (for example rows that reference each other), make the constraint DEFERRABLE so the check runs at COMMIT instead of after each statement.
BEGIN;
INSERT INTO customers (id, name) VALUES (99, 'Ali');
INSERT INTO orders (id, customer_id, total) VALUES (11, 99, 25);
COMMIT;
Remove or move the child rows before deleting the parent
Changes dataFor the "update or delete" form, decide what the child rows mean once the parent is gone. If they should go too, delete them first in the same transaction; if they belong to another parent, update them to point there.
Caution: This permanently deletes the child rows. Run the read-only query above first, check every child table the first diagnostic lists, and keep a backup or an archive of rows such as orders and invoices that you may be required to retain.
BEGIN;
DELETE FROM orders WHERE customer_id = 1;
DELETE FROM customers WHERE id = 1;
COMMIT;
Make the delete rule part of the design (ON DELETE CASCADE or SET NULL)
Changes schemaWhen child rows have no meaning without their parent (order lines without an order), ON DELETE CASCADE deletes them with it; when the link is optional, ON DELETE SET NULL keeps them and clears the reference (the column must allow NULL, or the delete fails with 23502). This is a data model decision, not a way to silence the error. Replace the constraint in one transaction.
Caution: CASCADE deletes child rows silently, and cascades can chain through several tables. Only use it where losing the children with the parent is always correct.
BEGIN;
ALTER TABLE order_lines DROP CONSTRAINT order_lines_order_id_fkey;
ALTER TABLE order_lines
ADD CONSTRAINT order_lines_order_id_fkey
FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE;
COMMIT;
Clean up orphans, then add the foreign key (NOT VALID first on large tables)
Changes schemaFix or remove the orphan rows found above, then add the constraint. On a large table, add it as NOT VALID (checked for new rows only, quick to add) and run VALIDATE CONSTRAINT later; validation takes only a SHARE UPDATE EXCLUSIVE lock on the table being altered and fails with this error if any orphan remains.
ALTER TABLE orders
ADD CONSTRAINT orders_customer_fk
FOREIGN KEY (customer_id) REFERENCES customers (id) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_customer_fk;
Examples
Inserting an order for a customer that does not exist
CREATE TABLE customers (id int PRIMARY KEY, name text);
CREATE TABLE orders (
id int PRIMARY KEY,
customer_id int REFERENCES customers (id),
total numeric
);
INSERT INTO customers VALUES (1, 'Sara');
INSERT INTO orders VALUES (10, 99, 5);
ERROR: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey" DETAIL: Key (customer_id)=(99) is not present in table "customers".
Reproduced on PGlite 0.5.11 (PostgreSQL 18.6 compiled to WebAssembly), 24 October 2026.
Deleting a customer who still has orders
Changing the parent key with UPDATE customers SET id = 2 WHERE id = 1 gives exactly the same message, because the old key is still referenced.
CREATE TABLE customers (id int PRIMARY KEY, name text);
CREATE TABLE orders (
id int PRIMARY KEY,
customer_id int REFERENCES customers (id),
total numeric
);
INSERT INTO customers VALUES (1, 'Sara');
INSERT INTO orders VALUES (10, 1, 5);
DELETE FROM customers WHERE id = 1;
ERROR: update or delete on table "customers" violates foreign key constraint "orders_customer_id_fkey" on table "orders" DETAIL: Key (id)=(1) is still referenced from table "orders".
Reproduced on PGlite 0.5.11 (PostgreSQL 18.6 compiled to WebAssembly), 24 October 2026.
Adding a foreign key to a table with orphan rows
CREATE TABLE customers (id int PRIMARY KEY, name text);
CREATE TABLE orders (id int, customer_id int);
INSERT INTO orders VALUES (1, 7);
ALTER TABLE orders
ADD CONSTRAINT orders_customer_fk
FOREIGN KEY (customer_id) REFERENCES customers (id);
ERROR: insert or update on table "orders" violates foreign key constraint "orders_customer_fk" DETAIL: Key (customer_id)=(7) is not present in table "customers".
Reproduced on PGlite 0.5.11 (PostgreSQL 18.6 compiled to WebAssembly), 24 October 2026.
A deferred foreign key fails at COMMIT, not at the INSERT
The INSERT succeeds and the error is raised by COMMIT, which then rolls the whole transaction back. Inserting customer 5 anywhere before COMMIT would have made it succeed.
CREATE TABLE customers (id int PRIMARY KEY);
CREATE TABLE orders (
id int,
customer_id int REFERENCES customers DEFERRABLE INITIALLY DEFERRED
);
BEGIN;
INSERT INTO orders VALUES (1, 5); -- accepted for now
COMMIT; -- the check runs here
ERROR: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey" DETAIL: Key (customer_id)=(5) is not present in table "customers".
Reproduced on PGlite 0.5.11 (PostgreSQL 18.6 compiled to WebAssembly), 24 October 2026.
How to prevent it
Load and write data in dependency order: parents before children on insert, children before parents on delete. Decide the ON DELETE rule for every foreign key when you design the table (NO ACTION, CASCADE, SET NULL) instead of discovering it when a delete fails.
Index the referencing columns yourself: PostgreSQL indexes the referenced key but not the child columns, and without an index every parent delete or key change scans the child table. In application code, handle SQLSTATE 23503 and turn it into a clear message such as "this customer still has orders", and never try to work around the constraint by disabling triggers; that leaves orphan rows the database can no longer trust.
Common questions
What does "is not present in table" mean in PostgreSQL?
The child row you inserted or updated refers to a key that the parent table does not contain. Insert the parent row first, or correct the value in the child row; the DETAIL line shows the exact column and value.
How do I delete a row that is "still referenced from table"?
Delete or re-point the child rows first, in the same transaction, or change the foreign key to ON DELETE CASCADE or SET NULL if that matches your data model. The first diagnostic query lists every table that references the parent.
Can a foreign key column be NULL?
Yes. With the default MATCH SIMPLE, a row whose referencing column is NULL is not checked at all. Add NOT NULL to the column if every row must have a parent.
Is it safe to disable the foreign key to load data faster?
No. Disabling the triggers that enforce it (or setting session_replication_role) lets orphan rows in without any later check. Load in dependency order, or add the constraint as NOT VALID and run VALIDATE CONSTRAINT after the load so the existing rows are still verified.
Learn more on CodeWithSQL
Official documentation
- PostgreSQL error codes (Appendix A)
- Constraints: foreign keys
- Tutorial: foreign keys
- ALTER TABLE (NOT VALID and VALIDATE CONSTRAINT)
Last technically reviewed 24 October 2026; examples and diagnostics run on PostgreSQL 18.6 (PGlite 0.5.11). SQL-level examples were run in PGlite (PostgreSQL compiled to WebAssembly); examples that need a running server are shown as documented in the PostgreSQL documentation.