Skip to content

Optimize Python session result decoding - #18638

Merged
jt2594838 merged 5 commits into
apache:masterfrom
hongzhi-gao:optimize-python-session-result-decoding
Sep 16, 2026
Merged

jt2594838 merged 5 commits into
apache:masterfrom
hongzhi-gao:optimize-python-session-result-decoding

Conversation

@hongzhi-gao

@hongzhi-gao hongzhi-gao commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Decode RPC result blocks directly into buffered Python tuples for row iteration.
  • Preserve the existing SessionDataSet, RowRecord, Field, and DataFrame APIs.
  • Keep the vectorized DataFrame path unchanged.

Performance

An end-to-end benchmark was run against a local single-node IoTDB.

The test queried 50,000 rows containing INT32, DOUBLE, and TEXT columns with a fetch size of 10,000. Each measurement included query execution, RPC fetching, complete result consumption, and result-set closure.

The client was warmed up before measurement. The before and after clients were tested in reversed order across two batches, with the median of 18 measured runs reported.

Operation Before After
SessionDataSet.next() 105,509 rows/s 489,846 rows/s
todf() 869,459 rows/s 868,214 rows/s

End-to-end row iteration is approximately 4.64x faster, while DataFrame performance remains unchanged.

source = values.tolist() if hasattr(values, "tolist") else list(values)
if nulls is None:
return source
if data_type == TSDataType.BOOLEAN and len(source) == row_count:

@hongzhi-gao hongzhi-gao Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For example, Boolean values=[True, False, True] with nulls=[False, True, False] must become [True, None, True]. Boolean blocks keep positional values, unlike other nullable columns that omit null entries.

rows.extend(self.__row_buffer.popleft() for _ in range(remaining))
return rows

def _fill_row_buffer(self):

@hongzhi-gao hongzhi-gao Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Example: for 50,000 rows with fetch_size=10,000, this method drains the current RPC page into the row buffer first. It calls fetch_results() for the next page only after that buffer is empty.

self.__data_type_for_tsblock_column[location],
)
)
return list(zip(*columns)) if columns else [tuple() for _ in range(row_count)]

@hongzhi-gao hongzhi-gao Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

A TSBlock is column-oriented. For example, Time=[1, 2] and s1=[10, 20] become row tuples [(1, 10), (2, 20)] through zip(*columns), without creating an intermediate DataFrame.

Comment thread iotdb-client/client-py/iotdb/utils/iotdb_rpc_dataset.py Outdated
Comment thread iotdb-client/client-py/iotdb/utils/SessionDataSet.py Outdated
@jt2594838
jt2594838 merged commit 0f64600 into apache:master Sep 16, 2026
43 checks passed
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