GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — fix: convert rows in batches for fetchone() and cursor iteration by maharanay22 · Pull Request #982 · databricks/databricks-sql-python

Conversation

@maharanay22

@maharanay22 maharanay22 commented Oct 9, 2026

Copy link
Copy Markdown

What type of PR is this?

  • Bug Fix

Description

Fixes #981.

Iterating a cursor (for row in cursor) or calling fetchone() in a loop costs about 0.6 to 0.8 ms per row, more than 100x slower than fetchall(). fetchone() fetched a single row and passed it through _convert_arrow_table, which builds a pandas DataFrame on every call. SQLAlchemy's default Result iteration calls cursor.fetchone() per row, so a 1M-row read through SQLAlchemy takes many minutes instead of seconds

This PR moves fetchone(), fetchmany() and fetchall() into the ResultSet base class so Thrift, SEA and kernel share one implementation. Each backend now provides _fetchmany_table, _fetchall_table and _convert_table. fetchone() fetches and converts arraysize rows at a time and serves them from a small buffer that keeps both the converted rows and the raw batch. Every other fetch method (fetchmany, fetchall, fetchmany_arrow, fetchall_arrow, and the columnar/JSON variants) drains that buffer first, so ordering and counts stay exact when row and Arrow fetches are mixed, for example fetchone() followed by fetchall_arrow(). rownumber still reports the rows handed to the caller. The buffer holds at most one batch of arraysize rows and is released as soon as it is drained

Timings on 20,000 rows (bigint + string) from an in-memory Arrow queue:

fetchall for row in cursor fetchone loop
pandas 3.0.6, before 0.07s 16.1s 11.6s
pandas 3.0.6, after 0.10s 0.10s 0.14s
pandas 2.3.3, before 0.06s 11.9s 14.8s
pandas 2.3.3, after 0.13s 0.10s 0.13s

One trade-off: the first fetchone() now fills up to arraysize rows, so on Thrift it can make the next FetchResults call earlier than before when the direct results hold fewer rows than that. Lowering cursor.arraysize restores the old behavior

How is this tested?

  • Unit tests
  • Manually

New tests in test_fetches.py, test_sea_result_set.py and test_kernel_result_set.py count calls to the conversion function and check that it runs once per batch instead of once per row. These fail on main (21, 6 and 10 calls instead of 3, 2 and 2) and pass with this change. Other new tests cover the interleavings: fetchone() then fetchall(), iteration across several queue batches, fetchone() then fetchmany() then fetchall_arrow(), fetchmany_arrow() spanning buffered and new rows, ColumnQueue and JSON paths, empty results, and rownumber at each step. test_sea_result_set.py::test_fetchone now asserts on rownumber instead of the private _next_row_index. Full unit suite: 1045 passed, 5 skipped with pandas 3, and the touched suites pass with pandas 2.3. black --check src and mypy src pass. The timings above were measured manually on both pandas versions

Related Tickets & Documents

Fixes #981. Affects every backend's fetchone() and therefore SQLAlchemy result iteration

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@maharanay22 maharanay22 mentioned this pull request Oct 10, 2026
fix: avoid ZeroDivisionError when a CloudFetch download takes no measurable time #977
Open
3 tasks done
@maharanay22
maharanay22 force-pushed the fix-row-iteration-performance branch 2 times, most recently from 06d09d1 to 1fae6cc Compare October 22, 2026 18:06
@maharanay22
fix: convert rows in batches for fetchone() and cursor iteration
2c5b5cc
fetchone() fetched a single row and ran it through _convert_arrow_table,
which builds a pandas DataFrame per call. That fixed cost made iterating a
cursor, or any fetchone() loop such as SQLAlchemy's default result
iteration, about 1 ms per row, more than 100x slower than fetchall().

ResultSet now implements fetchone(), fetchmany() and fetchall() once for
all backends. fetchone() fetches and converts arraysize rows at a time and
serves them from a small buffer that keeps both the converted rows and the
raw batch. Every other fetch method drains that buffer first, so ordering
and counts are unchanged when row and Arrow/columnar/JSON fetches are
mixed, and rownumber still reports the number of rows handed to the
caller.

Signed-off-by: Maha Rana Yadavalli <[email protected]>
@maharanay22
maharanay22 force-pushed the fix-row-iteration-performance branch from 1fae6cc to 2c5b5cc Compare October 29, 2026 04:04
@maharanay22 maharanay22 mentioned this pull request Oct 30, 2026
Iterating a cursor or calling fetchone() in a loop is 100x+ slower than fetchall() (affects SQLAlchemy) #981
Open
@maharanay22
Merge main into fix-row-iteration-performance
a05bef9
Signed-off-by: Maha Rana Yadavalli <[email protected]>
@maharanay22 maharanay22 mentioned this pull request Nov 7, 2026
fix: keep NaN distinct from NULL in float columns when pandas is enabled #979
Open
2 tasks done
@maharanay22
Merge main into fix-row-iteration-performance
bbac94b
Signed-off-by: Maha Rana Yadavalli <[email protected]>
@maharanay22

maharanay22 commented Nov 12, 2026

Copy link
Copy Markdown
Author

Hi @vuanhphung, could you take a look? Row iteration and fetchone() are now as fast as fetchall(), with tests.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@vuanhphung vuanhphung mentioned this pull request Nov 12, 2026
perf: convert large Arrow results to rows in slices #1039
Open
4 tasks done

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Iterating a cursor or calling fetchone() in a loop is 100x+ slower than fetchall() (affects SQLAlchemy)

Uh oh!

There was an error while loading. Please reload this page.

1 participant

@maharanay22
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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.