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

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
博客园_首页
Forbes - Security
Forbes - Security
WordPress大学
WordPress大学
P
Proofpoint News Feed
T
Threat Research - Cisco Blogs
L
LINUX DO - 热门话题
L
Lohrmann on Cybersecurity
Spread Privacy
Spread Privacy
D
Darknet – Hacking Tools, Hacker News & Cyber Security
大猫的无限游戏
大猫的无限游戏
博客园 - 三生石上(FineUI控件)
P
Privacy International News Feed
A
About on SuperTechFans
T
Tailwind CSS Blog
I
InfoQ
S
Securelist
云风的 BLOG
云风的 BLOG
罗磊的独立博客
Recent Announcements
Recent Announcements
T
The Exploit Database - CXSecurity.com
B
Blog RSS Feed
V
Visual Studio Blog
Know Your Adversary
Know Your Adversary
The GitHub Blog
The GitHub Blog
Jina AI
Jina AI
腾讯CDC
Cyberwarzone
Cyberwarzone
有赞技术团队
有赞技术团队
AWS News Blog
AWS News Blog
博客园 - 【当耐特】
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
F
Full Disclosure
S
Secure Thoughts
博客园 - 司徒正美
J
Java Code Geeks
Y
Y Combinator Blog
Google Online Security Blog
Google Online Security Blog
GbyAI
GbyAI
N
News and Events Feed by Topic
Help Net Security
Help Net Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Project Zero
Project Zero
T
Tenable Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Tor Project blog
MyScale Blog
MyScale Blog
Scott Helme
Scott Helme
小众软件
小众软件
K
Kaspersky official blog

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
PostgreSQL Query Anti-Patterns and Common Mistakes
Philip McCla · 2026-05-05 · via DEV Community

Most of the articles in this series are about making reasonable queries faster. This one is about queries that are wrong by construction — patterns that account for most production SQL performance incidents, not because the query is subtle, but because the pattern is near-universal and the fix is well-known to people who've seen it before. If you recognise these in your codebase, fixing them is almost always a straightforward win.

Each anti-pattern below maps to a specific plan signature or log symptom; cross-references to deeper treatment are inline.

1. N+1 queries — the ORM default

The single most common performance problem in applications talking to PostgreSQL. The pattern is one query to fetch a list of parent rows, plus one query per parent to fetch related data:

# One query — fetches 500 orders.
orders = db.query("SELECT * FROM orders WHERE status = 'pending'")

# Five hundred more queries — one per order.
for order in orders:
    items = db.query(
        "SELECT * FROM order_items WHERE order_id = %s",
        order.id,
    )

Enter fullscreen mode Exit fullscreen mode

501 network round trips. 501 query plans. 501 parse-and-execute cycles. Even if each individual query is 2 ms, you've burned a full second of wall-clock time on a dashboard query that could have been one SQL statement.

Fix: a single JOIN.

SELECT o.order_id, o.status, o.created_at,
       oi.item_id, oi.quantity, oi.unit_price_cents
FROM sim_bp_orders o
JOIN sim_bp_order_items oi ON oi.order_id = o.order_id
WHERE o.status = 'pending';

Enter fullscreen mode Exit fullscreen mode

One query. The application then groups the result by order_id to reconstruct the parent-children shape.

Detecting N+1 from PostgreSQL side: pg_stat_statements will show many rows of the same normalised order_items query with different parameter values. Most modern ORMs can eager-load related rows with a single JOIN when you ask — in SQLAlchemy it's joinedload(); in ActiveRecord, .includes(:items); in Django, prefetch_related(). Usually a one-line change.

2. SELECT * on wide tables in hot paths

Every column the query returns has a cost: bytes read from heap, bytes serialised over the wire, bytes deserialised on the client. A 400-column table with SELECT * returns hundreds of bytes per row that the application typically discards.

The worse consequence is plan quality. An Index Only Scan can only be chosen when every referenced column is available from the index — SELECT * forces heap fetches unconditionally. A covering index with INCLUDE becomes useless because the planner still has to visit the heap for the columns not in the index.

Fix: name the columns you actually need. The resulting plan can use covering indexes, narrower sort keys, and tighter network payload.

Where it's OK to keep SELECT *: database-admin queries, dump scripts, and CTEs that genuinely need every column. In production application code, it's almost always wrong.

3. Implicit type casts that disable indexes

Covered in detail in the WHERE clause article. The symptom: an index exists on the filtered column, but the plan shows Seq Scan with the filter appearing in a Filter: line instead of Index Cond:.

-- text column compared against integer literal.
SELECT * FROM t WHERE text_col = 123;

Enter fullscreen mode Exit fullscreen mode

PostgreSQL coerces the text column to an int for the comparison, which disables the index. The three fix patterns:

  • Use the correct literal type: WHERE text_col = '123'.
  • Cast the literal explicitly: WHERE text_col = 123::text.
  • If the column semantically should be an int, change the schema.

This anti-pattern also appears after schema migrations — a column type change from varchar(N) to citext or a domain type can silently shift which side of the comparison gets cast. Always re-run EXPLAIN on the key queries after any column-type change.

4. Functions on indexed columns

The other half of the non-sargable family. Wrapping an indexed column in a function disables the index:

SELECT * FROM sim_bp_users WHERE lower(email) = '...';
SELECT * FROM sim_bp_orders WHERE date_trunc('day', created_at) = '2024-01-15';

Enter fullscreen mode Exit fullscreen mode

On our dataset, WHERE lower(email) LIKE 'user12%' produces a parallel sequential scan at 122 ms. The same query against the plain column with a pattern-ops index takes 25 ms.

Fixes:

  • Normalise on write — store emails lowercased, dates as dates not timestamps, and so on.
  • Expression index that computes the same function — CREATE INDEX ON sim_bp_users (lower(email)).
  • Rewrite the predicate to avoid the function — WHERE created_at >= '2024-01-15' AND created_at < '2024-01-16' instead of date_trunc('day', created_at) = '2024-01-15'.

5. Missing LIMIT on exploratory joins

A query written in development against a 100-row test table can become a 10-million-row query in production. Joins without a LIMIT are the most common source of surprise-large result sets — a new feature ships, the WHERE clause on the production dataset matches a million rows instead of a hundred, and the application silently returns a million rows per request.

-- No LIMIT — returns however many rows happen to match.
SELECT o.*, u.email
FROM sim_bp_orders o JOIN sim_bp_users u ON u.user_id = o.user_id
WHERE o.status = 'pending';

Enter fullscreen mode Exit fullscreen mode

Fix: add a LIMIT. For user-facing queries, paginate (ideally with keyset — see query rewriting techniques). For background jobs, chunk the processing.

This is also a security posture question. A query without a LIMIT exposed through an API endpoint gives an attacker a trivial amplification attack — one HTTP request → one full-table read.

6. One-row-at-a-time INSERTs

Bulk-loading data with a loop of INSERT INTO t VALUES ($1, $2, $3) calls is common in migrations, ETL jobs, and CSV importers. Each statement is a full round trip plus a plan-and-execute cycle; for a million rows, that's a million of each.

Fixes, roughly in order of preference:

  • COPY FROM STDIN — PostgreSQL's bulk-load protocol. Orders of magnitude faster than INSERT because it avoids per-row parsing and planning. Most drivers expose it.
  • Multi-row VALUESINSERT INTO t (...) VALUES ($1, ...), ($2, ...), ($3, ...) packs multiple rows into one statement. Ten to a hundred per call is a sweet spot.
  • INSERT INTO t SELECT ... FROM source — if the source data is already in the database, do the load in SQL and skip the client round-trip entirely.

Wrap the bulk operation in a single transaction so WAL flushes happen at commit, not per row. synchronous_commit = off for the loading session is safe for non-durable data and further speeds things up.

7. Keeping transactions open

Transactions hold locks and prevent vacuum cleanup on the tables they've read. A transaction left open for an hour — usually by a stuck batch job or a long-running analytical query — blocks autovacuum across the whole database for the duration. Symptoms: growing table bloat, mysterious missed vacuum schedules, XID-wraparound warnings on busy databases.

The specific antipattern is usually application-level:

# Accidentally nests transactions; if one step hangs, the whole transaction hangs.
with db.transaction():
    rows = db.query("SELECT ...")  # reads a snapshot.
    for row in rows:
        call_external_api(row)  # could block for minutes.
        db.execute("UPDATE ...")

Enter fullscreen mode Exit fullscreen mode

Fix: keep transactions short. Never call out to external services inside a transaction. Batch the reads outside the transaction; inside the transaction, do only the minimum set of SQL changes that need to be atomic.

pg_stat_activity shows xact_start for every running transaction; any row where now() - xact_start > interval '5 minutes' is worth investigating. The idle in transaction state is the most common failure mode.

8. Storing dates, numbers, or booleans as strings

Every time a type-mismatched predicate runs, the planner either (a) casts the literal to text (sargable, OK) or (b) casts the column to the right type (not sargable, disables index). The schema wins: if the column is created_at text, every query that filters it numerically or by date pays a non-sargable cost.

-- created_at stored as text like '2024-01-15 14:22:00'.
SELECT * FROM logs
WHERE created_at::timestamp > now() - interval '1 hour';  -- non-sargable.

Enter fullscreen mode Exit fullscreen mode

Fix: migrate to the right type. ALTER TABLE ... ALTER COLUMN ... TYPE timestamptz USING created_at::timestamptz; — takes a brief ACCESS EXCLUSIVE lock, so pick a quiet moment; for very large tables do an online migration (new column, backfill, switch, drop old).

Indexes on the right type are smaller, faster, and actually usable by predicates. Indexes on text-representations of dates have all the downsides of neither — locale-sensitive comparison, extra parse cost, and usually the wrong sort order for natural ranges.

9. count(*) as a cheap operation

Applications often display "total records: 847,291" somewhere, computed with SELECT count(*) FROM big_table. On PostgreSQL, that's a full scan — the storage layer doesn't maintain row counts because of MVCC visibility. A 100-million-row count(*) takes seconds to minutes.

Fixes:

  • Accept an approximate count. pg_class.reltuples is the planner's cached estimate — good within a few percent on recently-analyzed tables, and free. SELECT reltuples::bigint FROM pg_class WHERE relname = 'big_table'.
  • Trigger-maintained counter table. If you truly need an exact count and it's shown on every page load, maintain it with an AFTER INSERT/DELETE trigger that updates a tiny summary table.
  • Avoid the need for a count. Pagination UIs that show "Page 127 of 3,482,195" rarely benefit from the total; "Page 127" with Next/Previous buttons is enough.

10. Ignoring pg_stat_statements

The extension that tracks query statistics — most-expensive queries, average time per execution, total time across all runs. On any non-toy database it's the single most useful diagnostic tool.

CREATE EXTENSION pg_stat_statements;

-- Top 10 queries by total time:
SELECT query,
       calls,
       total_exec_time,
       mean_exec_time,
       rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

Enter fullscreen mode Exit fullscreen mode

Total time (not per-call time) is the right sort: a 2 ms query called a million times is a bigger target than a 5 s query called once a day.

Set pg_stat_statements.track = all (not the default top) to capture statements inside functions too. On cloud-managed instances, this is often already on by default.

Catching anti-patterns automatically

Many of these patterns show up as specific plan shapes:

  • seq_scan_large → patterns 3, 4, 5 (non-sargable predicates, missing indexes).
  • excessive_filter_rows → pattern 5 (missing LIMIT, wide filter).
  • nested_loop_large → pattern 1 (N+1 at plan level, or missing join index).
  • no_index_usage → patterns 3, 4, 8 (index never usable because of casts or types).
  • hash_batches_spill, sort_on_disk, temp_blocks_writtenwork_mem / aggregate patterns from earlier articles.

The workflow is the same every time: capture a plan, identify the category, apply the fix, verify. Most of the value is in recognising the category quickly — the fixes are standard patterns once the category is clear. The pillar guide ties the full series together.


postgres #performance #database #sql

Originally published at https://mydba.dev/blog/postgres-query-anti-patterns