When a CREATE TABLE statement, SELECT list, or subquery result exceeds PostgreSQL's maximum column count, you will see an error like:
ERROR: tables can have at most 1664 columns
SQLSTATE: 54011
or, depending on where the limit is hit:
ERROR: target lists can have at most 1664 entries
SQLSTATE: 54011
The SQLSTATE is 54011 and the condition name is too_many_columns. It belongs to PostgreSQL error class 54 — Program Limit Exceeded — which covers hard internal limits rather than user data errors.
What This Error Means
PostgreSQL enforces a hard upper bound on the number of columns a table or query result can have. The documented limit is 1,600 columns per table, but the internal tuple descriptor structure actually allows up to 1,664 entries (the gap is reserved for system columns and dropped-column slots). In practice you will hit the error before reaching 1,664 because dropped columns in a table's history consume slots even after ALTER TABLE ... DROP COLUMN.
Error class 54 (Program Limit Exceeded) groups errors that arise from PostgreSQL's own architectural ceilings rather than bad data or syntax. Other members of this class include 54001 (statement_too_complex) and 54023 (too_many_arguments). Unlike class 22 (data exceptions) these errors are not recoverable by changing a value — the schema or query itself must change.
After 54011 is raised the current statement is aborted and, if you are inside an explicit transaction, that transaction is placed into an error state that requires a ROLLBACK (or ROLLBACK TO SAVEPOINT) before any further work can proceed. No data is written and no DDL is committed.
Common Causes
Wide table design with hundreds of EAV-style columns. A table built by repeatedly running
ALTER TABLE ... ADD COLUMNto store dynamic attributes can accumulate hundreds or thousands of columns over time, eventually crossing the 1,664-slot ceiling including dropped-column tombstones.ORM-generated migrations that never drop old columns. Some migration frameworks leave dropped columns as tombstones. If the live column count looks safe but the actual
pg_attributeslot count (including dropped columns) is near the limit, adding even one column triggers the error.SELECT * across heavily JOINed views or CTEs. A query that
SELECT *from a chain of views, each projecting many columns, can produce a target list that exceeds 1,664 entries even if no single base table is wide.Programmatic table creation. Code that generates a
CREATE TABLEstatement dynamically (for example, pivoting a result set into columns) can easily produce a column list that exceeds the limit.Inherited or partitioned tables. In table inheritance hierarchies, the child table's effective column count includes all columns from every parent. A deep hierarchy of moderately wide tables can collectively exceed the ceiling.
How to Fix too_many_columns
Redesign wide tables using a key-value or JSONB pattern.
Instead of hundreds of sparse columns, store dynamic attributes in a
jsonbcolumn:-- Before: one column per attribute ALTER TABLE events ADD COLUMN attr_001 text; ALTER TABLE events ADD COLUMN attr_002 text; -- ... hundreds more -- After: single JSONB column ALTER TABLE events ADD COLUMN attributes jsonb DEFAULT '{}'; -- Query a specific attribute SELECT attributes->>'attr_001' FROM events; -- Index a frequently queried attribute CREATE INDEX ON events ((attributes->>'attr_001'));Reclaim dropped-column slots by rebuilding the table.
Dropped columns leave tombstone slots in
pg_attribute.VACUUMdoes not reclaim these. A full table rebuild does:-- Check current live + dropped column count SELECT count(*) FROM pg_attribute WHERE attrelid = 'your_table'::regclass AND attnum > 0; -- includes dropped columns -- Rebuild to reclaim slots (rewrites the table) CLUSTER your_table USING your_table_pkey; -- or ALTER TABLE your_table SET (autovacuum_enabled = false); CREATE TABLE your_table_new AS SELECT * FROM your_table; -- then rename and swapA cleaner approach is
pg_dump+ restore, which only serializes live columns.Replace SELECT * with explicit column lists in wide queries.
-- Problematic: expands to >1664 columns across joins SELECT * FROM a JOIN b ON ... JOIN c ON ... JOIN d ON ...; -- Fixed: list only the columns you actually need SELECT a.id, a.name, b.status, c.created_at FROM a JOIN b ON ... JOIN c ON ... JOIN d ON ...;Normalize the schema. If many columns represent repeating groups, move them to a child table with a foreign key. This is the relational-model solution and improves storage efficiency as well:
-- Instead of order_col_1 ... order_col_50 on a parent table CREATE TABLE order_items ( order_id bigint REFERENCES orders(id), item_index int, value text, PRIMARY KEY (order_id, item_index) );Audit slot consumption before hitting the limit.
SELECT relname, count(*) FILTER (WHERE NOT attisdropped) AS live_columns, count(*) FILTER (WHERE attisdropped) AS dropped_columns, count(*) AS total_slots FROM pg_class c JOIN pg_attribute a ON a.attrelid = c.oid WHERE c.relkind = 'r' AND a.attnum > 0 GROUP BY relname ORDER BY total_slots DESC LIMIT 20;
Additional Information
- The 1,600-column soft limit and 1,664-slot hard limit have been present since at least PostgreSQL 8.x and have not changed through PostgreSQL 16.
- Related SQLSTATE codes in class
54:54001(statement_too_complex),54023(too_many_arguments— function has more than 100 parameters). - Most application-level drivers (libpq, psycopg2, JDBC, asyncpg) surface this error as a server-side
ProgrammingErrororPSQLExceptionwith SQLSTATE54011. The condition is not retryable; the application must fix the schema or query. - Wide tables with hundreds of columns impose a real per-row overhead: PostgreSQL's tuple header requires one null-bitmap bit per column even for non-null values. A 1,600-column row with mostly null values still pays the fixed overhead on every heap page read.
pg_dumponly serializes live columns, so a dump-and-restore cycle is a clean way to reset dropped-column slot counts when rebuilding a table offline.
Frequently Asked Questions
Why does PostgreSQL say "1664 columns" when the documented limit is 1600?
The 1,600-column limit is the safe operational ceiling documented in the PostgreSQL manual. Internally, the tuple descriptor can hold 1,664 entries, but the extra slots are reserved for system columns (like ctid and xmin) and for tombstones left by ALTER TABLE ... DROP COLUMN. You may reach the 1,664-slot error before accumulating 1,600 live columns if the table has a history of dropped columns.
Can I increase the column limit with a configuration parameter?
No. The limit is a compile-time constant (MaxTupleAttributeNumber) in PostgreSQL source code, not a GUC parameter. Changing it requires patching and recompiling PostgreSQL, which is not a supported configuration path.
My table only has 900 columns — why am I getting this error?
Dropped columns are not removed from pg_attribute; they leave tombstone entries that still consume attribute slots. If the table has been through many ADD COLUMN / DROP COLUMN cycles, the total slot count (live + dropped) can exceed 1,664 even though \d tablename only shows a few hundred live columns. Run the diagnostic query in the fix section to count both live and dropped slots.
Does this error affect foreign tables or views?
Yes. Foreign tables defined with CREATE FOREIGN TABLE obey the same column limit. Views do not store rows themselves, but a view's target list is checked when it is compiled into a query plan, so a view that expands to more than 1,664 column references will also raise 54011.