cog_geographic_rollup(): push aggregation and pagination into SQL #59
Closed
opened 2026-08-09 11:21:16 -04:00 by jared
·
2 comments
No Branch/Tag Specified
main
ci/mirror-canonical-tags
chore/release-47-badges-mirror-pr
docs/readme-perf-remeasure-56
feat/pagination-search-balances-57
feat/duckdb-threads-60
feat/cohort-predicates-58
fix/windows-backslash-paths
ci/mirror-to-github
ci/github-actions-matrix
feat/public-release-0.3.0
chore/fixture-sb203
ci/apt-https
fix/pushdown-pagination
feat/all-categories-37
fix/partial-coverage-signposting-9
fix/schema-v7
fix/cog-categories-balance-subtype
feat/cog-balances-25
feat/revenue-concepts-12
feat/expenditure-concepts-11
feat/coverage-disclosure-13
feat/complete-argument-18
fix/kodor-batch-14-15-16
fix/all-scoped-series-breaks-19
fix/regen-fixture-corpus-18
test/walkthrough-findings
feat/expenditure-concept
fix/3-url-trailing-slash
feat/phase-r3-signposting
fix/fixture-option-b-aggregates
feat/phase-r2-harmonization
feat/phase-r1-forward
feat/cog-gov-search-basket-mode
v0.4.0
Labels
Clear labels
kodor
kodor/feature-proposal
kodor/fix
kodor/needs-review
kodor/triaged
madison-walkthrough
severity/high
severity/low
severity/medium
south-guide
verdict/defect
verdict/definitional
kodor
kodor/feature-proposal
kodor/fix
kodor/needs-review
kodor/triaged
Kodor should process this issue
Kodor has written a feature proposal
Kodor should implement a fix (assigned to Kodor)
Kodor's work or failure needs Jared's review
Kodor has already triaged this issue (skip)
Surfaced while building the client-facing Southern API guide
needs
human
Cannot move without a person -- a decision, a check an agent cannot make, something outside the repo
origin
client
Came from a client ask
origin
obligation
Created by a change elsewhere
origin
review
Came from human review
origin
roborev
Promoted from a roborev finding
type
chore
Maintenance with no behaviour change
type
debt
Owed work -- docs, tests, cleanup a change obligated
type
decision
Needs a decision before work can proceed
type
defect
Something is wrong
type
feature
New capability
ws
api
Query verbs and results
ws
corpus
Corpus, mirror, provenance
ws
docs
Vignettes and guides
Assign a task to kodor
Kodor thinks this needs a feature.
Kodor should fix this
Kodor thinks the user is ready to review this.
Kodor is done with this issue.
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Civilytics/uscogdata#59
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
cog_geographic_rollup()callscog_spending()with no limit, then does adplyr::left_join(relationship = "many-to-many")and filters/reorders in R. It is thelast verb with no bound on what it materializes.
Measured
Through cog-api against the full corpus (schema v7),
/rollups?layer=city&category=Police&years=2022:paginating; 1,268 ms after. The remaining ~1.3 s is inside this verb.
when the caller asked for 1,000.
cog-api has now fixed everything it can on its side (it slices the data frame before
materializing rows), so the remaining cost is the unpaginated verb plus the R-side join.
/rollupsis the slowest route in the API by an order of magnitude, and the only onestill without pushdown.
Ask
limit/offsetwith the #39 semantics (NULLdefault, applied in SQL, unpaginatedcount as
total_rows).full fetch. The many-to-many join is the part most likely to be doing avoidable work.
Additive, adoptable behind the existing formals probe.
Measured before implementing, and the premise here does not hold
This issue proposes moving the aggregation and crosswalk join into SQL, on the theory that
"the many-to-many join is the part most likely to be doing avoidable work." I profiled it
against the full production corpus (
layer=city,category=Police,years=2022, 20,106governments, 19,236 rows out):
cog_spending()alonecog_geographic_rollup()end to endleft_join.coverage_table()The R-side post-processing this issue targets is ~13 ms, about 1% of the total. There is
essentially nothing to win by moving it into SQL.
Pagination does not help either, for a structural reason:
GROUP BYmust completebefore
LIMITapplies, and.coverage_table()plus thecoverage = "consistent"filterboth need the complete result — computing them on a page would report wrong coverage.
Measured through cog-api,
limit=10costs 1083 ms against 1332 ms forlimit=1000, so the~19% difference is DuckDB→R transfer, not recoverable query work.
I had estimated 3–4x for this issue earlier. That estimate was wrong — it read the
limit=10/limit=1000 gap as recoverable when the aggregate is the floor.
The real fix is #58, and it is a bigger win than expected
The cohort is passed to
cog_spending()as agovidvector, which.sql_lit_chr()rendersinto a 301,591-character
INlist embedded in 5–8 statements per call. Same aggregate,four ways:
IN (20,106 literals)— todaycanonical_fips_xwalk— what #58 proposesA 4.8x penalty purely from the IN list, and the #58 approach lands within 7% of the
theoretical floor. Since the list is embedded in several statements per call, the saving is
plausibly larger than the 355 ms this single query shows.
Suggestion
Close or re-scope this in favour of #58, which is where the rollup win actually lives. If
limit/offsetare still wanted on this verb for API symmetry, they should be added asergonomics with the honest note that they bound response size rather than query cost — and
they must not be allowed to reach the coverage computation.
Method:
scripts/bench.shinCivilytics/cog-api, plus direct DuckDB timings against theproduction corpus.
Closing in favour of #58, which is where the rollup win actually is.
Profiling (detail in the comment above) showed this issue targets ~1% of the runtime: the
many-to-many
left_joinis 6 ms and.coverage_table()7 ms, against 1442 ms insidecog_spending(). Pagination cannot help either —GROUP BYcompletes beforeLIMITapplies, and both the coverage table and
coverage = "consistent"need the full result, soa page-scoped computation would report wrong coverage.
The cost is the cohort predicate, not the join: expressing the same 20,106-government cohort
as a crosswalk predicate instead of a 301,591-character
INlist takes 94 ms instead of449 ms. That is #58.
Reopen if
limit/offsetare wanted here purely for API symmetry — but they would boundresponse size, not query cost, and must be kept away from the coverage computation.