SQLPerformance

How to Optimize a Slow SQL Query with AI

The nightly report used to finish before you got in. Now it is still running at 10am. Here is how to find what is slow and what to change — starting from the query you already have.

Aug 11, 202610 min

Look at the plan before changing anything

Query tuning without an execution plan is guessing. Every database will show you what it intends to do:

  • PostgreSQL and MySQL — EXPLAIN ANALYZE <query>
  • SQL Server — SET STATISTICS IO ON, or the Include Actual Execution Plan button
  • Oracle — EXPLAIN PLAN FOR <query> then SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY)

Two things to look for: a full table scan on a large table, and a row-count estimate that is wildly different from the actual. The second means the optimizer is planning against statistics that no longer describe your data, and no rewrite will fix that — refresh the statistics first.

What actually makes queries slow

Filters an index cannot be used for

This is the most common cause and the easiest to fix. Wrapping a column in a function hides it from the index:

-- scans every row
WHERE YEAR(created_at) = 2026

-- uses an index on created_at
WHERE created_at >= '2026-01-01'
  AND created_at <  '2027-01-01'

The same applies to WHERE UPPER(email) = ... and to leading wildcards: LIKE '%acme%' cannot use a normal index, while LIKE 'acme%' can.

Correlated subqueries

A subquery in the SELECT list that references the outer row runs once per row. At ten thousand rows that is ten thousand queries. Rewriting it as a join with a grouped subquery turns it into one pass:

-- once per customer
SELECT c.name,
       (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS orders
FROM customers c

-- once, total
SELECT c.name, COALESCE(o.cnt, 0) AS orders
FROM customers c
LEFT JOIN (SELECT customer_id, COUNT(*) AS cnt
           FROM orders GROUP BY customer_id) o
       ON o.customer_id = c.id

SELECT * on wide tables

Every unused column is read from disk and pushed over the network. Worse, it can prevent a covering index from being used: if the index contains every column the query needs, the database never touches the table at all — and SELECT * guarantees it has to.

Sorting and DISTINCT you did not need

ORDER BY on an unindexed column sorts the entire result set. DISTINCT is often there to paper over a join that duplicates rows; fixing the join is faster than deduplicating afterwards.

Getting the rewrite

Paste the query into the box at the top of this page. What comes back is an equivalent query plus a list of what changed and why it is faster — a filter made index-usable, a correlated subquery flattened, a redundant sort removed, a column list narrowed.

Treat it as a shape change, not a verdict. The rewrite cannot see your indexes, your data distribution or your plan, so the honest sequence is: rewrite the query, run the plan again, and keep the version the plan likes.

On adding indexes

An index is a trade: faster reads, slower writes, more storage. Add one when a column is selective, appears in a filter or join, and is used by queries that run often. Then confirm the plan switched to it — an index the optimizer ignores costs you on every write and returns nothing.

For composite indexes, order matters: put the column used in equality filters first, the range filter last. An index on (status, created_at) serves WHERE status = 'open' AND created_at > ...; reversed, it mostly does not.

A short checklist

  1. Get the execution plan. Find the scan.
  2. Check whether estimated and actual row counts agree. If not, refresh statistics.
  3. Make filters index-usable — no functions on the filtered column.
  4. Replace correlated subqueries with joins.
  5. Name the columns you need instead of *.
  6. Remove DISTINCT by fixing the join that duplicates.
  7. Re-run the plan and keep what is faster.

If the query is also hard to read, have it explained first — optimising a query you do not understand is how a fast wrong answer gets shipped.

Paste a slow query

Free, no account needed for the first one. Get a rewritten query and the reason it is faster.

Get Started Free

No credit card required