You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Rework the utf8 querying tip around the new lookup cost
The tip claimed a FULL index only pays off on a utf8 column while the
text repeats, and blamed the sorted rank list for growing with the number
of distinct values. The cost was the vocabulary load, not the lookup, and
it is gone now: the index is worth having at either cardinality.
The lookup figure gains warm bars beside the first-lookup ones (the
harness times one fresh process per variant, so it can only ever report
first lookups) and takes the full width; the index build panel below it
now reports peak memory as well as time, which is where the two flavours
differ most. Also fixes the previous subsection's claim that a utf8
column reads back in a tenth of the fixed-width memory: measured, it is
about a quarter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: doc/guides/optimization_tips.md
+5-3Lines changed: 5 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -384,12 +384,14 @@ The measurements below use a 1 Mrow table of free text averaging 76 bytes per ro
384
384
385
385

386
386
387
-
The time gap is real but modest — decompression dominates, and both flavours decompress about the same payload. The memory gap is the important one: the fixed-width array is *rows × 800 B* whatever the text actually weighs, so it does not depend on the data at all, while the `utf8()` array pays for the bytes that are there. How often titles repeat makes no difference either — the padding is charged per row, not per different value. And it scales linearly: the same column at 100 Mrows would need 80 GB of RAM to be read whole as `string(200)`, against roughly a tenth of that as `utf8()`.
387
+
The time gap is real but modest — decompression dominates, and both flavours decompress about the same payload. The memory gap is the important one: the fixed-width array is *rows × 800 B* whatever the text actually weighs, so it does not depend on the data at all, while the `utf8()` array pays for the bytes that are there. How often titles repeat makes no difference either — the padding is charged per row, not per different value. And it scales linearly: the same column at 100 Mrows would need 80 GB of RAM to be read whole as `string(200)`, against roughly a quarter of that as `utf8()`.
388
388
389
389
Anything that materializes the column benefits from this — a NumPy comparison, {meth}`to_pandas() <blosc2.CTable.to_pandas>`, a plot. UTF-8 is also the ecosystem's common currency: a `utf8()` column *is* int64 offsets plus a UTF-8 blob — Arrow's `large_string` layout — so {meth}`to_arrow() <blosc2.CTable.to_arrow>` builds straight from the stored buffers, and pandas, Polars and DuckDB take it from there. Fixed width has to transcode UCS-4 on the way out. See {ref}`utf8 and NumPy's StringDType <Utf8AndStringDType>`.
390
390
391
391
### Querying columns, with and without a FULL index
392
392
393
+
Reading a column whole is one thing; finding values on it is another. {meth}`where() <blosc2.CTable.where>` never materializes the column — it scans chunk by chunk — so the memory blow-up above does not happen here at all. A FULL index replaces that scan with a direct lookup, on either flavour.
394
+
393
395
```python
394
396
t.where("title == 'some exact title'") # scans, one chunk at a time
@@ -398,9 +400,9 @@ t.where("title == 'some exact title'") # looks it up, no scan
398
400
399
401

400
402
401
-
{meth}`where() <blosc2.CTable.where>` never materializes the column: it scans chunk by chunk, so the memory blow-up above simply does not happen on either flavour, and `utf8()`'s edge is just fewer bytes to decompress and compare.
403
+
A `utf8()` column is indexed by *alphabetical rank*: the query literal is located by bisecting the index's vocabulary, and the rows that match are a contiguous run of the sorted-positions sidecar. None of that depends on how many different values the column holds, so the index is worth having at either cardinality — a scan costs tens of milliseconds, a lookup a few. The first lookup of a session is the dearer one only because it opens the sidecars; later ones reuse them.
402
404
403
-
A FULL index turns that scan into a direct lookup — but on a `utf8()`column it only pays off while the text repeats. The index sorts the *different* values alphabetically and stores each row's position in that sorted list, so the more different values there are, the bigger that list gets and the more work the lookup does. When titles repeat, the index is a clear win, and it costs a fraction of what the fixed-width one costs to build. When almost every title is different, the lookup ends up *slower than no index at all*, while `string(200)` — whose index reads raw values straight out of a known slot — keeps the same lookup time either way. So index a `utf8()` column when its text repeats, and leave wide-open free text unindexed.
405
+
`string(200)`answers just as directly, from the values themselves. `utf8()` is the faster of the two — comparing ranks is integer work — and by far the cheaper to build: ~4.6x faster when titles repeat, 1.3x when they do not, and 2.4 GiB of peak memory for the fixed-width build whatever the data, against 200 MiB / 1.2 GiB.
404
406
405
407
Caveat emptor: that sorted list is built once, so adding rows leaves it out of date: blosc2 falls back to a scan (correct results, no speedup) until you call {meth}`rebuild_index() <blosc2.CTable.rebuild_index>`. Also, note how no index accelerates `startswith` or substring search, on any flavor.
0 commit comments