π PostgreSQL Indexes Demystified: Pick the Right One π
---
### Why Indexes?
β Speed up lookups, joins, and ORDER BY
β Reduce I/O and CPU on hot queries
β Enforce uniqueness & data quality
---
### The Quick Map
πΉ B-tree (default): equality/range on scalar types; ideal for
πΉ GIN: many-to-many & containment (JSONB, arrays, full-text).
πΉ GiST: proximity & custom types (geo, ranges, trigram).
πΉ BRIN: huge append-only tables with natural order (timestamps, IDs).
---
### Quick Start (copy & adapt)
---
### Check Itβs Used
π Look for
---
### Rules of Thumb
π§ Match index order to your WHERE + ORDER BY.
π Add a tiebreaker (e.g.,
βοΈ Use partial indexes for sparse filters:
π§© Prefer exact types (avoid implicit casts).
π° For JSONB:
π§Ή Maintain:
---
### Anti-Patterns to Avoid
β βOne index per columnβ on wide tables
β Functions in predicates without matching expression index
β Random BRIN on tiny or highly shuffled tables
β Over-indexing writes-heavy tables (insert/update cost!)
---
### Mini Checklist
βοΈ Query stable? (shape wonβt change tomorrow)
βοΈ Columns & order mirror filter/sort?
βοΈ Can a partial or covering index cut heap lookups?
βοΈ Verified with
#PostgreSQL #SQL #Database #Performance #DBA #DevOps
@postgres
---
### Why Indexes?
β Speed up lookups, joins, and ORDER BY
β Reduce I/O and CPU on hot queries
β Enforce uniqueness & data quality
---
### The Quick Map
πΉ B-tree (default): equality/range on scalar types; ideal for
=, <, >, BETWEEN, ORDER BY. πΉ GIN: many-to-many & containment (JSONB, arrays, full-text).
πΉ GiST: proximity & custom types (geo, ranges, trigram).
πΉ BRIN: huge append-only tables with natural order (timestamps, IDs).
---
### Quick Start (copy & adapt)
-- B-tree: common filters / sorts
CREATE INDEX idx_user_created ON users (created_at DESC);
-- Composite + deterministic order
CREATE INDEX idx_orders_ct_id ON orders (created_at DESC, id DESC);
-- INCLUDE to cover SELECT list
CREATE INDEX idx_invoice_cover ON invoices (customer_id) INCLUDE (total);
-- Expression index (avoid runtime functions)
CREATE INDEX idx_lower_email ON users ((lower(email)));
-- JSONB containment (GIN)
CREATE INDEX idx_prod_tags_gin ON products USING GIN (tags);
-- BRIN for massive time-series
CREATE INDEX idx_logs_brin ON logs USING BRIN (ts);
---
### Check Itβs Used
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE created_at >= now() - interval '7 days'
ORDER BY created_at DESC
LIMIT 50;
π Look for
Index Scan/Only Scan, low rows, no external sort.---
### Rules of Thumb
π§ Match index order to your WHERE + ORDER BY.
π Add a tiebreaker (e.g.,
id) for stable pagination. βοΈ Use partial indexes for sparse filters:
CREATE INDEX idx_active_only ON users (last_login) WHERE active = true;
π§© Prefer exact types (avoid implicit casts).
π° For JSONB:
@> + GIN; for full-text: GIN on to_tsvector(...). π§Ή Maintain:
VACUUM (ANALYZE), watch bloat, reindex if needed.---
### Anti-Patterns to Avoid
β βOne index per columnβ on wide tables
β Functions in predicates without matching expression index
β Random BRIN on tiny or highly shuffled tables
β Over-indexing writes-heavy tables (insert/update cost!)
---
### Mini Checklist
βοΈ Query stable? (shape wonβt change tomorrow)
βοΈ Columns & order mirror filter/sort?
βοΈ Can a partial or covering index cut heap lookups?
βοΈ Verified with
EXPLAIN (ANALYZE)?#PostgreSQL #SQL #Database #Performance #DBA #DevOps
@postgres
π₯1
π§© JSONB in Production: Fast Patterns That Scale π
Why
β Flexibility of JSON with the reliability of Postgres.
β Powerful indexing & operators for speed.
Query Patterns
Do β
Index by access pattern: containment β GIN; single field β expression index.
Keep JSONB lean (no huge blobs); normalize stable keys where it helps.
Use EXPLAIN (ANALYZE, BUFFERS) to confirm index usage.
Donβt β
Donβt store everything in one giant JSONB; joins on normalized tables can be faster.
Donβt cast at runtime without a matching expression index.
Donβt forget VACUUM (ANALYZE); JSONB updates can cause churn.
Mini Checklist
βοΈ Chosen operators: @>, ?, ?|, ->, ->> fit the query?
βοΈ Indexes aligned with filters/containment?
βοΈ Verified with EXPLAIN (ANALYZE) under realistic work_mem?
#PostgreSQL #JSONB #SQL #Database #Performance #DevOps @postgres
Why
β Flexibility of JSON with the reliability of Postgres.
β Powerful indexing & operators for speed.
Query Patterns
undefined
-- Containment (has all keys/values)
SELECT id FROM products
WHERE attrs @> '{"color":"black","size":"M"}';
-- Property path filter
SELECT id FROM orders
WHERE (data ->> 'status') = 'paid';
-- Any of these tags?
SELECT id FROM articles
WHERE (tags ?| array['pg','sql','tips']);
Indexes that Matter
undefined
-- General-purpose GIN for JSONB containment / existence
CREATE INDEX idx_prod_attrs_gin ON products USING GIN (attrs);
-- Expression index for a hot field
CREATE INDEX idx_orders_status ON orders ((data ->> 'status'));
-- Partial index to target common filter
CREATE INDEX idx_paid_recent ON orders ((data ->> 'status'))
WHERE (data ->> 'status') = 'paid';
Do β
Index by access pattern: containment β GIN; single field β expression index.
Keep JSONB lean (no huge blobs); normalize stable keys where it helps.
Use EXPLAIN (ANALYZE, BUFFERS) to confirm index usage.
Donβt β
Donβt store everything in one giant JSONB; joins on normalized tables can be faster.
Donβt cast at runtime without a matching expression index.
Donβt forget VACUUM (ANALYZE); JSONB updates can cause churn.
Mini Checklist
βοΈ Chosen operators: @>, ?, ?|, ->, ->> fit the query?
βοΈ Indexes aligned with filters/containment?
βοΈ Verified with EXPLAIN (ANALYZE) under realistic work_mem?
#PostgreSQL #JSONB #SQL #Database #Performance #DevOps @postgres
β€1π₯1
π Zero to Production PostgreSQL in 30 Minutes
Yesterday we covered the economics. Today: How to actually set up production PostgreSQL that won't embarrass you.
The One-Command Setup Nobody Teaches:
Most tutorials stop at apt install postgresql. That's not production. Here's production:
π Security Essentials (10 Minutes):
π Essential Monitoring (5 Minutes):
π Automated Backup Verification:
π° Cost Breakdown (Self-Hosted):
#PostgreSQL #Production #SelfHosted #SoloDev #DevOps
@postgres
Yesterday we covered the economics. Today: How to actually set up production PostgreSQL that won't embarrass you.
The One-Command Setup Nobody Teaches:
Most tutorials stop at apt install postgresql. That's not production. Here's production:
#!/bin/bash
# production-pg-setup.sh - Production PostgreSQL in one script
# 1. Install PostgreSQL 16
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list
apt update && apt install -y postgresql-16
# 2. Configure for production
cat >> /etc/postgresql/16/main/postgresql.conf << EOF
# Connection settings
max_connections = 100
shared_buffers = 1GB
effective_cache_size = 3GB
work_mem = 10MB
maintenance_work_mem = 256MB
# WAL settings
wal_buffers = 16MB
checkpoint_completion_target = 0.9
max_wal_size = 2GB
# Query planning
random_page_cost = 1.1
effective_io_concurrency = 200
# Monitoring
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
EOF
# 3. Set up automated backups to S3
pip3 install wal-g
cat > /root/.walrc << EOF
export WALG_S3_PREFIX="s3://my-backups/postgres"
export AWS_ACCESS_KEY_ID="your-key"
export AWS_SECRET_ACCESS_KEY="your-secret"
EOF
echo "0 2 * * * . /root/.walrc && wal-g backup-push /var/lib/postgresql/16/main" | crontab -
# 4. Set up monitoring
apt install -y prometheus-postgres-exporter
systemctl enable prometheus-postgres-exporter
# 5. Basic security
ufw allow 5432/tcp
ufw enable
echo "β Production PostgreSQL ready!"
π Security Essentials (10 Minutes):
-- 1. Create app user (NOT postgres superuser!)
CREATE USER myapp WITH PASSWORD 'strong-random-password';
CREATE DATABASE myapp_db OWNER myapp;
-- 2. Restrict permissions
REVOKE ALL ON SCHEMA public FROM PUBLIC;
GRANT USAGE ON SCHEMA public TO myapp;
-- 3. Enable SSL
-- In postgresql.conf:
-- ssl = on
-- ssl_cert_file = '/etc/ssl/certs/server.crt'
-- ssl_key_file = '/etc/ssl/private/server.key'
-- 4. Configure pg_hba.conf properly
-- hostssl myapp_db myapp 0.0.0.0/0 scram-sha-256
π Essential Monitoring (5 Minutes):
-- Install pg_stat_statements
CREATE EXTENSION pg_stat_statements;
-- Daily health check query
CREATE VIEW health_check AS
SELECT
'DB Size' as metric,
pg_size_pretty(pg_database_size(current_database())) as value
UNION ALL
SELECT
'Active Connections',
count(*)::text
FROM pg_stat_activity
WHERE state = 'active'
UNION ALL
SELECT
'Cache Hit Ratio',
round(100.0 * sum(blks_hit) / nullif(sum(blks_hit + blks_read), 0), 2)::text || '%'
FROM pg_stat_database;
-- Check it daily
SELECT * FROM health_check;
π Automated Backup Verification:
#!/bin/bash
# backup-verify.sh - Run weekly
# 1. Take backup
pg_dump myapp_db > /tmp/test_backup.sql
# 2. Restore to test database
createdb test_restore
psql test_restore < /tmp/test_backup.sql
# 3. Run sanity checks
psql test_restore -c "SELECT count(*) FROM users;" > /tmp/user_count.txt
# 4. Compare with production
if [ "$(cat /tmp/user_count.txt)" == "$(psql myapp_db -c 'SELECT count(*) FROM users;')" ]; then
echo "β Backup verified!"
else
echo "β Backup verification FAILED!"
# Send alert
fi
# 5. Cleanup
dropdb test_restore
π° Cost Breakdown (Self-Hosted):
VPS (Hetzner CPX21): $20/month
S3 backup storage (50GB): $1/month
Monitoring (self-hosted): $0/month
SSL cert (Let's Encrypt): $0/month
Your time (2 hours/month): Priceless learning
ββββββββββββββββββββββββββββββββββββββββββββββ
Total: $21/month
vs AWS RDS equivalent: $150/month
Annual savings: $1,548/year
#PostgreSQL #Production #SelfHosted #SoloDev #DevOps
@postgres
π―2β€1
π Docker vs Bare Metal: The Real Production Choice
Everyone says "use Docker!" But should you? Let's be honest about deployment for solo devs.
The Deployment Reality Check:
Docker Lovers Say:
"Consistent environments!"
"Easy scaling!"
"Portable everywhere!"
Your Actual Needs (Solo Dev):
One server
PostgreSQL + your app
Backups that work
Monitoring that doesn't lie
Real Deployment Comparison:
Option 1: Bare Metal ($20 VPS)
Option 2: Docker Compose
Option 3: Docker + Managed (Best of Both)
π― The Solo Dev Deployment Strategy:
Stage 1 (0-1K users): Bare metal
Simple, fast, cheap
Learn PostgreSQL deeply
Stage 2 (1K-10K users): Docker for app, bare metal DB
Easy app updates
Database stays stable
Stage 3 (10K+ users): Containers + managed DB
Now complexity pays off
Blue-Green Deployments (Without K8s):
CI/CD for Database Changes:
Real Developer Experiences:
"Spent 2 days fixing Docker networking. Switched to bare metal. Been running 14 months, zero issues." - Mike T.
"Docker-compose for dev/staging. Bare PostgreSQL for production. Perfect balance." - Sarah K.
Tomorrow: Monitoring That Actually Helps
What monitoring setup do you use? Docker or bare metal?
#PostgreSQL #Docker #Deployment #DevOps #SoloDev
@postgres
Everyone says "use Docker!" But should you? Let's be honest about deployment for solo devs.
The Deployment Reality Check:
Docker Lovers Say:
"Consistent environments!"
"Easy scaling!"
"Portable everywhere!"
Your Actual Needs (Solo Dev):
One server
PostgreSQL + your app
Backups that work
Monitoring that doesn't lie
Real Deployment Comparison:
Option 1: Bare Metal ($20 VPS)
# 10 minutes to production
apt install postgresql-16
systemctl enable postgresql
# Done. It just works.
Pros:
β Dead simple
β Max performance
β Direct access to logs
β No Docker overhead
Cons:
β Manual updates
β No container isolation
Option 2: Docker Compose
# docker-compose.yml
services:
db:
image: postgres:16
volumes:
- ./data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: secret
restart: always
Pros:
β Version locked
β Easy local dev parity
β One-command updates
Cons:
β Volume permission hell
β 10-15% performance hit
β Backup complexity
Option 3: Docker + Managed (Best of Both)
# App in Docker, DB managed
services:
app:
build: .
environment:
DATABASE_URL: ${RDS_URL}
π― The Solo Dev Deployment Strategy:
Stage 1 (0-1K users): Bare metal
Simple, fast, cheap
Learn PostgreSQL deeply
Stage 2 (1K-10K users): Docker for app, bare metal DB
Easy app updates
Database stays stable
Stage 3 (10K+ users): Containers + managed DB
Now complexity pays off
Blue-Green Deployments (Without K8s):
# Simple zero-downtime deployment
# 1. Deploy to new port
docker run -d -p 3001:3000 app:new
# 2. Test
curl localhost:3001/health
# 3. Switch nginx
sed -i 's/3000/3001/g' /etc/nginx/sites-enabled/app
nginx -s reload
# 4. Stop old version
docker stop app:old
CI/CD for Database Changes:
# .github/workflows/deploy.yml
- name: Run migrations
run: |
# Always forward, never back
psql $DATABASE_URL -f migrations/$(date +%Y%m%d).sql
# Test rollback locally first!
# psql local_db -f migrations/rollback.sql
Real Developer Experiences:
"Spent 2 days fixing Docker networking. Switched to bare metal. Been running 14 months, zero issues." - Mike T.
"Docker-compose for dev/staging. Bare PostgreSQL for production. Perfect balance." - Sarah K.
Tomorrow: Monitoring That Actually Helps
What monitoring setup do you use? Docker or bare metal?
#PostgreSQL #Docker #Deployment #DevOps #SoloDev
@postgres
β€1
π Stop Flying Blind: PostgreSQL Monitoring for $0
The Only 5 Metrics That Matter:
Free Monitoring Stack (Better than Paid):
1. Grafana + Prometheus (Local)
2. pg_stat_statements (Built-in)
Alert Fatigue Prevention:
DON'T Alert On:
CPU at 80% for 1 minute
Single slow query
Temporary connection spike
DO Alert On:
Disk space < 10%
Replication lag > 5 minutes
Connection pool > 90%
Same query slow > 10 times
Performance Baselines:
Tomorrow: [PREMIUM] Complete Admin Dashboard
Your biggest monitoring pain point?
#PostgreSQL #Monitoring #Observability #DevOps #Performance
@postgres
The Only 5 Metrics That Matter:
-- 1. Slow queries killing performance
CREATE VIEW monitoring.slow_queries AS
SELECT
SUBSTRING(query, 1, 50) as query_start,
calls,
mean_exec_time::numeric(10,2) as avg_ms,
total_exec_time::numeric(10,2)/1000 as total_sec,
100.0 * total_exec_time / sum(total_exec_time) OVER() as percentage
FROM pg_stat_statements
WHERE query NOT LIKE '%pg_stat%'
ORDER BY mean_exec_time DESC
LIMIT 10;
-- 2. Table bloat eating disk
CREATE VIEW monitoring.table_bloat AS
SELECT
schemaname || '.' || tablename as table_name,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as total_size,
n_dead_tup,
n_live_tup,
round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) as dead_percent
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
-- 3. Connection pool exhaustion
CREATE VIEW monitoring.connections AS
SELECT
state,
COUNT(*) as count,
COUNT(*) * 100.0 / current_setting('max_connections')::int as percentage
FROM pg_stat_activity
GROUP BY state
UNION ALL
SELECT
'total' as state,
COUNT(*) as count,
COUNT(*) * 100.0 / current_setting('max_connections')::int as percentage
FROM pg_stat_activity;
-- 4. Replication lag (if using replicas)
CREATE VIEW monitoring.replication_status AS
SELECT
client_addr,
state,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) as lag_size,
EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))::int as lag_seconds
FROM pg_stat_replication;
-- 5. Cache hit ratio (should be >99%)
CREATE VIEW monitoring.cache_performance AS
SELECT
sum(heap_blks_read) as disk_reads,
sum(heap_blks_hit) as cache_hits,
CASE
WHEN sum(heap_blks_hit) + sum(heap_blks_read) = 0 THEN 0
ELSE round(100.0 * sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)), 2)
END as cache_hit_ratio
FROM pg_statio_user_tables;
Free Monitoring Stack (Better than Paid):
1. Grafana + Prometheus (Local)
# 5-minute setup
docker run -d -p 9090:9090 prom/prometheus
docker run -d -p 9187:9187 wrouesnel/postgres_exporter
docker run -d -p 3000:3000 grafana/grafana
# Import dashboard: 9628
# Done. Professional monitoring.
2. pg_stat_statements (Built-in)
-- Enable it once
CREATE EXTENSION pg_stat_statements;
-- Find query killers
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 5;
Alert Fatigue Prevention:
DON'T Alert On:
CPU at 80% for 1 minute
Single slow query
Temporary connection spike
DO Alert On:
Disk space < 10%
Replication lag > 5 minutes
Connection pool > 90%
Same query slow > 10 times
Performance Baselines:
-- Save weekly baselines
CREATE TABLE monitoring.baselines (
week_of DATE,
avg_query_time NUMERIC,
total_connections INTEGER,
cache_hit_ratio NUMERIC,
largest_table_size BIGINT
);
-- Weekly snapshot
INSERT INTO monitoring.baselines
SELECT
date_trunc('week', NOW()),
(SELECT AVG(mean_exec_time) FROM pg_stat_statements),
(SELECT COUNT(*) FROM pg_stat_activity),
(SELECT round(100.0 * sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)), 2) FROM pg_statio_user_tables),
(SELECT MAX(pg_total_relation_size(schemaname||'.'||tablename)) FROM pg_stat_user_tables);
Tomorrow: [PREMIUM] Complete Admin Dashboard
Your biggest monitoring pain point?
#PostgreSQL #Monitoring #Observability #DevOps #Performance
@postgres
β€1