Repository navigation
perf(kernel): cut per-query Python work for small results - #1059
Open
vuanhphung wants to merge 2 commits into
Open
vuanhphung wants to merge 2 commits into
vuanhphung wants to merge 2 commits into
Conversation
Each query rebuilt the cursor description and a Row factory from the Arrow schema, and each row fetch combined the record batches into a Table, whose construction releases and re-takes the GIL. Connections now keep what they derive from the last result schema and reuse it while statements return an equal schema, and small results are converted from the batches directly (falling back to the Table when a batch schema does not match). Closing a sync result skips the async-handle bookkeeping, and staging-verb detection no longer copies the statement. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
vuanhphung
had a problem deploying
to
azure-prod
November 14, 2026 00:57 — with
GitHub Actions
Error
vuanhphung
temporarily deployed
to
azure-prod
November 14, 2026 00:57 — with
GitHub Actions
Inactive
There was a problem hiding this comment.
Verdict: 1 Low
Looks good overall. I found one low-severity issue: the kernel _convert_arrow_table override is no longer called from src/, so the main pandas-parity tests no longer cover the _rows path that production uses. Nit: in test_the_cost_budget_is_computed_once_per_schema_not_per_fetch, [1, 2, 3, 4] is split one element per line, which is hard to read; binding the fetched values to a local first would keep it on one line. I checked the schema cache, the close_command fast path and the staging-verb head-slice for correctness and found no problems.
Row fetches now go through KernelResultSet._rows, so its _convert_arrow_table override had no callers left; remove it and point the parity, out-of-range and fallback-logging tests at _rows. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
vuanhphung
temporarily deployed
to
azure-prod
November 14, 2026 01:14 — with
GitHub Actions
Inactive
vuanhphung
had a problem deploying
to
azure-prod
November 14, 2026 01:14 — with
GitHub Actions
Error
vuanhphung
had a problem deploying
to
azure-prod
November 14, 2026 01:14 — with
GitHub Actions
Error
vuanhphung
temporarily deployed
to
azure-prod
November 14, 2026 01:14 — with
GitHub Actions
Inactive
vuanhphung
temporarily deployed
to
azure-prod
November 14, 2026 01:40 — with
GitHub Actions
Inactive
vuanhphung
temporarily deployed
to
azure-prod
November 14, 2026 01:43 — with
GitHub Actions
Inactive
This was referenced Nov 14, 2026
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What type of PR is this?
Stacked on #1048 (base
perf/rd-py-overhead); review only the top commit.Description
With #1048, the GIL-held Python per query on the kernel path is small but still repeated work. Each query rebuilt the cursor description, a
Rowfactory and the to_pylist() cost budget from an Arrow schema that is the same for every run of a statement. Each row fetch also combined the record batches into apyarrow.Tableonly to read its columns back out, andTable.from_batchesreleases and re-takes the GIL, an extra hand-off per fetch that costs throughput when 200 threads share one GIL.Each connection now keeps what it derives from the last result schema and reuses it while statements return an equal schema (
schema.equals(..., check_metadata=True): about 0.05 µs, against 2-16 µs to rebuild). Small results are converted from the batches directly, with the same cost budget, allowlist and to_pylist() error fallback as #1048; a batch whose schema doesn't match goes through the Table, which raises as before. Two smaller items: closing a sync result no longer takes the async-handle lock for bookkeeping it never needed, and staging-verb detection reads the first 7 characters of the statement instead of copying it twice.How is this tested?
New tests cover reuse and invalidation of the derived schema data, rows built from several batches after partial fetches, a schema mismatch, a call-count check that the cost budget is computed once per schema rather than per fetch, the lock-free close, and more staging-verb cases (also fuzzed against the old implementation, 300k inputs, no differences). The result-set tests pass with pandas 1.3.5, 1.5.3, 2.0.3, 2.2.3 and 3.1.1, and on pyarrow 14.
Central C800 against a mock server (Rust mock with 20 ms server time, 4 processes x 200 sessions, 60 s, 16 client cores, kernel with the factory PRs #453 #455 #456 #457 #458, 2 rounds, medians; measured on this change's previous base, d965a66 vs fdada64):
Rebased on the final #1048 (51214d2 vs 5668a0e), locally (1 x 200 sessions, 60 s, Python mock with 20 ms server time, 8 cores, kernel main + #457 + #458): 3963 / 3858 vs 4320 / 4477 QPS and 0.52 vs 0.49 ms CPU per query; the pure-Python part of a one-row query drops from 31.5 to 23.7 µs.
Peak RSS: the central run showed +30 MB per process. Python heap retained per query is the same with and without this change (249 B, all of it
Connection._cursors, which #1052 fixes); at a fixed 60,000 queries peak RSS is equal (231 vs 232 MB), and with #1052 applied to both sides, 60 s runs reach 233 vs 237 MB while this change completes 13-18% more queries. The per-connection cache holds one schema entry.Live benchmark (staging Large Reyden warehouse)
TPC-H Q6 on a Large Real-Time warehouse, 800 sessions (4 processes x 200 threads), cold start, each session querying back to back for 120 s, 16 client cores. Isolated A/B: only this change vs #1048 51214d2 (this PR is stacked on it), 3 interleaved rounds, nothing else on the warehouse. Measured at 5668a0e. Median (min-max):
QPS is server-bound here; the CPU saving matters when the client is the bottleneck (see the mock numbers above).
SELECT 1 on an X-Large Real-Time warehouse (driver-bound). Q6 above is limited by the warehouse, so the same harness also ran
SELECT 1, where the client is the limit: X-Large warehouse, 4 processes x 200 threads, 120 s, 16 client cores, this change vs #1048 (51214d2), measured at bc2e0f2, run in rotation with the other factory PRs, 2 rounds. Median (min-max):Average QPS change per round: +9.0%, +1.0%.
Related Tickets & Documents
Stacked on #1048.
This pull request and its description were written by Isaac.