Build1 distinct publisher3 min readUpdated
A reporting page took 78 seconds for one customer because none of the table's four indexes could answer the filters or the GROUP BY. The fix was one six-column index, shipped in place.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A reporting page on a workplace learning platform took 78 seconds to load for one large customer, and the eventual fix was a single six-column index [1][2]. The useful part is not the index: the table already carried four indexes, including one on exactly the column the query filtered on, and none of them helped [3]. The account comes from a write-up by Nitish Anand on dev.to, submitted to DEV's Summer Bug Smash [1][20].
The page answers one question: for a given assessment, list every learner, their current status, and when they first engaged [4]. Each attempt is a row, learners retake, some rows are archived when a course is renewed and some are soft-deleted [5]. So the query is the standard group-and-aggregate shape: filter on `lesson_content_assessment_id`, `moved_to_history = 0` and `deleted_at IS NULL`, then `GROUP BY user_id` with `MAX(id)` and `MIN(created_at)` [6]. Fine for most tenants; one customer had roughly 20,000 attempts on a single assessment [7].
The four existing indexes were `(course_lesson_id)`, `(lesson_content_assessment_id)`, `(lesson_content_assessment_id, is_submitted, total_marks)` and `(user_id)` [3]. MySQL picked the second one, found the ~20,000 candidate rows, and then had nowhere to go: neither `moved_to_history` nor `deleted_at` appears in any index, so each candidate row had to be fetched from the clustered index to test the remaining filters [8]. That is 20,000 random lookups. Then, because the index is ordered by assessment id rather than `user_id`, the grouping could not ride on index order, so the engine materialised a temporary table and sorted it [9]. On a table of about 700,000 rows, those three steps together came to 78 seconds [10]. The candidate set was under 3 percent of the table [19].
The third index is the instructive one. It leads with the right column but its second and third columns are irrelevant to this query, so it collapses to the same behaviour as the single-column index [11]. "We have an index on that column" is not a statement about performance.
The replacement was `(lesson_content_assessment_id, moved_to_history, deleted_at, user_id, id, created_at)`, named `idx_ua_lca_history_user` [2]. Order is the whole design: three equality filters first so the engine seeks straight to the qualifying block, `user_id` next so grouping is a sequential walk with no temp table or filesort, then `id` and `created_at` last because the aggregates can be read out of the index entries [12]. Every column the query touches lives in the index, so the table is never read [13]. The pagination `COUNT(*)`, which had been paying the same cost on every page load, rides the same access path [14]. Reported result: 78 seconds down to 1 to 2 seconds [15], a 39x to 78x reduction [18].
The shipping risk is the part worth copying. Adding an index to a 700,000-row table under active writes is where this could still have gone wrong, because by default some index operations rebuild the table [16]; the migration therefore pins `algorithm: :inplace` and `if_not_exists: true` [17]. The published account breaks off mid-sentence at that point, so the operational detail of how the build behaved under load is not in the material [21].
Watch whether the covering index holds as the query grows. Add one column to the SELECT list that is not in the index and the covering property is gone, and you are back to row lookups. The same applies to any new filter on a column that sits after `user_id` in the key.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
MySQL used the (lesson_content_assessment_id) index to find the ~20,000 rows for the assessment, but because moved_to_history and deleted_at appear in no index, it had to fetch each candidate row from the clustered index to evaluate those filters: 20,000 random lookups.
Because the index used is ordered by lesson_content_assessment_id and not by user_id, the GROUP BY could not use index order, so MySQL built a temporary table and sorted.
On a table of roughly 700,000 rows, the combination of index range scan, 20,000 random I/Os and a materialised sorted temp table took 78 seconds.
Reported result: page time went from 78 seconds to 1 to 2 seconds.
A reporting page on a workplace learning platform took 78 seconds to load for one large customer; the account is a write-up by Nitish Anand published on dev.to.
The fix was a single index added via a Rails migration: add_index :user_assessments, %i[lesson_content_assessment_id moved_to_history deleted_at user_id id created_at], name: 'idx_ua_lca_history_user'.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Detailed but unverified single-author account
The post supplies unusually specific artifacts for a blog write-up: the verbatim query, the four pre-existing index definitions, the exact add_index call and column ordering, the raw ALTER with ALGORITHM=INPLACE / LOCK=NONE and the rake-task skeleton. What is missing is anything that would let a reader verify the causal story or the numbers: no EXPLAIN or ANALYZE output, no rows-examined or temp-table counters, no MySQL version or buffer-pool context, no timing methodology, and no second observer. Evidence quality is therefore mid-range: internally coherent and mechanistically plausible, externally unchecked.
One production deployment, self-reported
Adoption evidence is limited to a single deployment inside the author's own platform: the index was created on the live production table via online DDL and the page latency change was reported. There is no second organisation, no external practitioner confirming the pattern in this cluster, no usage numbers beyond the ~20,000-attempt customer and ~700,000-row table, and no indication of the technique spreading. The score reflects one concrete, dated production change rather than any ecosystem uptake.
Slightly overstated by framing, not by numbers
The substance is modest and largely matches its claims: the author does not generalise beyond one page, names the tradeoff-free-sounding result as a single index rather than a rewrite, and spends the back half on operational caution. The mild positive gap comes from packaging rather than fabrication - a '78 seconds to 2' headline and a '39x-78x' arithmetic reading rest on one unverified before/after with no EXPLAIN artifact, and the piece is silent on the write-path and storage cost of a six-column index on a hot table, so the fix reads as cost-free when it is not.
Sponsored contest entry and personal-reputation post
Two disclosed incentives shape the telling. The post is an entry in DEV's Summer Bug Smash: Smash Stories, powered by Sentry, which rewards clean, dramatic debugging narratives with a clear before/after, and it is a first-person account by an engineer describing his own successful fix, which selects for a story where the diagnosis was right and the rollout was clean. Offsetting factors: the sponsor's product is never invoked or credited for the find, nothing is being sold, the customer and employer are anonymised, and the operational section volunteers risks rather than hiding them.
Moderate-low: plausible mechanism, one voice
Confidence is limited by cluster structure more than by content. One publisher, one author, one deployment, no independent replication, and no machine-readable artifacts; the record also contains an internal inconsistency about where the supplied text truncates. Against that, the described mechanism (non-covering leading-column index forcing clustered-index lookups plus a filesort for GROUP BY, resolved by a covering index ordered filters-then-group-key-then-aggregates) is internally consistent with the schema and query given, so the account is credible as a firsthand report even though it is not verifiable from this cluster.
build
Slow Magento reindexes are a price index problem, and raw SQL makes it worse1 distinct publisher
build
Your Magento admin is slow because order state lives in fifteen tables, not because Varnish is off1 distinct publisher
build
The fence was fine, the test was green, and the injection still worked1 distinct publisher
build
A Prometheus that had written nothing for hours passed every health check1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 15, 2026