Repository navigation
perf(spanner): build ExecuteSqlRequest on the raw protobuf message - #18379
Conversation
Building an ExecuteSqlRequest through proto-plus costs about 14 us per query, because every keyword argument goes through a descriptor lookup and type coercion. The new _make_execute_sql_request() helper builds the request on the underlying protobuf message and wraps it, which takes 2 us. The helper lives in _helpers.py so the sync and async snapshots share one copy. Behaviour is unchanged: the tests compare both the message and its serialized bytes against the proto-plus constructor. The sync snapshot.py hunk was mirrored by hand, since regenerating that file today would delete unrelated live code. I will open a separate PR to fix that existing error.
There was a problem hiding this comment.
Code Review
This pull request optimizes query execution performance by introducing a fast builder helper, _make_execute_sql_request, which constructs ExecuteSqlRequest directly on the underlying protobuf message to bypass proto-plus overhead. This helper is integrated into both the synchronous and asynchronous execute_sql methods, and comprehensive unit tests are added. Feedback on the changes suggests explicitly checking if param_types is not None instead of if param_types to prevent an empty dictionary from being treated as falsy, ensuring consistent behavior with the proto-plus constructor.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a fast request builder helper, _make_execute_sql_request, along with _as_raw_pb in _helpers.py to optimize the creation of ExecuteSqlRequest on performance-critical hot paths. It updates both the synchronous and asynchronous snapshot implementations to use this helper and adds comprehensive unit tests. The review feedback highlights a bug where directed_read_options is resolved but omitted from the helper call in snapshot.py, and suggests performance optimizations for _as_raw_pb by adding an early None check and directly accessing the private _pb attribute of proto.Message.
Combines all non-draft, non-'do not merge' Spanner micro-optimization PRs: - #18329: perf(spanner): optimize built-in metrics hot path and harden concurrency - #18359: perf(spanner): optimize query parameter encoding with direct type dispatch - #18379: perf(spanner): build ExecuteSqlRequest on the raw protobuf message - #18408: perf(spanner): optimize request ID header generation and retry closures - #18420: perf(spanner): prune lock and begin event allocations on single-use snapshots - #18422: perf(spanner): avoid allocating empty RequestOptions on read and query paths - #18602: perf(spanner): optimize single-chunk results and single-row lookups - #18604: test(spanner): add tests for consuming PartialResultSet streams
🤖 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>
Building an ExecuteSqlRequest through proto-plus costs about 14 us per query, because every keyword argument goes through a descriptor lookup and type coercion. The new _make_execute_sql_request() helper builds the request on the underlying protobuf message and wraps it, which takes 2 us.
The helper lives in _helpers.py so the sync and async snapshots share one copy. Behaviour is unchanged: the tests compare both the message and its serialized bytes against the proto-plus constructor.
The sync snapshot.py hunk was mirrored by hand, since regenerating that file today would delete unrelated live code. I will open a separate PR to fix that existing error.