PostgreSQL Error 54011: Too Many Columns
PostgreSQL error code 54011 (too_many_columns) is thrown when you attempt to create or alter a table, view, or composite type with more than 1,600 columns — the hard limit imposed by PostgreSQL's internal tuple storage structure. This is not an arbitrary software restriction; it stems from the physical constraints of PostgreSQL's heap page layout. You'll most commonly encounter this when migrating legacy systems, building wide-table analytics schemas, or using automated column-generation patterns.
Top 3 Causes
1. Legacy System Migration Bringing Oversized Tables
Migrating from Oracle, MySQL, or spreadsheet-based systems often carries over tables that already have hundreds or even thousands of columns. The direct import of such structures into PostgreSQL triggers this error immediately.
-- This will fail if column_count > 1,600
CREATE TABLE legacy_import (
col1 TEXT, col2 TEXT, col3 TEXT,
-- ... col1601 TEXT <-- triggers ERROR 54011
);
-- Check your current column counts before migrating
SELECT table_name, COUNT(column_name) AS col_count
FROM information_schema.columns
WHERE table_schema = 'public'
GROUP BY table_name
ORDER BY col_count DESC;
2. Automated Dynamic Column Addition
SaaS platforms or applications that support custom user fields often implement them by running ALTER TABLE ADD COLUMN dynamically. Over time, this accumulates to hit the 1,600 limit.
-- Anti-pattern: Adding columns dynamically (eventually hits 54011)
ALTER TABLE user_data ADD COLUMN custom_field_1201 TEXT;
-- ERROR: 54011: too many columns
-- Better: Use JSONB for dynamic attributes
ALTER TABLE user_data ADD COLUMN IF NOT EXISTS extra JSONB DEFAULT '{}';
-- Store dynamic fields inside JSONB
UPDATE user_data
SET extra = extra || jsonb_build_object('custom_field_1201', 'some_value')
WHERE user_id = 42;
-- Index for fast lookups
CREATE INDEX idx_user_data_extra ON user_data USING GIN(extra);
3. Wide Table / God Table Anti-Pattern
Storing every possible attribute as a separate column without normalization leads to bloated table definitions. This is common in analytics pipelines or poorly designed reporting schemas.
-- Anti-pattern: One giant table with hundreds of flag/status columns
CREATE TABLE report_data (
id SERIAL PRIMARY KEY,
category_01 TEXT, category_02 TEXT, -- ... up to category_800
flag_01 BOOLEAN, flag_02 BOOLEAN -- ... up to flag_800
-- Total exceeds 1,600 → ERROR 54011
);
-- Fix: Normalize into related tables
CREATE TABLE report_data (
id SERIAL PRIMARY KEY,
report_name VARCHAR(200),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE report_categories (
id SERIAL PRIMARY KEY,
report_id INT REFERENCES report_data(id) ON DELETE CASCADE,
category_key VARCHAR(100),
category_val TEXT
);
CREATE TABLE report_flags (
report_id INT REFERENCES report_data(id) ON DELETE CASCADE,
flag_key VARCHAR(100),
flag_val BOOLEAN,
PRIMARY KEY (report_id, flag_key)
);
Quick Fix Solutions
Option A – Normalize the schema (recommended for structured data):
Split related column groups into child tables and join them via foreign keys or expose via a VIEW.
Option B – Use JSONB (recommended for dynamic/unstructured attributes):
-- Consolidate dynamic columns into a single JSONB field
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
attributes JSONB DEFAULT '{}'
);
-- Query specific attribute
SELECT product_id, name, attributes->>'color' AS color
FROM products
WHERE attributes @> '{"color": "red"}';
Option C – Horizontal table splitting (for unavoidable wide schemas):
-- Split into logical groups, reunite with a view
CREATE TABLE entity_core (id SERIAL PRIMARY KEY, col1 TEXT, col2 TEXT);
CREATE TABLE entity_details (id INT PRIMARY KEY REFERENCES entity_core(id), col3 TEXT, col4 TEXT);
CREATE VIEW v_entity AS
SELECT c.*, d.col3, d.col4
FROM entity_core c
LEFT JOIN entity_details d USING (id);
Prevention Tips
- Set a column count threshold policy. Treat any table exceeding 100–150 columns as a design smell requiring review. Add automated checks in your CI/CD pipeline to reject migration scripts that push column counts past a safe threshold.
-- Pre-deployment check: fail if any table exceeds 300 columns
SELECT table_name, COUNT(*) AS col_count
FROM information_schema.columns
WHERE table_schema = 'public'
GROUP BY table_name
HAVING COUNT(*) > 300;
-- If this returns rows, block the deployment
-
Default to JSONB for extensible attributes. When designing tables that will need user-defined or application-driven extensibility, always include a
JSONBcolumn from the start rather than reaching forALTER TABLE ADD COLUMNeach time a new attribute is needed. This future-proofs your schema against 54011 and keeps your table definitions clean and maintainable.
📖 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)