Sync: every contact costs two queries per run for its primary number and link, and every old interview one more #47
Labels
No labels
bug
documentation
enhancement
performance
security
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo-dav#47
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What exists today
postulo-dav on the forge's
main(83710e9):src/postulo_dav/sync.py:197-221card_forcallsprimary_phone_number(contact)andprimary_web_link(contact, LinkKind.SOCIAL)from the plugin surface. It runs for every visible contact on every run, changed or not, because the digest it feeds (sync.py:790) is how a change is noticed.core/phone_numbers.py:61-63(holder.phone_numbers.filter(is_primary=True).first()) andcore/web_links.py:241-243(holder.web_links.filter(kind=kind, is_primary=True).first()): one query each, and.filter()ignores anyprefetch_relatedthe caller did, so the plugin cannot batch them today.sync.py:684-688) hasselect_related("company")andwith_notes(owner), but no prefetch of numbers or links.Run.each(sync.py:181-187) wraps every contact, unchanged ones included, intransaction.atomic(), aSAVEPOINTandRELEASEpair each.sync.py:917-920, oneContact.objects.filter(pk=…).exists()per link (rare: a contact deleted or no longer shared); interviews,sync.py:1171-1175, oneInterview.objects.filter(pk=…).exists()per link whose interview started more than 90 days ago (HISTORY,sync.py:96). Those links are kept on purpose ("merely too old for the window"), so this second loop grows with the person's whole interview history and runs every interval.The scout's line numbers (129-144, 661) were from the stale local checkout.
What would change
Core (postulo):
phone_numbers.primary_forandweb_links.primary_forread the holder's prefetched rows whenphone_numbersorweb_linksis in its_prefetched_objects_cache, and query as today otherwise. Same answer, no new surface name; a test asserts zero queries for a prefetched holder.postulo-dav:
prefetch_related("phone_numbers", "web_links")to the contacts queryset (two queries per run in all), once the core above is pinned..exists()in both loops with onevalues_list("pk", flat=True)over the missing ids before the loop.run.each).django_assert_max_num_queriesaround a second, idle run.What it saves or protects
small: an idle run over 300 contacts and 200 past interviews makes about 800 single-row queries (2 per contact, 1 per old interview link), plus a savepoint pair per contact; with a prefetch and one batched lookup, a handful (counted from the code, not measured)
Accessibility
none
Decisions
primary_forhonours a prefetch) and one postulo-dav issue (prefetch, batched existence checks), the second repinning past the first.Found by the efficiency and reliability audit of 2026-10-30 (finding PR7; index postulo/postulo#1108). Checked against
mainatdaee30b15(2026-10-30).primary_forignores rows already prefetched, so a sync pays two queries per contact #1171The other half of this finding is postulo/postulo#1171.