Feature Request
Is your feature request related to a problem? Please describe:
When a query needs only string length (LENGTH / CHAR_LENGTH) and not the payload, TiFlash still reads both the chars and StringSizes streams from DMFile.
A typical case is a full-table MAX(CHAR_LENGTH(col)):
SELECT
MAX(CHAR_LENGTH(c1)) AS c1,
MAX(CHAR_LENGTH(c2)) AS c2,
-- ... a dozen or more string columns
MAX(CHAR_LENGTH(c16)) AS c16
FROM t;
In one observation, TableScan outbound was ~66 GB of strings per node and Projection outbound ~38 GB of Int64. At the compute layer, LENGTH already uses offsets and ignores data; CHAR_LENGTH (utf8mb4) must scan payload to count code points. The IO layer has no “this column needs length only” signal, so TableScan still reads the fat chars stream and saturates cloud-disk bandwidth.
This is not limited to MAX(): any projection or aggregation that depends only on length, not on string contents, over-reads payload.
This request is a follow-up of #11049.
Describe the feature you'd like:
Push “length only, no payload” down to the DMFile reader, and choose substreams from the function and pack contents.
String columns in DMFile are already split into:
StringSizes: per-row length (~4 B/row)
chars: payload
- nullmap (if any)
Expected behavior:
-
LENGTH(col)
Read only StringSizes and the nullmap; skip chars. Byte length comes directly from sizes.
-
CHAR_LENGTH(col) (utf8mb4)
Still read chars by default to count code points. If a pack is proven all-ASCII (e.g. pack-level all_ascii: no high-bit bytes), then CHAR_LENGTH = LENGTH and that pack can also read sizes only.
-
Push-down interface
Scan / Projection should declare a per-column read mode, e.g. NeedPayload / NeedByteLength / NeedCharLength. MAX(LENGTH(col)) / MAX(CHAR_LENGTH(col)) in HashAgg should pass that through to TableScan, instead of materializing full string columns and dropping payload in compute.
Expected effect: in the observation above, payload dominates outbound; sizes are ~4 B/row. Reading sizes only can drop disk IO for those columns by an order of magnitude. This is general: LENGTH(col), SUM(LENGTH(col)), MAX(LENGTH(col)), and similar queries all benefit.
Implementation notes:
CHAR_LENGTH on non-ASCII packs must fall back to reading chars, matching utf8mb4 semantics.
- If
all_ascii (or equivalent) is used, store it in pack metadata; missing flag on old files is unknown → fall back to chars.
- Nullable columns still need the nullmap; empty-string vs NULL length semantics stay unchanged.
- Complementary to pack-level
max_len skip: skip packs that cannot refresh MAX first, then sizes-only on the rest. This request does not depend on skip; landing it alone still helps full-table LENGTH scans.
Describe alternatives you've considered:
-
Pack-level max_len skip
For MAX(CHAR_LENGTH) / MAX(LENGTH), prune with pack upper bounds and possibly skip entire packs. It does not help queries that must see every row’s length (SUM(LENGTH(col)), SELECT LENGTH(col)). Combined with this request it pays off most, but it does not replace sizes-only.
-
Rewriting CHAR_LENGTH to LENGTH in SQL
Close on ASCII data, but TableScan still reads chars today, so only CPU is saved. Without this request, changing SQL does not cut IO.
-
Hidden / STORED generated column of per-row length
Broader coverage as a regular integer column, but more write amplification and schema cost. TiDB cannot add STORED generated columns via ALTER TABLE. The sizes stream already stores per-row byte length; prefer reading existing StringSizes rather than storing another column.
-
Folding CHAR_LENGTH into HashAgg only
Reduces Int64 intermediates, not chars reads. Wall time barely changes when IO-bound.
Teachability, Documentation, Adoption, Migration Strategy:
- Users: no SQL change. Queries that use only
LENGTH / CHAR_LENGTH and never the payload should automatically read less chars on TiFlash.
- When it applies: a read-path optimization; existing DMFiles need not be rewritten. Pack flags such as
all_ascii apply only to files written after the change (or after compaction); old files fall back to reading chars; results stay correct.
- Observability: distinguish
sizes vs chars bytes read in TableScan / tiflash_scan, and count packs that skipped chars due to ASCII.
- Compatibility: the reader must fall back safely without
all_ascii; it must not treat CHAR_LENGTH of non-ASCII data as byte length.
- Docs: note in TiFlash scan docs that length functions prefer the string sizes stream; utf8mb4
CHAR_LENGTH still reads payload when ASCII cannot be proven.
Feature Request
Is your feature request related to a problem? Please describe:
When a query needs only string length (
LENGTH/CHAR_LENGTH) and not the payload, TiFlash still reads both thecharsandStringSizesstreams from DMFile.A typical case is a full-table
MAX(CHAR_LENGTH(col)):In one observation, TableScan outbound was ~66 GB of strings per node and Projection outbound ~38 GB of Int64. At the compute layer,
LENGTHalready uses offsets and ignoresdata;CHAR_LENGTH(utf8mb4) must scan payload to count code points. The IO layer has no “this column needs length only” signal, so TableScan still reads the fatcharsstream and saturates cloud-disk bandwidth.This is not limited to
MAX(): any projection or aggregation that depends only on length, not on string contents, over-reads payload.This request is a follow-up of #11049.
Describe the feature you'd like:
Push “length only, no payload” down to the DMFile reader, and choose substreams from the function and pack contents.
String columns in DMFile are already split into:
StringSizes: per-row length (~4 B/row)chars: payloadExpected behavior:
LENGTH(col)Read only
StringSizesand the nullmap; skipchars. Byte length comes directly from sizes.CHAR_LENGTH(col)(utf8mb4)Still read
charsby default to count code points. If a pack is proven all-ASCII (e.g. pack-levelall_ascii: no high-bit bytes), thenCHAR_LENGTH = LENGTHand that pack can also read sizes only.Push-down interface
Scan / Projection should declare a per-column read mode, e.g.
NeedPayload/NeedByteLength/NeedCharLength.MAX(LENGTH(col))/MAX(CHAR_LENGTH(col))in HashAgg should pass that through to TableScan, instead of materializing full string columns and dropping payload in compute.Expected effect: in the observation above, payload dominates outbound; sizes are ~4 B/row. Reading sizes only can drop disk IO for those columns by an order of magnitude. This is general:
LENGTH(col),SUM(LENGTH(col)),MAX(LENGTH(col)), and similar queries all benefit.Implementation notes:
CHAR_LENGTHon non-ASCII packs must fall back to readingchars, matching utf8mb4 semantics.all_ascii(or equivalent) is used, store it in pack metadata; missing flag on old files is unknown → fall back tochars.max_lenskip: skip packs that cannot refreshMAXfirst, then sizes-only on the rest. This request does not depend on skip; landing it alone still helps full-tableLENGTHscans.Describe alternatives you've considered:
Pack-level
max_lenskipFor
MAX(CHAR_LENGTH)/MAX(LENGTH), prune with pack upper bounds and possibly skip entire packs. It does not help queries that must see every row’s length (SUM(LENGTH(col)),SELECT LENGTH(col)). Combined with this request it pays off most, but it does not replace sizes-only.Rewriting
CHAR_LENGTHtoLENGTHin SQLClose on ASCII data, but TableScan still reads
charstoday, so only CPU is saved. Without this request, changing SQL does not cut IO.Hidden / STORED generated column of per-row length
Broader coverage as a regular integer column, but more write amplification and schema cost. TiDB cannot add STORED generated columns via
ALTER TABLE. The sizes stream already stores per-row byte length; prefer reading existingStringSizesrather than storing another column.Folding
CHAR_LENGTHinto HashAgg onlyReduces Int64 intermediates, not
charsreads. Wall time barely changes when IO-bound.Teachability, Documentation, Adoption, Migration Strategy:
LENGTH/CHAR_LENGTHand never the payload should automatically read lesscharson TiFlash.all_asciiapply only to files written after the change (or after compaction); old files fall back to readingchars; results stay correct.sizesvscharsbytes read in TableScan /tiflash_scan, and count packs that skippedcharsdue to ASCII.all_ascii; it must not treatCHAR_LENGTHof non-ASCII data as byte length.CHAR_LENGTHstill reads payload when ASCII cannot be proven.