Skip to main content

database-postgres-query-optimization

Debugging slow queries in PostgreSQL

معلومات المصدر

المستودع
tools-only/X-Skills
آخر نشاط في المصدر
١ مارس ٢٠٢٦ في ٠٣:٣٧
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٧
التفرعات
١

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
database-postgres-query-optimization
description
Debugging slow queries in PostgreSQL
# PostgreSQL Query Optimization **Scope**: Query analysis, EXPLAIN plans, index strategies, query rewriting **Lines**: ~350 **Last Updated**: 2025-10-18 ## When to Use This Skill Activate this skill when: - Debugging slow queries in PostgreSQL - Analyzing EXPLAIN ANALYZE output - Designing index strategies - Optimizing JOIN operations - Investigating performance degradation - Planning database capacity ## Core Concepts ### Query Execution Stages 1. **Parsing** - SQL syntax validation 2. **Planning** - Query optimizer creates execution plan 3. **Execution** - Plan executed, data retrieved 4. **Results** - Data returned to client The planner's goal: find the lowest-cost plan. ### Cost Model PostgreSQL uses a cost-based optimizer: - **Startup cost**: Time before first row returned - **Total cost**: Time to return all rows - **Sequential scan cost**: Reading full table - **Index scan cost**: Reading index + table lookups ```sql -- Cost units are arbitrary (not milliseconds) -- Lower cost = better plan (usually) Seq Scan on users (cost=0.00..1234.56 rows=10000 width=64) Index Scan using idx_users_email (cost=0.29..8.31 rows=1 width=64) ``` --- ## Reading EXPLAIN ANALYZE Output ### Basic EXPLAIN ```sql EXPLAIN SELECT * FROM users WHERE email = 'user@example.com'; ``` Output shows the **planned** execution (no actual execution). ### EXPLAIN ANALYZE ```sql EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'user@example.com'; ``` Output shows **planned + actual** execution with real timing. **CRITICAL**: EXPLAIN ANALYZE actually runs the query (including writes!). Use with caution on production. ### Key Metrics to Watch ``` Seq Scan on users (cost=0.00..1234.56 rows=10000 width=64) (actual time=0.012..12.345 rows=1 loops=1) ^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLANNED cost/rows ACTUAL time/rows/loops ``` **Red flags**: - `actual rows >> estimated rows` - Statistics out of date - `actual time >> expected` - I/O bottleneck or missing index - `Seq Scan` on large tables - Usually needs index - `loops > 1` on expensive operations - Nested loop inefficiency --- ## Scan Types ### Sequential Scan ``` Seq Scan on orders (cost=0.00..15234.56 rows=500000 width=128) ``` **When it happens**: - No usable index - Table too small (faster to scan than use index) - Query reads >5-10% of table (index overhead not worth it) **Good for**: - Small tables (<1000 rows typically) - Reading most/all of table - Analytics queries on warehouses **Bad for**: - Large tables with selective filters - OLTP lookups - Real-time queries ### Index Scan ``` Index Scan using idx_orders_user_id on orders (cost=0.42..8.44 rows=1 width=128) Index Cond: (user_id = 12345) ``` **How it works**: 1. Traverse B-tree index to find entries 2. Fetch heap tuples (actual table rows) via pointers 3. Return results **Good for**: - Selective queries (<5% of rows) - Equality comparisons (`=`, `IN`) - Range scans on indexed columns - ORDER BY on indexed columns **Cost factors**: - Random I/O to fetch heap tuples (expensive) - Multiple index lookups if fetching many rows ### Index Only Scan ``` Index Only Scan using idx_orders_user_created on orders (cost=0.42..4.44 rows=1 width=8) Index Cond: (user_id = 12345) Heap Fetches: 0 ``` **How it works**: 1. Traverse index 2. Return data DIRECTLY from index (no heap lookup) 3. Check visibility map to verify row visibility **Requirements**: - All query columns in index (covering index) - Table must be vacuumed (visibility map current) **BEST PERFORMANCE**: No random I/O to heap. ```sql -- Create covering index for this query CREATE INDEX idx_orders_user_created ON orders(user_id) INCLUDE (created_at); -- Query can now use Index Only Scan SELECT created_at FROM orders WHERE user_id = 12345; ``` ### Bitmap Index Scan ``` Bitmap Heap Scan on orders (cost=123.45..5678.90 rows=5000 width=128) Recheck Cond: (status = 'pending' OR status = 'processing') -> BitmapOr (cost=123.45..123.45 rows=5000 width=0) -> Bitmap Index Scan on idx_orders_status (cost=0.00..61.00 rows=2500 width=0) Index Cond: (status = 'pending') -> Bitmap Index Scan on idx_orders_status (cost=0.00..61.00 rows=2500 width=0) Index Cond: (status = 'processing') ``` **How it works**: 1. Build in-memory bitmap of matching rows 2. Combine bitmaps (OR, AND operations) 3. Sort row locations 4. Fetch heap tuples in sequential order (reduces random I/O) **Good for**: - Combining multiple indexes (`OR` conditions) - Fetching moderate number of rows (5-25% of table) - Reducing random I/O **Better than**: - Multiple Index Scans → merge results - Full Seq Scan when filtered set is small enough --- ## Index Strategies ### Index Types #### B-tree (Default, 95% of use cases) ```sql CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_orders_composite ON orders(user_id, created_at); ``` **Good for**: - Equality: `email = 'foo@example.com'` - Range: `created_at > '2024-01-01'` - Sorting: `ORDER BY created_at` - Prefix matching: `email LIKE 'foo%'` (NOT `LIKE '%foo'`) **Multi-column indexes** (composite): - Order matters: `(user_id, created_at)` can be used for: - `user_id = X` - `user_id = X AND created_at > Y` - NOT efficient for `created_at > Y` alone #### Hash (Rare, equality only) ```sql CREATE INDEX idx_users_email_hash ON users USING HASH (email); ``` **Good for**: - Equality ONLY: `email = 'foo@example.com'` - Slightly smaller than B-tree **Cannot**: - Range queries - Sorting - Prefix matching **Usually not needed** - B-tree handles equality well. #### GiST (Geometric/Full-text) ```sql CREATE INDEX idx_locations_geom ON locations USING GIST (geom); CREATE INDEX idx_documents_fts ON documents USING GIST (to_tsvector('english', content)); ``` **Good for**: - Geometric data (PostGIS) - Full-text search - Range types - Custom data types #### GIN (Full-text, JSONB, arrays) ```sql CREATE INDEX idx_documents_fts ON documents USING GIN (to_tsvector('english', content)); CREATE INDEX idx_users_tags ON users USING GIN (tags); -- array column CREATE INDEX idx_metadata_json ON events USING GIN (metadata); -- jsonb column ``` **Good for**: - Full-text search (faster than GiST for static data) - JSONB queries: `metadata @> '{"status": "active"}'` - Array containment: `tags @> ARRAY['postgres']` **Trade-offs**: - Larger than GiST - Slower writes - Faster reads for containment queries #### BRIN (Block Range Index) ```sql CREATE INDEX idx_logs_created_brin ON logs USING BRIN (created_at); ``` **Good for**: - Very large tables with natural clustering (e.g., time-series) - Append-only data - Low-selectivity queries acceptable **Extremely small index** (1000x smaller than B-tree). **Trade-off**: Less precise, may scan extra blocks. --- ### Index Selection Decision Tree ``` Start: Do I need an index? │ ├─ Table < 1000 rows? → NO (Seq Scan is fine) ├─ Query reads >10% of table? → MAYBE (test both) └─ Query is selective? → YES │ ├─ What type of query? │ ├─ Equality (=, IN) → B-tree │ ├─ Range (<, >, BETWEEN) → B-tree │ ├─ Sorting (ORDER BY) → B-tree on sort columns │ ├─ Full-text search → GIN or GiST │ ├─ JSONB queries → GIN │ ├─ Geometric queries → GiST │ ├─ Time-series append-only → BRIN │ └─ Array containment → GIN │ ├─ Multiple columns in WHERE? │ ├─ Always used together → Composite index (user_id, created_at) │ ├─ Used independently → Separate indexes (or partial indexes) │ └─ OR conditions → Bitmap scan or separate indexes │ └─ Can I cover the query? → Add INCLUDE columns for Index Only Scan ``` --- ## Common Query Anti-Patterns ### 1. N+1 Query Problem ```sql -- Anti-pattern: Load users, then loop and query orders for each SELECT * FROM users; -- In application loop: -- SELECT * FROM orders WHERE user_id = ? -- Solution: JOIN or batch query SELECT u.*, o.* FROM users u LEFT JOIN orders o ON o.user_id = u.id; ``` ### 2. SELECT * Instead of Specific Columns ```sql -- Anti-pattern SELECT * FROM orders WHERE user_id = 123; -- Better: Select only needed columns SELECT id, total, created_at FROM orders WHERE user_id = 123; ``` **Why it matters**: - Enables Index Only Scans (covering indexes) - Reduces network transfer - Lower memory usage ### 3. Non-Sargable Queries (Can't Use Index) ```sql -- Anti-pattern: Function on indexed column SELECT * FROM users WHERE LOWER(email) = 'foo@example.com'; -- Solution: Functional index CREATE INDEX idx_users_email_lower ON users(LOWER(email)); -- Or store email in lowercase always ``` ```sql -- Anti-pattern: Wildcard at start SELECT * FROM users WHERE email LIKE '%@example.com'; -- Solution: Full-text search or trigram index CREATE EXTENSION pg_trgm; CREATE INDEX idx_users_email_trgm ON users USING GIN (email gin_trgm_ops); ``` ### 4. Unnecessary DISTINCT ```sql -- Anti-pattern: DISTINCT when not needed SELECT DISTINCT user_id FROM orders WHERE status = 'pending'; -- Better if user_id is already unique per status: SELECT user_id FROM orders WHERE status = 'pending'; ``` ### 5. OR Conditions Across Tables ```sql -- Anti-pattern: OR across tables SELECT * FROM orders WHERE user_id = 123 OR vendor_id = 456; -- Better: UNION SELECT * FROM orders WHERE user_id = 123 UNION SELECT * FROM orders WHERE vendor_id = 456; ``` --- ## Query Rewriting Patterns ### Pattern 1: Subquery → JOIN ```sql -- Slower: Correlated subquery SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 100); -- Faster: JOIN with DISTINCT SELECT DISTINCT u.* FROM users u INNER JOIN orders o ON o.user_id = u.id WHERE o.total > 100; -- Or EXISTS (often better for large datasets) SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.total > 100); ``` ### Pattern 2: JOIN → Semi-Join (EXISTS) ```sql -- When you only need to check existence: SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id); -- NOT: SELECT DISTINCT u.* FROM users u INNER JOIN orders o ON o.user_id = u.id; ``` **EXISTS is faster** when you don't need order data, just existence check. ### Pattern 3: Partial Index for Filtered Queries ```sql -- Common query: SELECT * FROM orders WHERE status = 'pending'; -- Create partial index CREATE INDEX idx_orders_pending ON orders(created_at) WHERE status = 'pending'; ``` **Benefits**: - Smaller index (only pending orders) - Faster writes (only updated when status = pending) - Perfect for skewed data (e.g., 1% pending, 99% completed) ### Pattern 4: Materialized View for Complex Aggregations ```sql -- Slow query run frequently: SELECT user_id, COUNT(*), SUM(total) FROM orders GROUP BY user_id; -- Create materialized view CREATE MATERIALIZED VIEW user_order_stats AS SELECT user_id, COUNT(*) as order_count, SUM(total) as total_spent FROM orders GROUP BY user_id; CREATE UNIQUE INDEX idx_user_order_stats_user ON user_order_stats(user_id); -- Refresh periodically REFRESH MATERIALIZED VIEW CONCURRENTLY user_order_stats; ``` --- ## Index Maintenance ### Statistics PostgreSQL uses table statistics to estimate row counts and plan queries. ```sql -- Outdated statistics cause bad plans -- Fix: Analyze the table ANALYZE users; ANALYZE orders; -- Auto-vacuum should handle this, but manual ANALYZE helps after bulk changes ``` ### Reindexing Indexes can become bloated over time. ```sql -- Check index bloat SELECT schemaname, tablename, indexname, pg_size_pretty(pg_relation_size(indexrelid)) as index_size FROM pg_stat_user_indexes ORDER BY pg_relation_size(indexrelid) DESC; -- Rebuild index REINDEX INDEX idx_users_email; REINDEX TABLE users; -- All indexes on table -- Or recreate index (allows CONCURRENTLY, less locking) CREATE INDEX CONCURRENTLY idx_users_email_new ON users(email); DROP INDEX CONCURRENTLY idx_users_email; ALTER INDEX idx_users_email_new RENAME TO idx_users_email; ``` ### Monitoring Index Usage ```sql -- Find unused indexes SELECT schemaname, tablename, indexname, idx_scan
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub