惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
月光博客
月光博客
AI
AI
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
CXSECURITY Database RSS Feed - CXSecurity.com
WordPress大学
WordPress大学
GbyAI
GbyAI
L
Lohrmann on Cybersecurity
O
OpenAI News
Schneier on Security
Schneier on Security
P
Palo Alto Networks Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Troy Hunt's Blog
V2EX - 技术
V2EX - 技术
W
WeLiveSecurity
L
LINUX DO - 最新话题
人人都是产品经理
人人都是产品经理
S
Security Affairs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
A
Arctic Wolf
Recorded Future
Recorded Future
Microsoft Security Blog
Microsoft Security Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
G
GRAHAM CLULEY
N
Netflix TechBlog - Medium
TaoSecurity Blog
TaoSecurity Blog
C
Check Point Blog
Scott Helme
Scott Helme
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
PCI Perspectives
PCI Perspectives
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Vercel News
Vercel News
The Hacker News
The Hacker News
Y
Y Combinator Blog
Latest news
Latest news
SecWiki News
SecWiki News
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Google Online Security Blog
Google Online Security Blog
Webroot Blog
Webroot Blog
Google DeepMind News
Google DeepMind News
Recent Commits to openclaw:main
Recent Commits to openclaw:main
C
Cisco Blogs
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
宝玉的分享
宝玉的分享
L
LINUX DO - 热门话题

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Debugging PostgreSQL Performance
Carlos Saldaña · 2026-06-02 · via DEV Community

Let's chat about performance, optimization, and debugging a PostgreSQL database. Can you read EXPLAIN ANALYZE output fluently? Do you know how to find which tenant is causing a slow query, how to use pg_stat_statements, how to spot lock contention? If you have a hard time answering those, don't worry. Today we're going to cover all of them and practice how to get better at it.

The goal here: you should be able to look at a slow API endpoint and trace it down to the exact query, the exact tenant, the exact missing index, in under 10 minutes. That's the bar.

EXPLAIN ANALYZE: read the plan, not the vibes

Let's start with EXPLAIN ANALYZE because everything else builds on it.

So why does this exist?

The query planner makes a cost-based guess about the best way to execute your SQL query. EXPLAIN ANALYZE actually runs the query and shows you whether that guess was correct.

Always use this form:

EXPLAIN (ANALYZE, BUFFERS) SELECT ...;

ANALYZE → Runs the query and reports what actually happened.

BUFFERS → Shows cache hits vs. disk reads. Without it, performance analysis is incomplete.

⚠️ ANALYZE executes the query for real, including writes. Use BEGIN and ROLLBACK to test without committing changes.

The single most important skill: estimated vs actual
Every node prints two row counts. When rows (estimate) and actual rows diverge by more than ~10x, the planner is working from bad information.

How to read the tree

The plan is a tree. Read the most-indented node first. That's where execution starts. Costs and times are cumulative: a parent node's time includes its children.

actual time=X..Y → X is time to first row, Y is time to last row, per loop. If loops=5000, the real total for that node is Y × 5000. This is how a "0.05 ms" node secretly costs 4 seconds.

The best resource if you're starting out is explain.depesz.com. You can paste any EXPLAIN output and it visualizes the slow nodes. Use it on your own queries until you can predict what it'll say before you paste.

Index Design

I'm not going to go deep into index design here. Instead, read Use The Index, Luke! by Markus Winand.

It's free, easy to read, and probably the best resource available on SQL indexes. Many senior engineers use indexes every day, but haven't taken the time to read it from start to finish.

If you read one resource on indexing, make it this one.

If you've ever thought indexes were some kind of database magic, you're not alone. Most of the confusion disappears once you realize they're just a clever way of keeping data sorted.

The key idea is that a B-tree index is a sorted copy of one or more columns, plus pointers to the corresponding table rows.

The fact that it's sorted is what gives it its power. PostgreSQL can quickly find the rows it needs instead of reading the entire table.

The rule that matters most: leftmost prefix

An index on (tenant_id, created_at, status) is sorted by tenant_id, then within
each tenant by created_at, then by status. So it can serve:

✅ WHERE tenant_id = ?
✅ WHERE tenant_id = ? AND created_at > ?
✅ WHERE tenant_id = ? AND created_at > ? AND status = ?
✅ ORDER BY created_at      (within a fixed tenant_id)
❌ WHERE created_at > ?     (skips the leading column, can't seek)
❌ WHERE status = ?         (skips two leading columns)

You can only use a contiguous prefix, starting from the left. This is why column
order in a composite index is a design decision
, not an afterthought.

Things that quietly break index usage

  • Using a function on the indexed column

    WHERE lower(email) = ? can't use a regular index on email.

    Fix: create an expression index:

    CREATE INDEX ON users (lower(email));

  • Modifying the column in the filter

    WHERE created_at + interval '1 day' > now() makes it harder to use the index.

    Instead, apply the calculation to the constant side of the comparison.

  • Leading wildcards

    LIKE '%foo' can't efficiently use a B-tree index.

    LIKE 'foo%' can.

    For substring searches, use a trigram index (pg_trgm).

  • Type mismatches

    Implicit casts on the column side can prevent index usage. Make sure the types match.

  • Low-selectivity columns

    An index on a boolean column or a status column with only a few possible values is often not useful by itself. PostgreSQL may choose a sequential scan instead. Consider a composite index or a partial index.

Index features worth knowing

  • Covering indexes (INCLUDE)

    Add extra columns to the index so PostgreSQL can answer the query without reading the table. This can enable an Index Only Scan, which is often faster.

    CREATE INDEX ON orders (tenant_id, created_at)
    INCLUDE (total, status);
    
  • Partial indexes

    Index only the rows you actually query. The index is smaller and cheaper to maintain. Often faster, too.

    CREATE INDEX ON jobs (created_at)
    WHERE status = 'pending';
    
  • B-tree is the one you'll use 95% of the time. Learn it first. The others exist for specialized data types and query patterns.

The trade-off: indexes aren't free

Indexes make reads faster, but they make writes more expensive.

Every INSERT, UPDATE, and DELETE must update the table and all of its indexes. More indexes mean more disk, more memory, more work on every write.

Regularly check for unused indexes (pg_stat_user_indexes.idx_scan = 0) and remove them when appropriate.

The goal isn't to have more indexes. The goal is to have the fewest indexes needed to support your important queries.

pg_stat_statements: where the server actually spends its life

When someone tells me "the database is slow," this is usually the first place I look. Most performance problems aren't caused by one terrible query. They're caused by a handful of queries quietly consuming time all day long.

A production application can execute thousands of different queries. You can't run EXPLAIN ANALYZE on all of them.

pg_stat_statements solves this problem by collecting execution statistics for every normalized query. Queries that differ only by parameter values are grouped together, making it easy to see where the database is spending its time.

To enable it, add the following to postgresql.conf:

shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all

Restart PostgreSQL, then create the extension:

CREATE EXTENSION pg_stat_statements;

Learn these columns:

  • calls → How many times the query ran.
  • total_exec_time → Total time spent executing the query.
  • mean_exec_time → Average execution time per call.
  • rows → Total rows returned or affected.
  • shared_blks_hit vs shared_blks_read → Cache hits vs disk reads.

The most useful question you can ask in production is:

Where is my database spending most of its time?

This query answers it:

SELECT
  substring(query, 1, 80) AS query,
  calls,
  round(total_exec_time) AS total_ms,
  round(mean_exec_time, 2) AS mean_ms,
  rows,
  round(
    100.0 * shared_blks_hit /
    nullif(shared_blks_hit + shared_blks_read, 0),
    1
  ) AS hit_pct
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

This tells you:

  • which queries are globally expensive,
  • slowest average queries,
  • most frequently executed queries.

This is often where real tuning starts in production.

Sort by total time, not average time

Many engineers focus on mean_exec_time, but total_exec_time is usually the more important metric.

A query that takes 3 ms and runs 2 million times per hour can consume far more resources than a report that takes 2 seconds and runs twice a day.

Think about it this way:

  • Sort by total execution time to find where the server is spending most of its effort.
  • Sort by average execution time to find individually slow queries.

Both views are useful, but total_exec_time is where you'll usually find the biggest wins.

Resetting the stats

When you want to start a fresh measurement window:

SELECT pg_stat_statements_reset();

Running the "Top 20 by total execution time" report every week is one of the simplest ways to catch performance problems before they become outages.

auto_explain: catch the slow query you can't reproduce

pg_stat_statements tells you which queries are slow overall. What it can't tell you is why a query was slow at 2 AM during an incident.

Execution plans can change as data grows, statistics become outdated, or different parameter values are used. A query that's normally fast can suddenly take a very different path.

That's where auto_explain helps.

It automatically logs the full execution plan for queries that exceed a time threshold, capturing the plan that was actually used when the slowdown happened.

Configure it in postgresql.conf:

session_preload_libraries = 'auto_explain'

auto_explain.log_min_duration = '500ms'
auto_explain.log_analyze = on
auto_explain.log_buffers = on
auto_explain.log_nested_statements = on
auto_explain.log_format = 'text'

Use it carefully in production

log_analyze = on measures actual execution times, which adds overhead to every logged query.

A safe approach is:

  • Start with a relatively high threshold (for example, 500 ms or 1 second).
  • Leave log_analyze off initially.
  • Enable more detailed logging only when investigating a performance issue.

Why it matters

Think of the tools as solving different problems:

  • pg_stat_statements answers: "Which queries are costing us the most?"
  • auto_explain answers: "What plan did PostgreSQL actually use when this query became slow?"

Together they form one of the most valuable performance debugging tool sets in PostgreSQL.

pg_stat_statements helps you find the problem.

auto_explain helps you understand it.

When you have both, performance investigations become much less about guessing and much more about reading the evidence.

pg_locks: who is blocking whom

When a query suddenly stops making progress, the problem is often not the query itself. It's waiting for a lock.

pg_locks shows one row for every lock currently held or being waited on by PostgreSQL sessions.

The most important column is:

  • granted = true → the lock has been acquired.
  • granted = false → the session is waiting for a lock held by someone else.

Joining it with pg_stat_activity gives you the lock plus the SQL behind it:

SELECT
  l.pid,
  l.locktype,
  l.mode,
  l.granted,
  left(a.query, 60) AS query
FROM pg_locks l
JOIN pg_stat_activity a ON a.pid = l.pid
ORDER BY l.granted, l.pid;

A waiting query is often a symptom of lock contention somewhere else in the system.

Deadlocks: not a database failure, a concurrency bug

A deadlock happens when two transactions are waiting on each other:

  1. Transaction A holds lock 1 and wants lock 2.
  2. Transaction B holds lock 2 and wants lock 1.

Neither transaction can continue, creating a cycle.

PostgreSQL detects this situation automatically (after deadlock_timeout, which defaults to 1 second) and resolves it by aborting one of the transactions with a deadlock detected error.

Your application should be prepared to catch this error and retry the transaction.

One lesson that's easy to forget: a deadlock isn't a database failure. It's a concurrency bug. PostgreSQL is doing exactly what it should, so the system can move forward.

For lock contention specifically, the canonical resource is the Postgres wiki page "Lock Monitoring." Search for it, bookmark it; the query at the bottom that shows blocking chains is one you'll run many times in your career.

Use it with pg_stat_activity

On its own, pg_locks is hard to read: raw pids and lock modes, no context.

That's why the query above joins it with pg_stat_activity. Together they show you:

  • Which queries are currently waiting.
  • Which sessions are blocking them.
  • How long they've been waiting.
  • The SQL being executed by both sides.

When debugging database issues, think of these views as a pair:

  • pg_stat_activity → What is everyone doing?
  • pg_locks → Who is blocking whom?

Together they help you diagnose lock contention, blocked queries, and deadlocks before they turn into production incidents.

Once you've worked through all of this, the natural next step is MVCC and VACUUM.

That's where you'll learn why idle in transaction causes bloat, why dead rows affect query performance, and why a perfectly reasonable index can still be slow.

In my experience, MVCC is the point where PostgreSQL starts to make sense as a system instead of a collection of features. Many performance problems that look mysterious become obvious once you understand how PostgreSQL stores and cleans up row versions.