Why you can't "just" concurrently index partitioned tables in Postgres We’ve been working on partitioning some of our fastest-growing tables recently. Since these are high-traffic, write-heavy tables, we tried to add indexes concurrently to avoid downtime. That's when we hit a classic wall: ERROR: cannot create index on partitioned table "..." concurrently. The "Why": CREATE INDEX CONCURRENTLY is designed to avoid locking your table. But since a partitioned table is a virtual parent, Postgres can’t coordinate a "no-lock" build across multiple child tables at once from a single command. It defaults to an Access Exclusive Lock, which, in a production environment, is a dealbreaker for performance. The "Create-and-Link" Pattern we used instead: • The Shell: Run CREATE INDEX ... ON ONLY parent_table. This adds the metadata to the parent but keeps it Invalid at first. • The Background Build: Run CREATE INDEX CONCURRENTLY on each individual partition. This builds the index without blocking our production traffic. • The Link: Attach each partition index to the parent: ALTER INDEX parent_idx ATTACH PARTITION child_idx. The Result: Once the final partition is linked, the parent index automatically becomes Valid. We got zero downtime, and any future partitions will now inherit this index automatically. It's a bit more manual work, but it's a lifesaver for keeping a high-growth system responsive. #PostgreSQL #DatabaseEngineering #SystemDesign #Backend #Scalability #SDE
Concurrently Indexing Partitioned Tables in Postgres
More Relevant Posts
-
Hot take: Most partitioned Postgres tables shouldn't be partitioned. We know. Controversial. Let us explain. Partitioning has become the default recommendation for any table over a certain size. "It's 100GB? Partition it." "Queries are slow? Partition it." "Scaling up? Partition it." But partitioning isn't free. It adds planning overhead. It complicates migrations. It makes some queries faster and others slower. And if your partition key doesn't match your query patterns, you've just turned one table into dozens of tables that Postgres has to scan one by one. That's not optimization. That's self-inflicted complexity. So here's the Data Drop #9 framework by Bhupathi Shameer — partition when: ✅ Your queries consistently filter by a predictable key (time range, tenant ID) ✅ You need to archive or drop old data without expensive DELETE operations ✅ Maintenance on the full table (VACUUM, REINDEX) is no longer manageable ✅ You can clearly articulate which partitions most queries will hit Don't partition when: ❌ You're hoping it'll magically speed up queries you haven't profiled yet ❌ Your queries don't filter by the partition key ❌ Your table is large but your actual problem is missing indexes or bad query plans ❌ You're adding it because a blog post said "partition everything over 10GB" The line between "this will save us" vs "this will haunt us" is thinner than most teams think. #AprilDataDrops #PostgreSQL #DataDrop9 #Partitioning #Database #Performance #DataModeling #OpenSourceDB
To view or add a comment, sign in
-
Ever wondered what happens when a SQL query runs? Let me tell you . That “simple” SELECT isn’t simple at all. SELECT * FROM users WHERE id = 17; Looks straightforward. Under the hood in PostgreSQL, it’s a four-stage decision engine. *1. Parse* PostgreSQL breaks your SQL into an internal structure, validates syntax, and resolves table and column names. Get this wrong and the query never runs. *2. Rewrite* Before execution, PostgreSQL applies rules and view transformations. Your query might already be reshaped before the planner even sees it. *3. Plan* This is where PostgreSQL earns its reputation. It weighs multiple execution paths: Index Scan, Sequential Scan, Bitmap Heap Scan. It picks the “cheapest” option based on table stats, row estimates, and data distribution. Cheapest ≠ fastest. It’s just what the model predicts will cost least in I/O and CPU. *4. Execute with MVCC* Now it finally touches data. But it also checks visibility. A row can exist on disk and still be invisible to you if your transaction shouldn’t see it yet. That’s MVCC in action. *The part most engineers overlook:* This entire flow happens for every SELECT. Every time. So when performance tanks, it’s usually not the query. It’s the context: - Outdated statistics → wrong plan - Poor indexing → wrong scan - Skewed data → bad estimates PostgreSQL isn’t just executing your query. It’s deciding _how_ to execute it. And that decision determines whether your query runs in milliseconds or minutes. What’s the most surprising query plan you’ve seen in production? #PostgreSQL #Databases #Performance #QueryTuning
To view or add a comment, sign in
-
What if thousands of UPDATEs and DELETEs are hitting your table every second? Your data looks correct. But your table size keeps growing. 📈 This is called Table Bloat. And it's a direct cost of MVCC. Here's why it happens: → PostgreSQL never overwrites a row on UPDATE or DELETE → It always creates a new version of the row instead → The old version stays behind as a dead tuple → At thousands of writes per second, dead tuples pile up fast The result? → Tables grow even when actual data hasn't changed → Queries slow down scanning through dead tuples → Indexes keep pointing to rows that no longer exist So what's the solution? VACUUM. 🧹 What VACUUM does: → Scans the table for dead tuples → Removes them and marks space as reusable → Updates the visibility map so queries stay fast → Prevents transaction ID wraparound — ignore this and PostgreSQL will shut itself down 🚨 One thing VACUUM does NOT do: → It does not shrink the file size on disk → For that you need VACUUM FULL — but it locks the table, use carefully And the best part? You don't have to run it manually. PostgreSQL's autovacuum does this in the background automatically. But autovacuum isn't magic. On high-write tables it can fall behind — tuning it for your workload is where the real DBA work begins. MVCC gave PostgreSQL speed and clean isolation. VACUUM is what keeps that trade-off from breaking you. 💡 #PostgreSQL #Database #DBA #VACUUM #TableBloat #DataEngineering
To view or add a comment, sign in
-
Database Indexing: A High-Level Explanation Your query worked fine yesterday. Today it is painfully slow. At small scale, databases can scan an entire table and the cost is barely noticeable. As data grows, that sequential scan increasingly dominates execution time. This shift in access cost is the core problem indexing addresses. An index is a separate data structure that helps the engine locate rows more efficiently. Like a book index, it allows the database to narrow the search space instead of examining every record. The engine maintains this structure and uses it to map searchable values to row locations. B-tree indexes are the default in most relational systems. They keep keys sorted and are structured to maintain shallow depth, allowing lookups to scale logarithmically as datasets grow. Because ordering is preserved, they support range conditions and ORDER BY operations naturally. Hash indexes trade ordering for faster equality lookups. They can be effective for exact matches but do not support ranges or sorting. For that reason, they are situational rather than general-purpose. Clustered indexes store table data in index order, shaping how rows are physically organized. Only one clustered index can exist per table. Non-clustered indexes, by contrast, store keys and references back to the underlying rows. That additional lookup step can still be beneficial when it significantly reduces the amount of data scanned. Composite indexes span multiple columns. Column order matters: the leftmost prefix rule determines which query patterns can take advantage of the structure. A well-designed composite index can often replace several single-column indexes. Indexes introduce trade-offs. They improve read efficiency but add write overhead. They consume storage and may require maintenance over time. Index columns that are frequently filtered, joined, or sorted. Prefer high-cardinality columns where selectivity meaningfully reduces search space. Index foreign keys to keep joins efficient. Avoid indexing tiny tables or low-cardinality flags. Be cautious with heavy indexing on write-intensive workloads such as logs or event streams. For wide text fields, consider partial or full-text indexing strategies. Measure first. Add the index second. Verify the execution plan always. #Database #DatabaseIndexing #SQL #PostgreSQL #MySQL #BackendDevelopment #SystemDesign #DevOps #DistributedSystems #Infrastructure #CloudEngineering
To view or add a comment, sign in
-
The MySQL binary log powers replication, CDC, and point-in-time recovery. But what's actually inside it? Part 6 of our binlog internals series cracks it open. Marcelo Altmann walks through the QUERY_EVENT — the event type that records DDL statements, transaction boundaries, and statement-based DML, along with the session state MySQL needs to replay them on a replica. Useful whether you're working with replication, building CDC tooling, or just curious about what MySQL is actually doing under the hood. Check it out 📲 https://lnkd.in/gf7mfGMD #MySQL #DatabaseEngineering #ReadySet
To view or add a comment, sign in
-
A question for every dev who's ever designed a database table: Did you design it for 10,000 rows or 10 million? Because when schemas are designed for the demo. For the MVP. For "let's just ship it and optimize later." And "later" arrives as a 3 AM P1 page, 18 months down the road, when the table that "works fine" has grown 1000x and suddenly nothing works fine. We call these the haunting patterns. Schema decisions that feel harmless at small scale and become structural nightmares at large scale. Data Drop #6 covers the big three: → UUIDs as primary keys — Random values fragment your B-tree indexes. At 500M rows, your index is bloated, your writes scatter across random pages, and your cache hit ratio craters. Sequential IDs exist for a reason. → "Just make it nullable" — The path of least resistance at design time. The source of a thousand bugs at query time. NULL doesn't equal NULL. Your aggregations silently skip rows. Your joins produce unexpected results. Nullable should be a conscious choice, not a default. → The EAV trap — Entity-Attribute-Value: the schema pattern that says "I don't want to commit to a data model." Congratulations, you now have a key-value store with the performance of a relational database and the flexibility of neither. Design for the table size you're going to have. Not the one you have today. Data Drop #6. Day 6 of 23. #AprilDataDrops #PostgreSQL #DataDrop6 #SchemaDesign #Database #Performance #OpenSourceDB OpenSource DB | Lahari Giddi
To view or add a comment, sign in
-
Most developers add database indexes expecting instant magic speed… …but many accidentally slow down their entire system instead. Here’s exactly how database indexing works under the hood — and why it’s a double-edged sword: Indexes are separate data structures that store a sorted map of your column values and point directly to the actual rows in the table. Instead of scanning every single row (a slow full table scan), the database can quickly jump to the right data — often in just a few steps. The Major Advantages: Lightning-fast reads: B-Tree indexes (the default in most databases) give O(log n) search time. They efficiently handle equality (=), range queries (>, <, BETWEEN), sorting, and JOINs. Specialized indexes unlock extra power: Hash indexes deliver true O(1) speed for exact matches, Bitmap indexes excel with low-cardinality data in analytics, and GiST/GIN handle full-text or spatial searches beautifully. Result: Queries that dragged for seconds now return in milliseconds, even on million-row tables. The Real Trade-Offs (Where It Hurts): Extra storage cost: Indexes can easily double or triple the size of your table. Slower writes: Every INSERT, UPDATE, or DELETE has to update all related indexes. This adds significant overhead and disk I/O, especially on high-write workloads. Maintenance burden: Choosing the wrong index type (like Hash for range queries) or creating too many indexes wastes space and can actually hurt performance. The smart approach: Focus indexes on columns frequently used in WHERE, ORDER BY, or JOIN conditions — especially on read-heavy tables. Regularly check which indexes are actually being used and drop the unused ones. Test changes carefully. Mastering this trade-off is what turns good backend systems into highly scalable ones. What’s your biggest indexing win — or the hardest lesson you learned about indexes? Drop it in the comments 👇 I read every single one. #DatabaseEngineering #SQL #PerformanceOptimization #BackendDevelopment #PostgreSQL #MySQL #DataEngineering #SystemDesign
To view or add a comment, sign in
-
-
If your PostgreSQL queries search inside JSONB, arrays, or text -- and you don't have GIN indexes -- every single one of those queries does a sequential scan. No exceptions. B-tree indexes handle scalar comparisons (equality, range, ordering). But they can't index values inside composite data types. A JSONB column with dozens of keys, an array of tags, a tsvector of text lexemes -- B-tree can't touch these. Without a GIN index, PostgreSQL reads every row in the table and evaluates the condition one by one. Three things most teams get wrong about GIN indexes: 1. Operator class choice matters. For JSONB, there are two options: the default jsonb_ops (supports @>, ?, ?|, ?&) and jsonb_path_ops (supports only @> but is 2-3x smaller and faster). If your queries only use containment checks (@>), jsonb_path_ops is the better choice. Create one with the wrong operator class and the index gets silently ignored. 2. fastupdate causes unpredictable latency. GIN indexes batch insertions into a pending list for efficiency. Most inserts are fast, but occasionally a query triggers a pending list flush, causing an unexpected slowdown. For consistent query latency, disable it: ALTER INDEX ... SET (fastupdate = off). The tradeoff is slower inserts. 3. LIKE '%pattern%' needs a trigram GIN index, not a regular index. B-tree indexes require a fixed prefix. Only a GIN index with pg_trgm's gin_trgm_ops operator class can accelerate leading-wildcard pattern matches and similarity searches. The rule is simple: every JSONB column queried with @> or ? needs a GIN index. Every array column queried with @> or && needs a GIN index. Every tsvector column needs a GIN index. Every text column searched with LIKE '%pattern%' needs a trigram GIN index. Build this into your schema design process. Don't wait for production complaints. Full guide with operator class comparison, full-text search setup, and performance tuning: https://lnkd.in/e4vWHuVb #PostgreSQL #GINIndex #JSONB #FullTextSearch #DatabasePerformance #DevOps
To view or add a comment, sign in
-
-
A PostgreSQL table with 500 million rows doesn't just slow down queries. It slows down everything: VACUUM takes hours, index builds lock the table, and deleting old data generates massive WAL and leaves bloat behind. Table partitioning splits a large table into smaller physical pieces while keeping it as a single logical table. The query planner uses partition pruning to scan only the relevant partitions. Here's what matters in practice: 1. The partition key must appear in your most common WHERE clauses. If 90% of your queries filter on event_timestamp, partition by timestamp. A partition key that queries don't filter on provides zero benefit and only adds planning overhead. Always verify with EXPLAIN before committing to a scheme -- if pruning doesn't activate, the partitioning isn't helping. 2. Partition count matters more than people think. Monthly partitions for 5 years = 60 partitions (reasonable). Daily partitions for 5 years = 1,825 partitions (the planner slows down noticeably). The planner evaluates each partition during query planning. Keep it manageable, or use TimescaleDB which is specifically optimized for high partition counts. 3. The biggest operational win is instant data removal. DROP TABLE on a partition takes milliseconds and generates no WAL. Compare that to DELETE FROM events WHERE event_timestamp < '2024-02-01' on 100 million rows -- that takes an hour, generates massive WAL, and leaves dead tuples for VACUUM to clean up. The biggest gotcha: you can't convert an existing table to a partitioned table in place. You need to create a new partitioned table, migrate data in batches, and swap with a rename in a single transaction. For zero-downtime migrations, add a trigger or logical replication to capture writes during the migration. Automate partition lifecycle with pg_partman or cron. A missing future partition causes INSERT failures. A forgotten old partition wastes storage. Full guide with range, list, hash strategies, migration patterns, and pg_partman setup: https://lnkd.in/eGUj8zXC #PostgreSQL #TablePartitioning #DatabasePerformance #DataEngineering #DevOps #SRE
To view or add a comment, sign in
-
-
🚀 PostgreSQL Indexing Explained (For Developers) If your queries are slow, indexing is usually the first thing you should look at 👇 🧠 What is Indexing? An index is like a table of contents for your database it helps PostgreSQL find data faster without scanning the entire table. ⚡ Why Indexing Matters? - Speeds up SELECT queries 🚀 - Reduces full table scans - Improves performance for large datasets 📚 Types of Indexes You Should Know: - B-Tree (default) → Best for equality & range queries (=, <, >) - Hash Index → Faster for exact matches (=) - GIN Index → Useful for JSONB, arrays, full-text search - Composite Index → Index on multiple columns 🛠️ Example: Without index → DB scans every row ❌ With index → Direct lookup ✅ 🔥 Real Use Case: Searching users by email → add index on "email" column to make it instant ⚠️ Important Trade-offs: - Indexes speed up reads ✅ - But slow down writes (INSERT/UPDATE) ❌ - Take extra storage 💡 Pro Tip: Don’t blindly add indexes use EXPLAIN ANALYZE to see if your query actually needs one #PostgreSQL #Database #BackendDeveloper #SystemDesign #Performance #SQL #LearnToCode
To view or add a comment, sign in
-
Explore content categories
- Career
- Productivity
- Finance
- Soft Skills & Emotional Intelligence
- Project Management
- Education
- Technology
- Leadership
- Ecommerce
- User Experience
- Recruitment & HR
- Customer Experience
- Real Estate
- Marketing
- Sales
- Retail & Merchandising
- Science
- Supply Chain Management
- Future Of Work
- Consulting
- Writing
- Economics
- Artificial Intelligence
- Employee Experience
- Workplace Trends
- Fundraising
- Networking
- Corporate Social Responsibility
- Negotiation
- Communication
- Engineering
- Hospitality & Tourism
- Business Strategy
- Change Management
- Organizational Culture
- Design
- Innovation
- Event Planning
- Training & Development
The "Create-and-Link" pattern addresses a significant PostgreSQL architectural constraint for concurrent indexing. Avoiding the Access Exclusive Lock on partitioned tables is vital for high-traffic systems.