Claude Code for PostgreSQL Performance: Indexes, Query Plans, and Partitioning — Claude Skills 360 Blog
Blog / Database / Claude Code for PostgreSQL Performance: Indexes, Query Plans, and Partitioning
Database

Claude Code for PostgreSQL Performance: Indexes, Query Plans, and Partitioning

Published: December 14, 2026
Read time: 10 min read
By: Claude Skills 360

PostgreSQL performance optimization is systematic: start with EXPLAIN ANALYZE to understand the query plan, check index usage, add missing indexes, and reformulate expensive queries. Partial indexes cover common filter patterns with a fraction of the storage. Table partitioning enables partition pruning for time-series data. PgBouncer handles connection multiplexing for serverless workloads. Claude Code analyzes query plans, suggests index strategies, generates partition schemes, and writes the configuration for production PostgreSQL deployments.

CLAUDE.md for PostgreSQL Performance

## PostgreSQL Context
- Version: PostgreSQL 16+ (Logical Replication, incremental sort improvements)
- Connection pooling: PgBouncer transaction mode (required for serverless)
- Extensions: pg_stat_statements (enabled), pg_trgm (trigram search), pgvector (embeddings)
- Monitoring: track_io_timing=on, track_functions=all for pg_stat_statements
- Autovacuum: aggressive settings for high-write tables (scale_factor=0.01, cost_delay=2)
- Indexes: analyze with pg_stat_user_indexes — drop unused (idx_scan = 0 after 30 days)
- Table sizes: use pg_size_pretty(pg_total_relation_size()) to check before/after

Reading EXPLAIN ANALYZE

-- Always include these options for complete plan
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT 
    o.id,
    o.status,
    o.total_cents,
    c.name AS customer_name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.status = 'pending'
  AND o.created_at >= NOW() - INTERVAL '7 days'
ORDER BY o.created_at DESC
LIMIT 50;

-- Common plan nodes and what they mean:
-- Seq Scan: Full table scan — might need an index
-- Index Scan: Uses B-tree index for lookup + fetch — good
-- Index Only Scan: Uses covering index — best (no heap fetch)
-- Bitmap Heap Scan: Multiple row fetch via bitmap — good for range scans
-- Hash Join: Larger table hashed in memory — watch for high Memory Usage
-- Nested Loop: Inner query runs N times for each outer row — bad at scale
-- Sort: Sorts in memory (workmem) or to disk — check "(Disk: )" in output

-- Red flags in output:
-- "rows=1 loops=50000" — N+1 pattern
-- "Buffers: read=50000 hit=100" — low cache hit
-- "Sort Method: external merge Disk: 48kB" — needs work_mem increase
-- "actual time=5000..5001" — last row almost as slow as first: parallel needed

Index Design Patterns

-- Pattern 1: Composite index for common WHERE + ORDER BY
-- Query: WHERE status = X ORDER BY created_at DESC LIMIT N
CREATE INDEX CONCURRENTLY idx_orders_status_created 
    ON orders (status, created_at DESC);
-- CONCURRENTLY builds without table lock (safe for production)

-- Pattern 2: Partial index for common filter values
-- If 90% of queries are for 'pending' orders only
CREATE INDEX CONCURRENTLY idx_orders_pending_created 
    ON orders (created_at DESC) 
    WHERE status = 'pending';
-- Much smaller — only ~10% of rows instead of 100%

-- Pattern 3: Covering index (include non-key columns to avoid heap fetch)
-- Query: SELECT id, customer_id, total_cents FROM orders WHERE status = 'pending'
CREATE INDEX CONCURRENTLY idx_orders_status_covering
    ON orders (status)
    INCLUDE (id, customer_id, total_cents);
-- Enables Index Only Scan — zero heap access

-- Pattern 4: Expression index for case-insensitive search
CREATE INDEX CONCURRENTLY idx_customers_email_lower
    ON customers (lower(email));
-- Query can use: WHERE lower(email) = lower('[email protected]')

-- Pattern 5: Trigram index for LIKE/ILIKE anywhere search
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX CONCURRENTLY idx_orders_notes_trgm
    ON orders USING gin (notes gin_trgm_ops);
-- Query: WHERE notes ILIKE '%shipping delay%' uses this index

-- Pattern 6: JSONB index for deep property access
CREATE INDEX CONCURRENTLY idx_orders_metadata_source
    ON orders ((metadata->>'acquisition_source'));
-- Or GIN for arbitrary JSONB queries:
CREATE INDEX CONCURRENTLY idx_orders_metadata_gin
    ON orders USING gin (metadata);

-- Check index usage after deploying (wait 24h for realistic traffic)
SELECT
    indexrelname,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch,
    pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE relname = 'orders'
ORDER BY idx_scan DESC;

-- Identify missing indexes (sequential scans on large tables)
SELECT
    relname AS table_name,
    seq_scan,
    idx_scan,
    n_live_tup,
    seq_scan - idx_scan AS seq_vs_idx
FROM pg_stat_user_tables
WHERE n_live_tup > 100000
  AND seq_scan > idx_scan
ORDER BY seq_scan DESC;

Table Partitioning

-- Range partitioning for time-series orders (most common pattern)
CREATE TABLE orders (
    id              UUID NOT NULL DEFAULT gen_random_uuid(),
    customer_id     UUID NOT NULL,
    status          TEXT NOT NULL,
    total_cents     INTEGER NOT NULL,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (created_at);

-- Create monthly partitions
CREATE TABLE orders_2026_10 PARTITION OF orders
    FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');

CREATE TABLE orders_2026_11 PARTITION OF orders
    FOR VALUES FROM ('2026-11-01') TO ('2026-12-01');

CREATE TABLE orders_2026_12 PARTITION OF orders
    FOR VALUES FROM ('2026-12-01') TO ('2027-01-01');

-- Default partition for data that doesn't fit (important for safety)
CREATE TABLE orders_default PARTITION OF orders DEFAULT;

-- Indexes on each partition (or create on parent after Pg 11)
CREATE INDEX ON orders (created_at, status);

-- Automate partition creation (run monthly via pg_cron)
CREATE OR REPLACE FUNCTION create_monthly_partition(year INT, month INT)
RETURNS VOID AS $$
DECLARE
    partition_name TEXT;
    start_date DATE;
    end_date DATE;
BEGIN
    partition_name := format('orders_%s_%s', year, lpad(month::TEXT, 2, '0'));
    start_date := make_date(year, month, 1);
    end_date := start_date + INTERVAL '1 month';
    
    EXECUTE format(
        'CREATE TABLE IF NOT EXISTS %I PARTITION OF orders FOR VALUES FROM (%L) TO (%L)',
        partition_name, start_date, end_date
    );
    
    RAISE NOTICE 'Created partition %', partition_name;
END;
$$ LANGUAGE plpgsql;

-- Verify partition pruning is working
EXPLAIN (ANALYZE, VERBOSE)
SELECT * FROM orders
WHERE created_at >= '2026-10-01' AND created_at < '2026-11-01';
-- Should show: "Partitions selected: 1" in the Append node

Query Optimization Patterns

-- Rewrite correlated subquery as JOIN
-- SLOW: correlated subquery runs once per outer row
SELECT 
    c.id,
    c.name,
    (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS order_count
FROM customers c;

-- FAST: lateral or aggregated join
SELECT 
    c.id,
    c.name,
    COALESCE(o.order_count, 0) AS order_count
FROM customers c
LEFT JOIN (
    SELECT customer_id, COUNT(*) AS order_count
    FROM orders
    GROUP BY customer_id
) o ON c.id = o.customer_id;

-- Efficient pagination with keyset (no OFFSET for large pages)
-- SLOW: OFFSET 10000 scans and discards 10000 rows
SELECT * FROM orders ORDER BY created_at DESC LIMIT 20 OFFSET 10000;

-- FAST: keyset pagination using last seen value
SELECT * FROM orders
WHERE created_at < '2026-10-15T12:00:00Z'  -- last_created_at from previous page
ORDER BY created_at DESC
LIMIT 20;

-- Window function instead of self-join
-- SLOW: self-join to calculate running total
SELECT 
    o.id,
    o.total_cents,
    (SELECT SUM(total_cents) FROM orders o2 WHERE o2.created_at <= o.created_at) AS running_total
FROM orders o;

-- FAST: window function
SELECT 
    id,
    total_cents,
    SUM(total_cents) OVER (ORDER BY created_at) AS running_total
FROM orders;

PgBouncer Configuration

# pgbouncer.ini — transaction mode for serverless/high-connection environments
[databases]
myapp = host=postgres port=5432 dbname=myapp pool_size=20

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 5432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt

# Transaction mode: connection returned to pool after each transaction
# Use for serverless functions, microservices
pool_mode = transaction

# Connection limits
max_client_conn = 1000       # Max incoming connections from app
default_pool_size = 20       # Max connections to PostgreSQL per database
reserve_pool_size = 5        # Extra connections for bursts
reserve_pool_timeout = 5     # Seconds to wait before using reserve

# Keep-alive
server_idle_timeout = 600
client_idle_timeout = 0      # No client timeout (app manages this)

# Logging
log_connections = 0          # Too noisy for production
log_disconnections = 0
log_pooler_errors = 1
stats_period = 60

For the Drizzle ORM that generates the SQL queries this guide optimizes, see the Drizzle ORM guide for query patterns and schema definitions. For the advanced PostgreSQL features in TimescaleDB for time-series data, the TimescaleDB guide covers hypertables and continuous aggregates. The Claude Skills 360 bundle includes PostgreSQL skill sets covering index design, query plan analysis, and partitioning strategies. Start with the free tier to try query optimization generation.

Keep Reading

Database

Claude Code for Neo4j: Graph Databases and Cypher Queries

Model and query graph data with Neo4j and Claude Code — Cypher query patterns, relationship traversal, graph algorithms with GDS, recommendation engines, fraud detection patterns, and Python/Node.js driver integration.

9 min read Dec 27, 2026
Database

Claude Code for Advanced PostgreSQL: Window Functions, CTEs, and Query Optimization

Master advanced PostgreSQL with Claude Code — window functions for analytics, recursive CTEs, lateral joins, partial indexes, query plan analysis with EXPLAIN ANALYZE, and materialized views.

9 min read Aug 24, 2026
AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free