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
PostgreSQL Table Partitioning for Performance and Scalability
More Relevant Posts
-
Every standard B-tree index lookup in PostgreSQL is a two-step process: scan the index to find row pointers, then fetch the actual row data from the heap table. That second step -- the heap fetch -- is the bottleneck. For queries returning many rows, each heap fetch is a random I/O operation scattered across the table. A covering index eliminates the heap entirely. If all the columns your query needs exist in the index, PostgreSQL reads everything from the compact, ordered index structure. No random heap access. No wasted I/O. Three things most teams miss about covering indexes: **1. INCLUDE is different from a composite index.** Before PostgreSQL 11, you had to create a composite index on all columns: `(customer_id, customer_name, customer_email)`. This sorts on all three columns even though nobody searches by customer_name. The `INCLUDE` clause adds columns to leaf pages without including them in the sort key: `CREATE INDEX ON customers (customer_id) INCLUDE (customer_name, customer_email)`. Smaller index, cleaner semantics. **2. Index-only scans depend on vacuum.** PostgreSQL can skip the heap only for pages marked "all-visible" in the visibility map. If vacuum falls behind, pages are not marked all-visible, and the planner falls back to regular index scans with heap fetches -- even with a covering index. A covering index without healthy vacuum is a wasted investment. **3. The performance gap widens dramatically with scale.** On a 100-million-row table, eliminating heap fetches can reduce query time from hundreds of milliseconds to single-digit milliseconds -- a 10-100x improvement. The larger the table and the more rows your query returns, the bigger the win. Look for `Index Scan` with high `Heap Fetches` in your EXPLAIN output. If you see `Heap Fetches: 1000` where an `Index Only Scan` with `Heap Fetches: 0` is possible, there is a covering index opportunity waiting. Practical guide with INCLUDE vs composite examples, dashboard query patterns, and vacuum considerations: https://lnkd.in/eFhhvyU6 #PostgreSQL #DatabasePerformance #Indexing #CoveringIndex #SoftwareEngineering #DevOps
To view or add a comment, sign in
-
-
🚀 SequelPG v0.11.1 is live If you work with PostgreSQL every day, this will feel familiar: You run a query You tweak it You come back to something from yesterday You try to remember what actually worked Most tools treat query history as just a log. I don’t think that’s enough. In this release, I rebuilt Query History from scratch. Now it’s something you actually use: Quickly find past queries Reuse them without rewriting Debug faster with less context switching I also refactored the Database Tools layer. You won’t “see” most of it — but you’ll feel it: More consistency Better performance Stronger foundation for what’s coming next I’m not trying to add more features. I’m trying to reduce friction when working with data. Full release notes: https://lnkd.in/dFmaV_xH If you use PostgreSQL, I’d really value your feedback. #PostgreSQL #DeveloperTools #IndieHacker #BuildInPublic #SwiftUI #DX
To view or add a comment, sign in
-
🐘 Say Goodbye to Migration Headaches with pgloader If you’ve ever had to migrate a database to PostgreSQL, you know the "schema vs. data" struggle. Most tools make you export the schema first, fix it, then import data separately. Enter pgloader—a powerful, open-source tool that automates the entire "Continuous Migration" process in a single command. 🛠️ What can pgloader transform? It doesn't just copy data; it intelligently transforms different source structures into a clean PostgreSQL structure. Supported sources include: ✅ Databases: MySQL, MS SQL Server, SQLite, and Redshift. ✅ Files: CSV, Fixed-format files, dBase (DBF), and IBM IXF. ✅ On-the-fly Transformation: It automatically handles type casting (like converting MySQL's 0000-00-00 dates to NULL) and re-indexes your tables. ⚙️ Flexibilty: Schema vs. Data One of the best features is how it handles different migration needs: SCHEMA ONLY: Use the CREATE NO DATA clause if you only want to replicate the structure. DATA ONLY: Use the CREATE NO SCHEMA clause if you’ve already prepared your target tables (popular for ORM-heavy projects). FULL MIGRATION: By default, it creates the schema, loads the data, and resets sequences—all in one go. 💡 Why I like it It’s built for speed. By using the PostgreSQL COPY protocol and parallel workers, it’s significantly faster than standard INSERT scripts. Plus, it generates a "Summary Report" at the end so you know exactly how many rows were moved and if any errors occurred. Are you a GUI person (like AWS SCT) or a CLI person (like pgloader)? Let’s discuss in the comments! #PostgreSQL #DatabaseMigration #OpenSource #SQL #DataEngineering #pgloader #BackendDevelopment
To view or add a comment, sign in
-
Database Sharding & Partitioning Strategies for 1M+ QPS – What Actually Works in Production 📊 After tuning databases that crossed 1M+ queries per second, I realized one hard truth: “Just add more replicas” is a myth at real scale. You need smart sharding + partitioning designed from day one. Here’s the practical decision framework I use in production: Sharding Strategies – When to Choose What: Range Sharding → Perfect for time-series data, logs, or sequential IDs (e.g., orders by order_date) Hash Sharding → Best for even distribution on high-cardinality keys like user_id, session_id, or tenant_id Composite / Directory-based → When you need both flexibility and low-latency routing PostgreSQL Declarative Partitioning (Still a Game-Changer in 2026): PostgreSQL’s native partitioning has matured beautifully. My go-to patterns: Range Partitioning — Time-based data + easy archiving (monthly/weekly) List Partitioning — Status, region, or category-based queries Hash Partitioning — Massive tables needing even row distribution My Real-World Checklist Before Sharding Anything: 1. Max out connection pooling, indexes, and query tuning first 2. Choose a shard key that covers 80%+ of your query patterns 3. Always plan for future re-sharding (it will happen) 4. Use native partitioning as long as possible — go to Citus or Vitess only when you need true horizontal distribution across nodes 5. Maintain a global lookup / routing table — never do blind hashing in the application layer Pro Tip: Partition pruning is your best friend. Make sure your most frequent WHERE clauses include the partition key. Backend & Database engineers — what sharding or partitioning strategy actually saved (or broke) your system at scale? Drop your war stories below 👇 Let’s exchange real architecture lessons! #DatabaseOptimization #PostgreSQL #Sharding #Partitioning #HighScaleSystems #SystemDesign #BackendDevelopment #Citus #JavaBackend #SeniorDeveloper #SpringBoot
To view or add a comment, sign in
-
-
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
-
How MVCC actually works in databases like PostgreSQL (Multi-Version Concurrency Control) When multiple users read and update the same data at the same time, databases need to maintain consistency without slowing everything down. Earlier systems used locking: Write → others wait Read → may block writes This works, but doesn’t scale well under load. What’s actually happening under MVCC Every transaction is assigned a transaction ID (TXID). When a query starts, it sees a snapshot based on visible TXIDs. Each row internally stores metadata like: created by TXID deleted/updated by TXID So visibility is not “current value” - it’s: “is this version valid for my transaction?” How reads work The database scans rows. For each row, it checks: → Was this version committed before my transaction started? If yes → visible If not → ignored So reads are basically filtering versions, not locking data. How writes work- An update does NOT modify the row in place. It creates a new row version with a new TXID. The old version is marked as expired (but not deleted yet). This is why: readers continue unaffected writers don’t wait for readers Isolation levels matter here MVCC behavior changes slightly depending on isolation level: Read Committed → sees latest committed version per query Repeatable Read → sees same snapshot for entire transaction Serializable → adds extra checks to avoid anomalies So “what we see” depends on isolation, not just MVCC itself. The hidden cost Because updates create new versions: Table bloat can happen if cleanup is slow Indexes also grow with multiple versions Long-running transactions delay cleanup Cleanup (e.g., VACUUM in Postgres) is critical - without it, performance degrades over time. Pros: No read/write blocking, Predictable reads, High concurrency Cons: More storage usage, Vacuum tuning required, Complex internals, Stale reads depending on isolation Reality: MVCC is a trade-off: we spend more storage + background work to get less waiting + better throughput That trade-off is why most modern relational databases use it. Article: https://lnkd.in/gVArBVgf #BackendEngineering #Database #PostgreSQL #MVCC
To view or add a comment, sign in
-
-
Boost PostgreSQL performance with 12 expert tips for large datasets from partitioning to partial indexes and avoiding bloat. Stay fast and scale. https://lnkd.in/dvibkzSy #PostgreSQL #SoftwareDevelopment #Performance
To view or add a comment, sign in
-
Our PostgreSQL queries were taking 8 seconds. I got them under 200ms. Here's exactly what changed and what didn't. Context: We were running a multi-tenant SaaS. As the user base grew, a core query started degrading badly. Users noticed. It had to be fixed. Step 1: EXPLAIN ANALYZE before touching anything -sql EXPLAIN ANALYZE SELECT * FROM bookings WHERE tenant_id = $1 AND status = 'confirmed' ORDER BY created_at DESC; Output showed: Sequential scan on 2M rows. No index on tenant_id + status. Step 2: Composite index -sql CREATE INDEX idx_bookings_tenant_status ON bookings (tenant_id, status, created_at DESC); This alone dropped the query from 8s to 900ms. Still not good enough. Step 3: Stop selecting * The query was pulling 40 columns. The UI needed 6. -sql SELECT id, title, start_time, end_time, status, attendee_id FROM bookings WHERE tenant_id = $1 AND status = 'confirmed' ORDER BY created_at DESC LIMIT 50; Down to 350ms. Step 4: N+1 was hiding in the ORM For each booking, the ORM was firing a separate query to fetch the attendee. Switched to a JOIN. -sql SELECT b.id, b.title, u.name as attendee_name FROM bookings b JOIN users u ON u.id = b.attendee_id WHERE b.tenant_id = $1 AND b.status = 'confirmed' ORDER BY b.created_at DESC LIMIT 50; Final result: 180ms. What didn't help: connection pooling tuning (already fine), adding a cache (premature fix the query first), switching databases (never needed to). 8 seconds → 180ms. No infrastructure changes. Just understanding what the database was actually doing. What's your go-to first step when a query goes slow? #PostgreSQL #SQL #DatabaseOptimization #QueryOptimization #BackendEngineering #FullStackDevelopment #SoftwareEngineering
To view or add a comment, sign in
-
Deep dive into a SQL query: A Journey Through PostgreSQL's Query Processing by Jesús Espino is the featured book 📖 on Leanpub! What really happens when PostgreSQL executes your query? Follow a SQL statement through every stage of PostgreSQL's internal pipeline—from raw text to returned results—and gain the deep understanding that transforms how you write, tune, and debug database applications. Link: https://lnkd.in/gibxA75B #postgresql #databases
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
-
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