Skip to content

perf(spanner): optimize query result decoding#17375

Open
olavloite wants to merge 3 commits into
mainfrom
spanner-optimize-decoding
Open

perf(spanner): optimize query result decoding#17375
olavloite wants to merge 3 commits into
mainfrom
spanner-optimize-decoding

Conversation

@olavloite
Copy link
Copy Markdown
Contributor

@olavloite olavloite commented Jun 4, 2026

Work in progress.

Optimizes the decoding and reading of (large) result sets for Spanner.

image

Copy link
Copy Markdown
Contributor

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

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 performance across Spanner streaming and helper modules by caching list append methods, pre-compiling regular expressions, and replacing helper functions with direct attribute getters or lambdas. However, several critical issues were identified in _helpers.py: missing imports for datetime and base64 will cause runtime errors, BYTES values are incorrectly encoded to UTF-8 instead of being base64-decoded, and the new timestamp parsing logic ignores timezone offsets for non-UTC inputs, resulting in incorrect times.

Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/_helpers.py Outdated
Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/_helpers.py
Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/_helpers.py Outdated
@olavloite
Copy link
Copy Markdown
Contributor Author

/gemini review

Copy link
Copy Markdown
Contributor

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

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 introduces several performance optimizations to the Spanner client library, such as caching local variables and methods during row merging, using lambdas and operator.attrgetter for type decoding, pre-compiling regex patterns, and optimizing timestamp parsing. The reviewer provided valuable feedback to further boost performance, including checking the more common number_value field first for float decoding, using string slicing instead of regular expressions for timestamp parsing, and removing the unused re import.

Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/_helpers.py Outdated
Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/_helpers.py
Comment thread packages/google-cloud-spanner/google/cloud/spanner_v1/_helpers.py Outdated
@olavloite
Copy link
Copy Markdown
Contributor Author

The time needed to decode 100,000 rows is significantly reduced from approx 6-7s to approx 3-3.5s. The increasing gap between the two lines is due to the nature of this benchmark. The benchmark simulates a randomized poisson load. A small burst in traffic is enough to completely overload the old implementation, and cause requests to queue up and start taking an exponentially increasing time.

image

@olavloite olavloite marked this pull request as ready for review June 4, 2026 16:30
@olavloite olavloite requested a review from a team as a code owner June 4, 2026 16:30
@olavloite olavloite changed the title perf(spanner): [WIP] optimize query result decoding perf(spanner): optimize query result decoding Jun 4, 2026
Copy link
Copy Markdown
Contributor

@rahul2393 rahul2393 left a comment

Choose a reason for hiding this comment

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

LGTM - with minor comments/questions and test assert fix

if val[19] == ".":
if val.endswith("Z"):
offset = "Z"
fraction = val[20:-1]
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.

Spanner Timestamp seems to be always UTC Z at the API/wire level. Non-Z offset only come from CAST(ts as String), which is a STRING column and routes through the STRING decoder.

+/-HH:MM handling plus the astimezone call won't ever be executed on real TIMESTAMP value? Can we drop it to shrink the risky code, or comment marking it deliberately defensive. The Z fast-path is the legitimate win and short-circuits before this anyway?


parsed = self._callFUT(value_pb, field_type, field_name)
self.assertIsInstance(parsed, datetime_helpers.DatetimeWithNanoseconds)
self.assertEqual(parsed, expected)
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.

DatetimeWithNanoseconds.eq is inherited from datetime and compares only to microsecond - it ignores .nanosecond. So this passes even if sub-us digits are wrong.

index = 0
else:
for value in values:
if value.HasField("null_value"):
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.

value.HasField("null_value") duplicates _parse_nullables's semantics(still used in lazy/array/single-value paths). Should we add comment here to keep in sync so duplication doesn't silently drift?

return operator.attrgetter("bool_value")
elif type_code == TypeCode.INT64:
return _parse_int64
return lambda value_pb: int(value_pb.string_value)
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.

Maybe a comment on why only STRING/BOOL use attrgetter(faster) while transform trypes use lambdas?

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.

2 participants