ERROR: new row for relation "appointments" violates check constraint "appointments_duration_check"
(SQLSTATE 23514) means a value you inserted or updated breaks a CHECK rule on
the table. The constraint name tells you which rule, and the DETAIL line
shows the row that broke it. Send a value that satisfies the rule.
Look the rule up with pg_get_constraintdef, compare it with the failing row,
and correct the data. Change or drop the constraint only if the rule itself is wrong.
The error
INSERT INTO appointments (patient, duration, starts_at, ends_at)
VALUES ('Maria Garcia', 0, '2026-11-10 09:00', '2026-11-10 09:30');
psql:/tmp/q8.sql:11: ERROR: new row for relation "appointments" violates check constraint "appointments_duration_check"
DETAIL: Failing row contains (1, Maria Garcia, 0, 2026-11-10 09:00:00, 2026-11-10 09:30:00).
A rule across two columns fails the same way, under its own name:
INSERT INTO appointments (patient, duration, starts_at, ends_at)
VALUES ('David Chen', 30, '2026-11-10 10:00', '2026-11-10 09:30');
psql:/tmp/q8.sql:14: ERROR: new row for relation "appointments" violates check constraint "appointments_ends_after_start"
DETAIL: Failing row contains (2, David Chen, 30, 2026-11-10 10:00:00, 2026-11-10 09:30:00).
Why it happens
A CHECK constraint is a condition every row must meet. This table has two:
an unnamed column check on duration, which PostgreSQL named
appointments_duration_check, and a named table check that the appointment
ends after it starts. The message names the one that failed; the rule text is in the
catalog:
SELECT conname, pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'appointments'::regclass AND contype = 'c' ORDER BY conname;
conname | pg_get_constraintdef
-------------------------------+-------------------------------------------------
appointments_duration_check | CHECK (((duration >= 5) AND (duration <= 120)))
appointments_ends_after_start | CHECK ((ends_at > starts_at))
(2 rows)
A duration of 0 is outside 5 to 120; an end time before the start time breaks the second
rule. The DETAIL row is in column order, starting with the identity value the
failed insert had already drawn.
The fix
INSERT INTO appointments (patient, duration, starts_at, ends_at)
VALUES ('Maria Garcia', 30, '2026-11-10 09:00', '2026-11-10 09:30');
INSERT 0 1
Updates are checked too: UPDATE appointments SET duration = 500 on that
row failed with the same error and a failing row showing 500. Note one gap: a
CHECK passes when its condition is null, so on a table with
CHECK (n > 0) inserting NULL succeeded while
-1 failed. Add NOT NULL if a value is required. Validate the same
rule in your application to give users a friendly message, and keep the constraint as the
last line of defence. From .NET, Npgsql reports this as a PostgresException
with SqlState 23514 and the name in ConstraintName.
How it was reproduced
A postgres:18 container (PostgreSQL 18.4, Debian build 18.4-1.pgdg13+1) on
Docker 29.9.2, with an appointments table holding a column check on
duration (5 to 120) and a named table check ends_at > starts_at,
and fictional patients. Both bad inserts and the bad update failed as shown; the corrected
insert succeeded. A verbose run printed ERROR: 23514 with a
CONSTRAINT NAME field.
Frequently asked
- How do I see the rule behind a PostgreSQL check constraint?
- Query pg_constraint with pg_get_constraintdef(oid), filtered on conrelid = 'tablename'::regclass and contype = 'c'. Compare the rule with the failing row in the DETAIL line.
- Why does my CHECK constraint allow NULL?
- A CHECK fails only when its condition is false. With NULL the condition is null, not false, so the row passes. Add NOT NULL to the column if the value is required.
- Does a check constraint apply to UPDATE as well as INSERT?
- Yes. An UPDATE that makes a row break the rule fails with the same 23514 error and a DETAIL line showing the new row.
More decoded errors in the Fixes category. For when to
use CHECK constraints in a design, see
constraints in Database Design for Developers.