Skip to content

perf: stop Tribe list and care summary from scanning the whole contact table (#6024) - #6121

Merged
atomantic merged 1 commit into
mainfrom
claim/issue-6024
Sep 3, 2026
Merged

perf: stop Tribe list and care summary from scanning the whole contact table (#6024)#6121
atomantic merged 1 commit into
mainfrom
claim/issue-6024

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Summary

Two Tribe read paths were paying for data nothing consumed.

listPeople() ran two correlated COUNT(*) subqueries per person row — one over tribe_touchpoints, one over tribe_memory_links — so a 200-contact install did 400 extra index scans on every Tribe page load, every identity-resolution context load, and every contact-sync pass. Neither touchpointCount nor linkedMemoryCount is read anywhere in client/src/. The subqueries are gone; rowToPerson already defaults both to 0 when the columns are absent, and getPerson(id) still hydrates them for the detail view.

getCareSummary(limit) answered "who is overdue" by calling listPeople() with no filter — hydrating notes, next_move, and the tags / emails / phones arrays for every contact (plus those 2N subqueries) only to discard all of it in JavaScript. It now issues its own lean query for the six columns the cadence math and the response actually use, with the external exclusion pushed into SQL:

SELECT id, name, ring, cadence_days, last_contact_on, channel
FROM tribe_people
WHERE deleted = FALSE AND ring <> 'external'

ring is NOT NULL DEFAULT 'tribe', so moving the exclusion into SQL matches the previous JS filter exactly. This is the hotter of the two paths: it runs on every Dashboard mount and on the proactive-alerts poll every two minutes.

Response shape and ordering of GET /api/tribe/people and GET /api/tribe/care are unchanged.

Test plan

  • cd server && npm test — full suite green (1917 files, 38679 tests).
  • server/services/tribe.test.js extended:
    • listPeople asserts no COUNT(*) / tribe_touchpoints / tribe_memory_links in the SQL, that the ring-rank → oldest-contact → name ordering is intact, and that rows still map to full person objects with both counts defaulted to 0.
    • getPerson asserts the counts are still hydrated from its own subqueries.
    • getCareSummary asserts the lean column projection plus the ring <> 'external' / deleted = FALSE predicate, keeps the missing-first → most-overdue ordering case, pins the exact key set of each overdue item (id, name, ring, channel, lastContact, state, daysOverdue), and covers a Date-typed last_contact_on rendering to an ISO date string.
    • Both new SQL assertions fail against the pre-change queries.
  • No *.db.test.js covers Tribe, so nothing DB-backed needed to run.

Closes #6024

https://claude.ai/code/session_01VjkWVTfzKyRuAv3HEsspwN

…t table (#6024)

Every Tribe list request made PostgreSQL run two correlated COUNT(*)
subqueries per person row — 400 extra index scans for a 200-contact
install — to produce touchpointCount/linkedMemoryCount that no view in
the client ever renders. listPeople() now selects the person rows only;
rowToPerson already defaults both counts to 0, and the person detail
query (getPerson) still hydrates them for the views that show them.

getCareSummary() went through that same list to answer "who is overdue",
hydrating notes, next move, tags, emails and phones for the entire table
before throwing all of it away in JavaScript. It now issues its own lean
query for the six columns the cadence math and the response actually use,
and excludes `external` members in SQL. That path runs on every Dashboard
mount and on the proactive-alerts poll every two minutes, so it was the
most frequently paid cost of the two.

Response shape and ordering of GET /api/tribe/people and GET /api/tribe/care
are unchanged.

Claude-Session: https://claude.ai/code/session_01VjkWVTfzKyRuAv3HEsspwN
@atomantic
atomantic merged commit a3ec8f3 into main Sep 3, 2026
7 checks passed
@atomantic
atomantic deleted the claim/issue-6024 branch September 3, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tribe listPeople executes 2N correlated count subqueries and getCareSummary hydrates full records on every dashboard poll

1 participant