Fix treatment listing block-sort regression from per-row shared-number subqueries

Re-review of afa350d found that the correlated-subquery rewrite in
listLeads regressed any page landing in a large sort-tie block (e.g. the
default prioridade:asc sort's prioridade=4/score=0 bucket): SQLite's block
sort materializes the whole tied group before LIMIT/OFFSET apply, so the
shared-number subqueries got evaluated for the entire block instead of just
the page (measured 8-9x slower on page 2+ of a 60k-row fixture).

Split listLeads into two queries: a cheap id-only pass that does the
sorting/pagination (same cost as before the shared-number feature existed),
then a hydrate pass scoped to WHERE leads.id IN (...) for just that page's
ids, which is where the shared-number subqueries now run. getLead was
already a single-id lookup and didn't need this.

Also fixed the accompanying test, which only checked page 1 (a small,
already-sorted block) and so couldn't have caught the regression it was
meant to guard against; it now also checks a page landing inside the large
tied block.
This commit is contained in:
Gustavo
2026-07-31 20:52:45 -03:00
parent afa350de5e
commit 240c70b715
2 changed files with 52 additions and 10 deletions
+25 -6
View File
@@ -95,18 +95,37 @@ export function listLeads(db, filtro = {}) {
const orderBy = SORTS[filtro.sort] || SORTS['prioridade:asc'];
const total = db.prepare(`SELECT COUNT(*) AS t FROM leads ${whereSql}`).get(...params).t;
const rows = db.prepare(
// Pagina por id primeiro, sem as subconsultas de numero compartilhado: em
// ordenacoes que so casam parcialmente com o indice (ex.: prioridade:asc),
// o sort do SQLite materializa o bloco empatado inteiro antes do
// LIMIT/OFFSET, o que reavaliaria as subconsultas para o bloco inteiro (ate
// dezenas de milhares de linhas) em vez de so a pagina. Selecionando so o id
// aqui, o custo desse sort volta a ser o mesmo de antes da funcionalidade de
// numero compartilhado existir.
const pageIds = db.prepare(
`SELECT leads.id AS id
FROM leads
${whereSql}
ORDER BY ${orderBy}
LIMIT ? OFFSET ?`
).all(...params, pageSize, (page - 1) * pageSize).map((row) => row.id);
if (pageIds.length === 0) return { total, page, pageSize, rows: [] };
const placeholders = pageIds.map(() => '?').join(', ');
const rowsById = new Map(db.prepare(
`SELECT leads.id AS id, empresa, legal_name, whatsapp, ddd, cidade, uf, segmento, segmento_label,
interesse, cnpj, cnae, score, score_qualidade, score_comercial, classificacao,
prioridade, produto_principal, telefone_valido, origem,
exported_at, captured_at, segments.affinity AS afinidade,
${sharedNumberColumnsSql('leads.whatsapp')}
FROM leads LEFT JOIN segments ON segments.name = leads.segmento
${whereSql}
ORDER BY ${orderBy}
LIMIT ? OFFSET ?`
).all(...params, pageSize, (page - 1) * pageSize)
.map((lead) => withSharedNumberState(withWhatsAppState(db, lead)));
WHERE leads.id IN (${placeholders})`
).all(...pageIds)
.map((lead) => [lead.id, withSharedNumberState(withWhatsAppState(db, lead))]));
const rows = pageIds.map((id) => rowsById.get(id));
return { total, page, pageSize, rows };
}