PostgreSQL is famous for its robust architecture and SQL standards compliance.
But what happens if you keep adding and dropping columns — again and again — 2000 times?
Spoiler alert: 🚨 You hit a hard wall. And it's not about performance first — it's about PostgreSQL's internal 1600 column limit.
Let's explore what happens under the hood when you repeatedly modify a table's schema!
💡 How PostgreSQL Handles ADD and DROP COLUMN
-
Adding a column without a default is instantaneous.
- It only updates system catalogs like
pg_attribute
andpg_class
. - No physical rewrite of the table data.
- It only updates system catalogs like
-
Dropping a column is lazy.
- PostgreSQL marks the column as dropped in
pg_attribute
. - The physical data still exists inside table tuples.
- PostgreSQL marks the column as dropped in
🔗 Dropped columns remain "invisible" but are still internally counted until a full table rewrite happens!
🧰 The Experiment: 2000 Schema Changes
CREATE TABLE crazy_table (id SERIAL PRIMARY KEY);
DO $$
BEGIN
FOR i IN 1..2000 LOOP
EXECUTE 'ALTER TABLE crazy_table ADD COLUMN temp_col' || i || ' TEXT';
EXECUTE 'ALTER TABLE crazy_table DROP COLUMN temp_col' || i;
END LOOP;
END
$$;
The idea:
- Add a temp column
- Drop it
- Repeat 2000 times!
📊 Results: What Actually Happened?
Metric | Result |
---|---|
Table still accessible? | ✅ Yes |
Physical table size? | 📈 Slightly bigger |
pg_attribute catalog size? |
📈 Significantly bigger |
Performance? | 🧰 Slightly slower queries |
Error? | ❌ Yes at ~1598 iterations |
After about 1598 add/drop cycles, PostgreSQL threw an error:
ERROR: tables can have at most 1600 columns
🔍 Why 1600 Columns?
PostgreSQL enforces a hard internal limit:
- Each row uses a bitmap for null/non-null tracking.
- Bitmap can track only 1600 columns max.
- Even dropped columns occupy slots until the table is fully rewritten!
In simple words:
Dropped columns are "ghosts" — they aren't truly gone until the table is rebuilt.
Thus, even invisible dropped columns block new schema changes after 1600 slots are filled.
🛠️ How to Fix After Hitting the Limit
If you hit the 1600-column error, you need to reclaim space properly.
Problem | Solution |
---|---|
Dropped column ghosts | VACUUM FULL table_name; |
Catalog bloat (pg_attribute ) |
REINDEX SYSTEM database_name; |
Persistent tuple bloat | Create a fresh table |
Example:
CREATE TABLE crazy_table_new AS
SELECT id FROM crazy_table;
DROP TABLE crazy_table;
ALTER TABLE crazy_table_new RENAME TO crazy_table;
This forces a physical rebuild, clearing all dropped column artifacts!
🔹 Best Practices for Evolving Schemas
- Avoid constantly altering table structure.
- Prefer JSONB columns for dynamic attributes.
- Monitor catalog bloat using:
SELECT COUNT(*) FROM pg_attribute WHERE attrelid = 'your_table_name'::regclass AND attisdropped;
- Regularly
VACUUM FULL
large frequently-modified tables. - Use pg_repack if downtime must be minimized.
🌟 Final Takeaway
PostgreSQL is resilient, but system catalog bloat is real.
Adding and dropping columns 2000 times won't immediately crash your database, but:
- You'll slow down planning and querying.
- You'll hit hard limits you can't ignore.
- You'll eventually need to rewrite tables manually.
Moral of the story:
🎩 Treat your schema with respect! Think carefully before designing dynamic column systems.
Happy hacking — and happy PostgreSQLing! 🚀📈