PostgreSQL Error 25007: Schema and Data Statement Mixing Not Supported
PostgreSQL error code 25007 is raised when DDL (schema) statements and DML (data) statements are mixed within the same transaction in contexts where this combination is explicitly forbidden. This restriction is most commonly enforced by logical replication decoders, Foreign Data Wrappers (FDW), and certain third-party extensions that cannot safely process schema changes and data changes as a single atomic unit. Understanding why PostgreSQL blocks this pattern is key to writing reliable, replication-safe database code.
Top 3 Causes
1. Logical Replication Active on the Server
When logical replication slots (e.g., using pgoutput or wal2json) are active, the WAL decoder must handle schema events and data events separately. Mixing them in one transaction causes an internal conflict in the decoder pipeline.
-- ❌ Triggers error 25007 when logical replication is active
BEGIN;
CREATE TABLE events_log (
id SERIAL PRIMARY KEY,
event_name TEXT,
occurred_at TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO events_log (event_name) VALUES ('app_start');
COMMIT;
-- ✅ Correct: split into two separate transactions
BEGIN;
CREATE TABLE events_log (
id SERIAL PRIMARY KEY,
event_name TEXT,
occurred_at TIMESTAMPTZ DEFAULT NOW()
);
COMMIT;
BEGIN;
INSERT INTO events_log (event_name) VALUES ('app_start');
COMMIT;
2. Foreign Data Wrapper (FDW) Constraints
Extensions like postgres_fdw manage their own internal transaction boundaries with remote servers. Altering a foreign table's options (a schema-level operation) and then querying it within the same transaction creates a state mismatch between the local and remote transaction contexts.
-- ❌ Mixing schema change and data access on a foreign table
BEGIN;
ALTER FOREIGN TABLE remote_orders OPTIONS (SET fetch_size '200');
SELECT COUNT(*) FROM remote_orders WHERE status = 'pending'; -- error here
COMMIT;
-- ✅ Correct approach
BEGIN;
ALTER FOREIGN TABLE remote_orders OPTIONS (SET fetch_size '200');
COMMIT;
BEGIN;
SELECT COUNT(*) FROM remote_orders WHERE status = 'pending';
COMMIT;
3. Dynamic DDL Followed by DML Inside Stored Procedures
Using EXECUTE to dynamically create a table and immediately manipulating its data in the same transaction block can trigger this error when replication or certain extensions are enabled.
-- ❌ Problematic pattern inside a procedure
CREATE OR REPLACE PROCEDURE init_daily_partition()
LANGUAGE plpgsql AS $$
BEGIN
EXECUTE format(
'CREATE TABLE IF NOT EXISTS metrics_%s (val NUMERIC, ts TIMESTAMPTZ)',
TO_CHAR(CURRENT_DATE, 'YYYYMMDD')
);
-- Error 25007 may fire here in replication environments
EXECUTE format(
'INSERT INTO metrics_%s VALUES (0, NOW())',
TO_CHAR(CURRENT_DATE, 'YYYYMMDD')
);
END;
$$;
-- ✅ Fix: use explicit COMMIT between DDL and DML (PostgreSQL 11+)
CREATE OR REPLACE PROCEDURE init_daily_partition()
LANGUAGE plpgsql AS $$
BEGIN
EXECUTE format(
'CREATE TABLE IF NOT EXISTS metrics_%s (val NUMERIC, ts TIMESTAMPTZ)',
TO_CHAR(CURRENT_DATE, 'YYYYMMDD')
);
COMMIT; -- end DDL transaction
EXECUTE format(
'INSERT INTO metrics_%s VALUES (0, NOW())',
TO_CHAR(CURRENT_DATE, 'YYYYMMDD')
);
COMMIT; -- end DML transaction
END;
$$;
CALL init_daily_partition();
Quick Fix Summary
| Scenario | Fix |
|---|---|
| Logical replication active | Split DDL and DML into separate BEGIN/COMMIT blocks |
| FDW schema changes | Commit schema change first, then open new transaction for data access |
| Stored procedure DDL+DML | Use explicit COMMIT between DDL and DML inside the procedure body |
Prevention Tips
Enforce DDL/DML separation in migration tooling. If you use tools like Flyway or Liquibase, adopt a policy of placing DDL changes and DML changes in separate migration files or versioned scripts. Add a SQL linter (e.g., sqlfluff) to your CI pipeline to catch mixed-transaction patterns before they reach production.
Mirror your replication and FDW setup in staging. Always test schema migration scripts in an environment that mirrors your production replication slots and FDW configuration. Many developers only discover error 25007 in production because their local development environment lacks active replication slots. Run SELECT * FROM pg_replication_slots; to verify slot status before executing complex migration scripts.
-- Handy pre-migration check
SELECT slot_name, plugin, active
FROM pg_replication_slots;
SELECT current_setting('wal_level') AS wal_level;
Related Errors
-
25001 (
active_sql_transaction): Triggered when certain DDL statements (e.g.,CREATE DATABASE) are run inside an active transaction block. -
25P01 (
no_active_sql_transaction): Fired whenCOMMITorROLLBACKis called outside a transaction, often seen alongside 25007 when transaction boundaries are mismanaged. -
0A000 (
feature_not_supported): Some FDW implementations raise this instead of 25007 when schema/data mixing is attempted.
📖 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)