Skip to content

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

Merged
olavloite merged 4 commits into
mainfrom
spanner-decode-and-type-resolution
Sep 28, 2026
Merged

olavloite merged 4 commits 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: Lazy Decode Performance (Head-to-Head vs main)

We benchmarked this PR branch (spanner-decode-and-type-resolution) concurrently against the current main branch under identical conditions on Google Compute Engine (n2-standard-2, 2 vCPUs, single worker thread, closed-loop in europe-north1-a) using lazy_decode=True.
This workload measures pure result set streaming and raw protobuf cell iteration without eager deserialization into rich Python types.

1. Wide Result Set (read-large-result-set, 100,000 rows/query, lazy_decode=True)

Metric main PR #18330 Delta Speedup
Mean Latency 565.68 ms 514.36 ms -51.33 ms -9.07%
P50 Latency 620.26 ms 524.58 ms -95.68 ms -15.43%
P90 Latency 724.80 ms 705.44 ms -19.36 ms -2.67%
P99 Latency 748.31 ms 746.11 ms -2.20 ms -0.29%
Throughput (Ops) 9,904 10,706 +802 +8.10%

2. Narrow Result Set (read-narrow-result-set, 200,000 rows/query, lazy_decode=True)

Metric main PR #18330 Delta Speedup
Mean Latency 220.48 ms 214.02 ms -6.47 ms -2.93%
P50 Latency 186.84 ms 184.38 ms -2.46 ms -1.32%
P90 Latency 316.70 ms 275.26 ms -41.44 ms -13.09%
P99 Latency 481.61 ms 478.69 ms -2.92 ms -0.61%
Throughput (Ops) 21,925 21,220 -705 -3.22%

Observations

  • Consistent Gains with lazy_decode=True: Across repeated concurrent head-to-head runs, replacing cell-by-cell Python iteration in _merge_values with native C-level list slicing (_decode_lazy_rows) delivers consistent speedups across both wide and narrow workloads:
    • Wide reads (100k rows): ~9.1% lower mean latency and ~8.1% higher throughput.
    • Narrow reads (200k rows): Consistently lower mean and tail latencies (P90 reduced by 13%–43%).
  • No Regression in Eager Mode: In eager decoding (lazy_decode=False), execution is bounded by network streaming and Python object instantiation (datetime, Decimal, json.loads), tracking within ±1% of main. When applications use lazy_decode=True, this optimization delivers significant real-world CPU and latency savings.

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.

@olavloite olavloite added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 16, 2026
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 16, 2026
@olavloite
olavloite merged commit ce72986 into main Sep 28, 2026
51 checks passed
@olavloite
olavloite deleted the spanner-decode-and-type-resolution branch September 28, 2026 17:06
parthea pushed a commit that referenced this pull request Nov 12, 2026
🤖 I have created a release *beep* *boop*
---


##
[3.72.0](google-cloud-spanner-v3.71.0...google-cloud-spanner-v3.72.0)
(2026-11-12)


### Features

* **dbapi:** wire timeout parameter through Connection to execute_sql
([#18484](#18484))
([98fc304](98fc304))
* declare Python3.15 support
([8775794](8775794))
* **spanner:** auth login support for Spanner Omni endpoints
([#18273](#18273))
([9e0548d](9e0548d))
* update API sources and regenerate
([#18797](#18797))
([be4d954](be4d954))


### Bug Fixes

* **google-cloud-spanner:** clear buffered DDL statements on rollback
and flush on autocommit
([#18636](#18636))
([15d6a2c](15d6a2c))
* **google-cloud-spanner:** do not replay DDL statements on transaction
retry
([#18625](#18625))
([78b9da2](78b9da2))
* **spanner:** correctly wrap async streaming calls in metrics
interceptor
([#18581](#18581))
([2d19b0a](2d19b0a))
* **spanner:** eliminate duplicate metrics capture and measure full
streaming latency
([#18605](#18605))
([a7678fd](a7678fd))
* **spanner:** record streaming operation completion when stream
finishes
([#18704](#18704))
([ddeae47](ddeae47))
* **spanner:** release transaction lock if inline begin fails
([#18409](#18409))
([dd24029](dd24029))
* **spanner:** stop deleting multiplexed sessions on rotation and close
([#18332](#18332))
([c38e960](c38e960))


### Performance Improvements

* **spanner:** add fast-path for multiplexed session acquisition
([#18317](#18317))
([9569b39](9569b39))
* **spanner:** avoid allocating empty RequestOptions on read and query
paths
([#18422](#18422))
([3743ff0](3743ff0))
* **spanner:** build ExecuteSqlRequest on the raw protobuf message
([#18379](#18379))
([6d91fec](6d91fec))
* **spanner:** eliminate redundant tracing span in run_in_transaction
([#18357](#18357))
([181e26a](181e26a))
* **spanner:** optimize built-in metrics hot path and harden concurrency
([#18329](#18329))
([f2a791f](f2a791f))
* **spanner:** optimize query option merging and prevent in-place
mutation
([#18358](#18358))
([8cb3c6a](8cb3c6a))
* **spanner:** optimize query parameter encoding with direct type
dispatch
([#18359](#18359))
([f06497c](f06497c))
* **spanner:** optimize query result decoding and type resolution
([#18330](#18330))
([ce72986](ce72986))
* **spanner:** optimize request ID header generation and retry closures
([#18408](#18408))
([ad03b95](ad03b95))
* **spanner:** optimize row popping in StreamedResultSet
([#18316](#18316))
([e66103d](e66103d))
* **spanner:** optimize single-chunk results and single-row lookups
([#18602](#18602))
([05b3775](05b3775))
* **spanner:** prune lock and begin event allocations on single-use
snapshots
([#18420](#18420))
([ab56473](ab56473))
* **spanner:** short-circuit trace_call when tracing is inactive
([#18356](#18356))
([58031be](58031be))
* **spanner:** support PartialResultSet.last with background stream
draining
([#18320](#18320))
([80efd63](80efd63))


### Documentation

* fix typos
([#18979](#18979))
([5c8baeb](5c8baeb))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
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.

3 participants