Skip to content
ThemesIonic — home
Troubleshooting

WordPress Database Error Duplicate Entry … for Key 'PRIMARY': The ID Column Lost AUTO_INCREMENT

When the duplicate value is 0, the table's ID column is no longer AUTO_INCREMENT, usually after a migration. Every insert after the first one fails, and the WordPress database update tool will not put the attribute back.

6 min read advanced

If the log says Duplicate entry '0' for key 'PRIMARY', the table's ID column has lost its AUTO_INCREMENT attribute, almost always during a migration or import. MySQL can no longer number new rows, so every insert after the first tries to reuse ID 0 and fails. The fix is to restore AUTO_INCREMENT on that column, after moving the stray row with ID 0 out of the way.

Read the whole log line

WordPress formats failed queries in wpdb::print_error() as WordPress database error %1$s for query %2$s made by %3$s and writes them to the PHP error log, or to wp-content/debug.log with debug logging on. A typical entry:

WordPress database error Duplicate entry '0' for key 'PRIMARY' for query
INSERT INTO `wp_posts` (`post_author`, `post_date`, …) VALUES (1, '2026-11-10 09:14:02', …)
made by …, wp_insert_post

The "made by" list is the call stack with wpdb's own methods left out, so its last entry is the function that ran the query.

Three parts matter:

Part What it tells you
The value in quotes ('0') 0 means a lost AUTO_INCREMENT; any other number means an explicit ID was inserted twice
The key name PRIMARY is the ID column. MySQL 8.0.19 and later print it as 'wp_posts.PRIMARY', which also names the table
The table in the query Which table to fix — wp_posts, wp_postmeta, wp_options, wp_comments or a plugin table

A different key name, such as 'option_name' on wp_options, is a separate issue: two requests adding the same option at the same moment. It is noisy but usually harmless.

Why the value is 0

Every core table with a numeric ID, which is all of them except the wp_term_relationships join table, defines it as bigint(20) unsigned NOT NULL auto_increment with a PRIMARY KEY on it. WordPress never supplies an ID when it creates a post, comment, meta row or option; it relies on MySQL to assign the next number.

Remove auto_increment from the column and the insert still runs, but the column takes its default value, 0. The first insert succeeds and creates a row with ID 0. The second one collides with it. That is why the symptoms look random: "I could publish one post, then nothing", "new orders fail", "settings will not save" — each depends on which table lost the attribute.

The usual causes:

  • An export that dropped table attributes. Some tools and hand-edited dumps write CREATE TABLE without AUTO_INCREMENT, or the import is done into tables created by a different tool.
  • Copying table structure by hand in a database GUI that does not carry the attribute over.
  • A partial import that recreated tables from a stripped schema, the same failure that produces Table doesn't exist errors when the tables are missing entirely.

Confirm it

Check the column definition, not the data:

SHOW CREATE TABLE wp_posts\G

A healthy table shows `ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT and, near the end, AUTO_INCREMENT= followed by the next ID. A broken one shows `ID` bigint(20) unsigned NOT NULL with nothing after it. Check every core table, since an import that broke one usually broke all of them:

SELECT TABLE_NAME, COLUMN_NAME, EXTRA
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
  AND COLUMN_KEY = 'PRI'
  AND TABLE_NAME LIKE 'wp\_%';

Any primary-key ID column with an empty EXTRA is missing auto_increment. wp_term_relationships will always show two rows with an empty EXTRA: its key is the pair object_id, term_taxonomy_id, which never had the attribute. Replace wp\_ with your own $table_prefix from wp-config.php.

Fix it

Take a database backup first. Then, for each affected table:

  1. Find the row with ID 0. SELECT * FROM wp_posts WHERE ID = 0; It is a real post, order or meta row someone created after the migration, so keep it.
  2. Give it a free ID. UPDATE wp_posts SET ID = (SELECT m FROM (SELECT MAX(ID) + 1 AS m FROM wp_posts) t) WHERE ID = 0; For posts, also update the rows that point to it: wp_postmeta.post_id, wp_term_relationships.object_id and wp_comments.comment_post_ID from 0 to the new ID. For meta and options tables, nothing points to the row's own ID.
  3. Restore the attribute:
ALTER TABLE wp_posts
  MODIFY ID bigint(20) unsigned NOT NULL AUTO_INCREMENT;

The column names differ by table: meta_id for the meta tables, option_id for wp_options, comment_ID for wp_comments, term_id for wp_terms, umeta_id for wp_usermeta. Use the definition from SHOW CREATE TABLE, changing only the AUTO_INCREMENT part.

Step 2 has to come first. If a row with ID 0 is still there, MySQL tries to renumber it while adding the attribute. That either fails with "ALTER TABLE causes auto_increment resequencing, resulting in duplicate entry", or gives the row a new ID without updating the meta and term rows that still point to 0.

The trap: the update and repair tools will not fix it

It seems natural to run the WordPress database upgrade or the repair screen. Neither helps. The repair screen enabled by WP_ALLOW_REPAIR runs CHECK TABLE and REPAIR TABLE, which fix corruption in tables that exist; the table here is not corrupt. The schema routine dbDelta() compares each column's type and default with WordPress's schema. A missing AUTO_INCREMENT changes neither, so bigint(20) unsigned matches and the table passes as healthy. Repairing the database is the right step for other problems, not this one.

Plugin tables have the same blind spot. Reactivating a plugin runs its own dbDelta() call, which also leaves the column alone. Plugin tables need the same manual ALTER.

When the value is not 0

Duplicate entry '4512' for key 'PRIMARY' means the insert named an ID explicitly. WordPress core does not do that for new content, so the query and the "made by" stack point at the code that did: an importer replaying a file into a site that already has those IDs, a sync job running twice in parallel, or a staging-to-live push merging databases. Stop the job, decide which copy of the data wins, and run it again in a mode that skips or updates existing IDs. A staging site is the place to rehearse that run.

Common mistakes

  • Deleting the row with ID 0 instead of renumbering it. It is real content created after the migration.
  • Fixing wp_posts and leaving wp_postmeta broken, so posts save but their custom fields do not.
  • Running the ALTER without checking for a 0 row first.
  • Importing the same broken dump again on the next migration. Fix the export settings, or the migration will bring the problem back.

Verify

  • The information_schema query shows auto_increment for every primary ID column.
  • Creating two drafts in a row works, and they get consecutive IDs above the old maximum.
  • debug.log shows no new Duplicate entry lines while you save posts, settings and, on a store, a test order.

Frequently asked

Without AUTO_INCREMENT, an insert that does not name an ID gets the column default, which is 0. The first insert creates a row with ID 0 and succeeds. Every later insert also tries ID 0 and fails with Duplicate entry '0' for key 'PRIMARY'.
No. The repair screen runs CHECK TABLE and REPAIR TABLE, which fix corruption, not column definitions. The schema upgrade routine compares column types and defaults, and AUTO_INCREMENT is neither, so a table without it passes unchanged.
Usually not. A real ID such as 4512 means something is inserting rows with explicit IDs, typically an import or sync tool replaying data that already exists, or two copies of the same import running at once. Read the query in the log line to see which code supplied the ID.

Related guides