Skip to content

perf(spanner): optimize row popping in StreamedResultSet - #18316

Merged
olavloite merged 1 commit into
mainfrom
spanner-row-popping
Sep 10, 2026
Merged

olavloite merged 1 commit into
mainfrom
spanner-row-popping

Conversation

@olavloite

@olavloite olavloite commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Eliminates an O(N^2) bottleneck when consuming query results:

  • Previously, StreamedResultSet yielded rows by calling iter_rows.pop(0) in a loop. Because Python lists are contiguous arrays, popping index 0 shifts all remaining elements in memory on every row yielded, causing quadratic overhead for larger result sets.
  • Replaces pop(0) with direct iteration (for row in iter_rows: yield row) and detaches the list directly (iter_rows, self._rows = self._rows, []), allowing row iteration to run in linear O(N) time at C speed.

Benchmark Performance Verification

Performance verification of this PR branch (spanner-row-popping) compared against the 7-day nightly baseline of the Spanner Python client running on GCE (n2-standard-2, 2 vCPUs, closed-loop single worker):

Results Summary

Benchmark Scenario Metric PR #18316 (spanner-row-popping) Baseline (main) Delta Speedup
Narrow Result Set
read-narrow-result-set
(200,000 rows, narrow columns)
Mean
P50
P90
P99
300.82 ms
374.98 ms
474.97 ms
497.50 ms
2,866.85 ms
730.31 ms
2,866.85 ms
2,866.85 ms
-89.51%
-48.65%
-83.43%
-82.65%
~9.5x faster
~1.9x faster
~6.0x faster
~5.8x faster
Wide Result Set
read-large-result-set
(100,000 rows, wide columns)
Mean
P50
P90
P99
2,223.01 ms
2,147.62 ms
2,336.65 ms
2,482.95 ms
2,565.57 ms
2,603.18 ms
2,725.26 ms
2,565.57 ms
-13.35%
-17.50%
-14.26%
-3.22%
~1.15x faster
~1.21x faster
~1.17x faster
~1.03x faster

Observations

  • Narrow Result Sets (~90% latency reduction / ~10x throughput): With 200,000 rows and few columns, row popping in StreamedResultSet was the dominant CPU bottleneck. Removing that overhead yields a massive performance leap.
  • Wide Result Sets (~13% - 17.5% latency reduction): While complex column deserialization accounts for more relative CPU time in wide rows, the row popping optimization still delivers a consistent ~13% to ~17.5% speedup across Mean, P50, and P90.

Eliminates an O(N^2) bottleneck when consuming query results:
- Previously, StreamedResultSet yielded rows by calling `iter_rows.pop(0)` in a loop. Because Python lists are contiguous arrays, popping index 0 shifts all remaining elements in memory on every row yielded, causing quadratic overhead for larger result sets.
- Replaces `pop(0)` with direct iteration (`for row in iter_rows: yield row`) and detaches the list directly (`iter_rows, self._rows = self._rows, []`), allowing row iteration to run in linear O(N) time at C speed.
@olavloite
olavloite requested a review from a team as a code owner September 9, 2026 09:33

@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 optimizes row streaming iteration in both the synchronous and asynchronous implementations of StreamedResultSet by replacing an inefficient O(N^2) list-popping loop with a standard O(N) for-loop. It also introduces comprehensive unit tests covering large batches, stepwise consumption, chunk boundaries, early termination, and mid-stream errors. As there are no review comments, I have no feedback to provide.

@olavloite olavloite added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 9, 2026
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 9, 2026
@olavloite olavloite added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 9, 2026
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 9, 2026
@olavloite

Copy link
Copy Markdown
Contributor Author

(The system test failures are unrelated)

@olavloite olavloite added the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 9, 2026
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Sep 9, 2026
@olavloite
olavloite merged commit e66103d into main Sep 10, 2026
47 checks passed
@olavloite
olavloite deleted the spanner-row-popping branch September 10, 2026 10:12
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