PostgreSQL Pro | Database Mastery
1.33K subscribers
1 photo
28 links
🐘 PostgreSQL Mastery Hub

🎯 What you get:
- Daily optimization tips
- Performance guides
- Real-world solutions
- Query debugging help
- Production best practices

πŸ“ˆ Join 500+ developers improving their PostgreSQL skills
Download Telegram
πŸš€ 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 =, <, >, 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
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:

#!/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)

# 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:
-- 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