Skip to content

HIVE-30019: ProbeDecode for the vectorized Parquet reader - #6758

Open
abstractdog wants to merge 3 commits into
apache:masterfrom
abstractdog:probe-decode-parquet
Open

HIVE-30019: ProbeDecode for the vectorized Parquet reader#6758
abstractdog wants to merge 3 commits into
apache:masterfrom
abstractdog:probe-decode-parquet

Conversation

@abstractdog

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Why are the changes needed?

Does this PR introduce any user-facing change?

How was this patch tested?

abstractdog and others added 3 commits September 6, 2026 21:22
Port the Hive ProbeDecode runtime path from the ORC-encoded LLAP reader to
the vectorized Parquet reader on Apache master. When a mapjoin is small and
has a much smaller key ratio than its big side, the compiler already tags
the big-side TableScan with a ProbeDecodeContext; this change makes
VectorizedParquetRecordReader honour that context.

Runtime flow, per batch:
  1. Look up the small-side VectorMapJoinHashTable from the ObjectCache via
     the cache key on ProbeDecodeContext (lazy, once per reader).
  2. Decode the probe-key column first (plain 3-arg readBatch).
  3. Probe each row against the long-key hash table -> bitmap filter.
  4. Decode the remaining columns via the new
     readBatch(int, ColumnVector, TypeInfo, ParquetProbeFilter) overload.
     For filtered-out rows the reader either skips the value on the page
     (dataColumn.skip()) or -- when the page is dict-encoded -- marks the
     dict-id slot null so decodeDictionaryIds' downstream materialisation
     short-circuits. The filter is honoured uniformly across every primitive
     read helper: readDictionaryIDs, readIntegers, readSmallInts,
     readTinyInts, readLongs, readFloats, readDoubles, readBooleans,
     readDecimal, readDecimal64, readString, readChar, readVarchar,
     readBinaries, readDate, readTimestamp. The biggest per-row saving is on
     byte-array types (string/char/varchar/binary) where filtered rows no
     longer trigger a BytesColumnVector.setVal allocation + copy;
     BinaryPlainValuesReader.skip() only reads the length prefix and
     advances the buffer.
  5. Compact the surviving rows into batch.selected[] so downstream operators
     don't re-test them.

Fast-path linkage to parquet-java:
  - readDictionaryIDs coalesces contiguous filtered rows into a single
    dataColumn.skip(n). That reaches DictionaryValuesReader.skip(int) ->
    RunLengthBitPackingHybridDecoder.skipInts, the bulk-skip fast-path
    added upstream in parquet-java, which consumes a whole RLE run in O(1).
    This is the largest single win because dictionary indices are always
    RLE / bit-packed regardless of column type.
  - Other readers use per-row dataColumn.skip(); the underlying PLAIN /
    DELTA_BINARY_PACKED / BinaryPlain readers have no bulk-skip fast-path
    (PLAIN is a byte-offset bump, DELTA is cumulative, BinaryPlain is a
    per-value length read), so coalescing would gain nothing.

Interface changes:
  - VectorizedColumnReader gains a default filter-aware readBatch overload
    (list/map/struct/dummy inherit the default no-op delegation, primitive
    reader overrides).
  - ParquetDataColumnReader gains skip() and skip(int); skip(int) delegates
    to ValuesReader.skip(int) so readDictionaryIDs' per-batch coalescing
    reaches the parquet-java bulk skip.

Compiler:
  - TezCompiler.removeSemijoinsParallelToMapJoin no longer gates the
    ProbeDecodeContext plumbing on LLAP mode -- Parquet reads on Tez
    non-LLAP consume it too now. Readers that don't consume it (regular
    ORC, text) simply ignore the extra hint on the TableScanOperator.

Scope for this first cut: single long/int key probe. String, multi-key and
Decimal64 key-probe variants and q-test golden updates follow up.

Unit tests: all 43 existing parquet-vector reader tests
(TestVectorizedColumnReader, TestVectorizedDictionaryEncodingColumnReader,
TestVectorizedListColumnReader, TestVectorizedMapColumnReader) still pass.
Adds three test artifacts covering the Parquet ProbeDecode path landed in
HIVE-30019:

1. Q-test (probedecode_mapjoin_simple_parquet.q) mirroring the ORC template
   probedecode_mapjoin_simple.q. Sets up item_dim_pq + orders_fact_pq stored
   as parquet, runs the join both with and without hive.optimize.scan.probedecode
   so the golden captures the plan (with EXPLAIN VECTORIZATION DETAIL) and
   asserts the result set matches the baseline. Auto-picked up by
   MiniLlapLocalCliConfig's sweep of ql/src/test/queries/clientpositive; the
   .q.out golden will be generated on first CI run.

2. TestParquetProbeFilter (10 tests) -- pure unit test of the
   ParquetProbeFilter contract: newBitmap null-guard, isSelected bounds and
   bitmap fidelity, compact() idempotency, and correct materialization of
   selected[] under all-accept / all-reject / mixed / empty / smaller-batch
   cases.

3. TestVectorizedParquetProbeDecodeReader (4 tests) -- end-to-end test that
   writes a Parquet file, opens VectorizedParquetRecordReader, reflects out
   the per-column readers, and drives the filter-aware readBatch signature
   directly (bypassing the MapJoin-operator plumbing that nextBatch needs to
   resolve ProbeDecodeState). Verifies:
     - Surviving rows decode correctly across int / long / double / string.
     - Filtered rows come back as null-marked slots.
     - noNulls is cleared once any filtered row is emitted.
     - allPass filter produces vectors identical to the unfiltered baseline.
   Runs on both dictionary-encoded pages (exercises readDictionaryIDs ->
   pendingSkip -> RunLengthBitPackingHybridDecoder.skipInts fast-path) and
   PLAIN-encoded pages (per-row dataColumn.skip()).

4. VectorizedParquetProbeDecodeBench (JMH) -- crosses two encodings (dict,
   plain) with four filter shapes (no-filter baseline, all-pass, all-fail,
   half). Establishes the regression floor for the isFilteredOut branch
   added to every primitive read helper and quantifies the win on filter=
   all-fail (dict path drops entire runs via skipInts).

Test results:
- TestParquetProbeFilter: 10/10 pass
- TestVectorizedParquetProbeDecodeReader: 4/4 pass
- No regression on TestVectorizedColumnReader (19), TestVectorized-
  DictionaryEncodingColumnReader (13), TestVectorizedListColumnReader (6),
  TestVectorizedMapColumnReader (5)
- itests/hive-jmh compiles clean

Signed-off-by: Laszlo Bodor <bodorlaszlo2011@gmail.com>
- HiveConf: text block for the plain-filter description (S6126, LineLength)
- VectorizedColumnReader: continuation indent for the filter-aware default
- VectorizedParquetRecordReader: split nextBatch into probe/plain helpers
  to drop cognitive complexity from 37 to well under 15 (S3776)
- VectorizedPrimitiveColumnReader: extract readDateValue helper (S3776);
  suppress S107 on the 9-arg constructor with a rationale
- ParquetProbeDecodeState: pattern-matching instanceof (S6201); replace
  the TODO comment with a follow-up note (S1135)
- ParquetProbeLongHashTable: switch expression over the hash-table kind
  (S6880); reshape the probe loop to remove the two 'continue's (S135)
- VectorizedParquetReadBench (JMH): checked exceptions in place of
  'throws Exception' (S112), FileInputFormat static setInputPaths
  (S3252), Files.deleteIfExists in tearDown (S4042/S899), package-private
  @PARAM fields (VisibilityModifier), and rationale-tagged suppressions
  for the reflection (S3011) and temp-dir (S5443) that are inherent to
  a JMH benchmark
- Tests: drop the LinkedHashMap declaration (IllegalTypeCheck), fix
  '{ x }' whitespace and single-line block layout, remove unused locals
  in TestVectorizedParquetProbeDecodeReader

Ignoring the switch-case indentation alerts on ParquetProbeLongHashTable
per the reviewer.

Verified: mvn compile (common, ql, itests/hive-jmh) and the three
probe-decode unit test classes still pass.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Sep 6, 2026

Copy link
Copy Markdown

*/
private ParquetProbeFilter probeFilter;

@Setup(Level.Trial)
@abstractdog

abstractdog commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

benchmark results: different filter selectivity, done for plain and dictionary types:


Benchmark                             (encoding)  (filter)  (plainFilter)  Mode  Cnt      Score     Error  Units
VectorizedParquetReadBench.readBatch        dict      none             on  avgt   28  10681.847 ± 219.584  us/op
VectorizedParquetReadBench.readBatch        dict      none            off  avgt   28  10626.990 ±  95.403  us/op
VectorizedParquetReadBench.readBatch        dict        10             on  avgt   28   8076.524 ± 219.829  us/op
VectorizedParquetReadBench.readBatch        dict        10            off  avgt   28   8412.107 ± 326.836  us/op
VectorizedParquetReadBench.readBatch        dict        50             on  avgt   28   8780.241 ± 173.559  us/op
VectorizedParquetReadBench.readBatch        dict        50            off  avgt   28   8695.815 ± 158.860  us/op
VectorizedParquetReadBench.readBatch        dict        90             on  avgt   28  10809.099 ± 139.926  us/op
VectorizedParquetReadBench.readBatch        dict        90            off  avgt   28   9796.215 ± 574.255  us/op
VectorizedParquetReadBench.readBatch       plain      none             on  avgt   28   9875.043 ± 267.754  us/op
VectorizedParquetReadBench.readBatch       plain      none            off  avgt   28  10013.462 ± 437.993  us/op
VectorizedParquetReadBench.readBatch       plain        10             on  avgt   28   9325.828 ± 182.182  us/op
VectorizedParquetReadBench.readBatch       plain        10            off  avgt   28   9591.766 ± 280.282  us/op
VectorizedParquetReadBench.readBatch       plain        50             on  avgt   28   9409.971 ± 174.348  us/op
VectorizedParquetReadBench.readBatch       plain        50            off  avgt   28   9639.939 ± 259.777  us/op
VectorizedParquetReadBench.readBatch       plain        90             on  avgt   28   9804.523 ± 341.500  us/op
VectorizedParquetReadBench.readBatch       plain        90            off  avgt   28   9163.814 ± 233.108  us/op

┌────────┬────────────────┬────────────────┬───────────────┬────────────────┐
│ filter │   dict × on    │   dict × off   │  plain × on   │  plain × off   │
├────────┼────────────────┼────────────────┼───────────────┼────────────────┤
│ none   │ baseline 10681 │ baseline 10627 │ baseline 9875 │ baseline 10013 │
├────────┼────────────────┼────────────────┼───────────────┼────────────────┤
│ 10     │ −24.4%         │ −20.8%         │ −5.6%         │ −4.2%          │
├────────┼────────────────┼────────────────┼───────────────┼────────────────┤
│ 50     │ −17.8%         │ −18.2%         │ −4.7%         │ −3.7%          │
├────────┼────────────────┼────────────────┼───────────────┼────────────────┤
│ 90     │ +1.2%          │ −7.8%          │ −0.7%         │ −8.5%          │
└────────┴────────────────┴────────────────┴───────────────┴────────────────┘

Dict path: the bulk-skip fast path pays. At 10% pass (skip 90% of rows), reject runs coalesce into single RunLengthBitPackingHybridDecoder.skipInts calls and the bench saves ~24% wall-clock. At 50% pass it's still ~18%. At 90% pass, the gap collapses because most rows still materialize.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants