sql-optimization
Skill8 starsChanged 8 days agoMITView on GitHub
Diagnoses and fixes slow SQL using real query plans, index design, sargable rewrites, keyset pagination and ORM fixes such as N+1 queries. Use when a query, endpoint, report or job is slow, database CPU or IO is high, an EXPLAIN plan needs reading, an index needs designing, or an ORM issues too many queries. Covers PostgreSQL and MySQL.
12 other projects use a version of this.
Adopted by
Most recently taken up by
- bmsuisse/skillstheir own version, first seen Nov 6, 2026
- github/awesome-copilottheir own version, first seen Oct 25, 2026
- XGENT-ai/skillstheir own version, first seen Oct 13, 2026
- z-Zihan/Evirtheir own version, first seen Oct 13, 2026
- Adhamxon/claude-code-skillstheir own version, first seen Jul 30, 2026
- jgamaraalv/delivery-looptheir own version, first seen Jul 4, 2026
Used with
Projects with it also use test-driven-development.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
OpenAISkilltest-driven-development
Use when implementing any feature or bugfix, before writing implementation code
Also in 3 projects that use this
Other versions12 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 12
- github/awesome-copilot★ 41ksame name
- totvs/engpro-advpl-tlpp-skills★ 166same name
- z-Zihan/Evir★ 11same name
- Adhamxon/claude-code-skills★ 2same name
- bmsuisse/skills★ 2same name
- iCesofT/awesome-ai★ 1same name
- XGENT-ai/skills★ 1same name
- jgamaraalv/delivery-loop★ 0same name
- KenLiu0607/skills★ 0same name
- ReaperOAK/ForgeOS★ 0same name
2 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository60 other files it gives its agents.
- api-designSkill
- auth-implementationSkill
- background-jobsSkill
- building-agentsSkill
- caching-strategiesSkill
- ci-cd-pipelinesSkill
- cloud-cost-optimizationSkill
- codebase-onboardingSkill
- code-review-changesSkill
- concurrency-patternsSkill
- database-migrationsSkill
- database-schema-designSkill
- database-testingSkill
- data-pipelinesSkill
- data-privacy-retentionSkill
- dependency-upgradesSkill
- design-docsSkill
- design-systemsSkill
- dockerfilesSkill
- domain-driven-designSkill
- event-driven-architectureSkill
- file-storage-uploadsSkill
- frontend-securitySkill
- git-workflowSkill
- graphql-apisSkill
- grpc-servicesSkill
- incident-responseSkill
- infrastructure-as-codeSkill
- internationalizationSkill
- kubernetes-deploymentsSkill
- large-scale-migrationsSkill
- legacy-refactoringSkill
- llm-evalsSkill
- llm-integrationSkill
- mcp-serversSkill
- micro-frontendsSkill
- ml-experimentsSkill
- monorepo-managementSkill
- nosql-designSkill
- observabilitySkill
- orm-patternsSkill
- performance-profilingSkill
- postgres-operationsSkill
- rag-systemsSkill
- react-patternsSkill
- realtime-apisSkill
- resilience-patternsSkill
- root-cause-debuggingSkill
- search-integrationSkill
- security-auditSkill
- seo-and-ssrSkill
- system-designSkill
- test-strategySkill
- threat-modelingSkill
- typescript-patternsSkill
- unit-testingSkill
- verify-before-doneSkill
- web-accessibilitySkill
- web-performanceSkill
- writing-skillsSkill
The file
We split this skill into the rules it gives an agent
5 rules. 0 are common, 3 are distinctive, and 2 are very specific to this project.
0
Common
Most of the 12 edited versions we read keep these.
3
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 19
Not for: changing schemas safely on a live database (use database-migrations.
- Very specific: line 84
Covering indexes: avoid table lookups: PostgreSQL 11+ CREATE INDEX ...
- Very specific: line 93
Remove redundant indexes: (a) is redundant when (a, b) exists, unless it is needed for a unique constraint.
- Distinctive: line 99
Write predicates the index can use directly:
- Distinctive: line 182
Indexing every column separately: instead of designing composite indexes for the actual queries.
“Common” is measured: kept by at least 6 in 10 of the 12 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
SQL Optimization
Measure, don't guess. Find the query that actually costs the most, read its real plan with real parameters, change one thing, and measure again on realistic data. An index added on a hunch is a permanent write cost that may not help.
When to use
- A page, API endpoint, report or background job is slow and the time is in the database.
- High database CPU, IO, or lock waits.
- Reading or explaining an
EXPLAINplan. - Designing indexes for a new query or table.
- An ORM generates many small queries (N+1) or huge ones.
Not for: changing schemas safely on a live database (use database-migrations; it covers building indexes without blocking writes).
Workflow
- Find the expensive queries. Rank by total time (calls × mean), not just the slowest single run.
- PostgreSQL:
pg_stat_statements(needsshared_preload_libraries = 'pg_stat_statements'andCREATE EXTENSION pg_stat_statements;). - MySQL 8:
performance_schema.events_statements_summary_by_digest, or the slow query log summarised withpt-query-digest. - APM traces and ORM query logs show which code path issues each query.
- PostgreSQL:
- Get the exact SQL and real parameter values from logs or traces. A plan for
WHERE status = 'active'can differ completely from one forWHERE status = 'archived'. - Get the real plan.
- PostgreSQL:
EXPLAIN (ANALYZE, BUFFERS) <query>; - MySQL 8.0.18+:
EXPLAIN ANALYZE <query>;(older:EXPLAIN FORMAT=TREEor plainEXPLAIN, estimates only) EXPLAIN ANALYZEexecutes the statement. ForINSERT/UPDATE/DELETE, wrap it inBEGIN; ... ROLLBACK;. Prefer a replica or a production-sized copy. Ask the user before running it against production.
- PostgreSQL:
- Read the plan. Find where the time goes, and compare estimated vs actual rows. Read references/reading-query-plans.md for node types and red flags.
- Pick the fix from the diagnosis table below.
- Apply one change and re-measure with the same parameters and similar cache state (run each version a few times). Keep the before/after plans and timings.
- Count the cost. For a new index: check for existing overlapping indexes and weigh the extra write and storage cost. For a rewrite: confirm the results are identical.
- Ship it safely. Build indexes without blocking writes (
CREATE INDEX CONCURRENTLYor MySQL online DDL), and add a regression test where possible (a query-count assertion, or a plan check in CI for critical queries).
Top queries
-- PostgreSQL 13+ (older versions: total_time / mean_time)
SELECT queryid, calls,
round(total_exec_time::numeric) AS total_ms,
round(mean_exec_time::numeric, 2) AS mean_ms,
rows,
left(query, 120) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;-- MySQL 8 (timer columns are in picoseconds)
SELECT DIGEST_TEXT,
COUNT_STAR AS calls,
ROUND(SUM_TIMER_WAIT / 1e12, 1) AS total_s,
ROUND(AVG_TIMER_WAIT / 1e9, 2) AS avg_ms,
SUM_ROWS_EXAMINED, SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 20;Turning on the slow query log or changing long_query_time changes server configuration. Ask before doing it on a shared server.
Diagnosis → fix
| Symptom in the plan | Likely cause | Fix |
Seq Scan / type: ALL on a large table, few rows returned | No usable index | Add an index matching the predicate (rules below) |
| Index exists but is not used | Non-sargable predicate, type mismatch, low selectivity, stale stats | Rewrite the predicate; fix types; ANALYZE; check the estimates |
| Estimated rows far off actual (10× or more) | Stale or insufficient statistics, correlated columns | ANALYZE; raise the statistics target; extended statistics |
Large Rows Removed by Filter | Index covers only part of the predicate | Composite or partial index |
Sort spilling to disk / Using filesort on a big set | No index providing the order | Index on (filter columns, sort columns) |
| Nested Loop with a large outer side and loops in the thousands | Bad estimate or missing index on the inner side | Fix stats; index the join key |
| Fast in the plan, slow in the app | N+1, network round trips, over-fetching, lock waits | Count queries per request; check locks |
| Slow deep pages | OFFSET scanning and discarding rows | Keyset pagination |
Index design rules
- Equality columns first, then range or sort columns. For
WHERE tenant_id = ? AND created_at > ? ORDER BY created_at, use(tenant_id, created_at). - Leftmost prefix: an index on
(a, b, c)servesa,a, banda, b, c. It does not servebalone. - Match WHERE + ORDER BY + LIMIT so the database reads the first N rows in order and stops:
WHERE user_id = ? ORDER BY created_at DESC LIMIT 20→(user_id, created_at). - Covering indexes avoid table lookups: PostgreSQL 11+
CREATE INDEX ... (customer_id) INCLUDE (status, total);. Index-only scans in PostgreSQL also depend on the visibility map (a well-vacuumed table). InnoDB secondary indexes already contain the primary key. - Partial indexes (PostgreSQL only) for hot subsets:
CREATE INDEX ... ON jobs (run_at) WHERE status = 'pending';. The query's predicate must imply the index'sWHERE. - Expression indexes for computed predicates: PostgreSQL
CREATE INDEX ... ON users (lower(email));; MySQL 8.0.13+ functional key partsINDEX ((LOWER(email))). - Low-cardinality columns (booleans, small status sets) rarely help as a leading column on their own. Combine them, or use a partial index.
- Foreign key columns used in joins or cascaded deletes usually need an index. PostgreSQL does not create one automatically.
Every index has a cost
- Each index slows
INSERT,UPDATEandDELETEand uses memory and disk. In PostgreSQL, indexing a frequently updated column also prevents HOT updates. - Remove redundant indexes:
(a)is redundant when(a, b)exists, unless it is needed for a unique constraint. - Find unused indexes before adding more: PostgreSQL
SELECT * FROM pg_stat_user_indexes WHERE idx_scan = 0;(counts since the last stats reset; check replicas too, since they have their own counters). MySQLSELECT * FROM sys.schema_unused_indexes;. - Dropping an index is a schema change. Confirm with the user and follow database-migrations.
Sargable predicates
Write predicates the index can use directly:
| Instead of | Write |
WHERE date(created_at) = '2026-01-15' | WHERE created_at >= '2026-01-15' AND created_at < '2026-01-16' |
WHERE lower(email) = ? (no expression index) | Add an expression index, or store a normalised column (PostgreSQL: citext) |
WHERE name LIKE '%smith' | Full-text search, or PostgreSQL pg_trgm with a GIN/GiST index |
WHERE varchar_col = 123 (MySQL implicit cast) | Compare with the same type: WHERE varchar_col = '123' |
| Joining columns of different types or collations | Make the types and collations match |
WHERE a = ? OR b = ? | UNION ALL of two indexed queries (PostgreSQL can also combine indexes with BitmapOr) |
WHERE id NOT IN (SELECT ...) with NULLs possible | WHERE NOT EXISTS (SELECT 1 ...), which is correct with NULLs and usually faster |
SELECT * on a hot path | Only the columns you need, which may let a covering index serve it |
ORM N+1 queries
The pattern: one query for a list, then one query per row for a relation. Each is fast; together they are slow.
Detect: log queries per request and look for the same statement repeated with different ids. Tools include Bullet (Rails), django-debug-toolbar and assertNumQueries (Django), Hibernate statistics, EF Core logging, and Laravel's preventLazyLoading.
Fix by eager loading or batching:
| ORM | Fix |
| Django | select_related() for FK/one-to-one (JOIN); prefetch_related() for many-valued relations |
| Rails | includes, preload or eager_load |
| SQLAlchemy | selectinload() (usually best for collections) or joinedload() |
| Hibernate / JPA | JOIN FETCH, @EntityGraph, or batch fetching (@BatchSize) |
| EF Core | .Include() / .ThenInclude(); consider .AsSplitQuery() for multiple collections |
| Prisma | include / select with relations |
| GORM | Preload() |
| GraphQL resolvers | A DataLoader that batches ids into one WHERE id IN (...) |
Lock it in with a test that asserts the query count for the endpoint.
Pagination
OFFSET 100000 still reads and throws away 100,000 rows. Use keyset (seek) pagination for deep or infinite lists:
-- First page
SELECT id, created_at, title FROM events
ORDER BY created_at DESC, id DESC
LIMIT 50;
-- Next page: pass the last row's (created_at, id) from the previous page
SELECT id, created_at, title FROM events
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 50;- Index
(created_at, id); B-tree indexes can be scanned backwards. - Include a unique tiebreaker (
id) so rows with equal timestamps are not skipped or repeated. - On MySQL, check with
EXPLAINthat the row-value comparison uses a range scan on the index. If it does not, use the expanded form:WHERE created_at < :c OR (created_at = :c AND id < :id). - Keyset pagination cannot jump to page N. If the product needs page numbers, cap the maximum offset.
Statistics
- PostgreSQL:
ANALYZE table_name;after bulk loads or large deletes (autovacuum does it eventually). For skewed columns:ALTER TABLE t ALTER COLUMN c SET STATISTICS 1000;thenANALYZE. For correlated columns (city,zip):CREATE STATISTICS ... (dependencies) ON city, zip FROM t;thenANALYZE. - MySQL:
ANALYZE TABLE t;. MySQL 8.0 histograms for non-indexed columns:ANALYZE TABLE t UPDATE HISTOGRAM ON c WITH 64 BUCKETS;.
Locks and long transactions
A query can be slow because it waits, not because it works.
- PostgreSQL: query
pg_stat_activitywithpg_blocking_pids(pid)and look atwait_event_type = 'Lock'. Look foridle in transactionsessions. - MySQL:
SELECT * FROM sys.innodb_lock_waits;andinformation_schema.innodb_trx. - Fix at the source: shorter transactions, no network calls or user think-time inside a transaction, consistent lock ordering, batched bulk updates.
- Killing or cancelling another session affects real users. Ask first.
Prepared statements and plan caching
A query that is fast in a SQL console but slow from the app may be getting a different plan:
- PostgreSQL may switch a prepared statement to a generic plan after several executions. Compare with
EXPLAIN (ANALYZE) EXECUTE stmt(...)and look atplan_cache_mode. - Drivers may send parameters as a different type than the column (for example text vs integer, or
varcharvsnvarcharon SQL Server), which blocks index use.
Anti-patterns
- Adding indexes without a plan — measure first; most "missing index" guesses are wrong.
- Testing on a tiny dev database — the planner picks different plans for 1,000 rows than for 100 million.
- Caching to hide a slow query you have not understood. It comes back on a cache miss or a cold start.
- Optimizer hints as the first fix — they freeze a plan that goes stale as the data changes. Fix statistics and indexes first.
- Indexing every column separately instead of designing composite indexes for the actual queries.
- Running
EXPLAIN ANALYZEon a productionDELETEwithout a transaction you roll back.
Done when
- The target query was measured before and after with the same parameters on realistic data, and the improvement is recorded.
- The new plan uses the intended access path, and estimated and actual rows are close.
- Any new index is justified, overlapping indexes were checked, and it is built without blocking writes.
- Results are unchanged (same rows, same order where order matters).
- N+1 fixes have a query-count test.
References
- references/reading-query-plans.md — read when interpreting PostgreSQL
EXPLAIN (ANALYZE, BUFFERS)or MySQLEXPLAIN/EXPLAIN ANALYZEoutput.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

