{"post":{"slug":"how-to-optimize-sql-queries-2026","title":"How to optimize SQL queries in 2026: indexes, plans, and real fixes","excerpt":"Slow SQL is rarely a mystery — the query plan tells you exactly what is wrong. This 2026 guide covers the tools and techniques to find and fix slow queries fast.","category":"code","date":"April 1, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">S</span>low SQL queries are the most common performance problem in web applications — and almost always self-inflicted. The good news is that SQL databases are remarkably transparent about what they are doing. The query plan tells you exactly where the time is going; you just have to know how to read it. This is the 2026 playbook for finding and fixing slow queries in PostgreSQL and MySQL.\n\n## What changed in 2026\n\n- **pgvector is mainstream.** Hybrid queries mixing vector similarity (`<->`) with traditional SQL filters are common. Vector indexes (IVFFlat, HNSW) have their own optimisation rules — most importantly, pre-filtering before ANN search.\n- **Query plan visualisers are better.** `EXPLAIN` output is still text, but tools like `explain.dalibo.com`, PgAdmin 4, and Datadog's query analysis visualise plans interactively. Use them.\n- **CTE optimisation changed.** PostgreSQL 12+ inlines simple CTEs by default. If you are relying on a CTE as an \"optimisation fence,\" add `MATERIALIZED` explicitly.\n- **Connection pooling is non-negotiable.** PgBouncer and Pgpool-II are standard in 2026 stacks; direct Postgres connections at scale cause contention that no index can fix.\n\n## Step 1: find the slow query\n\nDo not optimise by intuition. Use your database's slow query log.\n\n```sql\n-- PostgreSQL: enable in postgresql.conf\nlog_min_duration_statement = 1000  -- log queries > 1 second\n\n-- Or query pg_stat_statements for the top offenders\nSELECT query, calls, mean_exec_time, total_exec_time\nFROM pg_stat_statements\nORDER BY total_exec_time DESC\nLIMIT 10;\n```\n\n```sql\n-- MySQL: enable slow query log\nSET GLOBAL slow_query_log = 'ON';\nSET GLOBAL long_query_time = 1;\n```\n\n## Step 2: read EXPLAIN ANALYZE\n\n```sql\nEXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) \nSELECT u.email, COUNT(o.id) AS order_count\nFROM users u\nJOIN orders o ON o.user_id = u.id\nWHERE u.created_at > '2026-01-01'\nGROUP BY u.email;\n```\n\nKey things to look for in the output:\n\n| Signal | What it means |\n| --- | --- |\n| `Seq Scan` on a large table | No usable index — add one |\n| `rows=1` estimated vs `rows=500000` actual | Stale statistics — run `ANALYZE table_name` |\n| `Hash Join` vs `Nested Loop` | Nested loop on large sets is slow; planner may need a hint or index |\n| High `Buffers: shared hit=0 read=X` | Cold data fetched from disk — consider caching layer |\n| `Sort (cost=...)` on large dataset | Missing index on `ORDER BY` column |\n\n## The N+1 problem\n\nThe most common production killer in ORMs:\n\n```python\n# Bad: 1 query for users + 1 per user for orders = N+1\nusers = db.query(User).all()\nfor user in users:\n    orders = db.query(Order).filter(Order.user_id == user.id).all()\n    # → 1 + N queries\n\n# Good: 1 query with a JOIN\nusers = db.query(User).options(joinedload(User.orders)).all()\n# → 1 query\n```\n\nDetect N+1 with query logging: if you see the same query repeated with different parameter values, you have N+1. Tools like `sqlalchemy-utils` `explain` or Django Debug Toolbar will surface it automatically.\n\n## Index strategy\n\n```sql\n-- Index columns you filter on\nCREATE INDEX idx_orders_user_id ON orders(user_id);\n\n-- Composite index: most selective column first\nCREATE INDEX idx_orders_status_created ON orders(status, created_at);\n\n-- Index for ORDER BY + LIMIT (avoid filesort)\nCREATE INDEX idx_users_created_desc ON users(created_at DESC);\n\n-- Partial index: only index the rows you actually query\nCREATE INDEX idx_orders_pending ON orders(created_at)\nWHERE status = 'pending';\n\n-- Index for text search (PostgreSQL)\nCREATE INDEX idx_products_name_gin ON products USING GIN (to_tsvector('english', name));\n```\n\n**Do not index everything.** Each index adds ~10–30% overhead to INSERT/UPDATE/DELETE on that table. Add indexes only when `EXPLAIN` shows a Seq Scan on a large table and you have confirmed the query is slow in production.\n\n## Common slow query patterns and fixes\n\n```sql\n-- Pattern 1: LIKE with leading wildcard (can't use B-tree index)\n-- Bad\nWHERE name LIKE '%smith%'\n-- Fix: use full-text search or trigram index\nCREATE EXTENSION IF NOT EXISTS pg_trgm;\nCREATE INDEX idx_name_trgm ON users USING GIN (name gin_trgm_ops);\n\n-- Pattern 2: function on indexed column disables the index\n-- Bad\nWHERE LOWER(email) = 'user@example.com'\n-- Fix: use a functional index\nCREATE INDEX idx_email_lower ON users (LOWER(email));\n\n-- Pattern 3: implicit type cast\n-- Bad (email is varchar, but passing an int coerces)\nWHERE user_id = '42'   -- in a typed column\n-- Fix: match types exactly\n\n-- Pattern 4: SELECT * with an index-only scan opportunity\n-- Bad\nSELECT * FROM orders WHERE status = 'pending';\n-- Good: only the columns needed\nSELECT id, user_id, total FROM orders WHERE status = 'pending';\n```\n\n## Pagination and large result sets\n\n```sql\n-- Offset pagination degrades with large offsets (Postgres must scan all skipped rows)\nSELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 100000;  -- slow at scale\n\n-- Keyset pagination: always fast\nSELECT * FROM orders WHERE id > :last_seen_id ORDER BY id LIMIT 20;\n```\n\n## How to pick an optimisation approach\n\n1. **Profile first** — `pg_stat_statements` or slow query log.\n2. **Read the plan** — `EXPLAIN (ANALYZE, BUFFERS)`.\n3. **Fix N+1 first** — highest ROI, requires no schema change.\n4. **Add a targeted index** — only if `EXPLAIN` shows Seq Scan on a large table.\n5. **Rewrite the query** — switch OFFSET to keyset, eliminate correlated subqueries.\n6. **Denormalise or cache** — only as a last resort after the above are exhausted.\n\n## Common mistakes\n\n**Adding indexes before profiling.** You might add the wrong one and slow down writes for no benefit.\n\n**Not updating statistics.** `EXPLAIN` estimates are based on table statistics. Run `ANALYZE` after large bulk loads.\n\n**Correlated subqueries in SELECT.** A subquery in the SELECT list runs once per row — use a JOIN instead.\n\n**Ignoring query reuse.** The same query run 1,000 times/second benefits enormously from connection pooling and prepared statements.\n\n## What to skip\n\n- **Query hints in PostgreSQL** — they do not exist in Postgres (unlike MySQL). Fix the root cause instead.\n- **Premature denormalisation.** Add indexes first; denormalise only when you have proven the indexed query is still too slow.\n- **Caching before fixing the query.** Cache a fast query; do not cache to hide a slow one — the slow query will still fire on cache miss.\n\n## FAQ\n\n**How do I know if my query is using an index?**\nRun `EXPLAIN (ANALYZE)` and look for `Index Scan` or `Index Only Scan`. `Seq Scan` means no index is being used.\n\n**My EXPLAIN estimate is wildly off from actual rows. Why?**\nTable statistics are stale. Run `ANALYZE table_name` or increase `default_statistics_target` for skewed columns.\n\n**How many indexes is too many?**\nThere is no hard rule, but if a table has more indexes than columns it is worth auditing. Use `pg_stat_user_indexes` to find indexes with zero scans and drop them.\n\n**Does the ORM slow things down?**\nThe ORM itself is not usually the bottleneck — the queries it generates are. Always log and inspect the SQL your ORM produces in development.\n\n## Where to go next\n\n- [How to pick a database in 2026](/blog/how-to-pick-a-database-2026)\n- [How to handle errors gracefully in 2026](/blog/how-to-handle-errors-gracefully-2026)\n- [How to monitor a service in 2026](/blog/how-to-monitor-a-service-2026)","hue":309,"metaTitle":"How to Optimize SQL Queries in 2026: Indexes and Query Plans","metaDescription":"2026 guide to SQL query optimization. EXPLAIN ANALYZE, index strategy, fixing N+1 queries, and the common mistakes that slow down PostgreSQL and MySQL","keywords":["how to optimize sql queries 2026","sql query optimization 2026","sql indexes explained","explain analyze postgresql","n+1 query problem","slow sql queries fix","sql performance tuning 2026","database index strategy","postgres query optimization","sql join optimization"],"tldr":["**EXPLAIN ANALYZE before you guess** — the query plan shows the actual row counts, costs, and whether your index is used.","**N+1 queries are the most common production killer** — one query per loop iteration; fix with a JOIN or batch load.","**Index the columns you filter, join, and order by** — but every index slows writes; add them surgically.","**SELECT * is almost always wrong** — fetch only the columns you need; it reduces I/O and disables index-only scans.","**Skip:** premature query optimisation before you have measured the actual slow query in a production-like dataset."],"readTime":"5 min read","wordCount":1164},"related":[{"slug":"how-to-optimize-database-queries-2026","title":"How to optimize database queries in 2026: a measured approach","excerpt":"Most slow queries come from missing indexes and bad query shapes, not the database engine. Here is how to measure, fix, and verify query performance.","category":"code","date":"May 22, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":222},{"slug":"indexes-explained-2026","title":"Database indexes explained in 2026: what every developer must know","excerpt":"Indexes make or break query performance. Here is the 2026 guide covering B-tree, GIN, GiST, and partial indexes with concrete examples and tradeoffs.","category":"code","date":"April 6, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":309},{"slug":"database-indexing-explained-2026","title":"Database indexing explained in 2026: what every developer must know","excerpt":"Bad indexes cause more production incidents than almost any other root cause. Here is the 2026 guide to choosing, building, and maintaining the right ones.","category":"code","date":"March 10, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":318}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-open-a-roth-ira-for-a-kid-2026","title":"How to open a Roth IRA for a kid in 2026: custodial accounts and compound time","excerpt":"A Roth IRA opened for a child can compound tax-free for 60+ years. Here is who qualifies, which brokers to use, and how to fund it legally in 2026.","category":"finance","date":"April 1, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":290},"older":{"slug":"how-to-organize-a-pantry-2026","title":"How to organize a pantry in 2026: a system that stays tidy","excerpt":"A well-organized pantry cuts grocery waste and stops the \"I already have that\" double-buy. This 2026 guide gives you a repeatable zone-and-label system.","category":"guides","date":"April 1, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":270}}