PostgreSQL Error 42P07: duplicate table
PostgreSQL error code 42P07 (duplicate_table) is thrown when you attempt to create a table using a name that already exists within the same schema. Unlike a runtime data error, this is a DDL-level conflict that PostgreSQL detects immediately at statement execution time, before any data is touched. It most commonly surfaces during repeated migration runs, automated deployments, or application startup routines that lack proper idempotency guards.
Top 3 Causes
1. Running DDL Scripts More Than Once
The most frequent cause. If a migration script is executed twice without any guard clause, PostgreSQL will refuse the second CREATE TABLE call with 42P07.
-- First run: succeeds
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
amount NUMERIC(10, 2) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Second run: throws ERROR 42P07 - relation "orders" already exists
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
amount NUMERIC(10, 2) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Fix: always use IF NOT EXISTS
CREATE TABLE IF NOT EXISTS orders (
id SERIAL PRIMARY KEY,
amount NUMERIC(10, 2) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
2. Schema or search_path Confusion
When search_path is not set explicitly, PostgreSQL resolves unqualified table names against the first schema in the path. This can lead to unexpected collisions when you believe you are creating a table in one schema but are actually hitting another.
-- Check current search_path
SHOW search_path;
-- A table already exists in public schema
-- Developer attempts to create in a different schema but forgets to qualify
CREATE TABLE users (...); -- resolves to public.users → ERROR 42P07
-- Fix: always qualify with the target schema
CREATE SCHEMA IF NOT EXISTS app;
CREATE TABLE IF NOT EXISTS app.users (
id BIGSERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
3. ORM or App Initialization Without Idempotency
Frameworks like SQLAlchemy or Django ORM sometimes execute raw CREATE TABLE statements during startup. Without IF NOT EXISTS, any restart against an existing database triggers 42P07.
-- Check whether a table already exists before creating it
SELECT EXISTS (
SELECT 1
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
AND c.relname = 'sessions'
AND c.relkind = 'r'
) AS table_exists;
-- Conditional creation using a PL/pgSQL DO block
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public' AND c.relname = 'sessions'
) THEN
CREATE TABLE public.sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INTEGER NOT NULL,
expires_at TIMESTAMPTZ NOT NULL
);
RAISE NOTICE 'Table sessions created.';
ELSE
RAISE NOTICE 'Table sessions already exists, skipping.';
END IF;
END;
$$;
Quick Fix Solutions
| Situation | Fix |
|---|---|
| Simple script re-run | Add IF NOT EXISTS to CREATE TABLE
|
| Wrong schema target | Qualify table name with explicit schema |
| Need to recreate cleanly |
DROP TABLE IF EXISTS then CREATE TABLE
|
| Complex migration logic | Use a DO $$ BEGIN ... END; $$ block |
-- Safe drop-and-recreate pattern (staging/temp tables only)
DROP TABLE IF EXISTS staging.import_buffer;
CREATE TABLE staging.import_buffer (
id SERIAL PRIMARY KEY,
payload JSONB,
loaded_at TIMESTAMPTZ DEFAULT NOW()
);
⚠️ Never use
DROP TABLE IF EXISTSon production tables without a full backup and explicit approval.
Prevention Tips
Standardize
IF NOT EXISTSacross all DDL scripts. Make it a non-negotiable team convention enforced via code review checklists and SQL linters likesqlfluffintegrated into your CI pipeline. EveryCREATE TABLE,CREATE INDEX, andCREATE SEQUENCEshould carry this clause.Adopt a proper migration tool. Tools like Flyway or Liquibase maintain an execution history table and guarantee each migration script runs exactly once. This eliminates
42P07at the process level rather than patching individual scripts, and gives you a reliable audit trail and rollback strategy for production DDL changes.
Related Error Codes
-
42P06(duplicate_schema) — Thrown byCREATE SCHEMAwhen the schema already exists. UseCREATE SCHEMA IF NOT EXISTS. -
42710(duplicate_object) — Covers duplicate indexes, sequences, and types. UseCREATE INDEX IF NOT EXISTS(PostgreSQL 9.5+). -
42701(duplicate_column) — Raised byALTER TABLE ... ADD COLUMNwhen the column already exists. UseADD COLUMN IF NOT EXISTS(PostgreSQL 9.6+).
📖 Want a more detailed guide?
Check out the full in-depth version (Korean) on oraerror.com — includes detailed analysis, additional SQL examples, and prevention tips.
Top comments (0)