ENGINEERING WORK 02 / MYSQL QUERY PATH

300ms to 133ms by removing work the result never needed.

Not every optimization needs a new cache layer or architectural rewrite. Sometimes the database is simply doing more work than the caller asked for.

QUERY LATENCY 300ms → 133ms EXPLAIN ANALYZE
01
PROBLEM

A frequently executed query multiplied rows through joins and then paid again to deduplicate them.

The final result was small, but the execution plan showed more than 25,000 rows flowing through an inner-join branch before a temporary table removed duplicates. That is a useful smell: the database is doing substantial relationship work that the caller may not actually need.

INTERMEDIATE25k+

rows in the expensive join branch

→
CHANGENARROW

reduce candidates before relationship work

→
AFTER133ms

same caller-visible result

02
EVIDENCE / ACTUAL PLAN SHAPE

EXPLAIN ANALYZE showed exactly where rows exploded.

The following fragment is generalized from the observed MySQL 8 plan. Internal table, column and index names are replaced, and row counts are rounded while preserving the execution-plan shape.

EXPLAIN ANALYZE / GENERALIZED EXCERPTRow multiplication + deduplication
-> Table scan on <temporary>
    (actual time≈300ms rows≈100)
    -> Temporary table with deduplication
        (actual time≈300ms rows≈100)
        -> Nested loop inner join
            (actual time≈0.3..271ms rows≈25k)
            -> Filter:
               ((r.flagged = 1) and (r.archived_at is null))
               (actual time≈0.2..67ms rows≈13k)
               -> Intersect rows sorted by row ID
                  (actual time≈0.2..60ms rows≈13k)
                  -> Index range scan on r using idx_flagged

The useful number is not simply “300ms.” It is the shape: roughly 13k candidate rows become roughly 25k joined rows, then a temporary structure throws almost all of that work away to return only about 100 rows.

03
QUERY SHAPE / SIMPLIFIED

Constrain existence without materializing every matching relationship row.

Where the caller only needs to know whether a related record exists, an existence check can be cheaper than joining every matching row and then using DISTINCT to repair the multiplication.

BEFORE / REPRESENTATIVEJoin then deduplicate
SELECT DISTINCT r.record_id
FROM records AS r
JOIN record_members AS rm
  ON rm.record_id = r.record_id
WHERE r.flagged = 1
  AND r.archived_at IS NULL
  AND rm.archived_at IS NULL;
AFTER / REPRESENTATIVETest relationship existence
SELECT r.record_id
FROM records AS r
WHERE r.flagged = 1
  AND r.archived_at IS NULL
  AND EXISTS (
      SELECT 1
      FROM record_members AS rm
      WHERE rm.record_id = r.record_id
        AND rm.archived_at IS NULL
  );

The SQL above illustrates the access-pattern change rather than reproducing the full proprietary query verbatim. The measured plan/timings are from the production investigation.

04
METHOD

Measure the execution plan, then change the access pattern—not the business contract.

  • Captured actual timing with EXPLAIN ANALYZE
  • Identified the join producing most intermediate rows
  • Checked whether the caller needed joined columns or only relationship existence
  • Narrowed the candidate/relationship path earlier
  • Retained the result shape expected by the application
  • Re-ran the plan and measured the improvement
RESULT

Less work, same contract.

BEFORE300msjoin + deduplicate path
→
AFTER133msnarrower path
ABOUT THIS ENGINEERING EXAMPLE

This is based on a production MySQL query-optimization investigation. The measured timing is retained; the plan fragment is rounded and internal identifiers are generalized; identifying application context and the complete proprietary query are intentionally omitted.