PostgreSQL 38001 오류 원인과 해결 방법 완벽 가이드

38001
2026년 06월 29일 | DBMS Error 가이드

이 글에서 다루는 내용

38001 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.

38001 containing sql not permitted 는?

PostgreSQL 에러 코드 38001 (containing_sql_not_permitted)은 SQL을 포함할 수 없는 컨텍스트에서 SQL 문을 실행하려고 할 때 발생하는 에러입니다. 주로 PL/pgSQL, PL/Python, PL/Perl 등의 프로시저 언어로 작성된 함수나 트리거에서, 해당 함수가 선언된 속성(예: NO SQL 또는 CONTAINS SQL)과 실제 함수 내부의 SQL 실행이 충돌할 때 나타납니다. 특히 외부 언어(External Language) 함수나 SQL/PSM 표준을 엄격히 따르는 환경에서 자주 발생하며, 함수의 SQL 데이터 접근 수준(SQL Data Access Level)을 잘못 설정했을 때 트리거됩니다.


주요 발생 원인

  • 함수의 SQL 데이터 접근 속성을 NO SQL로 선언한 후 내부에서 SQL 실행

PostgreSQL 함수를 정의할 때 NO SQL 속성을 명시하면, 해당 함수는 어떠한 SQL 문도 포함하지 않겠다는 계약을 맺는 것입니다. 그런데 실제 함수 본문에서 SELECT, INSERT, UPDATE, DELETE 등의 SQL 문을 실행하려 하면 PostgreSQL은 이 계약 위반을 감지하고 38001 에러를 발생시킵니다. 이는 특히 함수를 처음 작성할 때 속성을 복사·붙여넣기 하거나 템플릿을 잘못 사용했을 때 빈번하게 발생합니다.

“`sql

— 잘못된 예: NO SQL로 선언했지만 내부에서 SQL 실행

CREATE FUNCTION bad_function()

RETURNS INTEGER

LANGUAGE plpgsql

NO SQL — 이 선언이 문제의 원인

AS $$

DECLARE

result INTEGER;

BEGIN

SELECT COUNT(*) INTO result FROM users; — 38001 에러 발생!

RETURN result;

END;

$$;

“`

  • PL/Python, PL/Perl 등 외부 언어 함수에서 잘못된 SQL 접근 레벨 설정

PostgreSQL은 외부 언어(untrusted language)로 작성된 함수에 대해 SQL 접근 수준을 엄격하게 검사합니다. PL/Python (plpythonu) 또는 PL/Perl (plperlu) 함수 내부에서 데이터베이스 커서나 plpy.execute() 등을 통해 SQL을 실행하려는데, 함수 선언 시 NO SQL 또는 그와 동등한 제약이 설정되어 있으면 이 에러가 발생합니다. 외부 언어 함수는 신뢰성 문제로 인해 SQL 접근 속성에 더욱 엄격한 검증이 적용됩니다.

“`sql

— 잘못된 예: PL/Python 함수에서 NO SQL 선언 후 SQL 실행 시도

CREATE FUNCTION py_bad_function()

RETURNS TEXT

LANGUAGE plpythonu

NO SQL

AS $$

result = plpy.execute(“SELECT version()”) — 38001 에러 발생!

return result[0][‘version’]

$$;

“`

  • 트리거 함수 또는 이벤트 트리거에서 SQL 데이터 접근 레벨 부적절 설정

트리거 함수(Trigger Function)는 특정 테이블 이벤트(INSERT, UPDATE, DELETE)에 반응하여 자동으로 실행됩니다. 트리거 함수 내부에서 다른 테이블을 조회하거나 수정하는 SQL을 실행해야 하는데, 해당 함수가 NO SQL이나 부적절한 SQL 접근 레벨로 선언된 경우 38001 에러가 발생합니다. 이 경우는 특히 레거시 코드 마이그레이션이나 다른 데이터베이스 시스템에서 PostgreSQL로 전환할 때 자주 나타납니다.

“`sql

— 잘못된 예: 트리거 함수에 부적절한 SQL 접근 레벨 설정

CREATE FUNCTION audit_trigger_func()

RETURNS TRIGGER

LANGUAGE plpgsql

NO SQL — 트리거 함수에서 NO SQL은 부적절

AS $$

BEGIN

INSERT INTO audit_log(table_name, action, changed_at)

VALUES (TG_TABLE_NAME, TG_OP, NOW()); — 38001 에러 발생!

RETURN NEW;

END;

$$;

“`


해결 방법

원인 1 해결: 함수의 SQL 접근 레벨을 올바르게 수정

NO SQL 선언을 제거하거나, 실제 함수 동작에 맞는 적절한 SQL 접근 레벨로 변경해야 합니다. PostgreSQL에서 사용할 수 있는 SQL 데이터 접근 레벨은 다음과 같습니다:

  • NO SQL: SQL 문을 전혀 포함하지 않음
  • CONTAINS SQL: SQL 문을 포함하지만 데이터를 읽거나 쓰지 않음
  • READS SQL DATA: SQL 문으로 데이터를 읽기만 함
  • MODIFIES SQL DATA: SQL 문으로 데이터를 읽고 쓸 수 있음
-- 올바른 예: SQL을 읽기만 하는 함수
CREATE OR REPLACE FUNCTION good_read_function()
RETURNS INTEGER
LANGUAGE plpgsql
READS SQL DATA  -- 데이터를 읽기만 하는 경우
AS $$
DECLARE
    result INTEGER;
BEGIN
    SELECT COUNT(*) INTO result FROM users;
    RETURN result;
END;
$$;

-- 올바른 예: SQL로 데이터를 수정하는 함수
CREATE OR REPLACE FUNCTION good_write_function(p_user_id INTEGER)
RETURNS VOID
LANGUAGE plpgsql
MODIFIES SQL DATA  -- 데이터를 수정하는 경우
AS $$
BEGIN
    UPDATE users
    SET last_login = NOW()
    WHERE user_id = p_user_id;
END;
$$;

-- 또는 가장 간단한 해결책: 해당 속성 자체를 제거
CREATE OR REPLACE FUNCTION simple_function()
RETURNS INTEGER
LANGUAGE plpgsql
-- SQL 접근 레벨 속성을 생략하면 기본값(CONTAINS SQL)이 적용됨
AS $$
DECLARE
    result INTEGER;
BEGIN
    SELECT COUNT(*) INTO result FROM users WHERE is_active = TRUE;
    RETURN result;
END;
$$;

원인 2 해결: 외부 언어 함수의 SQL 접근 레벨 수정

PL/Python이나 PL/Perl 함수에서 SQL을 실행해야 할 경우, NO SQL 선언을 제거하고 적절한 레벨로 변경합니다.

-- 올바른 예: PL/Python 함수에서 SQL 실행
CREATE OR REPLACE FUNCTION py_good_function()
RETURNS TEXT
LANGUAGE plpythonu
READS SQL DATA  -- SQL 데이터를 읽는 경우 명시
AS $$
result = plpy.execute("SELECT version()")
return result[0]['version']
$$;

-- 데이터를 조회하고 처리하는 PL/Python 함수
CREATE OR REPLACE FUNCTION py_process_users(min_age INTEGER)
RETURNS INTEGER
LANGUAGE plpythonu
READS SQL DATA
AS $$
result = plpy.execute(
    "SELECT COUNT(*) as cnt FROM users WHERE age >= %d" % min_age
)
return result[0]['cnt']
$$;

원인 3 해결: 트리거 함수의 올바른 선언

트리거 함수는 일반적으로 다른 테이블에 데이터를 쓰거나 읽어야 하므로, MODIFIES SQL DATA 또는 아무 속성도 지정하지 않는 것이 안전합니다.

-- 올바른 예: 트리거 함수 정상 선언
CREATE OR REPLACE FUNCTION audit_trigger_func()
RETURNS TRIGGER
LANGUAGE plpgsql
-- SQL 접근 레벨 속성 생략 또는 MODIFIES SQL DATA 사용
AS $$
BEGIN
    IF TG_OP = 'INSERT' THEN
        INSERT INTO audit_log(table_name, action, record_id, changed_at)
        VALUES (TG_TABLE_NAME, TG_OP, NEW.id, NOW());
    ELSIF TG_OP = 'UPDATE' THEN
        INSERT INTO audit_log(table_name, action, record_id, changed_at)
        VALUES (TG_TABLE_NAME, TG_OP, NEW.id, NOW());
    ELSIF TG_OP = 'DELETE' THEN
        INSERT INTO audit_log(table_name, action, record_id, changed_at)
        VALUES (TG_TABLE_NAME, TG_OP, OLD.id, NOW());
    END IF;
    RETURN NEW;
END;
$$;

-- 트리거 생성
CREATE TRIGGER users_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE FUNCTION audit_trigger_func();

기존 함수 확인 및 수정 방법

현재 데이터베이스에 잘못된 SQL 접근 레벨을 가진 함수가 있는지 확인하는 쿼리입니다.

-- 모든 함수의 SQL 접근 레벨 확인
SELECT
    n.nspname AS schema_name,
    p.proname AS function_name,
    p.prokind AS function_kind,
    CASE p.provolatile
        WHEN 'i' THEN 'IMMUTABLE'
        WHEN 's' THEN 'STABLE'
        WHEN 'v' THEN 'VOLATILE'
    END AS volatility,
    l.lanname AS language,
    p.prosrc AS function_body_preview
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
JOIN pg_language l ON p.prolang = l.oid
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY n.nspname, p.proname;

-- 특정 함수의 전체 정의 확인
SELECT pg_get_functiondef(oid)
FROM pg_proc
WHERE proname = 'your_function_name';

예방 방법

  • 함수 생성 시 SQL 접근 레벨을 명시적으로 문서화하고 코드 리뷰 프로세스에 포함

함수를 작성하거나 리뷰할 때, SQL 접근 레벨 속성(NO SQL, CONTAINS SQL, READS SQL DATA, MODIFIES SQL DATA)이 실제 함수 동작과 일치하는지 반드시 확인하는 체크리스트를 팀 내에 공유하세요. 특히 템플릿이나 기존 함수를 복사하여 새 함수를 만들 때 이 속성이 그대로 복사되어 문제가 생기는 경우가 많습니다. CI/CD 파이프라인에 함수 정의 검증 단계를 추가하면 배포 전에 이런 문제를 자동으로 잡아낼 수 있습니다.

“`sql

— 함수 배포 전 속성 검증용 쿼리 (CI/CD 파이프라인에 통합)

SELECT proname, prosrc

FROM pg_proc

WHERE proname = ‘target_function’

AND prosrc LIKE ‘%SELECT%’ — SQL이 포함되어 있는지 확인

;

“`

  • 가능한 한 NO SQL 속성 사용을 지양하고, 속성 생략 또는 명확한 접근 레벨 지정

실무에서는 대부분의 PL/pgSQL 함수가 어떤 형태로든 SQL을 사용하게 됩니다. 따라서 NO SQL은 순수하게 연산만 수행하는 함수(예: 수학 계산, 문자열 변환 등)에만 사용하고, 나머지는 READS SQL DATA 또는 MODIFIES SQL DATA를 명확하게 지정하는 것을 팀 코딩 컨벤션으로 정해두는 것이 좋습니다. 속성을 아예 생략하는 것도 기본값(CONTAINS SQL)이 적용되므로 안전한 대안이 될 수 있습니다.


관련 에러

  • 38000 (external_routine_exception): 외부 루틴 실행 중 발생하는 일반적인 예외로, 38001의 상위 에러 클래스입니다.
  • 38002 (modifying_sql_data_not_permitted): READS SQL DATA로 선언된 함수에서 데이터 수정(INSERT/UPDATE/DELETE)을 시도할 때 발생합니다.
  • 38003 (prohibited_sql_statement_attempted): 허용되지 않은 SQL 문을 실행하려 할 때 발생하며, 38001과 유사한 맥락에서 나타납니다.
  • 38004 (reading_sql_data_not_permitted): NO SQL이나 CONTAINS SQL로 선언된 함수에서 데이터를 읽으려 할 때 발생합니다.
  • 39P01 (trigger_protocol_violated): 트리거 함수가 올바른 반환 값을 제공하지 않을 때 발생하며, 트리거 관련 38001 에러와 함께 나타날 수 있습니다.

DBMS 에러 코드 시리즈

주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.

본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.

댓글 남기기