perf(predicate): evaluate IN and NOT IN with arrow::compute::is_in - #265
Open
lucasfang wants to merge 4 commits into
Open
perf(predicate): evaluate IN and NOT IN with arrow::compute::is_in#265lucasfang wants to merge 4 commits into
lucasfang wants to merge 4 commits into
Conversation
lucasfang
marked this pull request as draft
August 31, 2026 09:16
lucasfang
force-pushed
the
dev10
branch
2 times, most recently
from
August 31, 2026 10:12
4ec1e48 to
d78d924
Compare
lucasfang
marked this pull request as ready for review
September 1, 2026 06:02
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.
Purpose
Linked issue: close #262
MultiLiteralsLeafFunction::Testevaluates anIN/NOT INpredicate by materializing the whole column intoLiteralobjects, one heap allocation per row, and then comparing every row against every literal. That isO(rows × literals)comparisons plusO(rows)allocations per batch, and with the largeINlists that get pushed down from SQL it becomes a measurable part of scan time even though membership testing only needs a set lookup.This PR builds the literals into an arrow array once per batch and probes the whole column with
arrow::compute::is_in, which turns the evaluation intoO(literals)of setup plus anO(rows)hash probe with no per-row allocation. EveryLeafFunctionis a shared stateless singleton, so the value set cannot be cached on the function; building it per batch is what buys the vectorized probe.Changes:
LiteralConverter::ConvertLiteralsToArrayis added as the reverse of the existingConvertLiteralsFromArray. It writesBOOLEAN,TINYINT,SMALLINT,INT,BIGINT,FLOAT,DOUBLE,DATE,STRING,BINARY,DECIMALandTIMESTAMP, keeps a null literal as an arrow null so the result has exactly one entry per literal, and returnsStatus::Invalidfor any other field type. ForDECIMALthe arrow type carries a precision and a scale that aFieldTypealone does not settle, so the array is written with the pair the literals carry themselves: at least one literal has to be non null to settle it, and every non null one has to carry the same pair, because rescaling a value to another scale loses digits or overflows. The type is built witharrow::Decimal128Type::Make, sincearrow::decimal128checks the precision fatally. ForTIMESTAMPthe array is written with the finest time unit that keeps every value (nanosecond if any nanos-of-millisecond is not a whole microsecond, microsecond if any is non zero, millisecond otherwise), which again needs one non null literal to settle.MultiLiteralsLeafFunction::Testmoves out of the header into a newmulti_literals_leaf_function.cpp. It takes the value set path for the field types listed above;FLOATandDOUBLEjoin it except for a NaN literal, because arrow hashes the raw bits of a float whileFieldsComparator::CompareFloatingPointmakes every NaN equal.DECIMALprobes only a column carrying the scale of the literals, becauseis_incompares a value set against a column of another scale by casting one side to the other, and that cast fails on a value that does not fit the other scale whereLiteral::CompareTorescales one value at a time and merely finds no match.TIMESTAMPprobes only a column of the very unit the values need and without a time zone, because casting to a coarser unit fails on a value the unit does not keep, casting to a finer one can overflow int64, andis_inrefuses to compare a zoned timestamp against an unzoned one while a literal has no zone to speak of.SetLookupOptions::EMIT_NULLgives the exact null semantics the predicate already has: the nulls of the value set are ignored, which is whatINdoes with a null literal, and a null input row becomes a null output row, which bothINandNOT INreport as false.Literal::CompareToerror rather than being built into one typed value set, andNOT INholding a null literal, which is false for every row anyway. The fallback compares by value, so a decimal literal of another scale or a timestamp literal of another unit still matches the same number or instant.Beyond the performance change this also fixes an evaluation failure:
ConvertLiteralsFromArrayrejects several dictionary layouts, for example int32 indices over an int64 dictionary or int8 indices over a utf8 dictionary, and anINpredicate over such a column used to fail the whole evaluation.is_indecodes the dictionary itself, so those columns now evaluate normally. It also promotes both sides to their common type when the column is read as a wider or narrower arrow type than the value set, and only reports a mismatch when the field type genuinely disagrees with the column.Tests
New unit tests in
src/paimon/common/predicate/multi_literals_leaf_function_test.cpp, each asserting bothINandNOT INresults:TestInt,TestBigInt,TestInt64Boundaries,TestTinyIntAndSmallInt,TestDate,TestBoolean,TestString,TestStringWithoutEmptyLiteral,TestBinary,TestFloatAndDouble,TestNegativeZeroDoesNotMatchPositiveZeroTestDecimalincluding a column of the same scale but another precision and values beyond 64 bits,TestTimestampfor the millisecond and the nanosecond unit,TestDecimalNullLiteralsandTestTimestampNullLiteralsfor null literals in the set and for nothing but null literalsTestDecimalOffTheValueSetPathfor another scale, mixed precision and scale, and the scale pair whereis_inwould have failed the cast,TestTimestampOffTheValueSetPathfor a coarser unit, a finer unit and a column with a time zone,TestNanLiteralStaysOffTheValueSetPathTestDictionaryString,TestLargeStringDictionary,TestDictionaryWithNullValueTestNullLiteralIgnoredForIn,TestOnlyNullLiterals,TestEmptyLiteralsTestMixedLiteralTypesReportTheErrorTestProbeResolvesArrowTypeOnItsOwn, which also pins the dictionary layouts thatConvertLiteralsFromArrayrejects, andTestProbeFailsOnUnrelatedArrowTypeTestSlicedArrayfor sliced primitive and string arraysNew unit tests in
src/paimon/common/predicate/literal_converter_test.cpp:TestLiteralsToArray: every writable field type,BOOLEAN,TINYINT,SMALLINT,INT,BIGINT,FLOAT,DOUBLE,DATE,STRING,BINARY,DECIMALandTIMESTAMPwith one case per millisecond, microsecond and nanosecond unit, each with a null literal in the middle, asserting the arrow array and that it reads back throughConvertLiteralsFromArrayas the literals it was built fromTestLiteralsToArrayWithoutValue: empty input, and input that is all nullsTestLiteralsToArrayUnsupportedType:TIMESTAMPandDECIMALwithout a non null literal, decimal literals that do not share one precision and scale, a decimal precision arrow rejects instead of aborting, andBLOB,ARRAY,MAP,STRUCTandUNKNOWNall report the field type in the errorNew end-to-end tests in
src/paimon/common/predicate/predicate_test.cpp:TestLargeStringIn: a 1000-literal STRINGIN/NOT INover an array that includes the empty string and a nullTestInAfterRebind: rebinding by field name and by field index keeps results identicalTestInt64BoundaryIn:IN/NOT INwithINT64_MINandINT64_MAXthroughPredicateBuilderVerified with the full
paimon-common-testsuite: 1537 tests from 178 test suites, all passed, plus the wider regressionpaimon-parquet-format-test(202 tests),paimon-orc-format-test(119 tests) andpaimon-core-test(1863 tests), whose existing decimal and timestampIN/NOT INcases now run through the new value set path.API and Format
No. No header under
include/is touched, and neither the storage format nor the protocol changes.LiteralConvertergains one static method, which is purely additive.Documentation
No. This is a performance optimization, plus the dictionary-column fix described above, with no user-visible API or configuration change.
Generative AI tooling
Generated-by: Qoder