Skip to content

AlpRdEncodingEncoder never emits zone-map min/max stats, silently defeating pruning on any column it encodes #382

Description

@dfa1

Summary

AlpRdEncodingEncoder (vortex.alprd, writer module) never computes or reports zone-map min/max stats — both its result-building paths hardcode null, null regardless of the input data or cascading depth. Any column the cascade selects ALP-RD for loses zone-map pruning entirely: RowFilter-driven pruning (ScanIterator#canPruneChunk) can never skip a chunk with no stats to prune with, so a filter fetches the whole column no matter how narrow the range or how disjoint from the data's actual domain.

This is a different root cause from #379 (cascade-path dropping stats that the terminal path did compute): ALP-RD's own encode() never computes stats at all, in any path.

Where

writer/src/main/java/io/github/dfa1/vortex/writer/encode/AlpRdEncodingEncoder.java:

  • buildEncodeResult(...) — line 299: return new EncodeResult(root, List.copyOf(allBuffers), null, null);
  • emptyResult(...) — line 325: same, null, null

Compare the sibling (non-RD) AlpEncodingEncoder, which does compute real stats on every path (writer/src/main/java/io/github/dfa1/vortex/writer/encode/AlpEncodingEncoder.java:192-193):

byte[] statsMin = n > 0 ? scalarF64(min) : null;
byte[] statsMax = n > 0 ? scalarF64(max) : null;

ALP-RD tracks left/right bit-split codes and exception patches but never keeps a running min/max over the input double[]/float[] while doing so.

Reproduction

File: timestamp:i64:series(1700000000000,1000), price:f64:range(50,150), 200,000,000 rows, WriteOptions.cascading(3) (the demo's default). vortex-cli inspect confirms price alone is missing min=/max= in its summary line, while timestamp/volume/symbol all show one:

timestamp: vortex.stats(...) → ...  min=1700000000000 max=1899999999000
symbol:    vortex.stats(...) → ...  min=0 max=29
price:     vortex.stats(...) → ...                       <-- no min=/max= at all
volume:    vortex.stats(...) → ...  min=100 max=10000

Consequence: a price range filter completely outside the column's actual domain (e.g. [5, 6], when every value is in [50, 150)) still fetches ~54% of the file (1.3 GB of a 2.3 GB file) instead of pruning every chunk. A filter that does overlap the domain (e.g. [50, 60]) fetches the same ~54% regardless of how narrow the overlap is — every chunk is decoded in full either way.

Impact

Silent, not a crash. Any file with a float/double column that the cascade routes through ALP-RD loses pruning on that column entirely, on top of paying the bandwidth/decode cost of every chunk regardless of filter selectivity. AlpEncodingEncoder shows the fix shape already exists in the codebase; ALP-RD just needs the same running min/max kept alongside its dictionary training and exception encoding.

How this was found

Found live-testing vortex-demo (#377) against a real 200M-row/2.4 GB file: after fixing #378/#379/#380 and a correctness bug in the demo's own row-counting, a price filter still fetched roughly the same ~54% of the file no matter how narrow or how completely outside the data's range the filter was — pointing at missing stats rather than the "no natural clustering" explanation that fit the symbol/enum case.

🤖 Filed via Claude Code

https://claude.ai/code/session_01P4ijFsGW1MHEcGiu26vNzi

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions