Skip to content

perf: cache row count on VirtualDataFrame - #1903

Open
Charisn wants to merge 1 commit into
sinaptik-ai:mainfrom
Charisn:perf/cache-virtual-dataframe-row-count
Open

perf: cache row count on VirtualDataFrame#1903
Charisn wants to merge 1 commit into
sinaptik-ai:mainfrom
Charisn:perf/cache-virtual-dataframe-row-count

Conversation

@Charisn

@Charisn Charisn commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem

VirtualDataFrame.rows_count runs a SELECT COUNT(*) against the source on every read, and DataframeSerializer.serialize reads it every time a prompt is built. The head() method three lines above it caches into _head; rows_count did not cache at all.

So every chat turn pays a full-table count, and so does every retry. Tracing turns against a stubbed Postgres source:

  • a successful turn: 1 COUNT(*)
  • a turn that exhausted max_retries=3: 4 COUNT(*)

At a 250 ms round trip, those counts were 41% of wall time. On a local view over a 2M-row parquet the count took 26.9 ms, 29% of the turn, since it re-reads both parquet files and redoes the join on each call. Against a large remote table it costs considerably more than that, and it repeats per turn.

The count exists only to fill in the dimensions attribute of the serialized table, and that attribute was already half broken. columns_count measures the empty backing frame, so a 98,765,432-row table serialized as:

dimensions="98765432x0"

Fix

Cache the count in _rows_count, following the _head pattern already in the class. Override columns_count to read the schema, falling back to the cached head when the schema declares no columns. Neither path adds a query, because serialize calls head() regardless.

Follow-up turns now issue no COUNT(*), and dimensions reports both axes.

Cache lifetime

_rows_count lives as long as _head does, so a long-lived VirtualDataFrame keeps reporting the row count from its first read. That is the same staleness the head cache already accepts, which is why the count is cached the same way rather than given its own invalidation rule.

Tests

Six tests in tests/unit_tests/dataframe/test_virtual_dataframe.py. Five of them fail without this change; the sixth checks that two dataframes from one loader keep separate counts, guarding against a shared cache later.

Full unit suite: 539 passed, 40 skipped.

rows_count ran a SELECT COUNT(*) against the source on every read, and
the dataframe serializer reads it on every prompt build. That is one
full-table count per chat turn, repeated on every retry: a turn that
exhausted max_retries=3 issued four of them.

The count only fills in the dimensions attribute of the serialized
table, and that attribute was already wrong. columns_count measured the
empty backing frame, so tables serialized as dimensions="98765432x0".

Cache the count in _rows_count, following the _head pattern already in
the class, and read columns_count from the schema, falling back to the
cached head when the schema declares no columns. Neither path adds a
query, since serialize calls head() regardless.
@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant