2026년 08월 28일 | DBMS Error 가이드
이 글에서 다루는 내용
25007 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
25007 schema and data statement mixing not supported 는?
PostgreSQL 에러 코드 25007은 하나의 트랜잭션 블록 안에서 스키마 변경 구문(DDL, Data Definition Language) 과 데이터 조작 구문(DML, Data Manipulation Language) 을 혼합하여 사용할 수 없는 특정 컨텍스트에서 발생합니다. 주로 PL/pgSQL 함수, 프로시저, 또는 특정 트랜잭션 격리 수준 환경에서 DDL과 DML을 동일한 실행 단위 안에 함께 배치했을 때 이 에러가 트리거됩니다. 이 에러는 PostgreSQL이 내부적으로 트랜잭션의 일관성과 MVCC(Multi-Version Concurrency Control) 정합성을 보호하기 위해 발생시키는 안전 장치입니다.
주요 발생 원인
1. 읽기 전용 트랜잭션(Read-Only Transaction) 내에서 DDL 혼합 시도
읽기 전용 트랜잭션(SET TRANSACTION READ ONLY)으로 설정된 블록 안에서 CREATE, ALTER, DROP 같은 DDL 구문을 실행하려 할 때 이 에러가 발생합니다. PostgreSQL은 읽기 전용 트랜잭션에서는 데이터베이스 객체의 구조적 변경을 허용하지 않으며, 이를 위반하면 25007 에러를 반환합니다. 이 상황은 특히 리포트용 세션이나 슬레이브(Replica) 연결에서 실수로 DDL을 실행하려 할 때 자주 목격됩니다.
2. 파이프라인(Pipeline) 또는 비동기 쿼리 실행 환경에서의 혼합 사용
libpq의 파이프라인 모드나 일부 ORM(Object-Relational Mapper)이 내부적으로 사용하는 비동기 쿼리 실행 환경에서, DDL과 DML을 단일 파이프라인에 혼합하면 이 에러가 발생할 수 있습니다. 파이프라인 모드에서 PostgreSQL은 구문의 종류가 혼합될 경우 처리 순서와 트랜잭션 경계를 보장할 수 없어 에러를 반환합니다. 이는 애플리케이션 레이어에서 마이그레이션 스크립트를 자동화할 때 특히 주의해야 하는 상황입니다.
3. PL/pgSQL 또는 함수 내부에서 DDL과 DML의 순서 및 컨텍스트 충돌
PL/pgSQL 함수나 DO 블록 내에서 DDL(예: CREATE TABLE, ALTER TABLE)을 실행한 직후, 동일한 블록에서 해당 객체에 대한 DML(예: INSERT, SELECT)을 수행하려 할 때 특정 조건에서 에러가 발생합니다. PostgreSQL의 플래너(Planner)는 함수 실행 계획을 미리 수립하는데, 런타임에 새로 생성된 객체를 동일 트랜잭션 컨텍스트에서 즉시 참조할 수 없는 경우 이 에러가 나타납니다. 이는 동적 DDL 생성 패턴을 사용하는 멀티테넌트 아키텍처나 파티셔닝 자동화 스크립트에서 자주 마주치는 문제입니다.
해결 방법
원인 1 해결: 트랜잭션 모드 확인 및 분리
먼저 트랜잭션이 읽기 전용으로 설정되어 있는지 확인하고, DDL과 DML을 별도의 트랜잭션으로 분리하세요.
-- 잘못된 예시: 읽기 전용 트랜잭션에서 DDL 시도
BEGIN;
SET TRANSACTION READ ONLY;
CREATE TABLE test_table (id SERIAL PRIMARY KEY, name TEXT); -- ERROR 25007 발생
SELECT * FROM some_table;
COMMIT;
-- 올바른 예시: DDL은 별도의 일반 트랜잭션에서 수행
BEGIN;
CREATE TABLE test_table (id SERIAL PRIMARY KEY, name TEXT);
COMMIT;
-- 이후 읽기 전용 트랜잭션에서 DML만 수행
BEGIN;
SET TRANSACTION READ ONLY;
SELECT * FROM test_table;
COMMIT;
현재 트랜잭션 상태를 확인하는 방법:
-- 현재 세션의 트랜잭션 읽기/쓰기 상태 확인
SHOW transaction_read_only;
-- 또는 pg_stat_activity를 통해 현재 세션 상태 확인
SELECT pid, usename, state, query
FROM pg_stat_activity
WHERE pid = pg_backend_pid();
원인 2 해결: 파이프라인 모드에서 DDL과 DML 실행 단계 분리
파이프라인 환경에서는 DDL 배치와 DML 배치를 완전히 분리하여 순차 실행하도록 구성하세요.
-- 마이그레이션 스크립트 작성 시 DDL 전용 트랜잭션을 먼저 완료
-- Step 1: DDL 전용 트랜잭션
BEGIN;
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_id INT NOT NULL,
order_date TIMESTAMPTZ DEFAULT NOW(),
status TEXT DEFAULT 'pending'
);
CREATE INDEX idx_orders_customer ON orders(customer_id);
COMMIT;
-- Step 2: DDL 트랜잭션이 완전히 COMMIT된 이후에만 DML 실행
BEGIN;
INSERT INTO orders (customer_id, status) VALUES (101, 'confirmed');
INSERT INTO orders (customer_id, status) VALUES (102, 'pending');
COMMIT;
원인 3 해결: PL/pgSQL에서 EXECUTE를 활용한 동적 DDL 처리
PL/pgSQL 내부에서 DDL을 실행한 후 즉시 해당 객체를 사용해야 한다면, EXECUTE를 사용한 동적 SQL과 함께 올바른 구조로 작성하세요.
-- 잘못된 예시: 동일 블록에서 DDL 후 즉시 정적 DML 참조
DO $$
BEGIN
CREATE TABLE temp_report (
id SERIAL,
data TEXT
);
-- 이 방식은 플래너가 temp_report를 사전에 인식하지 못해 문제 발생 가능
INSERT INTO temp_report (data) VALUES ('sample');
END;
$$;
-- 올바른 예시: EXECUTE를 이용한 동적 DML 실행으로 플래너 우회
DO $$
BEGIN
-- DDL 실행
EXECUTE 'CREATE TABLE IF NOT EXISTS temp_report (id SERIAL, data TEXT)';
-- EXECUTE로 DML을 동적으로 실행하여 플래너 캐시 문제 회피
EXECUTE 'INSERT INTO temp_report (data) VALUES ($1)' USING 'sample_data';
-- 데이터 확인도 동적으로
EXECUTE 'SELECT COUNT(*) FROM temp_report';
END;
$$;
-- 실무에서 자주 사용하는 파티션 자동 생성 + 데이터 이동 패턴
DO $$
DECLARE
v_partition_name TEXT;
v_start_date DATE := '2024-01-01';
v_end_date DATE := '2024-02-01';
BEGIN
v_partition_name := 'orders_2024_01';
-- 파티션 테이블 생성 (DDL)
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF orders
FOR VALUES FROM (%L) TO (%L)',
v_partition_name, v_start_date, v_end_date
);
-- 생성 후 데이터 삽입은 EXECUTE로 수행 (DML)
EXECUTE format(
'INSERT INTO %I (customer_id, status) VALUES ($1, $2)',
v_partition_name
) USING 201, 'active';
RAISE NOTICE '파티션 % 생성 및 데이터 삽입 완료', v_partition_name;
END;
$$;
예방 방법
1. DDL과 DML을 명확히 분리하는 마이그레이션 전략 수립
실무에서는 DDL 전용 마이그레이션 스크립트와 DML 전용 데이터 스크립트를 파일 레벨에서부터 분리하는 관례를 정착시키는 것이 가장 효과적인 예방책입니다. Flyway, Liquibase와 같은 마이그레이션 도구를 사용할 경우에도 V1__create_schema.sql(DDL), V2__seed_data.sql(DML)처럼 마이그레이션 파일을 명확하게 역할별로 구분하세요. 이렇게 하면 에러 발생 가능성을 원천적으로 줄이고, 롤백 시에도 영향 범위를 최소화할 수 있습니다.
-- 마이그레이션 관리 테이블 예시 (자체 관리 시)
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
type TEXT CHECK (type IN ('DDL', 'DML')),
applied_at TIMESTAMPTZ DEFAULT NOW(),
description TEXT
);
-- DDL 마이그레이션 기록
INSERT INTO schema_migrations (version, type, description)
VALUES ('20240101001', 'DDL', 'Create orders table');
-- DML 마이그레이션 기록 (DDL 완료 후 별도 트랜잭션)
INSERT INTO schema_migrations (version, type, description)
VALUES ('20240101002', 'DML', 'Seed initial order data');
2. 트랜잭션 시작 전 세션 설정 검증 루틴 추가
애플리케이션에서 DB 커넥션을 획득한 직후, 해당 세션이 읽기 전용 모드인지 혹은 DDL이 허용된 상태인지를 검증하는 루틴을 반드시 추가하세요. 특히 커넥션 풀(PgBouncer, pgpool-II 등)을 사용하는 환경에서는 이전 세션의 설정이 잔류할 수 있으므로, 커넥션 체크아웃 시 항상 RESET ALL 또는 명시적인 모드 설정을 수행하는 것이 좋습니다.
-- 커넥션 획득 후 상태 검증 쿼리 예시
SELECT
current_setting('transaction_read_only') AS is_read_only,
current_setting('default_transaction_read_only') AS default_read_only,
pg_is_in_recovery() AS is_replica;
-- 필요 시 세션 리셋
RESET ALL;
SET default_transaction_read_only = OFF;
관련 에러
- 25001 (active_sql_transaction): 이미 활성화된 트랜잭션 내에서 트랜잭션 설정 변경 시 발생하며, 25007과 함께 트랜잭션 제어 오류 계열에 속합니다.
- 25006 (read_only_sql_transaction): 읽기 전용 트랜잭션에서 쓰기 작업(DML)을 시도할 때 발생하는 에러로, 25007과 매우 유사한 맥락에서 나타납니다. 특히 스트리밍 복제 리플리카에 잘못 연결된 경우 빈번하게 발생합니다.
- 0A000 (feature_not_supported): 특정 컨텍스트에서 지원되지 않는 기능을 실행할 때 발생하며, PL/pgSQL 내 DDL 처리 제한과 관련하여 25007과 혼동되기도 합니다.
- 55006 (object_in_use): DDL로 생성한 객체를 다른 세션이 동시에 참조 중일 때 발생하며, DDL/DML 혼합 시나리오에서 함께 마주칠 수 있습니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.