Skip to content

perf(spanner): optimize query result decoding and type resolution - #18330

Open
olavloite wants to merge 1 commit into
mainfrom
spanner-decode-and-type-resolution
Open

perf(spanner): optimize query result decoding and type resolution#18330
olavloite wants to merge 1 commit into
mainfrom
spanner-decode-and-type-resolution

Conversation

@olavloite

@olavloite olavloite commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Improve query result decoding performance in StreamedResultSet and simplify type decoder resolution in _helpers.

  • Optimize row construction in StreamedResultSet._merge_values:
    • Process complete rows in batch using direct column indexing rather than iterating cell-by-cell with repeated append() calls and boundary checks.
    • For lazy decoding, slice raw values directly into rows.
    • Add an explicit guard for width == 0 to prevent unbounded buffer growth on empty schemas.
    • Separate partial row boundary handling from complete row decoding by extracting _append_to_current_row, _decode_eager_rows, and _decode_lazy_rows, removing duplicated cell-appending logic.
  • Optimize type resolution in _helpers:
    • Replace the if/elif cascade in _get_type_decoder with an integer-keyed _SCALAR_DECODERS lookup table.
    • Pre-allocate static decoder callables at module scope to avoid lambda recreation when resolving column metadata.

Benchmark Results: Query Result Decoding & Type Resolution

NOTE: These benchmark results are unfortunately no longer fully valid, as they now also include the other changes that have been added to the main branch. The biggest contributor to the latency decrease in these benchmarks is not actually this change, but rather an earlier change where we got rid of the .pop(0) calls when iterating through the rows. These benchmark results should therefore rather be seen as 'the performance compared to main is still much better', and not 'it got this much better due to this change'.

We benchmarked this branch against the 7-day nightly baseline on main using the automated closed-loop result set benchmarks on Google Compute Engine (n2-standard-2 in europe-north1-a). Both benchmarks measure client-side row iteration and cell deserialization latency across all rows in the query result.

1. Read Narrow Result Set (2 int64 columns)

Metric Baseline (main 7-day avg) PR #18330 Improvement
Mean Latency 2,797.79 ms 329.68 ms -88.2% (-2,468.1 ms)
P50 Latency 724.04 ms 374.99 ms -48.2% (-349.1 ms)
P90 Latency 5,735.24 ms 475.00 ms -91.7% (-5,260.2 ms)
P99 Latency 5,973.52 ms 497.49 ms -91.7% (-5,476.0 ms)

2. Read Large Result Set (12 varied type columns)

Types: bool, bytes, date, float32, float64, interval, json, int64, numeric, string, timestamp, uuid

Metric Baseline (main 7-day avg) PR #18330 Improvement
Mean Latency 2,537.85 ms 2,150.44 ms -15.3% (-387.4 ms)
P50 Latency 2,563.41 ms 2,125.00 ms -17.1% (-438.4 ms)
P90 Latency 2,718.38 ms 2,224.96 ms -18.2% (-493.4 ms)
P99 Latency 2,863.97 ms 2,247.43 ms -21.5% (-616.5 ms)

Baseline reflects nightly runs over the preceding 7 days (2026-09-03 to 2026-09-10 UTC). PR results reflect 15-minute runs excluding initial warmup.

Improve query result decoding performance in StreamedResultSet and simplify
type decoder resolution in _helpers.

* Optimize row construction in StreamedResultSet._merge_values:
  - Process complete rows in batch using direct column indexing rather than
    iterating cell-by-cell with repeated append() calls and boundary checks.
  - For lazy decoding, slice raw values directly into rows.
  - Add an explicit guard for width == 0 to prevent unbounded buffer growth
    on empty schemas.
  - Separate partial row boundary handling from complete row decoding by
    extracting _append_to_current_row, _decode_eager_rows, and
    _decode_lazy_rows, removing duplicated cell-appending logic.
* Optimize type resolution in _helpers:
  - Replace the if/elif cascade in _get_type_decoder with an integer-keyed
    _SCALAR_DECODERS lookup table.
  - Pre-allocate static decoder callables at module scope to avoid lambda
    recreation when resolving column metadata.
@olavloite
olavloite requested a review from a team as a code owner September 10, 2026 10:26

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors and optimizes row decoding and value merging in both synchronous and asynchronous streamed result sets, introducing batch-decoding helpers and mapping scalar type codes to a pre-defined dictionary of decoders. Reviewers suggested further performance optimizations, including removing redundant integer conversions for IntEnum keys in _get_type_decoder, and using zip with sliced lists in _append_to_current_row and _decode_eager_rows to avoid the overhead of index lookups and enumeration.

Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/_helpers.py
Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/streamed.py
Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/streamed.py
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.

1 participant