Accept state=/type= predicates on the money verbs instead of a 40k-id IN list #58
Closed
opened 2026-08-09 11:20:44 -04:00 by jared
·
3 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#58
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.
The flat routes in cog-api resolve a cohort with
cog_gov_search()and then pass theresulting ids to
cog_spending()/cog_revenue()as agovidvector..sql_lit_chr()renders that into a quoted
IN (...)list, and.verb_spendrev()embeds it into 5–8separate SQL statements per request (scope check, main aggregate, per-capita join,
harmonization block, suggestion queries).
For
state=CA&type=citythat is ~1,500 quoted ids repeated 5–8 times. For a nationwidequery it approaches 40,000 — a multi-hundred-kilobyte SQL string, re-parsed and re-planned
per statement.
Ask
Accept
stateandtypedirectly oncog_spending()/cog_revenue()(and ideallycog_balances()), as an alternative to an explicitgovidvector, so the cohort isexpressed as a join or subquery against
canonical_fips_xwalkinside the same queryrather than round-tripped through R.
Semantics should match
cog_gov_search()'s exactly — notestatethere is a postalabbreviation translated internally, while the crosswalk column
fips_stateholds a FIPScode. That mismatch is worth preserving in one place rather than reimplementing: filtering
fips_state == "WI"in R silently matches nothing, which is a trap cog-api hit whileoptimizing this same path.
Additive (both stay
NULLby default,govidkeeps working), so it can be adopted behinda formals probe.
Measured: this is worth more than the issue claims, and it is the fix for #59 too
Same aggregate over the 20,106-government
type = citycohort (FY2022), with the cohortexpressed four ways against the production corpus:
IN (20,106 literals)— what happens todaycanonical_fips_xwalk— this issueThe rendered
INlist is 301,591 characters, and.verb_spendrev()embeds it in 5–8separate statements per call, so the real saving is likely larger than the 355 ms a single
query shows.
4.8x on this query, and within 7% of the no-filter floor. I had estimated 20–40% for
this issue earlier from a state-sized cohort; that was too conservative — the penalty is
superlinear in cohort size, so it only becomes visible at fleet scale.
This also makes #58 the fix for #59: profiling showed
/rollupsspends ~1% of its time inthe R-side join that #59 targets, and effectively all of it inside
cog_spending()over awide cohort. See my note there.
Worth keeping in mind for the implementation:
cog_gov_search(state = )takes a postalabbreviation while
canonical_fips_xwalk.fips_stateholds a FIPS code, so thepredicate needs the same translation the search verb does internally. cog-api hit exactly
this trap while optimizing the same path — an R-side
fips_state == "WI"filter silentlymatches nothing.
Implementation notes (from the profiling above)
Where the IN list is built today
.sql_lit_chr()—R/spending.R:737. Renders'a','b',...; harmless in itself..build_verb_sql()—R/spending.R:743, takesgovidand embeds it in the WHERE clause..check_govids_in_scope()—R/session.R:71-73, embeds the SAME list again.R/complete.R:79— again, inside the completion grid.That repetition is why the measured saving is likely larger than the 355 ms a single
aggregate showed: one 301,591-character string is parsed and planned 5–8 times per call.
Suggested shape
Add optional
state/typearguments tocog_spending()/cog_revenue()(and ideallycog_balances()), mutually compatible withgovidbeingNULL. When set, express thecohort as a subquery instead of a literal list:
Measured: 94 ms versus 449 ms for the literal list, within 7% of the no-filter floor.
Reuse, do not reimplement
.coerce_state_to_fips()and.coerce_type()already exist (R/search.R:117,:121) andare exactly what
cog_gov_search()uses. The API takes a postal abbreviation ("WI")while
canonical_fips_xwalk.fips_stateholds a FIPS code ("55") — a predicate writtenagainst the raw parameter silently matches nothing and returns an empty result that looks
like "this government reported nothing." cog-api hit this exact trap optimizing the same
path. Reusing the existing coercers keeps one definition of the translation.
Constraints
state/typedefault toNULL; everyexisting
govid-based call must be byte-identical in behaviour.govidandstate/typetogether means — either an erroror an intersection, but not a silent precedence rule.
formals()(seeverb_supports_pagination()in cog-api,api/R/session.R), so nothing needs to deploy in lockstep.Verifying the win
Civilytics/cog-apihasscripts/bench.sh; the fleet routes (/spending?state=..&type=..,/rollups) are where this should show. Point it at a local server reading the productioncorpus. Also worth re-running the raw comparison in the profiling comment above, since it
isolates the predicate from everything else the verb does.
Implemented in #61 — measured, and it reaches the floor
Same four-way comparison re-run against the production corpus with the shipped
implementation as one of the arms.
DUCKDB_THREADS=2, median of 5 reps after awarm-up, FY2022 aggregate over the 20,106-government
type = "city"cohort.Your original figures in brackets:
IN (20,106 literals)— 0.3.0canonical_fips_xwalk— this issueThe rendered
INlist measured 301,589 characters, matching your 301,591.The predicate arm is at the floor — 102 ms against 105 ms for querying with
no cohort restriction whatsoever. The two are inside run-to-run noise of each
other, so the cohort filter is now effectively free rather than merely cheaper.
End to end through the verb
Your prediction that the real saving exceeds what a single query shows, because
.verb_spendrev()embeds the list 5–8 times, holds:cog_spending(category = "Police")govid = <20,106 ids>type = "city"3.99x, an 809 ms saving against the 330 ms the single aggregate accounts
for. The rest is exactly the repetition you identified.
Equivalence
Verified on the production corpus rather than only the fixture: both spellings
of the cohort return 19,236 identical rows with identical
amt_nominalsums. The suite covers the same property for all three verbs, and under
per_capita/adjust_to_year/ pagination.The two decisions the issue asked to be made explicitly
govid+state/typeintersect. An error would have been theconservative call, but it could never be relaxed later without breaking
callers, whereas the intersection can be tightened by deprecation. A disjoint
intersection returns zero rows, and there is a test asserting that rather
than one of the two silently winning.
scope$govids_found/govids_missingempty and addsscope$cohortwithstate,typeandn_governments. Resolving the ids purely to report them would have put20,000 govids into every fleet-scale response body — cog-api passes
provenance through verbatim — which is the cost this issue exists to remove.
govid-named calls report exactly as before.Two things worth knowing
#59 is not closed by this. The note above says #58 is also the fix for
/rollups, and the query-level premise is right, butcog_geographic_rollup()takesgovidsas a named list of per-layer idvectors and uses it to build the govid→layer map in its own output — so it
structurally needs the ids today. Its layers are already exactly
state/county/city, the same vocabulary astype, so a predicate-namedlayer is very doable; it just changes that verb's public argument shape and
needs its own issue. I did not fold it in here.
A latent bug surfaced and is fixed in the PR.
.state_abbrev_to_fipsis anamed character vector, so
[[on an absent name throws base R's "subscriptout of bounds" — which made the curated
"Unknown state abbreviation"abortbelow it unreachable dead code.
cog_gov_search(state = "ZZ")has always giventhe cryptic error. Fixed because the new
state=argument routes into the samecoercer.