Columnar storage layer and the .qfc file format - #1
Merged
Merged
Conversation
The layer everything above it reads from. Columns are typed buffers with a packed validity bitmap and, for strings, a single byte buffer plus offsets rather than a String per row. The file format writes column chunks grouped into row groups, with a footer that records each chunk's byte range, its encoding and its zone map. A reader seeks to the footer, decides what it needs, and reads only those chunks — which is what makes projection and predicate pushdown physical rather than cosmetic later on. Each chunk picks its own encoding by measuring the data: long runs go RLE, low cardinality goes dictionary, anything else stays plain. Measured on the tests' own fixtures, RLE is over 10x smaller than plain on a sorted column and the dictionary is 4x smaller on a shuffled four-value string column. Corrupt input is treated as an expected failure rather than a bug: bad magic, a truncated file, a footer length past the end of the file, a dictionary code past the end of its dictionary and an RLE run longer than the declared row count are all reported, and each has a test.
Welcome to Codecov 🎉Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests. ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment Thanks for integrating Codecov - We've got you covered ☂️ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The foundation the rest of the engine sits on.
Arrays and batches. A column is a typed buffer plus an optional packed validity bitmap — optional because a column with no nulls should not pay for a mask, and most don't. Strings use one contiguous byte buffer plus an offsets vector rather than a
Stringper row, so scanning a string column touches two allocations instead of a million.RecordBatchis the unit that moves between operators; nothing in the engine has a "next row" method.The
.qfcformat. Magic, row groups of column chunks, then a footer holding the schema and, per chunk, its byte range, encoding and zone map. The footer is at the end with its length as the last field, so a reader seeks tolen - 8and learns the whole layout without touching data. That is what lets a query over two of forty columns read two chunks per row group, and a query whose predicate falls outside a row group's[min, max]read none of it.Encodings are chosen by measurement, not by type. Two signals decide it — average run length and distinct/row ratio — because the right answer changes between row groups of the same column. On the tests' fixtures RLE comes out >10x smaller than plain on a sorted column, and the dictionary 4x smaller on a shuffled four-value string column; both assertions are in the suite.
Things I decided deliberately, and why:
WHERE.Value'sPartialEq/Hashare grouping semantics (two NULLs group together,Int64(7)andFloat64(7.0)hash alike so an int/float hash join doesn't silently drop rows).sql_comparestays the authority for predicates and returnsNonefor NULL.may_matchdefaults to "may match" when it cannot prove otherwise.<>only prunes a row group that is a single value with no nulls.SELECT count(*)after a filter would come back zero.Verification. 122 tests in this crate. Corrupt-input paths are tested explicitly: bad magic, truncation, a footer length past EOF, a dictionary code past its dictionary, an RLE run longer than the declared row count, a validity mask of the wrong width.