PostgreSQL enforces a hard limit of 1,600 columns per table. This error occurs when you exceed that limit or hit the 1,664-column tuple limit in SELECT statements. Redesign your schema using arrays, JSON, or table partitioning.
PostgreSQL has hard-coded limits on the number of columns it can handle. The primary limit is 1,600 columns per table (MaxHeapAttributeNumber), and a secondary limit of 1,664 for tuple target lists in SELECT statements (MaxTupleAttributeNumber). These limits exist because the tuple structure has fixed-size field offsets in its header, making it impossible to exceed without recompiling PostgreSQL. Additionally, dropped columns continue to count toward the limit—PostgreSQL never reclaims space from dropped columns.
Run this query to see how many columns your table has:
SELECT COUNT(*) as column_count
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'your_table_name';If the result is near or over 1,600, you've hit the limit.
Break wide tables into multiple normalized tables with foreign keys:
-- Before (too wide)
CREATE TABLE users (
id INT,
name TEXT,
prop_1 TEXT, prop_2 TEXT, ... -- 1,500+ properties
);
-- After (normalized)
CREATE TABLE users (id INT PRIMARY KEY, name TEXT);
CREATE TABLE user_properties (
id INT PRIMARY KEY,
user_id INT REFERENCES users(id),
key TEXT,
value TEXT
);This pattern scales to unlimited properties.
If you have many dynamic or optional fields, store them in a single JSONB column:
CREATE TABLE products (
id INT PRIMARY KEY,
name TEXT,
metadata JSONB -- stores hundreds of optional attributes
);
INSERT INTO products VALUES (1, 'Widget', '{"color": "red", "weight": 2.5, "tags": ["sale", "new"]}'::jsonb);
SELECT metadata->'color' FROM products WHERE id = 1;JSONB supports indexing and querying while keeping your table structure clean.
For columnar repetition (like multiple phone numbers or addresses), use arrays:
CREATE TABLE contacts (
id INT PRIMARY KEY,
name TEXT,
phone_numbers TEXT[], -- can store 100+ numbers in one column
addresses TEXT[]
);
INSERT INTO contacts VALUES (1, 'Alice', ARRAY['555-1234', '555-5678'], ARRAY['123 Main St']);
SELECT phone_numbers[1] FROM contacts WHERE id = 1;If joining views causes the error, explicitly select only needed columns instead of SELECT *:
-- Instead of this:
SELECT * FROM view_a
JOIN view_b ON view_a.id = view_b.id; -- ERROR: too many columns
-- Do this:
SELECT view_a.id, view_a.name, view_b.description
FROM view_a
JOIN view_b ON view_a.id = view_b.id;This reduces the tuple size and stays within limits.
If you've reached 1,600 columns and many are marked as dropped, you must rebuild the table:
-- Dump schema and data
pg_dump -t your_table_name your_db > table.sql
-- Create new table (ideally redesigned)
DROP TABLE your_table_name CASCADE;
-- Restore from dump or insert redesigned data
psql -d your_db < table.sqlNote: This is a last resort after redesigning your schema. PostgreSQL does not provide a way to reclaim the dropped-column slots without recreation.
The 1,600 and 1,664 limits are hard-coded in PostgreSQL source (src/include/access/htup_details.h) and cannot be changed without recompiling. These limits exist because tuple headers use fixed-size offsets for field positions. Additionally, the actual per-row storage size is further constrained by PostgreSQL's 8KB heap page size—even if you stay under 1,600 columns, if your row data exceeds ~8KB, it cannot be stored. Dropped columns permanently consume a slot in the tuple structure and count toward the 1,600 limit, so aggressive column dropping over time can unexpectedly hit the limit. For OLAP or data warehouse scenarios with hundreds of semi-structured attributes, using JSONB or Parquet external tables is recommended. For OLTP with many optional nullable columns, normalization or EAV (Entity-Attribute-Value) patterns are more appropriate.
insufficient columns in unique constraint for partition key
How to fix "insufficient columns in unique constraint for partition key" in PostgreSQL
ERROR 42501: must be owner of table
How to fix "must be owner of table" in PostgreSQL
trigger cannot change partition destination
How to fix "Trigger cannot change partition destination" in PostgreSQL
SSL error: certificate does not match host name
SSL error: certificate does not match host name in PostgreSQL
No SSL connection
No SSL connection to PostgreSQL