2026년 07월 17일 | DBMS Error 가이드
이 글에서 다루는 내용
54011 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
54011 too many columns 는?
PostgreSQL 에러 코드 54011 too many columns는 테이블, 뷰, 또는 쿼리 결과에 허용된 최대 컬럼 수를 초과했을 때 발생하는 오류입니다. PostgreSQL은 단일 테이블 또는 뷰에서 최대 1,600개의 컬럼만 허용하며, 이 제한을 넘어서는 DDL 또는 DML 작업을 수행하려 할 때 이 에러가 발생합니다. 실무에서는 레거시 시스템의 마이그레이션, 잘못 설계된 EAV(Entity-Attribute-Value) 패턴의 플래트닝, 또는 대규모 PIVOT 쿼리를 생성할 때 빈번하게 마주치는 오류입니다.
주요 발생 원인
1. 과도하게 넓은 테이블 설계 (Wide Table Anti-Pattern)
가장 흔한 원인으로, 수백~수천 개의 속성을 하나의 테이블에 컬럼으로 정의하려는 경우입니다. 예를 들어, 설문조사 결과 테이블에 문항 하나당 컬럼 하나씩 추가하거나, 날짜별로 컬럼을 생성하는 방식으로 설계하면 금세 1,600개 한도에 도달하게 됩니다. 이는 데이터베이스 설계 원칙(정규화)을 위반한 전형적인 안티패턴으로, 단순히 컬럼 수 제한 문제를 넘어 성능과 유지보수성에도 심각한 영향을 줍니다.
-- 잘못된 설계 예시: 날짜별 매출 컬럼을 수백 개 정의
CREATE TABLE daily_sales (
product_id INT,
sales_2024_01_01 NUMERIC,
sales_2024_01_02 NUMERIC,
sales_2024_01_03 NUMERIC,
-- ... 1,600개 이상 계속 추가 시 에러 발생
sales_2028_12_31 NUMERIC -- ERROR: 54011 too many columns
);
2. 다른 DBMS에서의 마이그레이션 시 스키마 변환 문제
Oracle, MySQL, SQL Server 등 다른 DBMS에서 PostgreSQL로 마이그레이션할 때, 원본 DB의 광폭 테이블을 그대로 이식하려 할 경우 발생합니다. 일부 레거시 시스템에서는 ERP, CRM 데이터를 수천 개의 컬럼으로 비정규화하여 관리하는 경우가 있으며, 이를 아무런 재설계 없이 PostgreSQL로 옮기면 즉시 54011 에러를 만나게 됩니다. 마이그레이션 전 반드시 대상 스키마에 대한 컬럼 수 분석과 재설계가 필요합니다.
-- 마이그레이션 전 컬럼 수 확인 쿼리
SELECT
table_name,
COUNT(column_name) AS column_count
FROM information_schema.columns
WHERE table_schema = 'public'
GROUP BY table_name
HAVING COUNT(column_name) > 1500
ORDER BY column_count DESC;
3. 동적 PIVOT 또는 크로스탭 쿼리의 폭발적 컬럼 생성
애플리케이션 레이어나 보고서 툴에서 동적으로 크로스탭(PIVOT) 쿼리를 생성할 때, 카테고리 값의 수가 많아지면 SELECT 절의 컬럼 수가 1,600개를 넘어서는 경우가 생깁니다. 특히 crosstab() 함수나 동적 SQL을 사용하는 경우, 입력 데이터의 고유값 수가 늘어남에 따라 예상치 못하게 이 에러가 발생할 수 있습니다. 이 경우 결과 컬럼 수를 제어하는 로직이 반드시 필요합니다.
-- 문제가 되는 동적 PIVOT 예시 (의사 코드 포함)
-- category 값이 2,000가지인 경우 아래 쿼리는 54011 발생 가능
SELECT
user_id,
MAX(CASE WHEN category = 'cat_001' THEN value END) AS cat_001,
MAX(CASE WHEN category = 'cat_002' THEN value END) AS cat_002,
-- ... category가 1,600개 이상이면 에러
MAX(CASE WHEN category = 'cat_2000' THEN value END) AS cat_2000
FROM user_events
GROUP BY user_id;
해결 방법
해결책 1: 테이블 정규화 (Wide → Tall 변환)
가장 근본적인 해결책은 광폭 테이블을 정규화하여 행(row) 기반 구조로 변환하는 것입니다.
-- 기존 잘못된 구조
-- daily_sales(product_id, sales_2024_01_01, sales_2024_01_02, ...)
-- 정규화된 올바른 구조
CREATE TABLE daily_sales_normalized (
id BIGSERIAL PRIMARY KEY,
product_id INT NOT NULL,
sales_date DATE NOT NULL,
sales_amount NUMERIC(15, 2) NOT NULL DEFAULT 0,
CONSTRAINT uq_product_date UNIQUE (product_id, sales_date)
);
CREATE INDEX idx_daily_sales_product ON daily_sales_normalized (product_id);
CREATE INDEX idx_daily_sales_date ON daily_sales_normalized (sales_date);
-- 데이터 마이그레이션 (기존 wide 테이블 → normalized 테이블)
INSERT INTO daily_sales_normalized (product_id, sales_date, sales_amount)
SELECT product_id, '2024-01-01'::DATE, sales_2024_01_01 FROM daily_sales WHERE sales_2024_01_01 IS NOT NULL
UNION ALL
SELECT product_id, '2024-01-02'::DATE, sales_2024_01_02 FROM daily_sales WHERE sales_2024_01_02 IS NOT NULL;
-- 이하 반복...
해결책 2: JSONB 또는 hstore를 활용한 동적 속성 저장
컬럼이 동적으로 늘어나는 구조라면 JSONB 타입을 활용하여 유연하게 속성을 저장할 수 있습니다.
-- JSONB를 활용한 동적 속성 저장
CREATE TABLE survey_responses (
id BIGSERIAL PRIMARY KEY,
survey_id INT NOT NULL,
respondent_id INT NOT NULL,
answers JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 데이터 삽입 예시
INSERT INTO survey_responses (survey_id, respondent_id, answers)
VALUES (
1,
101,
'{"q1": "매우 만족", "q2": 5, "q3": "개선 필요 없음", "q4": true}'::JSONB
);
-- JSONB 인덱스 생성 (GIN 인덱스)
CREATE INDEX idx_survey_answers ON survey_responses USING GIN (answers);
-- 특정 응답 조회
SELECT respondent_id, answers->>'q1' AS q1_answer
FROM survey_responses
WHERE survey_id = 1
AND (answers->>'q2')::INT >= 4;
해결책 3: PIVOT 쿼리 컬럼 수 제한 및 분할 처리
동적 PIVOT 쿼리로 인해 발생한 경우, 상위 N개의 카테고리만 컬럼으로 전개하고 나머지는 기타로 묶는 방식으로 해결합니다.
-- 상위 50개 카테고리만 PIVOT하고 나머지는 'others'로 집계
WITH top_categories AS (
SELECT category
FROM user_events
GROUP BY category
ORDER BY COUNT(*) DESC
LIMIT 50 -- 컬럼 수를 50개로 제한
),
categorized AS (
SELECT
user_id,
CASE WHEN category IN (SELECT category FROM top_categories)
THEN category
ELSE 'others'
END AS category_group,
value
FROM user_events
)
SELECT
user_id,
SUM(CASE WHEN category_group = 'cat_001' THEN value ELSE 0 END) AS cat_001,
SUM(CASE WHEN category_group = 'cat_002' THEN value ELSE 0 END) AS cat_002,
-- ... 최대 50개까지
SUM(CASE WHEN category_group = 'others' THEN value ELSE 0 END) AS others
FROM categorized
GROUP BY user_id;
예방 방법
1. 테이블 설계 단계에서 컬럼 수 임계치 모니터링 자동화
운영 환경에서 컬럼 수가 특정 임계치(예: 500개)를 초과하는 테이블이 생성되면 즉시 알림을 받을 수 있도록 모니터링 쿼리를 주기적으로 실행하는 것이 중요합니다. CI/CD 파이프라인에 스키마 검증 단계를 추가하여 마이그레이션 파일 적용 전에 컬럼 수를 사전 검사하는 것을 권장합니다.
-- 컬럼 수 임계치 모니터링 쿼리 (500개 초과 테이블 경보)
SELECT
n.nspname AS schema_name,
c.relname AS table_name,
COUNT(a.attnum) AS column_count,
CASE
WHEN COUNT(a.attnum) > 1500 THEN '🔴 CRITICAL'
WHEN COUNT(a.attnum) > 1000 THEN '🟠 WARNING'
WHEN COUNT(a.attnum) > 500 THEN '🟡 CAUTION'
ELSE '🟢 NORMAL'
END AS status
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_attribute a ON a.attrelid = c.oid
WHERE c.relkind = 'r' -- 일반 테이블만
AND a.attnum > 0 -- 시스템 컬럼 제외
AND NOT a.attisdropped -- 삭제된 컬럼 제외
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
GROUP BY n.nspname, c.relname
HAVING COUNT(a.attnum) > 500
ORDER BY column_count DESC;
2. 설계 리뷰 단계에서 정규화 원칙 강제화
데이터베이스 스키마 변경 시 반드시 동료 DBA 또는 아키텍트의 코드 리뷰를 거치도록 프로세스를 확립하세요. 특히 신규 테이블의 컬럼 수가 100개를 초과할 경우, 정규화 필요성을 검토하는 체크리스트를 의무화하는 것이 효과적입니다. 또한 ORM을 사용하는 개발팀이라면 엔티티 정의 시 컬럼 수에 대한 린트(lint) 규칙을 추가하여 PR 단계에서 사전 차단하는 방법도 매우 실용적입니다.
관련 에러
- 54000
program_limit_exceeded: PostgreSQL 내부 프로그램 제한을 초과했을 때 발생하는 상위 카테고리 에러입니다. 54011은 이 카테고리에 속합니다. - 54001
statement_too_complex: 쿼리의 복잡도(조인, 서브쿼리 깊이 등)가 파서의 처리 한계를 초과할 때 발생하며, 광폭 테이블과 함께 복잡한 쿼리를 수행할 경우 함께 발생할 수 있습니다. - 54023
too_many_arguments: 함수 호출 시 인자 수가 PostgreSQL의 한계(기본 100개)를 초과할 때 발생하며, 동적으로 컬럼을 함수 인자로 전달하는 패턴에서 54011과 함께 나타날 수 있습니다. - 42P16
invalid_table_definition: 테이블 정의 자체가 유효하지 않을 때 발생하며, 컬럼 제약 조건이나 타입 정의 오류와 함께 나타나는 경우가 있습니다.
주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.