- Result code
- SQLITE_CONSTRAINT (19); extended SQLITE_CONSTRAINT_UNIQUE (2067) or SQLITE_CONSTRAINT_PRIMARYKEY (1555)
- Product
- SQLite library
- Versions
- SQLite 3.x (examples run on 3.39.2; shell output from 3.50.6)
- Category
- Constraint Violations
- Last reviewed
- 24 October 2026
On this page
What it means
This error means a row would store a value that already exists in a column (or set of columns) that must be unique, so SQLite rejected the statement.
A UNIQUE constraint, a unique index or a PRIMARY KEY allows each value only once. When an INSERT or UPDATE would create a second row with the same value, SQLite stops the statement and reports the table and column that rejected it (customers.email). For a key over several columns, every column of the key is listed.
SQLite uses the same message for PRIMARY KEY conflicts; only the extended result code differs (1555 instead of 2067), and most drivers show just the primary code, 19 (SQLITE_CONSTRAINT). A column declared INTEGER PRIMARY KEY is an alias for the table's rowid: leave it out of the INSERT and SQLite assigns the next free value, which avoids most duplicate-id errors. Other constraint failures share code 19 too, such as NOT NULL constraint failed and FOREIGN KEY constraint failed, so check the message, not only the code.
Common causes
- The same row is inserted twice: a retried request, a double-tapped button or an import or migration script that runs again.
- The application supplies ids itself (for example copied from a server database or another device) and one of them is already taken.
- An UPDATE changes a unique column, such as an email address, to a value another row already has.
- CREATE UNIQUE INDEX is run on a column that already contains duplicate values.
Diagnose it
Read-only These statements only read the schema (sqlite_schema and read-only PRAGMA queries) or your own data. They do not change data, settings or the database file, so they are safe to run first.
Which unique indexes does the table have?
UNIQUE and PRIMARY KEY constraints are enforced by indexes; constraints written in CREATE TABLE get automatic names such as sqlite_autoindex_customers_1. A rowid alias (INTEGER PRIMARY KEY) has no separate index.
PRAGMA index_list(customers);
PRAGMA index_info(sqlite_autoindex_customers_1); index_list shows each index with unique = 1 for unique ones and origin u (UNIQUE), pk (PRIMARY KEY) or c (CREATE INDEX). index_info lists the columns of one index.
Find the row that already holds the value
SELECT *
FROM customers
WHERE email = 'sara@example.com'; -- the value you tried to write The existing row. Compare it with the data you were writing to decide whether to skip the new row, update the existing one or reject the request.
Find duplicates before adding a unique index
SELECT name, COUNT(*) AS copies
FROM customers
GROUP BY name
HAVING COUNT(*) > 1
ORDER BY copies DESC; Each value that appears more than once. CREATE UNIQUE INDEX on that column fails until these are resolved.
Read the table definition
SELECT sql
FROM sqlite_schema
WHERE type IN ('table', 'index') AND tbl_name = 'customers'; The CREATE statements for the table and its indexes, including UNIQUE and PRIMARY KEY clauses. Older versions name this table sqlite_master, and every version still accepts that name.
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.
Update the existing row instead (upsert)
Changes dataWhen a matching value should update the row rather than fail, use INSERT ... ON CONFLICT (column) DO UPDATE (available from SQLite 3.24.0). excluded refers to the row you tried to insert. Name the conflict column so only that constraint is handled.
INSERT INTO customers (email, name)
VALUES ('sara@example.com', 'Sara Ahmed')
ON CONFLICT (email) DO UPDATE SET name = excluded.name;
Skip rows whose value already exists
Changes dataFor re-runs and imports, insert only what is missing. ON CONFLICT DO NOTHING skips just the conflicting row and keeps the rest of a multi-row insert.
Caution: INSERT OR REPLACE also avoids the error, but it deletes the existing row and inserts a new one: columns you did not supply are lost, the rowid can change, and ON DELETE actions on child tables run. Use an upsert instead unless you really want the row replaced.
INSERT INTO customers (email, name)
VALUES ('sara@example.com', 'Sara'), ('ali@example.com', 'Ali')
ON CONFLICT (email) DO NOTHING;
Let SQLite assign the key
Changes dataWith an INTEGER PRIMARY KEY column, leave the id out (or pass NULL) and SQLite assigns one. Read it back with last_insert_rowid() or your driver's equivalent, or with RETURNING on SQLite 3.35.0 and later.
INSERT INTO customers (email, name) VALUES ('ali@example.com', 'Ali'); -- no id supplied
SELECT last_insert_rowid();
Resolve existing duplicates, then add the unique index
Changes schemaFind the duplicates with the read-only query above and agree which row to keep (or merge them). Then create the index. Take a copy of the database file first.
CREATE UNIQUE INDEX ux_customers_name ON customers (name);
Examples
Inserting an email address that is already registered
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
email TEXT UNIQUE,
name TEXT
);
INSERT INTO customers (email, name) VALUES ('sara@example.com', 'Sara');
INSERT INTO customers (email, name) VALUES ('sara@example.com', 'Sara B');
UNIQUE constraint failed: customers.email
Reproduced on SQLite 3.39.2, 24 October 2026.
Supplying a primary key value that is taken
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
email TEXT,
name TEXT
);
INSERT INTO customers (id, email, name) VALUES (1, 'sara@example.com', 'Sara');
INSERT INTO customers (id, email, name) VALUES (1, 'ali@example.com', 'Ali'); -- id 1 already exists
UNIQUE constraint failed: customers.id
Reproduced on SQLite 3.39.2, 24 October 2026.
A duplicate in a multi-column key
CREATE TABLE order_lines (
order_id INTEGER,
line_no INTEGER,
PRIMARY KEY (order_id, line_no)
);
INSERT INTO order_lines (order_id, line_no) VALUES (10, 1);
INSERT INTO order_lines (order_id, line_no) VALUES (10, 1);
UNIQUE constraint failed: order_lines.order_id, order_lines.line_no
Reproduced on SQLite 3.39.2, 24 October 2026.
Adding a unique index over existing duplicates
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO customers (name) VALUES ('Sara'), ('Sara');
CREATE UNIQUE INDEX ux_customers_name ON customers (name);
UNIQUE constraint failed: customers.name
Reproduced on SQLite 3.39.2, 24 October 2026.
How to prevent it
Let SQLite assign surrogate keys with INTEGER PRIMARY KEY, and make writes that may run twice idempotent: an upsert for "create or update" actions, ON CONFLICT DO NOTHING for imports. When data comes from several devices or servers, use a natural unique key (such as an email address or a UUID) rather than copying numeric ids between databases.
Keep the unique constraint even when the application checks first, and handle the constraint error in your code (in Python, catch sqlite3.IntegrityError and check the message) so a duplicate becomes a clear message such as "this email is already registered".
Common questions
How do I fix "UNIQUE constraint failed" in SQLite?
Read the table and column from the message, find the row that already holds the value, then skip the new row (ON CONFLICT DO NOTHING), update the existing one (ON CONFLICT DO UPDATE), or stop supplying ids and let INTEGER PRIMARY KEY assign them.
Why does the message say UNIQUE when it is my PRIMARY KEY?
SQLite reports PRIMARY KEY conflicts with the same "UNIQUE constraint failed" text. Only the extended result code tells them apart: 1555 (SQLITE_CONSTRAINT_PRIMARYKEY) for a primary key, 2067 (SQLITE_CONSTRAINT_UNIQUE) for a UNIQUE constraint or index.
Is INSERT OR REPLACE a safe way to avoid the error?
Usually not. REPLACE deletes the conflicting row and inserts a new one, so columns you did not supply become their defaults and delete actions on related tables run. An upsert with ON CONFLICT DO UPDATE changes only the columns you name.
Learn more on CodeWithSQL
Official documentation
- SQLite result and error codes
- UPSERT (INSERT ... ON CONFLICT)
- ON CONFLICT clause (OR REPLACE, OR IGNORE)
- ROWIDs and the INTEGER PRIMARY KEY
Last technically reviewed 24 October 2026; examples and diagnostics run on SQLite 3.39.2 (PHP PDO_SQLITE) and the sqlite3 shell 3.50.6. Messages are the text the SQLite library itself returns (sqlite3_errmsg) for the versions shown.