GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — perf: convert large Arrow results to rows in slices by vuanhphung · Pull Request #1039 · databricks/databricks-sql-python Conversation
|
| Python | conversion: main → this PR | + later GC: main → this PR | peak RSS: main → this PR |
|---|---|---|---|
| 3.9 | 25.9 → 23.7 s | 27.5 → 25.4 s | 4.12 → 3.70 GB |
| 3.10 | 26.2 → 24.1 s | 27.8 → 25.7 s | 4.07 → 3.65 GB |
| 3.11 | 24.4 → 22.7 s | 26.6 → 25.0 s | 5.28 → 4.00 GB |
| 3.12 | 26.5 → 24.5 s | 28.3 → 26.3 s | 5.21 → 4.80 GB |
| 3.13 | 23.7 → 21.2 s | 26.1 → 23.7 s | 5.39 → 4.97 GB |
| 3.14 | 21.3 → 19.6 s | 23.5 → 21.9 s | 6.37 → 4.93 GB |
Before the last two commits, live fetchall() over Thrift CloudFetch for this query on Python 3.11 had a 5.99 → 4.45 GB peak RSS. _disable_pandas=True is unaffected.
This pull request and its description were written by Isaac.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
fetchall() and large fetchmany() converted the whole Arrow table with one to_pandas() call and one object ndarray, so a DataFrame, the object array and the final Row list were all alive at the peak. Convert 100,000 rows at a time instead. Columns whose pandas conversion depends on the column's other values (list/map/struct: pyarrow picks the element dtype from the whole column) are still converted over the whole table, so every value and type is unchanged. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
✅ No issues identified by the review bot.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Rows were built through Row.__call__ -> _create_row -> Row.__new__, three Python calls per row. Build them with tuple.__new__ and the usual __fields__ assignment instead, which gives the same Row objects (tested against Row(*names)(*values)). With more values than column names it falls back to ResultRow(*values), which raises the same error. Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
On Python 3.13+ Decimal values are tracked by the cyclic GC (as Timedelta and DateOffset are on every version). The whole-table to_pandas() on main created all of them before any row, so the GC's long-lived generation was already large when rows were built and it ran 5 full collections for 5M mixed rows. Created slice by slice between rows, they made it run 13, which made the sliced conversion 7-15% slower than main on 3.13/3.14. Convert decimal, duration and interval columns over the whole table like nested ones; their values are unchanged. The whole-column DataFrame gets a null placeholder column so it stays multi-block like the whole table's (pandas < 2.2 returns a lone duration column as ints otherwise). Signed-off-by: Vu Anh Phung <[email protected]> Co-authored-by: Isaac <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Verdict: 1 Low
Looks good, with one low-severity wording/performance note. The sliced conversion keeps nested, decimal, duration and interval columns in a single whole-table pass, as main does. Placeholder columns keep the same pandas block layout, and the _make_rows fast path builds the same objects as _create_row (__fields__ is the ResultRow itself). The tests compare each value and its type against the unsliced conversion.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| append = rows.append | ||
| for v in values: | ||
| row = new_row(Row, v) | ||
| row.__fields__ = ResultRow |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Low — row.__fields__ = ResultRow still calls the Python-level Row.__setattr__ once per row. That makes the CHANGELOG line "build the rows without per-row Python calls" (and the _make_rows docstring) slightly wrong: it went from three calls per row to one, not zero. You could either make the wording precise ("with one Python call per row instead of three") or, if you want the extra speed, write row.__dict__["__fields__"] = ResultRow. That does the same thing as Row.__setattr__ without the method call. _assert_same_as_factory_row already checks vars(row).keys(), so the existing tests would cover the change.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
21e3b439
Deployed
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.