Advanced PostgreSQL
Materialized Views and Partitioning
Pre-computing queries and splitting massive tables on disk.
1. Introduction
Materialized Views pre-compute and store the result of a complex query as a physical table, dramatically speeding up read-heavy workloads. Table Partitioning (PG 10+ declarative) splits a large table into smaller physical pieces based on a partition key (range, list, or hash), improving query performance, maintenance operations, and data lifecycle management. Both are essential tools for scaling PostgreSQL to billions of rows.
2. Why It Matters
A dashboard query joining 10 tables with aggregations might take 30 seconds on raw data but 50ms from a materialized view. A 500 GB table scanned with partition pruning might only read 2 GB of relevant data. Without these tools, large datasets force expensive full-table scans and slow aggregation queries that can't meet SLA requirements.
3. Real-World Analogy
A materialized view is like a printed report that you generate once and hand out — readers get instant answers, but the data becomes stale until you reprint (REFRESH). Partitioning is like organizing a warehouse by month — when someone asks for "March orders," you only walk to the March aisle instead of searching every shelf.
4. How It Works
- Materialized Views: Created with
CREATE MATERIALIZED VIEW. The query is executed once and results stored on disk. Refreshed viaREFRESH MATERIALIZED VIEW(full rewrite) orCONCURRENTLY(requires a UNIQUE index, doesn't block reads). - Partitioning strategies: Range (dates, IDs), List (categories, regions), Hash (even distribution). The planner uses partition pruning to skip irrelevant partitions at query time.
- Declarative syntax (PG 10+):
PARTITION BY RANGE (created_at)on the parent table, thenCREATE TABLE ... PARTITION OF ... FOR VALUES FROM ... TO ....
5. Internal Architecture
Partitions are separate tables linked to the parent via inheritance metadata. Each partition has its own indexes, statistics, and storage. The planner inspects the WHERE clause against partition bounds and eliminates non-matching partitions before execution (static pruning). Some joins and subqueries enable dynamic pruning at execution time. Materialized views are stored as regular heap tables with their own indexes — they don't auto-update when underlying data changes.
6. Visual Explanation
The diagram shows a parent orders table partitioned by month, with queries using partition pruning to access only relevant partitions, plus a materialized view caching aggregated dashboard data.
7. Practical Example
8. Common Mistakes
- Forgetting to refresh materialized views: Data becomes stale. Schedule refreshes via
pg_cronor application schedulers. - Querying partitioned table without the partition key in WHERE: The planner can't prune and scans all partitions — same as an unpartitioned table.
- Missing indexes on partitions: Indexes on the parent don't auto-create on partitions. Create indexes on each partition or use
CREATE INDEX ON parent(...)which propagates. - Over-partitioning: Too many partitions (thousands) slow down the planner due to partition metadata overhead.
9. Quick Quiz
Q1: What is required for REFRESH MATERIALIZED VIEW CONCURRENTLY?
A) A primary key B) A UNIQUE index C) A GIN index D) Nothing special
Answer: B) A UNIQUE index on the materialized view
10. Scenario-Based Challenge
You have a 2 TB events table queried mostly by date ranges. Design a partitioning scheme (monthly partitions), create a materialized view for daily active user counts, set up a pg_cron job to refresh the view every hour, and write a migration to add a new future partition automatically.
11. Debugging Exercise
12. Interview Questions
- Q: What's the difference between a VIEW and a MATERIALIZED VIEW?
A: A VIEW is a stored query that executes on every access (always fresh, slower). A MATERIALIZED VIEW stores the query result on disk (fast reads, but stale until refreshed). - Q: When would you choose hash partitioning over range?
A: Hash partitioning is ideal when there's no natural range/list key and you need even data distribution (e.g., by user_id). Range is better for time-series data where queries filter by date ranges.
13. Production Considerations
- Partition maintenance: Create future partitions in advance via pg_cron. Detach old partitions with
ALTER TABLE ... DETACH PARTITIONfor archival. - REFRESH CONCURRENTLY: Always use this in production — non-concurrent refresh takes an ACCESS EXCLUSIVE lock.
- Foreign keys: PG 12+ supports FK references to partitioned tables. Earlier versions don't.
- Partition count limit: Keep partitions under ~1,000 for optimal planner performance. Use sub-partitioning if needed.