Repository navigation
perf(spanner): optimize row popping in StreamedResultSet - #18316
Merged
Merged
Conversation
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.
Contributor
There was a problem hiding this comment.
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.
Contributor
Author
|
(The system test failures are unrelated) |
sinhasubham
approved these changes
Sep 10, 2026
This was referenced Sep 10, 2026
Merged
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>
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.
Eliminates an O(N^2) bottleneck when consuming query results:
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.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
spanner-row-popping)main)read-narrow-result-set(200,000 rows, narrow columns)
P50
P90
P99
374.98 ms
474.97 ms
497.50 ms
730.31 ms
2,866.85 ms
2,866.85 ms
-48.65%
-83.43%
-82.65%
~1.9x faster
~6.0x faster
~5.8x faster
read-large-result-set(100,000 rows, wide columns)
P50
P90
P99
2,147.62 ms
2,336.65 ms
2,482.95 ms
2,603.18 ms
2,725.26 ms
2,565.57 ms
-17.50%
-14.26%
-3.22%
~1.21x faster
~1.17x faster
~1.03x faster
Observations
StreamedResultSetwas the dominant CPU bottleneck. Removing that overhead yields a massive performance leap.