diff --git a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java index b8aea76155a9..4931594e0d93 100644 --- a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java +++ b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java @@ -2667,6 +2667,15 @@ public static enum ConfVars { + "e.g., use the cached MapJoin hashtable created on the small table side to filter out row columns that are not going " + "to be used when reading the large table data. This will result less CPU cycles spent for decoding unused data."), + HIVE_OPTIMIZE_SCAN_PROBEDECODE_PARQUET_PLAIN_FILTER( + "hive.optimize.scan.probedecode.parquet.plain.filter.enabled", true, + "When ProbeDecode is on, gates the per-row filter check applied to Parquet PLAIN-encoded pages. \n" + + "The check trades a small per-row overhead for skipping the value decode + null-set on filtered rows; \n" + + "at very selective join hit rates (~5-15%, common in TPC-DS) it wins by ~2-3%, at inclusive hit rates \n" + + "(>50%) it can regress the read path by 2-4%. The dictionary-encoded path is unaffected and always \n" + + "honours the filter via the bulk-skip fast-path. Disable this only on workloads where PLAIN-encoded \n" + + "fact columns dominate and hash-join hit rates are typically high; see HIVE-30019 for the crossover data."), + HIVE_OPTIMIZE_HMS_QUERY_CACHE_ENABLED("hive.optimize.metadata.query.cache.enabled", true, "This property enables caching metadata for repetitive requests on a per-query basis"), diff --git a/itests/hive-jmh/pom.xml b/itests/hive-jmh/pom.xml index 196882ecefdb..b07556077aea 100644 --- a/itests/hive-jmh/pom.xml +++ b/itests/hive-jmh/pom.xml @@ -98,6 +98,24 @@ ${mockito-inline.version} + + + + org.apache.maven.plugins + maven-compiler-plugin + + full + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + + + + perf @@ -118,7 +136,11 @@ org.openjdk.jmh.Main + + true + + diff --git a/itests/hive-jmh/src/main/java/org/apache/hive/benchmark/vectorization/parquet/VectorizedParquetReadBench.java b/itests/hive-jmh/src/main/java/org/apache/hive/benchmark/vectorization/parquet/VectorizedParquetReadBench.java new file mode 100644 index 000000000000..6412df3b7d36 --- /dev/null +++ b/itests/hive-jmh/src/main/java/org/apache/hive/benchmark/vectorization/parquet/VectorizedParquetReadBench.java @@ -0,0 +1,385 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hive.benchmark.vectorization.parquet; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.util.concurrent.TimeUnit; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hadoop.hive.ql.exec.Utilities; +import org.apache.hadoop.hive.ql.exec.vector.BytesColumnVector; +import org.apache.hadoop.hive.ql.exec.vector.DoubleColumnVector; +import org.apache.hadoop.hive.ql.exec.vector.LongColumnVector; +import org.apache.hadoop.hive.ql.exec.vector.VectorizedRowBatch; +import org.apache.hadoop.hive.ql.exec.vector.VectorizedRowBatchCtx; +import org.apache.hadoop.hive.ql.io.IOConstants; +import org.apache.hadoop.hive.ql.io.parquet.vector.VectorizedColumnReader; +import org.apache.hadoop.hive.ql.io.parquet.vector.VectorizedParquetRecordReader; +import org.apache.hadoop.hive.ql.io.parquet.vector.probe.ParquetProbeFilter; +import org.apache.hadoop.hive.ql.plan.MapWork; +import org.apache.hadoop.hive.serde2.ColumnProjectionUtils; +import org.apache.hadoop.hive.serde2.typeinfo.TypeInfo; +import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoFactory; +import org.apache.hadoop.mapred.JobConf; +import org.apache.hadoop.mapreduce.Job; +import org.apache.parquet.example.data.Group; +import org.apache.parquet.example.data.simple.SimpleGroupFactory; +import org.apache.parquet.hadoop.ParquetInputFormat; +import org.apache.parquet.hadoop.ParquetWriter; +import org.apache.parquet.hadoop.example.GroupWriteSupport; +import org.apache.parquet.hadoop.metadata.CompressionCodecName; +import org.apache.parquet.io.api.Binary; +import org.apache.parquet.schema.MessageType; +import org.apache.parquet.schema.MessageTypeParser; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +/** + * JMH benchmark that compares the pre-patch decode path (a bare 3-arg + * {@code readBatch(total, col, type)} call — what {@code VectorizedParquetRecordReader.nextBatch} + * would issue before HIVE-30019) against the post-patch filter-aware path + * ({@code readBatch(total, col, type, ParquetProbeFilter)} with a supplied bitmap) across a + * sweep of filter selectivities. + * + *

A single {@code @Benchmark} method ({@link #readBatch}) drives every projected column + * through {@link #BATCHES_PER_INVOCATION} batches per invocation; the {@link #filter} @Param + * chooses which shape to call: + *

    + *
  • filter=none — baseline, unfiltered decode via the 3-arg readBatch. Represents + * the reader's behaviour before HIVE-30019: every row materialises regardless of whether + * a downstream operator will drop it.
  • + *
  • filter=P (integer pass-percentage) — filter-aware readBatch with a clumpy bitmap + * that accepts the first {@code P} rows of every 100-row block and rejects the rest. On + * dict-encoded pages the coalesced skip fast-path + * ({@code readDictionaryIDs} → {@code DictionaryValuesReader.skip(int)} + * → {@code RunLengthBitPackingHybridDecoder.skipInts}) drops each reject run in one call; + * on PLAIN pages each filtered row still costs a per-row {@code dataColumn.skip()} but + * never a value-materialise / null-set / setVal copy.
  • + *
+ * + *

Two encodings are covered: + *

    + *
  • dict — small distinct-value set, so pages use dictionary encoding.
  • + *
  • plain — every value distinct, so pages fall back to PLAIN.
  • + *
+ * + *

Note on worst case: the current sweep uses a clumpy shape ({@code (i % 100) < P}), + * which matches realistic ProbeDecode join-key hit distributions and is favourable to both the + * dict bulk-skip and to branch prediction on PLAIN. An alternating half-filter + * ({@code i % 2 == 0}) is the branch-predictor worst case but is not the shape ProbeDecode + * produces in practice. + * + *

Run: {@code + * java -jar itests/hive-jmh/target/benchmarks.jar + * org.apache.hive.benchmark.vectorization.parquet.VectorizedParquetReadBench + * -wi 5 -i 15 -f 2 -bm avgt -tu us + * } + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(1) +@State(Scope.Benchmark) +public class VectorizedParquetReadBench { + + // Enough rows in a single row group that the fixed per-invocation cost -- reader open, split + // init, checkEndOfRowGroup, JobConf lookup -- is amortised across many readBatch calls. With + // 131072 rows (128 batches of 1024) written into one row group, each invocation drives ~128 + // filter-aware calls per column vs one, so the isFilteredOut / skip fast-path signal is no + // longer diluted by the ~5 ms setup floor. + private static final int N_ROWS = 131072; + private static final int BATCHES_PER_INVOCATION = N_ROWS / VectorizedRowBatch.DEFAULT_SIZE; + private static final MessageType WRITE_SCHEMA = MessageTypeParser.parseMessageType( + "message pd_read { " + + "required int32 int_col; " + + "required int64 long_col; " + + "required double dbl_col; " + + "required binary str_col (UTF8); " + + "}"); + + @Param({"dict", "plain"}) + public String encoding; + + /** + * Selectivity sweep. {@code none} = call the 3-arg readBatch (baseline / pre-patch shape); + * every other value is an integer pass-percentage in {@code [0, 100]} used to build a bitmap + * where the first {@code p} rows of every 100-row block accept and the remaining + * {@code 100 - p} reject. Clumpy, not alternating: matches real-world join-key hit distributions + * and lets the dict {@code readDictionaryIDs} bulk-skip coalesce the reject runs. {@code 50} + * with a clumpy shape is not the same worst-case as an alternating half-filter (see the + * "worst-case" note in the class javadoc). + */ + @Param({"none", "10", "50", "90"}) + public String filter; + + /** + * Toggles {@code hive.optimize.scan.probedecode.parquet.plain.filter.enabled}. When + * {@code off}, the PLAIN-path {@code isFilteredOutPlain} check is constant-folded away by the + * JIT and every row on a PLAIN page is materialised; the dictionary path is unaffected. + * Included as a bench param so the JIT-elimination claim can be verified end-to-end (i.e. the + * {@code plain × × off} cells should match {@code plain × none} within noise). + * + *

Ignored when {@code filter=none}: the 3-arg readBatch path never calls the check + * regardless of the config, so a {@code none × off} run is not informative and only widens + * the sweep matrix. + */ + @Param({"on", "off"}) + public String plainFilter; + + private File dataDir; + private Path dataFile; + private JobConf jobConf; + + private LongColumnVector intVec; + private LongColumnVector longVec; + private DoubleColumnVector dblVec; + private BytesColumnVector strVec; + + private TypeInfo intType; + private TypeInfo longType; + private TypeInfo dblType; + private TypeInfo strType; + + /** + * Filter for this trial, built from the {@link #filter} @Param. {@code null} when + * {@code filter=none}, in which case the bench calls the 3-arg readBatch (pre-patch shape). + */ + private ParquetProbeFilter probeFilter; + + @Setup(Level.Trial) + public void setUp() throws Exception { + dataDir = Files.createTempDirectory("pd-read-").toFile(); + dataFile = new Path(new File(dataDir, "data.parquet").toURI()); + + boolean dict = "dict".equals(encoding); + + Configuration writeConf = new Configuration(); + GroupWriteSupport.setSchema(WRITE_SCHEMA, writeConf); + SimpleGroupFactory gf = new SimpleGroupFactory(WRITE_SCHEMA); + // Row-group size: 256 MB so all N_ROWS live in a single row group. Page size: 1 MB so each + // column still has several pages inside the row group (exercises page-boundary handling + // inside readBatch). Dictionary size: 1 MB so 8 distinct values easily fit. + try (ParquetWriter writer = new ParquetWriter<>(dataFile, new GroupWriteSupport(), + CompressionCodecName.UNCOMPRESSED, 256 * 1024 * 1024, 1024 * 1024, 1024 * 1024, + dict, false, ParquetWriter.DEFAULT_WRITER_VERSION, writeConf)) { + for (int i = 0; i < N_ROWS; i++) { + int intV = dict ? (i % 8) : i; + long lngV = dict ? (i % 8) : (long) i; + double dblV = dict ? (i % 8) : (double) i; + String strV = dict ? ("v" + (i % 8)) : ("v" + i); + writer.write(gf.newGroup() + .append("int_col", intV) + .append("long_col", lngV) + .append("dbl_col", dblV) + .append("str_col", Binary.fromString(strV))); + } + } + + jobConf = buildJobConf(); + + int batchSize = VectorizedRowBatch.DEFAULT_SIZE; + intVec = new LongColumnVector(batchSize); + longVec = new LongColumnVector(batchSize); + dblVec = new DoubleColumnVector(batchSize); + strVec = new BytesColumnVector(batchSize); + + intType = TypeInfoFactory.getPrimitiveTypeInfo("int"); + longType = TypeInfoFactory.getPrimitiveTypeInfo("bigint"); + dblType = TypeInfoFactory.getPrimitiveTypeInfo("double"); + strType = TypeInfoFactory.getPrimitiveTypeInfo("string"); + + probeFilter = buildFilter(filter, batchSize); + } + + /** + * Build a {@link ParquetProbeFilter} matching the {@link #filter} @Param, or {@code null} for + * {@code "none"} (baseline, 3-arg readBatch). + * + *

Accept bits are clumpy: for pass-percentage {@code p}, the first {@code p} rows of each + * 100-row block accept and the remaining {@code 100 - p} reject. Realistic ProbeDecode + * hit-distributions come in runs, and this shape lets the dict {@code readDictionaryIDs} + * bulk-skip coalesce a whole reject run into a single {@code skipInts} call. An alternating + * half-filter would be the branch-predictor worst case but is not the shape ProbeDecode + * produces in practice. + */ + private static ParquetProbeFilter buildFilter(String mode, int batchSize) { + if ("none".equals(mode)) { + return null; + } + int pass = Integer.parseInt(mode); + if (pass < 0 || pass > 100) { + throw new IllegalArgumentException("filter pass % must be in [0, 100], got: " + mode); + } + boolean[] bits = new boolean[batchSize]; + for (int i = 0; i < batchSize; i++) { + bits[i] = (i % 100) < pass; + } + return ParquetProbeFilter.newBitmap(bits); + } + + private JobConf buildJobConf() throws Exception { + Configuration conf = new Configuration(); + conf.set(IOConstants.COLUMNS, "int_col,long_col,dbl_col,str_col"); + conf.set(IOConstants.COLUMNS_TYPES, "int,bigint,double,string"); + conf.setBoolean(ColumnProjectionUtils.READ_ALL_COLUMNS, false); + conf.set(ColumnProjectionUtils.READ_COLUMN_IDS_CONF_STR, "0,1,2,3"); + HiveConf.setBoolVar(conf, HiveConf.ConfVars.HIVE_VECTORIZATION_ENABLED, true); + HiveConf.setVar(conf, HiveConf.ConfVars.PLAN, "//tmp"); + HiveConf.setBoolVar(conf, + HiveConf.ConfVars.HIVE_OPTIMIZE_SCAN_PROBEDECODE_PARQUET_PLAIN_FILTER, + "on".equals(plainFilter)); + + MapWork mapWork = new MapWork(); + VectorizedRowBatchCtx rbCtx = new VectorizedRowBatchCtx(); + rbCtx.init(org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory + .getStandardStructObjectInspector( + java.util.Collections.emptyList(), + java.util.Collections.emptyList()), + new String[0]); + mapWork.setVectorMode(true); + mapWork.setVectorizedRowBatchCtx(rbCtx); + Utilities.setMapWork(conf, mapWork); + + Job job = new Job(conf, "pd-read"); + ParquetInputFormat.setInputPaths(job, dataFile); + return new JobConf(conf); + } + + private VectorizedParquetRecordReader openReader() throws Exception { + Job job = new Job(jobConf, "pd-read-split"); + ParquetInputFormat.setInputPaths(job, dataFile); + ParquetInputFormat pif = new ParquetInputFormat<>( + org.apache.parquet.hadoop.example.GroupReadSupport.class); + org.apache.hadoop.mapreduce.InputSplit inputSplit = pif.getSplits(job).get(0); + org.apache.hadoop.mapred.FileSplit fs = new org.apache.hadoop.mapred.FileSplit(dataFile, 0L, + inputSplit.getLength(), inputSplit.getLocations()); + return new VectorizedParquetRecordReader(fs, jobConf); + } + + @SuppressWarnings("unchecked") + private VectorizedColumnReader[] primeReaders(VectorizedParquetRecordReader r) throws Exception { + Method m = VectorizedParquetRecordReader.class.getDeclaredMethod("checkEndOfRowGroup"); + m.setAccessible(true); + m.invoke(r); + Field f = VectorizedParquetRecordReader.class.getDeclaredField("columnReaders"); + f.setAccessible(true); + return (VectorizedColumnReader[]) f.get(r); + } + + private void resetVectors() { + intVec.reset(); + intVec.init(); + longVec.reset(); + longVec.init(); + dblVec.reset(); + dblVec.init(); + strVec.reset(); + strVec.init(); + } + + @TearDown(Level.Trial) + public void tearDown() { + File f = new File(dataFile.toUri()); + if (f.exists()) { + f.delete(); + } + if (dataDir != null && dataDir.exists()) { + dataDir.delete(); + } + } + + /** + * Drive {@link VectorizedColumnReader#readBatch} for every projected column across all + * {@link #BATCHES_PER_INVOCATION} batches in the row group. When {@link #probeFilter} is + * {@code null} ({@code filter=none}), calls the 3-arg readBatch (pre-patch shape); otherwise + * calls the 4-arg filter-aware readBatch. The branch is once per invocation, not per row -- + * JMH resolves it as a single conditional at the top and the loop bodies run without further + * decisions. + * + *

The fixed per-invocation cost (openReader, split init, checkEndOfRowGroup, JobConf lookup + * -- collectively ~5 ms in earlier runs) is amortised over ~128 readBatch calls per column so + * the isFilteredOut / skip fast-path signal is not diluted by setup. + */ + @Benchmark + public void readBatch(Blackhole bh) throws Exception { + try (VectorizedParquetRecordReader r = openReader()) { + VectorizedColumnReader[] readers = primeReaders(r); + int n = VectorizedRowBatch.DEFAULT_SIZE; + if (probeFilter == null) { + for (int b = 0; b < BATCHES_PER_INVOCATION; b++) { + resetVectors(); + readers[0].readBatch(n, intVec, intType); + readers[1].readBatch(n, longVec, longType); + readers[2].readBatch(n, dblVec, dblType); + readers[3].readBatch(n, strVec, strType); + consume(bh); + } + } else { + for (int b = 0; b < BATCHES_PER_INVOCATION; b++) { + resetVectors(); + readers[0].readBatch(n, intVec, intType, probeFilter); + readers[1].readBatch(n, longVec, longType, probeFilter); + readers[2].readBatch(n, dblVec, dblType, probeFilter); + readers[3].readBatch(n, strVec, strType, probeFilter); + consume(bh); + } + } + } + } + + private void consume(Blackhole bh) { + bh.consume(intVec.vector); + bh.consume(longVec.vector); + bh.consume(dblVec.vector); + bh.consume(strVec.vector); + bh.consume(intVec.isNull); + bh.consume(longVec.isNull); + bh.consume(dblVec.isNull); + bh.consume(strVec.isNull); + } + + public static void main(String[] args) throws Exception { + Options opt = new OptionsBuilder() + .include(VectorizedParquetReadBench.class.getSimpleName()) + .build(); + new Runner(opt).run(); + } +} diff --git a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/ParquetDataColumnReader.java b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/ParquetDataColumnReader.java index 1e0c89ee64d4..843273db349b 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/ParquetDataColumnReader.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/ParquetDataColumnReader.java @@ -43,6 +43,32 @@ public interface ParquetDataColumnReader { */ int readValueDictionaryId(); + /** + * Consume the next value on this page without materialising it. Used by the ProbeDecode path + * ({@code VectorizedPrimitiveColumnReader.readBatch(..., ParquetProbeFilter)}) to advance the + * underlying {@code ValuesReader} past filtered-out rows so page offsets stay aligned while + * the expensive dictionary lookup and type-conversion work is skipped. + * + *

Concrete implementations should delegate to the underlying {@code ValuesReader.skip()}; + * the default here throws for safety in case a subclass forgets to override. + */ + default void skip() { + throw new UnsupportedOperationException("skip() not supported by " + getClass().getName()); + } + + /** + * Consume the next {@code n} values on this page without materialising them. The default + * implementation loops {@link #skip()} {@code n} times; {@code DefaultParquetDataColumnReader} + * overrides it to delegate to {@link org.apache.parquet.column.values.ValuesReader#skip(int)} + * so that dictionary/RLE readers can use the bulk skip fast-path added in + * {@code RunLengthBitPackingHybridDecoder.skipInts(int)}. + */ + default void skip(int n) { + for (int i = 0; i < n; i++) { + skip(); + } + } + /** * @return the next Long from the page */ diff --git a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/ParquetDataColumnReaderFactory.java b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/ParquetDataColumnReaderFactory.java index bafc1226583b..5c76b7ae4fe4 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/ParquetDataColumnReaderFactory.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/ParquetDataColumnReaderFactory.java @@ -263,10 +263,16 @@ public int readValueDictionaryId() { return valuesReader.readValueDictionaryId(); } + @Override public void skip() { valuesReader.skip(); } + @Override + public void skip(int n) { + valuesReader.skip(n); + } + @Override public Dictionary getDictionary() { return dict; diff --git a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/VectorizedColumnReader.java b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/VectorizedColumnReader.java index 0b63a024cad1..49aa00c4c8a1 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/VectorizedColumnReader.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/VectorizedColumnReader.java @@ -20,6 +20,7 @@ package org.apache.hadoop.hive.ql.io.parquet.vector; import org.apache.hadoop.hive.ql.exec.vector.ColumnVector; +import org.apache.hadoop.hive.ql.io.parquet.vector.probe.ParquetProbeFilter; import org.apache.hadoop.hive.serde2.typeinfo.TypeInfo; import java.io.IOException; @@ -38,6 +39,33 @@ void readBatch( ColumnVector column, TypeInfo columnType) throws IOException; + /** + * Read {@code total} values, but skip the decoding of values at row positions the caller has + * marked as filtered-out via {@code probeFilter} -- the value is consumed from the underlying + * page (so state stays coherent), the corresponding vector slot is left null, and any + * type-conversion / dictionary lookup that {@link #readBatch(int, ColumnVector, TypeInfo)} + * would have done is skipped. + * + *

Used by the {@link VectorizedParquetRecordReader} ProbeDecode path: after the join-key + * column has been decoded and a hash-table probe has produced a selected-row bitmap, the + * remaining columns are read via this overload so their values are only fully materialized for + * rows that will survive the hash join. + * + *

The default implementation ignores the filter and delegates to the 3-arg overload, so + * readers that don't participate in probe-decode (list, map, struct, dummy) don't need to + * override anything. Concrete primitive readers should override this to actually consult the + * filter. + * + * @param probeFilter selected-row bitmap for this batch, or {@code null} to decode every row + */ + default void readBatch( + int total, + ColumnVector column, + TypeInfo columnType, + ParquetProbeFilter probeFilter) throws IOException { + readBatch(total, column, columnType); + } + default int[] getDefinitionLevels() { return null; } diff --git a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/VectorizedParquetRecordReader.java b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/VectorizedParquetRecordReader.java index 236f6f3095f0..8c4014fb40bb 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/VectorizedParquetRecordReader.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/VectorizedParquetRecordReader.java @@ -43,6 +43,8 @@ import org.apache.hadoop.hive.ql.io.SyntheticFileId; import org.apache.hadoop.hive.ql.io.parquet.ParquetRecordReaderBase; import org.apache.hadoop.hive.ql.io.parquet.read.DataWritableReadSupport; +import org.apache.hadoop.hive.ql.io.parquet.vector.probe.ParquetProbeDecodeState; +import org.apache.hadoop.hive.ql.io.parquet.vector.probe.ParquetProbeFilter; import org.apache.hadoop.hive.ql.metadata.HiveException; import org.apache.hadoop.hive.ql.plan.MapWork; import org.apache.hadoop.hive.ql.plan.PartitionDesc; @@ -153,6 +155,24 @@ public class VectorizedParquetRecordReader extends ParquetRecordReaderBase private Object cacheKey = null; private CacheTag cacheTag = null; + /** + * ProbeDecode state: resolved lazily on the first {@link #nextBatch} call because {@link MapWork} + * carries the small-side hash-table cache key set by the planner. If probe-decode isn't enabled + * for this scan (no {@code ProbeDecodeContext}, key column pruned, or the small hash table isn't + * populated yet) this stays at {@link ParquetProbeDecodeState#disabled()} and the reader runs + * the plain decode path. + */ + private ParquetProbeDecodeState probeState = ParquetProbeDecodeState.disabled(); + private boolean probeStateResolved = false; + + /** + * Snapshot of {@link HiveConf.ConfVars#HIVE_OPTIMIZE_SCAN_PROBEDECODE_PARQUET_PLAIN_FILTER} at + * reader construction time. Threaded through into every {@link VectorizedPrimitiveColumnReader} + * so the JIT can constant-fold the PLAIN-path filter check when it's off. Only affects PLAIN + * pages; the dictionary path always honours the filter via its bulk-skip fast-path. + */ + private final boolean plainFilterEnabled; + public VectorizedParquetRecordReader(InputSplit oldInputSplit, JobConf conf) throws IOException { this(oldInputSplit, conf, null, null, null); } @@ -161,6 +181,8 @@ public VectorizedParquetRecordReader(InputSplit oldInputSplit, JobConf conf, Fil DataCache dataCache, Configuration cacheConf, ParquetMetadata parquetMetadata, Map initialDefaults) throws IOException { super(conf, oldInputSplit); + this.plainFilterEnabled = HiveConf.getBoolVar( + conf, ConfVars.HIVE_OPTIMIZE_SCAN_PROBEDECODE_PARQUET_PLAIN_FILTER); try { this.metadataCache = metadataCache; this.cache = dataCache; @@ -420,8 +442,61 @@ private boolean nextBatch(VectorizedRowBatch columnarBatch) throws IOException { } checkEndOfRowGroup(); + if (!probeStateResolved) { + // Resolve ProbeDecode state once we've read the first row group and know the projected + // column list. Failing here is fine -- state stays disabled and we run the plain path. + probeState = ParquetProbeDecodeState.of(jobConf, Utilities.getMapWork(jobConf), columnNamesList); + probeStateResolved = true; + } + int num = (int) Math.min(VectorizedRowBatch.DEFAULT_SIZE, totalCountLoadedSoFar - rowsReturned); - if (colsToInclude.size() > 0) { + if (!colsToInclude.isEmpty()) { + ParquetProbeFilter probeFilter; + int probeReaderIdx = -1; + if (probeState.isEnabled()) { + // Find which reader index corresponds to the probe key column. columnReaders[i] renders + // into columnarBatch.cols[colsToInclude.get(i)], so we match on the projected slot. + int keyColSlot = probeState.getKeyColumnIndex(); + for (int i = 0; i < columnReaders.length; ++i) { + if (columnReaders[i] != null && colsToInclude.get(i) == keyColSlot) { + probeReaderIdx = i; + break; + } + } + } + + if (probeReaderIdx >= 0) { + // Probe-decode path: decode the key column, run the hash-table probe to build a filter, + // then decode the remaining columns with the filter so unmatched rows skip decode / + // conversion work. + columnarBatch.cols[colsToInclude.get(probeReaderIdx)].isRepeating = true; + columnReaders[probeReaderIdx].readBatch(num, columnarBatch.cols[colsToInclude.get(probeReaderIdx)], + columnTypesList.get(colsToInclude.get(probeReaderIdx))); + try { + probeFilter = probeState.getProbe().probe( + columnarBatch.cols[colsToInclude.get(probeReaderIdx)], num); + } catch (IOException e) { + throw e; + } catch (Exception e) { + LOG.warn("ProbeDecode probe failed, falling back to unfiltered decode", e); + probeFilter = null; + } + for (int i = 0; i < columnReaders.length; ++i) { + if (i == probeReaderIdx || columnReaders[i] == null) { + continue; + } + columnarBatch.cols[colsToInclude.get(i)].isRepeating = true; + columnReaders[i].readBatch(num, columnarBatch.cols[colsToInclude.get(i)], + columnTypesList.get(colsToInclude.get(i)), probeFilter); + } + // Physical decode consumed `num` rows; filtered logical size becomes the batch size. + int filteredSize = applyProbeFilterToBatch(columnarBatch, probeFilter, num); + lastReturnedRowCount = num; + rowsReturned += num; + columnarBatch.size = filteredSize; + return true; + } + // else: fallthrough to the plain decode path below. for (int i = 0; i < columnReaders.length; ++i) { if (columnReaders[i] == null) { continue; @@ -431,12 +506,42 @@ private boolean nextBatch(VectorizedRowBatch columnarBatch) throws IOException { columnTypesList.get(colsToInclude.get(i))); } } + // Plain path: no probe filter, so no compacted selected[] to hand downstream. + columnarBatch.selectedInUse = false; lastReturnedRowCount = num; rowsReturned += num; columnarBatch.size = num; return true; } + /** + * Set {@code batch.selected} / {@code selectedInUse} so downstream operators see only the rows + * that survived the probe. Filtered-out rows are already null in every column (their decode + * path skipped materialisation), so the batch is safe to hand over either way -- populating + * {@code selected[]} avoids re-testing the same rows in the join operator. + * + * @return the surviving row count, matching Hive's convention that {@code batch.size} is the + * number of rows that qualify (i.e. after filtering) + */ + private static int applyProbeFilterToBatch(VectorizedRowBatch batch, ParquetProbeFilter filter, + int batchSize) { + if (filter == null) { + return batchSize; + } + ParquetProbeFilter compact = filter.compact(batchSize); + int[] selected = compact.getSelected(); + int size = compact.getSelectedSize(); + if (selected == null) { + return batchSize; + } + if (batch.selected == null || batch.selected.length < selected.length) { + batch.selected = new int[selected.length]; + } + System.arraycopy(selected, 0, batch.selected, 0, size); + batch.selectedInUse = size < batchSize; + return size; + } + private void checkEndOfRowGroup() throws IOException { if (rowsReturned != totalCountLoadedSoFar) { return; @@ -549,7 +654,7 @@ private VectorizedColumnReader buildVectorizedParquetReader( } return new VectorizedPrimitiveColumnReader(descriptors.get(0), pages.getPageReader(descriptors.get(0)), skipTimestampConversion, writerTimezone, skipProlepticConversion, - legacyConversionEnabled, type, typeInfo); + legacyConversionEnabled, type, typeInfo, plainFilterEnabled); case STRUCT: StructTypeInfo structTypeInfo = (StructTypeInfo) typeInfo; List fieldReaders = new ArrayList<>(); diff --git a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/VectorizedPrimitiveColumnReader.java b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/VectorizedPrimitiveColumnReader.java index 3d878c2d5dbd..1f7e8e619dd4 100644 --- a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/VectorizedPrimitiveColumnReader.java +++ b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/VectorizedPrimitiveColumnReader.java @@ -33,6 +33,7 @@ import org.apache.hadoop.hive.serde2.typeinfo.TypeInfo; import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoFactory; import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoUtils; +import org.apache.hadoop.hive.ql.io.parquet.vector.probe.ParquetProbeFilter; import org.apache.parquet.column.ColumnDescriptor; import org.apache.parquet.column.page.PageReader; import org.apache.parquet.schema.LogicalTypeAnnotation.DecimalLogicalTypeAnnotation; @@ -52,6 +53,26 @@ */ public class VectorizedPrimitiveColumnReader extends BaseVectorizedColumnReader { + /** + * Non-null while a ProbeDecode batch is being decoded. When set, per-row helpers below + * ({@link #readDictionaryIDs}, {@link #readIntegers}, {@link #readLongs}) call + * {@code dataColumn.skip()} on filtered-out rows instead of decoding, and leave the + * corresponding slot in the column vector marked null. + */ + private ParquetProbeFilter probeFilter; + + /** + * When {@code false}, the PLAIN-path helpers ignore the filter and materialise every row, + * relying on downstream {@code batch.selected[]} for filtering. Controlled by + * {@code hive.optimize.scan.probedecode.parquet.plain.filter.enabled}. The dictionary path + * is unaffected and always honours the filter (its bulk-skip fast-path is uniformly a win). + * + *

Marked {@code final} so the JIT can constant-fold the check away in {@link + * #isFilteredOutPlain} when disabled -- there should be zero residual cost on the hot path + * when the config is off. + */ + private final boolean plainFilterEnabled; + public VectorizedPrimitiveColumnReader( ColumnDescriptor descriptor, PageReader pageReader, @@ -60,10 +81,48 @@ public VectorizedPrimitiveColumnReader( boolean skipProlepticConversion, boolean legacyConversionEnabled, Type type, - TypeInfo hiveType) + TypeInfo hiveType, + boolean plainFilterEnabled) throws IOException { super(descriptor, pageReader, skipTimestampConversion, writerTimezone, skipProlepticConversion, legacyConversionEnabled, type, hiveType); + this.plainFilterEnabled = plainFilterEnabled; + } + + @Override + public void readBatch( + int total, + ColumnVector column, + TypeInfo columnType, + ParquetProbeFilter probeFilter) throws IOException { + this.probeFilter = probeFilter; + try { + readBatch(total, column, columnType); + } finally { + this.probeFilter = null; + } + } + + /** + * Dict-path filter check. Always honours an active filter -- filtered dict-ids feed into the + * {@code pendingSkip} coalescing loop that reaches {@code DictionaryValuesReader.skip(int)} -> + * {@code RunLengthBitPackingHybridDecoder.skipInts}, which drops whole reject runs in O(runs). + * This win is uniform across selectivities, so no config gates it. + */ + private boolean isFilteredOutDict(int rowId) { + return probeFilter != null && !probeFilter.isSelected(rowId); + } + + /** + * PLAIN-path filter check. Gated by {@link #plainFilterEnabled}: when the config is off, the + * JIT constant-folds this to always return {@code false} and the surrounding {@code if/else} + * in every {@code readXxx} helper collapses to just the materialise branch. The trade-off is + * that filtered PLAIN rows will then be materialised into the column vector (downstream + * {@code batch.selected[]} still filters them out for the query result, but callers reading + * the vector directly will see decoded values in those slots). + */ + private boolean isFilteredOutPlain(int rowId) { + return plainFilterEnabled && probeFilter != null && !probeFilter.isSelected(rowId); } @Override @@ -166,32 +225,62 @@ private static void setNullValue(ColumnVector c, int rowId) { private void readDictionaryIDs(int total, LongColumnVector c, int rowId) { int left = total; + // Dictionary indices are always encoded RLE / bit-packed, so contiguous filtered-out rows + // (definitionLevel == maxDefLevel) can be coalesced into a single dataColumn.skip(n) at + // the run boundary. That reaches DictionaryValuesReader.skip(int) -> + // RunLengthBitPackingHybridDecoder.skipInts, which consumes a whole RLE run in O(1) by + // decrementing currentCount, so filtered rows cost O(runs) rather than O(filtered rows). + // This coalescing is not applied to readIntegers / readLongs because those handle non-dict + // pages whose values reader (PLAIN / DELTA_BINARY_PACKED) has no bulk-skip fast-path. + int pendingSkip = 0; while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.vector[rowId] = dataColumn.readValueDictionaryId(); - c.isNull[rowId] = false; - c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + if (isFilteredOutDict(rowId)) { + pendingSkip++; + setNullValue(c, rowId); + } else { + if (pendingSkip > 0) { + dataColumn.skip(pendingSkip); + pendingSkip = 0; + } + c.vector[rowId] = dataColumn.readValueDictionaryId(); + c.isNull[rowId] = false; + c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + } } else { + // Null-in-file row: no value on the page, so the skip counter is unaffected. setNullValue(c, rowId); } rowId++; left--; } + if (pendingSkip > 0) { + dataColumn.skip(pendingSkip); + } } private void readIntegers(int total, LongColumnVector c, int rowId) { + // Only reached for non-dict-encoded pages; dict-encoded INT columns go through + // readDictionaryIDs. The underlying PlainValuesReader / DeltaBinaryPackingValuesReader + // has no bulk-skip fast-path (PLAIN's per-row skip is one ByteBufferInputStream index + // bump; DELTA is O(n) by construction), so per-row dataColumn.skip() is fine. int left = total; while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.vector[rowId] = dataColumn.readInteger(); - if (dataColumn.isValid()) { - c.isNull[rowId] = false; - c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); - } else { - c.vector[rowId] = 0; + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); setNullValue(c, rowId); + } else { + c.vector[rowId] = dataColumn.readInteger(); + if (dataColumn.isValid()) { + c.isNull[rowId] = false; + c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + } else { + c.vector[rowId] = 0; + setNullValue(c, rowId); + } } } else { setNullValue(c, rowId); @@ -206,13 +295,18 @@ private void readSmallInts(int total, LongColumnVector c, int rowId) { while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.vector[rowId] = dataColumn.readSmallInt(); - if (dataColumn.isValid()) { - c.isNull[rowId] = false; - c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); - } else { - c.vector[rowId] = 0; + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); setNullValue(c, rowId); + } else { + c.vector[rowId] = dataColumn.readSmallInt(); + if (dataColumn.isValid()) { + c.isNull[rowId] = false; + c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + } else { + c.vector[rowId] = 0; + setNullValue(c, rowId); + } } } else { setNullValue(c, rowId); @@ -227,13 +321,18 @@ private void readTinyInts(int total, LongColumnVector c, int rowId) { while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.vector[rowId] = dataColumn.readTinyInt(); - if (dataColumn.isValid()) { - c.isNull[rowId] = false; - c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); - } else { - c.vector[rowId] = 0; + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); setNullValue(c, rowId); + } else { + c.vector[rowId] = dataColumn.readTinyInt(); + if (dataColumn.isValid()) { + c.isNull[rowId] = false; + c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + } else { + c.vector[rowId] = 0; + setNullValue(c, rowId); + } } } else { setNullValue(c, rowId); @@ -248,13 +347,18 @@ private void readDoubles(int total, DoubleColumnVector c, int rowId) { while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.vector[rowId] = dataColumn.readDouble(); - if (dataColumn.isValid()) { - c.isNull[rowId] = false; - c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); - } else { - c.vector[rowId] = 0; + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); setNullValue(c, rowId); + } else { + c.vector[rowId] = dataColumn.readDouble(); + if (dataColumn.isValid()) { + c.isNull[rowId] = false; + c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + } else { + c.vector[rowId] = 0; + setNullValue(c, rowId); + } } } else { setNullValue(c, rowId); @@ -269,9 +373,14 @@ private void readBooleans(int total, LongColumnVector c, int rowId) { while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.vector[rowId] = dataColumn.readBoolean() ? 1 : 0; - c.isNull[rowId] = false; - c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); + setNullValue(c, rowId); + } else { + c.vector[rowId] = dataColumn.readBoolean() ? 1 : 0; + c.isNull[rowId] = false; + c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + } } else { setNullValue(c, rowId); } @@ -281,17 +390,24 @@ private void readBooleans(int total, LongColumnVector c, int rowId) { } private void readLongs(int total, LongColumnVector c, int rowId) { + // See readIntegers -- same argument: only reached for non-dict pages, no bulk-skip fast-path + // is available in the underlying values reader, so per-row dataColumn.skip() is optimal. int left = total; while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.vector[rowId] = dataColumn.readLong(); - if (dataColumn.isValid()) { - c.isNull[rowId] = false; - c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); - } else { - c.vector[rowId] = 0; + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); setNullValue(c, rowId); + } else { + c.vector[rowId] = dataColumn.readLong(); + if (dataColumn.isValid()) { + c.isNull[rowId] = false; + c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + } else { + c.vector[rowId] = 0; + setNullValue(c, rowId); + } } } else { setNullValue(c, rowId); @@ -306,13 +422,18 @@ private void readFloats(int total, DoubleColumnVector c, int rowId) { while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.vector[rowId] = dataColumn.readFloat(); - if (dataColumn.isValid()) { - c.isNull[rowId] = false; - c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); - } else { - c.vector[rowId] = 0; + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); setNullValue(c, rowId); + } else { + c.vector[rowId] = dataColumn.readFloat(); + if (dataColumn.isValid()) { + c.isNull[rowId] = false; + c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + } else { + c.vector[rowId] = 0; + setNullValue(c, rowId); + } } } else { setNullValue(c, rowId); @@ -330,13 +451,18 @@ private void readDecimal(int total, DecimalColumnVector c, int rowId) { while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - decimalData = dataColumn.readDecimal(); - if (dataColumn.isValid()) { - c.vector[rowId].set(decimalData, c.scale); - c.isNull[rowId] = false; - c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); - } else { + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); setNullValue(c, rowId); + } else { + decimalData = dataColumn.readDecimal(); + if (dataColumn.isValid()) { + c.vector[rowId].set(decimalData, c.scale); + c.isNull[rowId] = false; + c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + } else { + setNullValue(c, rowId); + } } } else { setNullValue(c, rowId); @@ -351,10 +477,18 @@ private void readString(int total, BytesColumnVector c, int rowId) { while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.setVal(rowId, dataColumn.readString()); - c.isNull[rowId] = false; - // TODO figure out a better way to set repeat for Binary type - c.isRepeating = false; + if (isFilteredOutPlain(rowId)) { + // Avoids the per-row byte-array allocation + copy into BytesColumnVector for filtered + // rows. The underlying BinaryPlainValuesReader.skip() only reads the length prefix + // and advances the buffer by that many bytes. + dataColumn.skip(); + setNullValue(c, rowId); + } else { + c.setVal(rowId, dataColumn.readString()); + c.isNull[rowId] = false; + // TODO figure out a better way to set repeat for Binary type + c.isRepeating = false; + } } else { setNullValue(c, rowId); } @@ -368,10 +502,15 @@ private void readChar(int total, BytesColumnVector c, int rowId) { while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.setVal(rowId, dataColumn.readChar()); - c.isNull[rowId] = false; - // TODO figure out a better way to set repeat for Binary type - c.isRepeating = false; + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); + setNullValue(c, rowId); + } else { + c.setVal(rowId, dataColumn.readChar()); + c.isNull[rowId] = false; + // TODO figure out a better way to set repeat for Binary type + c.isRepeating = false; + } } else { setNullValue(c, rowId); } @@ -385,10 +524,15 @@ private void readVarchar(int total, BytesColumnVector c, int rowId) { while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.setVal(rowId, dataColumn.readVarchar()); - c.isNull[rowId] = false; - // TODO figure out a better way to set repeat for Binary type - c.isRepeating = false; + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); + setNullValue(c, rowId); + } else { + c.setVal(rowId, dataColumn.readVarchar()); + c.isNull[rowId] = false; + // TODO figure out a better way to set repeat for Binary type + c.isRepeating = false; + } } else { setNullValue(c, rowId); } @@ -402,10 +546,15 @@ private void readBinaries(int total, BytesColumnVector c, int rowId) { while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.setVal(rowId, dataColumn.readBytes()); - c.isNull[rowId] = false; - // TODO figure out a better way to set repeat for Binary type - c.isRepeating = false; + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); + setNullValue(c, rowId); + } else { + c.setVal(rowId, dataColumn.readBytes()); + c.isNull[rowId] = false; + // TODO figure out a better way to set repeat for Binary type + c.isRepeating = false; + } } else { setNullValue(c, rowId); } @@ -420,14 +569,19 @@ private void readDate(int total, DateColumnVector c, int rowId) { while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - c.vector[rowId] = skipProlepticConversion ? - dataColumn.readLong() : CalendarUtils.convertDateToProleptic((int) dataColumn.readLong()); - if (dataColumn.isValid()) { - c.isNull[rowId] = false; - c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); - } else { - c.vector[rowId] = 0; + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); setNullValue(c, rowId); + } else { + c.vector[rowId] = skipProlepticConversion ? + dataColumn.readLong() : CalendarUtils.convertDateToProleptic((int) dataColumn.readLong()); + if (dataColumn.isValid()) { + c.isNull[rowId] = false; + c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + } else { + c.vector[rowId] = 0; + setNullValue(c, rowId); + } } } else { setNullValue(c, rowId); @@ -443,21 +597,26 @@ private void readTimestamp(int total, TimestampColumnVector c, int rowId) throws while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - switch (descriptor.getType()) { - //INT64 is not yet supported - case INT96: - c.set(rowId, dataColumn.readTimestamp().toSqlTimestamp()); - break; - case INT64: - c.set(rowId, dataColumn.readTimestamp().toSqlTimestamp()); - break; - default: - throw new IOException( - "Unsupported parquet logical type: " + type.getLogicalTypeAnnotation().toString() + " for timestamp"); + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); + setNullValue(c, rowId); + } else { + switch (descriptor.getType()) { + //INT64 is not yet supported + case INT96: + c.set(rowId, dataColumn.readTimestamp().toSqlTimestamp()); + break; + case INT64: + c.set(rowId, dataColumn.readTimestamp().toSqlTimestamp()); + break; + default: + throw new IOException( + "Unsupported parquet logical type: " + type.getLogicalTypeAnnotation().toString() + " for timestamp"); + } + c.isNull[rowId] = false; + c.isRepeating = + c.isRepeating && ((c.time[0] == c.time[rowId]) && (c.nanos[0] == c.nanos[rowId])); } - c.isNull[rowId] = false; - c.isRepeating = - c.isRepeating && ((c.time[0] == c.time[rowId]) && (c.nanos[0] == c.nanos[rowId])); } else { setNullValue(c, rowId); } @@ -708,9 +867,14 @@ private void readDecimal64(int total, Decimal64ColumnVector c, int rowId) { while (left > 0) { readRepetitionAndDefinitionLevels(); if (definitionLevel >= maxDefLevel) { - setDecimal64Value(c, rowId, fast, dataColumn, -1, valueScale); - if (!c.isNull[rowId]) { - c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + if (isFilteredOutPlain(rowId)) { + dataColumn.skip(); + setNullValue(c, rowId); + } else { + setDecimal64Value(c, rowId, fast, dataColumn, -1, valueScale); + if (!c.isNull[rowId]) { + c.isRepeating = c.isRepeating && (c.vector[0] == c.vector[rowId]); + } } } else { setNullValue(c, rowId); diff --git a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/probe/ParquetProbeDecodeState.java b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/probe/ParquetProbeDecodeState.java new file mode 100644 index 000000000000..df4af6388706 --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/probe/ParquetProbeDecodeState.java @@ -0,0 +1,219 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hadoop.hive.ql.io.parquet.vector.probe; + +import java.util.List; +import java.util.Set; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hadoop.hive.ql.exec.MapJoinOperator; +import org.apache.hadoop.hive.ql.exec.ObjectCache; +import org.apache.hadoop.hive.ql.exec.ObjectCacheFactory; +import org.apache.hadoop.hive.ql.exec.OperatorUtils; +import org.apache.hadoop.hive.ql.exec.TableScanOperator.ProbeDecodeContext; +import org.apache.hadoop.hive.ql.exec.persistence.MapJoinTableContainer; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinHashTable; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinLongHashTable; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinTableContainer; +import org.apache.hadoop.hive.ql.plan.MapWork; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Runtime state for the Parquet ProbeDecode path: given a {@link MapWork} that carries a + * {@link ProbeDecodeContext}, resolve the small-side hash table from the {@link ObjectCache} and + * wrap it in a {@link ParquetProbeHashTable} so the vectorized Parquet reader can apply a probe + * filter on every batch. + * + *

The state is intentionally null-safe: any failure to resolve (no probe context, cache miss, + * unsupported key type) produces a {@link #disabled()} instance whose {@link #isEnabled()} is + * {@code false}, and the reader falls back to the original decode path unchanged. + * + *

The MVP handles only single-long / int keys through {@link ParquetProbeLongHashTable}. + * Bytes-key and multi-key variants will land in follow-up commits and plug in via {@link #of}. + */ +public final class ParquetProbeDecodeState { + + private static final Logger LOG = LoggerFactory.getLogger(ParquetProbeDecodeState.class); + + private static final ParquetProbeDecodeState DISABLED = new ParquetProbeDecodeState(-1, null); + + private final int keyColumnIndex; + private final ParquetProbeHashTable probe; + + private ParquetProbeDecodeState(int keyColumnIndex, ParquetProbeHashTable probe) { + this.keyColumnIndex = keyColumnIndex; + this.probe = probe; + } + + public static ParquetProbeDecodeState disabled() { + return DISABLED; + } + + /** + * Resolve probe state from the current job's {@link MapWork}. Returns {@link #disabled()} if + * anything is missing (no probe context, unknown key column, missing hash table, non-long key) + * so the caller can unconditionally consult {@link #isEnabled()} without special-casing. + * + * @param conf the job conf + * @param mapWork the MapWork, whose {@code probeDecodeContext} names the small table + * @param projectedColumns the projected column names in reader order -- used to locate the + * probe key column within the batch + */ + public static ParquetProbeDecodeState of(Configuration conf, MapWork mapWork, + List projectedColumns) { + if (mapWork == null) { + return DISABLED; + } + ProbeDecodeContext ctx = mapWork.getProbeDecodeContext(); + if (ctx == null) { + return DISABLED; + } + String keyCol = ctx.getMjBigTableKeyColName(); + if (keyCol == null || projectedColumns == null) { + return DISABLED; + } + int keyIdx = projectedColumns.indexOf(keyCol); + if (keyIdx < 0) { + // The probe key column isn't in the projected schema for this reader -- e.g. because + // column pruning already dropped it. Nothing to probe against; run un-probed. + LOG.debug("ProbeDecode: probe key column {} not in projected columns {}", + keyCol, projectedColumns); + return DISABLED; + } + + String queryId = HiveConf.getVar(conf, HiveConf.ConfVars.HIVE_QUERY_ID); + try { + ObjectCache cache = ObjectCacheFactory.getCache(conf, queryId, false); + // MapJoinOperator.initializeOp stores the hash table under either the raw MapJoinDesc cache + // key (when conf's cacheKey was null at compile time) or `cacheKey + "_" + concreteOpClass` + // (when Shared Work Optimization / ProbeDecode compilation set the cache key upfront). The + // ProbeDecodeContext only carries the raw key -- resolve the concrete-class suffix by + // matching the MapJoinOperator in the MapWork tree, mirroring the loader's key exactly. + String actualKey = resolveActualCacheKey(mapWork, ctx.getMjSmallTableCacheKey()); + Object cached = cache.retrieve(actualKey); + if (cached == null) { + LOG.debug("ProbeDecode: no cached hash table for key {} (base {})", + actualKey, ctx.getMjSmallTableCacheKey()); + return DISABLED; + } + // MapJoinOperator caches the loaded hash tables as a + // `Pair` -- unwrap and pick the + // small-table container by its position. + MapJoinTableContainer container = unwrapContainer(cached, ctx.getMjSmallTablePos()); + if (container == null) { + LOG.debug("ProbeDecode: could not extract small-table container from cached {} (pos {})", + cached.getClass().getName(), ctx.getMjSmallTablePos()); + return DISABLED; + } + VectorMapJoinHashTable ht; + if (container instanceof VectorMapJoinTableContainer) { + ht = ((VectorMapJoinTableContainer) container).vectorMapJoinHashTable(); + } else { + // Non-vectorized container -- probe-decode has no dictionary-of-keys to intersect with, + // so bail out and let the plain decode path run. + LOG.debug("ProbeDecode: cached container is non-vectorized ({}); skipping probe", + container.getClass().getName()); + return DISABLED; + } + + if (ht instanceof VectorMapJoinLongHashTable) { + LOG.info("ProbeDecode: enabled for key column {} (idx {}) via {}", + keyCol, keyIdx, ht.getClass().getSimpleName()); + return new ParquetProbeDecodeState(keyIdx, ParquetProbeLongHashTable.of(ht)); + } + // TODO: bytes-key + multi-key variants when the corresponding ParquetProbeHashTable + // implementations land. + LOG.debug("ProbeDecode: no ParquetProbeHashTable for key type {}", ht.getClass().getName()); + return DISABLED; + } catch (Exception e) { + LOG.warn("ProbeDecode: hash-table resolution failed, falling back to plain decode", e); + return DISABLED; + } + } + + /** + * Peel a cached hash-table entry stored by {@link MapJoinOperator#loadHashTable} back to the + * small-side container. The op caches a {@code Pair}; some code + * paths (older tests, refactored fixtures) store a bare container or a bare hash table -- keep + * those working too. + */ + private static MapJoinTableContainer unwrapContainer(Object cached, byte smallPos) { + if (cached instanceof Pair) { + Object left = ((Pair) cached).getLeft(); + if (left instanceof MapJoinTableContainer[]) { + MapJoinTableContainer[] tables = (MapJoinTableContainer[]) left; + if (smallPos >= 0 && smallPos < tables.length && tables[smallPos] != null) { + return tables[smallPos]; + } + // Fall back to the first non-null entry -- some plans leave big-table slots null. + for (MapJoinTableContainer t : tables) { + if (t != null) { + return t; + } + } + } + return null; + } + if (cached instanceof MapJoinTableContainer) { + return (MapJoinTableContainer) cached; + } + return null; + } + + /** + * Mirror {@code MapJoinOperator.initializeOp}'s cacheKey computation so our lookup finds the + * hash table the loader stored. When the ProbeDecodeContext's raw cacheKey is non-null, the + * operator appends {@code "_" + this.getClass().getName()} -- and the vectorizer has by now + * substituted the {@link MapJoinOperator} with a concrete {@code VectorMapJoin*Operator} + * subclass, so we read the class off the operator in the MapWork tree. + */ + static String resolveActualCacheKey(MapWork mapWork, String baseCacheKey) { + if (baseCacheKey == null || mapWork == null) { + return baseCacheKey; + } + try { + Set mjs = OperatorUtils.findOperators(mapWork.getWorks(), MapJoinOperator.class); + for (MapJoinOperator mj : mjs) { + if (mj.getConf() != null && baseCacheKey.equals(mj.getConf().getCacheKey())) { + return baseCacheKey + "_" + mj.getClass().getName(); + } + } + } catch (Exception e) { + LOG.debug("ProbeDecode: could not resolve concrete cacheKey suffix, falling back to raw key", + e); + } + return baseCacheKey; + } + + public boolean isEnabled() { + return probe != null; + } + + /** @return column index (within the reader's projected columns) of the probe key column. */ + public int getKeyColumnIndex() { + return keyColumnIndex; + } + + public ParquetProbeHashTable getProbe() { + return probe; + } +} diff --git a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/probe/ParquetProbeFilter.java b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/probe/ParquetProbeFilter.java new file mode 100644 index 000000000000..4767124f24f6 --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/probe/ParquetProbeFilter.java @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hadoop.hive.ql.io.parquet.vector.probe; + +/** + * Row-level selection state produced by the Parquet ProbeDecode path. + * + *

After the join-key column has been decoded and probed against the small-table hash table + * (see {@link ParquetProbeHashTable}), the surviving row positions within the current batch are + * recorded here so that the remaining non-key columns can be read via + * {@code VectorizedColumnReader.readBatch(total, column, type, ParquetProbeFilter)} and skip + * decode / conversion work for rows that will be filtered out anyway. + * + *

The filter is advisory for correctness: a reader is free to decode every row and + * ignore the filter (the default interface method does exactly that). Slots that are marked + * filtered-out must still be advanced in the underlying page state so subsequent reads stay + * aligned; concrete readers achieve that by calling {@code skip()} on the {@code ParquetDataColumnReader}. + * + *

Lifecycle within a batch: {@link ParquetProbeHashTable#probe} returns a filter in bitmap form + * (one boolean per row); per-row column readers query it via {@link #isSelected(int)} while + * decoding; once all columns are read, {@link VectorizedParquetRecordReader} calls + * {@link #compact(int)} to materialize a {@code selected[]} that + * {@link org.apache.hadoop.hive.ql.exec.vector.VectorizedRowBatch} consumes downstream. The + * compacted form is not queried per row -- it flows directly into + * {@code VectorizedRowBatch.selected}. + */ +public final class ParquetProbeFilter { + + private final boolean[] bitmap; + private int[] selected; + private int selectedSize; + + private ParquetProbeFilter(boolean[] bitmap) { + this.bitmap = bitmap; + } + + public static ParquetProbeFilter newBitmap(boolean[] bitmap) { + if (bitmap == null) { + throw new IllegalArgumentException("bitmap must be non-null"); + } + return new ParquetProbeFilter(bitmap); + } + + /** + * Test whether the given row index survives the filter. + * + * @param rowId 0-based row index within the current batch + * @return {@code true} if the row should be decoded fully, {@code false} if the reader may skip + * decode / conversion for that row + */ + public boolean isSelected(int rowId) { + return rowId >= 0 && rowId < bitmap.length && bitmap[rowId]; + } + + /** + * Materialize a compact {@code selected[]} representation over the first {@code batchSize} rows + * from the underlying bitmap. Idempotent: repeated calls are cheap no-ops after the first. + * + * @return {@code this} for chaining + */ + public ParquetProbeFilter compact(int batchSize) { + if (selected != null) { + return this; + } + int upper = Math.min(batchSize, bitmap.length); + int[] out = new int[upper]; + int n = 0; + for (int i = 0; i < upper; i++) { + if (bitmap[i]) { + out[n++] = i; + } + } + this.selected = out; + this.selectedSize = n; + return this; + } + + /** @return the compact {@code selected[]} array; {@code null} until {@link #compact(int)} runs. */ + public int[] getSelected() { + return selected; + } + + /** @return the number of live entries in {@link #getSelected()}; {@code 0} until {@link #compact(int)} runs. */ + public int getSelectedSize() { + return selectedSize; + } +} diff --git a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/probe/ParquetProbeHashTable.java b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/probe/ParquetProbeHashTable.java new file mode 100644 index 000000000000..a92e46c9e119 --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/probe/ParquetProbeHashTable.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hadoop.hive.ql.io.parquet.vector.probe; + +import java.io.IOException; +import org.apache.hadoop.hive.ql.exec.vector.ColumnVector; + +/** + * Row-batch probe against a small-table hash table for the Parquet ProbeDecode path. + * + *

Given a {@link ColumnVector} that carries the just-decoded join-key column, implementations + * probe each row against the small-side hash table and produce a {@link ParquetProbeFilter} whose + * bitmap marks the rows that survive the join. Subsequent non-key columns in the same batch are + * then read via {@code VectorizedColumnReader.readBatch(..., ParquetProbeFilter)} so their values + * skip decode / conversion work for filtered-out rows. + * + *

Structured after the ORC-side {@code OrcProbeHashTable} family: the key type-specialisations + * (single long/int, single string, multi-key) live in their own subclasses so the per-row probe + * stays a monomorphic virtual call. This first cut ships only the long/int variant + * ({@link ParquetProbeLongHashTable}); the string and multi-key variants will land later. + */ +public interface ParquetProbeHashTable { + + /** + * Probe every row in {@code keyColumn} (positions 0..batchSize-1) against the small-side hash + * table and return a filter over the surviving rows. Rows marked null in the key column never + * match (join semantics: NULL != NULL). + * + * @param keyColumn key-column vector, already decoded by the primary readBatch() call + * @param batchSize number of rows in the current batch + * @return a bitmap-form {@link ParquetProbeFilter} of length {@code batchSize} + */ + ParquetProbeFilter probe(ColumnVector keyColumn, int batchSize) throws IOException; +} diff --git a/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/probe/ParquetProbeLongHashTable.java b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/probe/ParquetProbeLongHashTable.java new file mode 100644 index 000000000000..4c5f4a10fdf2 --- /dev/null +++ b/ql/src/java/org/apache/hadoop/hive/ql/io/parquet/vector/probe/ParquetProbeLongHashTable.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hadoop.hive.ql.io.parquet.vector.probe; + +import java.io.IOException; +import java.util.Arrays; + +import org.apache.hadoop.hive.ql.exec.JoinUtil; +import org.apache.hadoop.hive.ql.exec.vector.ColumnVector; +import org.apache.hadoop.hive.ql.exec.vector.LongColumnVector; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinHashMapResult; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinHashMultiSetResult; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinHashSetResult; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinHashTable; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinLongHashMap; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinLongHashMultiSet; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinLongHashSet; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinLongHashTable; + +/** + * Probe a {@link LongColumnVector} against a single-long-key + * {@link org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinLongHashTable}. + * + *

Covers the three concrete long-key hash table flavours emitted by Hive for a mapjoin: + *

    + *
  • hash map (inner / left-outer with payload)
  • + *
  • hash multi-set (semijoin with duplicates)
  • + *
  • hash set (semijoin)
  • + *
+ * All three funnel their probe result through {@code JoinUtil.JoinResult}; a row survives when + * the probe returns {@code MATCH} (and, for a multi-set/set, {@code SPILL} which we treat as a + * hit to be safe -- we prefer over-decoding to under-decoding). + * + *

The probe iterates every row exactly once, so it is O(batchSize). NULL keys never match, + * matching Hive's join semantics. + */ +public final class ParquetProbeLongHashTable implements ParquetProbeHashTable { + + private enum Kind { MAP, MULTISET, SET } + + private final Kind kind; + private final VectorMapJoinLongHashMap map; + private final VectorMapJoinLongHashMultiSet multiSet; + private final VectorMapJoinLongHashSet set; + private final boolean useMinMax; + private final long min; + private final long max; + private final VectorMapJoinHashMapResult mapResult; + private final VectorMapJoinHashMultiSetResult multiSetResult; + private final VectorMapJoinHashSetResult setResult; + + public static ParquetProbeLongHashTable of(VectorMapJoinHashTable ht) { + if (ht instanceof VectorMapJoinLongHashMap) { + return new ParquetProbeLongHashTable(Kind.MAP, (VectorMapJoinLongHashTable) ht); + } + if (ht instanceof VectorMapJoinLongHashMultiSet) { + return new ParquetProbeLongHashTable(Kind.MULTISET, (VectorMapJoinLongHashTable) ht); + } + if (ht instanceof VectorMapJoinLongHashSet) { + return new ParquetProbeLongHashTable(Kind.SET, (VectorMapJoinLongHashTable) ht); + } + throw new IllegalArgumentException("Not a long-key hash table: " + ht.getClass().getName()); + } + + private ParquetProbeLongHashTable(Kind kind, VectorMapJoinLongHashTable longHT) { + this.kind = kind; + this.useMinMax = longHT.useMinMax(); + this.min = longHT.min(); + this.max = longHT.max(); + if (kind == Kind.MAP) { + this.map = (VectorMapJoinLongHashMap) longHT; + this.multiSet = null; + this.set = null; + this.mapResult = map.createHashMapResult(); + this.multiSetResult = null; + this.setResult = null; + } else if (kind == Kind.MULTISET) { + this.map = null; + this.multiSet = (VectorMapJoinLongHashMultiSet) longHT; + this.set = null; + this.mapResult = null; + this.multiSetResult = multiSet.createHashMultiSetResult(); + this.setResult = null; + } else { + this.map = null; + this.multiSet = null; + this.set = (VectorMapJoinLongHashSet) longHT; + this.mapResult = null; + this.multiSetResult = null; + this.setResult = set.createHashSetResult(); + } + } + + @Override + public ParquetProbeFilter probe(ColumnVector keyColumn, int batchSize) throws IOException { + if (!(keyColumn instanceof LongColumnVector)) { + throw new IllegalArgumentException( + "Expected LongColumnVector for long-key probe, got " + keyColumn.getClass().getName()); + } + LongColumnVector v = (LongColumnVector) keyColumn; + boolean[] bitmap = new boolean[batchSize]; + + if (v.isRepeating) { + // Single-value fast path: probe once, splat the result over the whole batch. Cheap dictionary + // encoded columns land here often. + boolean nullKey = !v.noNulls && v.isNull[0]; + boolean hit = !nullKey && probeOne(v.vector[0]); + if (hit) { + Arrays.fill(bitmap, true); + } + // else all filtered out; bitmap already false + return ParquetProbeFilter.newBitmap(bitmap); + } + + for (int i = 0; i < batchSize; i++) { + if (!v.noNulls && v.isNull[i]) { + continue; // NULL keys never match + } + long key = v.vector[i]; + if (useMinMax && (key < min || key > max)) { + continue; // small-table min/max exclusion -- pure arithmetic, no hash-table touch + } + bitmap[i] = probeOne(key); + } + return ParquetProbeFilter.newBitmap(bitmap); + } + + private boolean probeOne(long key) throws IOException { + JoinUtil.JoinResult r; + switch (kind) { + case MAP: + r = map.lookup(key, mapResult); + break; + case MULTISET: + r = multiSet.contains(key, multiSetResult); + break; + case SET: + r = set.contains(key, setResult); + break; + default: + throw new AssertionError(kind); + } + // Treat SPILL as a hit: if the small-table row is on a spilled partition we can't know + // whether it matches without going to disk, so we let the row through and let the join + // operator handle it downstream -- prefer over-decoding to filtering a valid row. + return r == JoinUtil.JoinResult.MATCH || r == JoinUtil.JoinResult.SPILL; + } +} diff --git a/ql/src/test/org/apache/hadoop/hive/ql/io/parquet/vector/probe/TestParquetProbeDecodeState.java b/ql/src/test/org/apache/hadoop/hive/ql/io/parquet/vector/probe/TestParquetProbeDecodeState.java new file mode 100644 index 000000000000..17a52dc4f437 --- /dev/null +++ b/ql/src/test/org/apache/hadoop/hive/ql/io/parquet/vector/probe/TestParquetProbeDecodeState.java @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hadoop.hive.ql.io.parquet.vector.probe; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.UUID; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hadoop.hive.llap.io.api.LlapProxy; +import org.apache.hadoop.hive.ql.CompilationOpContext; +import org.apache.hadoop.hive.ql.exec.MapJoinOperator; +import org.apache.hadoop.hive.ql.exec.ObjectCache; +import org.apache.hadoop.hive.ql.exec.ObjectCacheFactory; +import org.apache.hadoop.hive.ql.exec.Operator; +import org.apache.hadoop.hive.ql.exec.TableScanOperator.ProbeDecodeContext; +import org.apache.hadoop.hive.ql.exec.persistence.MapJoinTableContainer; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinHashTable; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinLongHashSet; +import org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinTableContainer; +import org.apache.hadoop.hive.ql.plan.MapJoinDesc; +import org.apache.hadoop.hive.ql.plan.MapWork; +import org.apache.hadoop.hive.ql.plan.OperatorDesc; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Regression coverage for the cacheKey-suffix contract between {@link MapJoinOperator} (the writer + * that seeds the {@link ObjectCache}) and {@link ParquetProbeDecodeState#of} (the reader that + * looks it up). The vectorized Parquet reader must reproduce the loader's key exactly, else it + * looks up under the raw base key, finds nothing, and silently disables probe-decode -- the + * pre-fix behaviour of HIVE-30019. + * + *

{@link MapJoinOperator#initializeOp} computes: + *

{@code cacheKey = conf.getCacheKey() + "_" + this.getClass().getName();}
+ * The {@link ProbeDecodeContext} that Tez compilation writes into {@link MapWork} carries only + * the raw {@code conf.getCacheKey()} half. {@link ParquetProbeDecodeState#resolveActualCacheKey} + * closes that gap by finding the concrete {@link MapJoinOperator} subclass in the + * {@code MapWork} tree at read time and appending its class name. + * + *

These tests pin both halves of the contract: + *

    + *
  • {@link #resolveActualCacheKeyAppendsConcreteClass()} — the resolver returns + * {@code base + "_" + } when {@code MapJoinOperator}'s + * {@code MapJoinDesc.cacheKey} equals {@code base}. If a future change removes the suffix + * from either side without updating the other, this test fails loudly.
  • + *
  • {@link #ofResolvesSeededHashTableUnderSuffixedCacheKey()} — end-to-end: seed the + * {@link ObjectCache} under the fully-suffixed key, call {@link ParquetProbeDecodeState#of}, + * and assert it comes back enabled with the correct key column index.
  • + *
  • {@link #ofDisabledWhenOnlyRawKeyPresent()} — negative: seed the same cache under the raw + * base only. If the resolver ever silently reverts to the raw key, this test flips to green + * instead of red, but paired with the seeded suffixed-key test it still traps the + * regression: the reader is finding the value only because it looked up the suffixed key.
  • + *
+ */ +public class TestParquetProbeDecodeState { + + private static final String BASE_CACHE_KEY = "HASH_MAP_MAPJOIN_25_container"; + private static final byte SMALL_TABLE_POS = (byte) 1; + private static final String KEY_COL = "key2"; + private static final List PROJECTED_COLS = Arrays.asList("nokey", KEY_COL, "dt"); + + private HiveConf conf; + private String queryId; + private boolean priorIsDaemon; + + @Before + public void before() { + // Route ObjectCacheFactory through the LLAP daemon branch so a single LlapObjectCache instance + // is shared for both our seed and the state's lookup (the MR branch hands out a fresh cache + // on every getCache call, so the seed would never be visible to the state). + priorIsDaemon = LlapProxy.isDaemon(); + LlapProxy.setDaemon(true); + + conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_EXECUTION_ENGINE, "tez"); + conf.setBoolVar(HiveConf.ConfVars.LLAP_OBJECT_CACHE_ENABLED, true); + queryId = "test-probe-decode-" + UUID.randomUUID(); + conf.setVar(HiveConf.ConfVars.HIVE_QUERY_ID, queryId); + } + + @After + public void after() { + ObjectCacheFactory.removeLlapQueryCache(queryId); + LlapProxy.setDaemon(priorIsDaemon); + } + + /** + * Pin the resolver in isolation: given a {@link MapWork} whose {@link MapJoinOperator}'s + * {@code MapJoinDesc.cacheKey} equals {@code base}, {@code resolveActualCacheKey} must return + * {@code base + "_" + }, matching what + * {@link MapJoinOperator#initializeOp} would have stored. + * + *

Uses the plain {@code MapJoinOperator} class here — the vectorizer swaps it for a + * concrete {@code VectorMapJoin*Operator} subclass at runtime, but the resolver only reads + * {@code Class#getName()} off whatever operator is in the tree, so this is representative. + */ + @Test + public void resolveActualCacheKeyAppendsConcreteClass() { + MapWork mapWork = mapWorkWith(mapJoinOperatorWithCacheKey(BASE_CACHE_KEY)); + + String resolved = ParquetProbeDecodeState.resolveActualCacheKey(mapWork, BASE_CACHE_KEY); + + assertEquals(BASE_CACHE_KEY + "_" + MapJoinOperator.class.getName(), resolved); + } + + /** + * End-to-end: seed the ObjectCache under the fully-suffixed key that + * {@link MapJoinOperator#initializeOp} would store under, then call + * {@link ParquetProbeDecodeState#of}. The state must resolve the suffix, find the entry, unwrap + * the {@link ImmutablePair} to the small-side container, and expose an enabled probe with the + * key column at index 1 (the position of {@code KEY_COL} in {@code PROJECTED_COLS}). + */ + @Test + public void ofResolvesSeededHashTableUnderSuffixedCacheKey() throws Exception { + MapWork mapWork = mapWorkWith(mapJoinOperatorWithCacheKey(BASE_CACHE_KEY)); + seedObjectCache(BASE_CACHE_KEY + "_" + MapJoinOperator.class.getName(), + pairWithSmallContainerAtPos(SMALL_TABLE_POS)); + mapWork.setProbeDecodeContext( + new ProbeDecodeContext(BASE_CACHE_KEY, SMALL_TABLE_POS, KEY_COL, 1.0)); + + ParquetProbeDecodeState state = ParquetProbeDecodeState.of(conf, mapWork, PROJECTED_COLS); + + assertTrue("probe-decode should be enabled once the suffixed key resolves", + state.isEnabled()); + assertEquals("probe key column index must match position in projected columns", + PROJECTED_COLS.indexOf(KEY_COL), state.getKeyColumnIndex()); + } + + /** + * Negative case: the {@link ObjectCache} carries an entry only under the raw base key -- no + * suffix. This is the pre-fix layout (and also what happens if a future refactor accidentally + * strips the suffix from {@link MapJoinOperator#initializeOp}). The state must report + * {@code isEnabled() == false} rather than picking up the un-suffixed entry, keeping the + * suffixed-key path the sole lookup mechanism. + */ + @Test + public void ofDisabledWhenOnlyRawKeyPresent() throws Exception { + MapWork mapWork = mapWorkWith(mapJoinOperatorWithCacheKey(BASE_CACHE_KEY)); + seedObjectCache(BASE_CACHE_KEY, pairWithSmallContainerAtPos(SMALL_TABLE_POS)); + mapWork.setProbeDecodeContext( + new ProbeDecodeContext(BASE_CACHE_KEY, SMALL_TABLE_POS, KEY_COL, 1.0)); + + ParquetProbeDecodeState state = ParquetProbeDecodeState.of(conf, mapWork, PROJECTED_COLS); + + assertFalse("state must not resolve when only the raw (unsuffixed) key is cached", + state.isEnabled()); + } + + private static MapJoinOperator mapJoinOperatorWithCacheKey(String cacheKey) { + MapJoinOperator mj = new MapJoinOperator(new CompilationOpContext()); + MapJoinDesc desc = new MapJoinDesc(); + desc.setCacheKey(cacheKey); + mj.setConf(desc); + return mj; + } + + private static MapWork mapWorkWith(MapJoinOperator mj) { + MapWork mapWork = new MapWork(); + LinkedHashMap> aliasToWork = new LinkedHashMap<>(); + // OperatorUtils#findOperators walks child operators from each entry in aliasToWork, and also + // checks the entry itself -- putting the MapJoinOperator directly in the map is enough for + // the resolver, and avoids the full TS → RS → MJ chain a real plan would carry. + aliasToWork.put("test-alias", mj); + mapWork.setAliasToWork(aliasToWork); + return mapWork; + } + + private void seedObjectCache(String key, Object value) throws Exception { + ObjectCache cache = ObjectCacheFactory.getCache(conf, queryId, false); + // ObjectCache exposes no public put(); retrieve(key, Callable) stores the callable's result + // when the key is absent, which is what the loader effectively does too. + cache.retrieve(key, () -> value); + } + + private static ImmutablePair pairWithSmallContainerAtPos( + byte smallPos) { + // The pre-fix bug was that the cached object is a Pair whose left slot is a + // MapJoinTableContainer[] indexed by the mapjoin position -- mirror that exact shape so the + // state's unwrap logic is exercised, not shortcut. + MapJoinTableContainer[] tables = new MapJoinTableContainer[Math.max(2, smallPos + 1)]; + tables[smallPos] = smallSideVectorContainer(); + return ImmutablePair.of(tables, /* serdes, unused by the state */ null); + } + + private static VectorMapJoinTableContainer smallSideVectorContainer() { + VectorMapJoinLongHashSet ht = mock(VectorMapJoinLongHashSet.class); + // ParquetProbeLongHashTable's constructor reads useMinMax / min / max and calls + // createHashSetResult on the SET branch -- give it just enough to run without NPE. + when(ht.useMinMax()).thenReturn(false); + when(ht.createHashSetResult()).thenReturn(new StubHashSetResult()); + VectorMapJoinTableContainer c = mock(VectorMapJoinTableContainer.class); + when(c.vectorMapJoinHashTable()).thenReturn((VectorMapJoinHashTable) ht); + return c; + } + + /** + * Minimal concrete {@code VectorMapJoinHashSetResult}. The parent classes are abstract but have + * no unimplemented methods, so a bare subclass instantiates cleanly. + */ + private static final class StubHashSetResult + extends org.apache.hadoop.hive.ql.exec.vector.mapjoin.hashtable.VectorMapJoinHashSetResult { + } +} diff --git a/ql/src/test/org/apache/hadoop/hive/ql/io/parquet/vector/probe/TestParquetProbeFilter.java b/ql/src/test/org/apache/hadoop/hive/ql/io/parquet/vector/probe/TestParquetProbeFilter.java new file mode 100644 index 000000000000..790bb97ef7bc --- /dev/null +++ b/ql/src/test/org/apache/hadoop/hive/ql/io/parquet/vector/probe/TestParquetProbeFilter.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hadoop.hive.ql.io.parquet.vector.probe; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; + +/** + * Pure unit tests for the {@link ParquetProbeFilter} lifecycle: the ProbeDecode path calls + * {@link ParquetProbeFilter#newBitmap} after probing, per-row column readers query + * {@link ParquetProbeFilter#isSelected}, and once all columns are read + * {@link ParquetProbeFilter#compact} materialises the {@code selected[]} that + * {@code VectorizedRowBatch} consumes. + */ +public class TestParquetProbeFilter { + + @Test + public void newBitmapRejectsNull() { + try { + ParquetProbeFilter.newBitmap(null); + fail("expected IAE for null bitmap"); + } catch (IllegalArgumentException expected) { + // ok + } + } + + @Test + public void isSelectedReflectsBitmap() { + boolean[] bits = { true, false, true, true, false }; + ParquetProbeFilter f = ParquetProbeFilter.newBitmap(bits); + + assertTrue(f.isSelected(0)); + assertFalse(f.isSelected(1)); + assertTrue(f.isSelected(2)); + assertTrue(f.isSelected(3)); + assertFalse(f.isSelected(4)); + } + + @Test + public void isSelectedOutOfBoundsReturnsFalse() { + // Guards against off-by-one bugs in callers that pass a rowId near the batch tail. + ParquetProbeFilter f = ParquetProbeFilter.newBitmap(new boolean[] { true, true }); + assertFalse(f.isSelected(-1)); + assertFalse(f.isSelected(2)); + assertFalse(f.isSelected(Integer.MAX_VALUE)); + } + + @Test + public void compactBeforeCallReturnsNullArrays() { + // Explicit contract: getSelected() / getSelectedSize() are meaningless until compact() runs. + ParquetProbeFilter f = ParquetProbeFilter.newBitmap(new boolean[] { true, false, true }); + assertNull(f.getSelected()); + assertEquals(0, f.getSelectedSize()); + } + + @Test + public void compactMaterialisesSelected() { + boolean[] bits = { true, false, true, true, false, true }; + ParquetProbeFilter f = ParquetProbeFilter.newBitmap(bits); + + ParquetProbeFilter same = f.compact(bits.length); + assertSame("compact must return this for chaining", f, same); + + assertArrayEquals(new int[] { 0, 2, 3, 5 }, java.util.Arrays.copyOf(f.getSelected(), f.getSelectedSize())); + assertEquals(4, f.getSelectedSize()); + } + + @Test + public void compactHonoursBatchSize() { + // batchSize can be smaller than the bitmap when the last row group ends mid-batch; + // entries beyond batchSize must be ignored. + boolean[] bits = { true, true, true, true }; + ParquetProbeFilter f = ParquetProbeFilter.newBitmap(bits); + f.compact(2); + + assertEquals(2, f.getSelectedSize()); + assertArrayEquals(new int[] { 0, 1 }, java.util.Arrays.copyOf(f.getSelected(), f.getSelectedSize())); + } + + @Test + public void compactHandlesEmptyBatch() { + ParquetProbeFilter f = ParquetProbeFilter.newBitmap(new boolean[0]); + f.compact(0); + assertEquals(0, f.getSelectedSize()); + assertArrayEquals(new int[0], f.getSelected()); + } + + @Test + public void compactHandlesAllRejected() { + ParquetProbeFilter f = ParquetProbeFilter.newBitmap(new boolean[] { false, false, false }); + f.compact(3); + assertEquals(0, f.getSelectedSize()); + // getSelected() is allowed to return an over-provisioned array; only [0, selectedSize) counts. + assertEquals(3, f.getSelected().length); + } + + @Test + public void compactHandlesAllAccepted() { + ParquetProbeFilter f = ParquetProbeFilter.newBitmap(new boolean[] { true, true, true, true }); + f.compact(4); + assertEquals(4, f.getSelectedSize()); + assertArrayEquals(new int[] { 0, 1, 2, 3 }, f.getSelected()); + } + + @Test + public void compactIsIdempotent() { + // VectorizedParquetRecordReader.applyProbeFilterToBatch may call compact() more than once + // if the same filter is passed to several helpers; a second call must be a no-op. + boolean[] bits = { true, false, true }; + ParquetProbeFilter f = ParquetProbeFilter.newBitmap(bits); + f.compact(bits.length); + int[] firstArray = f.getSelected(); + int firstSize = f.getSelectedSize(); + + f.compact(bits.length); + assertSame("compact must not reallocate on repeat", firstArray, f.getSelected()); + assertEquals(firstSize, f.getSelectedSize()); + } +} diff --git a/ql/src/test/org/apache/hadoop/hive/ql/io/parquet/vector/probe/TestVectorizedParquetProbeDecodeReader.java b/ql/src/test/org/apache/hadoop/hive/ql/io/parquet/vector/probe/TestVectorizedParquetProbeDecodeReader.java new file mode 100644 index 000000000000..3e927633909c --- /dev/null +++ b/ql/src/test/org/apache/hadoop/hive/ql/io/parquet/vector/probe/TestVectorizedParquetProbeDecodeReader.java @@ -0,0 +1,329 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.hadoop.hive.ql.io.parquet.vector.probe; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hadoop.hive.ql.exec.vector.BytesColumnVector; +import org.apache.hadoop.hive.ql.exec.vector.ColumnVector; +import org.apache.hadoop.hive.ql.exec.vector.DoubleColumnVector; +import org.apache.hadoop.hive.ql.exec.vector.LongColumnVector; +import org.apache.hadoop.hive.ql.exec.vector.VectorizedRowBatch; +import org.apache.hadoop.hive.ql.io.parquet.VectorizedColumnReaderTestBase; +import org.apache.hadoop.hive.ql.io.parquet.vector.VectorizedColumnReader; +import org.apache.hadoop.hive.ql.io.parquet.vector.VectorizedParquetRecordReader; +import org.apache.hadoop.hive.serde2.ColumnProjectionUtils; +import org.apache.hadoop.hive.serde2.typeinfo.TypeInfo; +import org.apache.hadoop.hive.serde2.typeinfo.TypeInfoFactory; +import org.apache.hadoop.mapred.JobConf; +import org.apache.parquet.example.data.Group; +import org.apache.parquet.example.data.simple.SimpleGroupFactory; +import org.apache.parquet.hadoop.ParquetWriter; +import org.apache.parquet.hadoop.example.GroupWriteSupport; +import org.apache.parquet.hadoop.metadata.CompressionCodecName; +import org.apache.parquet.io.api.Binary; +import org.apache.parquet.schema.MessageType; +import org.apache.parquet.schema.MessageTypeParser; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * End-to-end test that the ProbeDecode filter path in {@code VectorizedPrimitiveColumnReader} + * (a) leaves filtered rows as null-marked slots in the column vector and (b) still decodes + * surviving rows correctly. + * + *

The test bypasses {@code VectorizedParquetRecordReader.nextBatch}'s ProbeDecodeState + * resolution (which requires a live MapJoin operator + hash table) and instead reflects into + * the reader to reach the primitive column readers directly. It then calls the filter-aware + * {@code readBatch(total, column, type, ParquetProbeFilter)} signature that + * {@code VectorizedPrimitiveColumnReader} exposes to the outer reader -- the same call + * {@code nextBatch} would make on the probe path. + * + *

Both dictionary-encoded and PLAIN-encoded pages are exercised so both the coalesced-skip + * path ({@code readDictionaryIDs} + {@code pendingSkip} + {@code skipInts}) and the per-row + * skip path ({@code readIntegers}/{@code readLongs}/{@code readDoubles}/{@code readBinaries}) + * are hit. + */ +public class TestVectorizedParquetProbeDecodeReader extends VectorizedColumnReaderTestBase { + + private static final int N_ROWS = 256; + private static final MessageType WRITE_SCHEMA = MessageTypeParser.parseMessageType( + "message test { " + + "required int32 int_col; " + + "required int64 long_col; " + + "required double dbl_col; " + + "required binary str_col (UTF8); " + + "}"); + + private java.io.File tempDir; + private Path tempFile; + + @Before + public void setUpFile() throws Exception { + tempDir = java.nio.file.Files.createTempDirectory("probe-decode-").toFile(); + tempFile = new Path(new java.io.File(tempDir, "data.parquet").toURI()); + } + + @After + public void tearDownFile() { + if (tempDir != null && tempDir.exists()) { + for (java.io.File f : tempDir.listFiles()) { + f.delete(); + } + tempDir.delete(); + } + } + + /** Filter accepts rows whose index is even (0, 2, 4, ...). */ + private static ParquetProbeFilter halfFilter(int size) { + boolean[] bits = new boolean[size]; + for (int i = 0; i < size; i++) { + bits[i] = (i % 2 == 0); + } + return ParquetProbeFilter.newBitmap(bits); + } + + /** + * When {@code dictionary=true}, values cycle through a small distinct set so Parquet + * dictionary-encodes each page; otherwise every value is unique so the writer falls back to + * PLAIN encoding. + */ + private void writeFile(boolean dictionary) throws IOException { + Configuration conf = new Configuration(); + GroupWriteSupport.setSchema(WRITE_SCHEMA, conf); + SimpleGroupFactory gf = new SimpleGroupFactory(WRITE_SCHEMA); + + try (ParquetWriter writer = new ParquetWriter<>(tempFile, new GroupWriteSupport(), + CompressionCodecName.UNCOMPRESSED, 1024 * 1024, 1024 * 1024, 512, dictionary, false, + ParquetWriter.DEFAULT_WRITER_VERSION, conf)) { + for (int i = 0; i < N_ROWS; i++) { + int intVal = dictionary ? (i % 4) : i; + long longVal = dictionary ? (i % 4) : (long) i; + double dblVal = dictionary ? (i % 4) : (double) i; + String strVal = dictionary ? ("v" + (i % 4)) : ("v" + i); + Group g = gf.newGroup() + .append("int_col", intVal) + .append("long_col", longVal) + .append("dbl_col", dblVal) + .append("str_col", Binary.fromString(strVal)); + writer.write(g); + } + } + } + + private VectorizedParquetRecordReader openReader() throws Exception { + Configuration readerConf = new Configuration(); + readerConf.set(org.apache.hadoop.hive.ql.io.IOConstants.COLUMNS, + "int_col,long_col,dbl_col,str_col"); + readerConf.set(org.apache.hadoop.hive.ql.io.IOConstants.COLUMNS_TYPES, + "int,bigint,double,string"); + readerConf.setBoolean(ColumnProjectionUtils.READ_ALL_COLUMNS, false); + readerConf.set(ColumnProjectionUtils.READ_COLUMN_IDS_CONF_STR, "0,1,2,3"); + HiveConf.setBoolVar(readerConf, HiveConf.ConfVars.HIVE_VECTORIZATION_ENABLED, true); + HiveConf.setVar(readerConf, HiveConf.ConfVars.PLAN, "//tmp"); + org.apache.hadoop.mapreduce.Job vectorJob = new org.apache.hadoop.mapreduce.Job(readerConf, "read"); + org.apache.parquet.hadoop.ParquetInputFormat.setInputPaths(vectorJob, tempFile); + initialVectorizedRowBatchCtx(readerConf, null); + return new VectorizedParquetRecordReader(getFileSplit(vectorJob, tempFile), new JobConf(readerConf)); + } + + /** + * Force {@code checkEndOfRowGroup} to run on the reader without consuming any rows through + * {@code nextBatch}. This leaves {@code columnReaders} populated and page state at row 0. + */ + @SuppressWarnings("unchecked") + private static VectorizedColumnReader[] primeColumnReaders(VectorizedParquetRecordReader r) + throws ReflectiveOperationException { + Method m = VectorizedParquetRecordReader.class.getDeclaredMethod("checkEndOfRowGroup"); + m.setAccessible(true); + m.invoke(r); + Field f = VectorizedParquetRecordReader.class.getDeclaredField("columnReaders"); + f.setAccessible(true); + return (VectorizedColumnReader[]) f.get(r); + } + + /** + * Expected value at row {@code i} for the given column and encoding, matching what + * {@link #writeFile} wrote. + */ + private static long expectedInt(int i, boolean dict) { return dict ? (i % 4) : i; } + private static long expectedLong(int i, boolean dict) { return dict ? (i % 4) : (long) i; } + private static double expectedDbl(int i, boolean dict) { return dict ? (i % 4) : (double) i; } + private static String expectedStr(int i, boolean dict) { return dict ? ("v" + (i % 4)) : ("v" + i); } + + private void runFilterHonoringTest(boolean dictionary) throws Exception { + writeFile(dictionary); + + VectorizedParquetRecordReader reader = openReader(); + try { + VectorizedColumnReader[] readers = primeColumnReaders(reader); + assertNotNull("columnReaders must be populated after checkEndOfRowGroup", readers); + assertEquals(4, readers.length); + + int batchSize = Math.min(VectorizedRowBatch.DEFAULT_SIZE, N_ROWS); + ParquetProbeFilter filter = halfFilter(batchSize); + + LongColumnVector intVec = new LongColumnVector(batchSize); + LongColumnVector longVec = new LongColumnVector(batchSize); + DoubleColumnVector dblVec = new DoubleColumnVector(batchSize); + BytesColumnVector strVec = new BytesColumnVector(batchSize); + for (ColumnVector v : new ColumnVector[] { intVec, longVec, dblVec, strVec }) { + v.init(); + } + // Vectors start with noNulls=true; every column reader clears that when it hits a + // filtered slot (see setNullValue in VectorizedPrimitiveColumnReader). + + // Types must line up with the columns projection. + TypeInfo intType = TypeInfoFactory.getPrimitiveTypeInfo("int"); + TypeInfo longType = TypeInfoFactory.getPrimitiveTypeInfo("bigint"); + TypeInfo dblType = TypeInfoFactory.getPrimitiveTypeInfo("double"); + TypeInfo strType = TypeInfoFactory.getPrimitiveTypeInfo("string"); + + // Drive the filter-aware read path on each column reader. This is the same call that + // VectorizedParquetRecordReader.nextBatch issues on non-key columns when the ProbeDecode + // path is active. + readers[0].readBatch(batchSize, intVec, intType, filter); + readers[1].readBatch(batchSize, longVec, longType, filter); + readers[2].readBatch(batchSize, dblVec, dblType, filter); + readers[3].readBatch(batchSize, strVec, strType, filter); + + // Each vector must now carry the surviving rows' values in the accepted slots and a + // null-marked slot everywhere else. noNulls must be false since we produced nulls. + for (int i = 0; i < batchSize; i++) { + boolean selected = (i % 2 == 0); + if (selected) { + assertFalse("row " + i + " must not be null in int_col", intVec.isNull[i]); + assertFalse("row " + i + " must not be null in long_col", longVec.isNull[i]); + assertFalse("row " + i + " must not be null in dbl_col", dblVec.isNull[i]); + assertFalse("row " + i + " must not be null in str_col", strVec.isNull[i]); + + assertEquals("int_col value at row " + i, expectedInt(i, dictionary), intVec.vector[i]); + assertEquals("long_col value at row " + i, expectedLong(i, dictionary), longVec.vector[i]); + assertEquals("dbl_col value at row " + i, expectedDbl(i, dictionary), dblVec.vector[i], 0.0); + String actual = new String(strVec.vector[i], strVec.start[i], strVec.length[i], + StandardCharsets.UTF_8); + assertEquals("str_col value at row " + i, expectedStr(i, dictionary), actual); + } else { + assertTrue("row " + i + " must be null in int_col", intVec.isNull[i]); + assertTrue("row " + i + " must be null in long_col", longVec.isNull[i]); + assertTrue("row " + i + " must be null in dbl_col", dblVec.isNull[i]); + assertTrue("row " + i + " must be null in str_col", strVec.isNull[i]); + } + } + assertFalse("noNulls must be cleared once a filtered row is emitted (int_col)", + intVec.noNulls); + assertFalse("noNulls must be cleared once a filtered row is emitted (long_col)", + longVec.noNulls); + assertFalse("noNulls must be cleared once a filtered row is emitted (dbl_col)", + dblVec.noNulls); + assertFalse("noNulls must be cleared once a filtered row is emitted (str_col)", + strVec.noNulls); + } finally { + reader.close(); + } + } + + /** + * Filter should be a no-op when it accepts every row: the resulting vectors are identical to + * a baseline unfiltered read. This is the "no regression" case for a filter that matches all. + */ + private void runAllPassNoOpTest(boolean dictionary) throws Exception { + writeFile(dictionary); + + VectorizedParquetRecordReader baselineReader = openReader(); + VectorizedParquetRecordReader filteredReader = openReader(); + try { + VectorizedColumnReader[] baseline = primeColumnReaders(baselineReader); + VectorizedColumnReader[] filtered = primeColumnReaders(filteredReader); + int batchSize = Math.min(VectorizedRowBatch.DEFAULT_SIZE, N_ROWS); + + boolean[] allTrue = new boolean[batchSize]; + java.util.Arrays.fill(allTrue, true); + ParquetProbeFilter allPass = ParquetProbeFilter.newBitmap(allTrue); + + TypeInfo intType = TypeInfoFactory.getPrimitiveTypeInfo("int"); + TypeInfo longType = TypeInfoFactory.getPrimitiveTypeInfo("bigint"); + TypeInfo dblType = TypeInfoFactory.getPrimitiveTypeInfo("double"); + TypeInfo strType = TypeInfoFactory.getPrimitiveTypeInfo("string"); + + LongColumnVector baseInt = new LongColumnVector(batchSize); + baseInt.init(); + LongColumnVector filtInt = new LongColumnVector(batchSize); + filtInt.init(); + baseline[0].readBatch(batchSize, baseInt, intType); + filtered[0].readBatch(batchSize, filtInt, intType, allPass); + for (int i = 0; i < batchSize; i++) { + assertEquals("int_col allPass row " + i, baseInt.vector[i], filtInt.vector[i]); + assertEquals("int_col allPass isNull row " + i, baseInt.isNull[i], filtInt.isNull[i]); + } + + BytesColumnVector baseStr = new BytesColumnVector(batchSize); + baseStr.init(); + BytesColumnVector filtStr = new BytesColumnVector(batchSize); + filtStr.init(); + baseline[3].readBatch(batchSize, baseStr, strType); + filtered[3].readBatch(batchSize, filtStr, strType, allPass); + for (int i = 0; i < batchSize; i++) { + assertEquals("str_col allPass isNull row " + i, baseStr.isNull[i], filtStr.isNull[i]); + String b = new String(baseStr.vector[i], baseStr.start[i], baseStr.length[i], StandardCharsets.UTF_8); + String f = new String(filtStr.vector[i], filtStr.start[i], filtStr.length[i], StandardCharsets.UTF_8); + assertEquals("str_col allPass row " + i, b, f); + } + } finally { + baselineReader.close(); + filteredReader.close(); + } + } + + @Test + public void filterHonoredOnDictionaryEncodedPages() throws Exception { + // Exercises readDictionaryIDs -> pendingSkip -> DictionaryValuesReader.skip(int) + // -> RunLengthBitPackingHybridDecoder.skipInts (the O(runs) fast-path). + runFilterHonoringTest(true); + } + + @Test + public void filterHonoredOnPlainEncodedPages() throws Exception { + // Exercises the per-row skip path in readIntegers / readLongs / readDoubles / readBinaries. + runFilterHonoringTest(false); + } + + @Test + public void allPassFilterIsNoOpDict() throws Exception { + runAllPassNoOpTest(true); + } + + @Test + public void allPassFilterIsNoOpPlain() throws Exception { + runAllPassNoOpTest(false); + } +} diff --git a/ql/src/test/queries/clientpositive/probedecode_mapjoin_simple_parquet.q b/ql/src/test/queries/clientpositive/probedecode_mapjoin_simple_parquet.q new file mode 100644 index 000000000000..a709f3bfb5e5 --- /dev/null +++ b/ql/src/test/queries/clientpositive/probedecode_mapjoin_simple_parquet.q @@ -0,0 +1,53 @@ +-- Parquet mirror of probedecode_mapjoin_simple.q. +-- +-- Exercises the Parquet ProbeDecode path: the vectorizer wires the big-side +-- TableScan with a ProbeDecodeContext, VectorizedParquetRecordReader decodes +-- the join-key column first, probes the small-side hash table, and passes the +-- resulting ParquetProbeFilter down to the remaining columns' readBatch calls +-- so filtered rows skip decode / conversion. +-- +-- The correctness bar for this test is that the join result must be identical +-- with and without hive.optimize.scan.probedecode enabled; the fast-path is +-- purely a performance optimisation (rows that would be filtered anyway are +-- read as nulls into the batch, then dropped via batch.selected[]). +set hive.stats.column.autogather=false; +set hive.mapred.mode=nonstrict; +set hive.explain.user=false; +SET hive.auto.convert.join=true; +SET hive.auto.convert.join.noconditionaltask=true; +SET hive.auto.convert.join.noconditionaltask.size=1000000000; +SET hive.vectorized.execution.enabled=true; +set hive.vectorized.execution.mapjoin.native.fast.hashtable.enabled=true; +set hive.fetch.task.conversion=none; +SET mapred.min.split.size=1000; +SET mapred.max.split.size=5000; + +CREATE TABLE item_dim_pq (key1 int, name string) stored as parquet; +CREATE TABLE orders_fact_pq (nokey int, key2 int, dt timestamp) stored as parquet; + +INSERT INTO item_dim_pq values(101, "Item 101"); +INSERT INTO item_dim_pq values(102, "Item 102"); + +INSERT INTO orders_fact_pq values(12345, 101, '2001-01-30 00:00:00'); +INSERT INTO orders_fact_pq values(23456, 104, '2002-02-30 00:00:00'); +INSERT INTO orders_fact_pq values(34567, 108, '2003-03-30 00:00:00'); +INSERT INTO orders_fact_pq values(45678, 102, '2004-04-30 00:00:00'); +INSERT INTO orders_fact_pq values(56789, 109, '2005-05-30 00:00:00'); +INSERT INTO orders_fact_pq values(67891, 110, '2006-06-30 00:00:00'); + +-- Baseline: probedecode disabled. Result set is the reference for equivalence. +SET hive.optimize.scan.probedecode=false; + +select key1, key2, name, dt from orders_fact_pq join item_dim_pq on (orders_fact_pq.key2 = item_dim_pq.key1) +order by key2; + +-- Now enable probedecode. The plan should carry the ProbeDecodeContext on the +-- big-side (orders_fact_pq) TableScan; the join result must be identical. +SET hive.optimize.scan.probedecode=true; + +EXPLAIN VECTORIZATION DETAIL +select key1, key2, name, dt from orders_fact_pq join item_dim_pq on (orders_fact_pq.key2 = item_dim_pq.key1); + +-- Two keys match (101, 102); the other four rows are dropped by the probe filter. +select key1, key2, name, dt from orders_fact_pq join item_dim_pq on (orders_fact_pq.key2 = item_dim_pq.key1) +order by key2; diff --git a/ql/src/test/results/clientpositive/llap/probedecode_mapjoin_simple_parquet.q.out b/ql/src/test/results/clientpositive/llap/probedecode_mapjoin_simple_parquet.q.out new file mode 100644 index 000000000000..7784044e5469 --- /dev/null +++ b/ql/src/test/results/clientpositive/llap/probedecode_mapjoin_simple_parquet.q.out @@ -0,0 +1,300 @@ +PREHOOK: query: CREATE TABLE item_dim_pq (key1 int, name string) stored as parquet +PREHOOK: type: CREATETABLE +PREHOOK: Output: database:default +PREHOOK: Output: default@item_dim_pq +POSTHOOK: query: CREATE TABLE item_dim_pq (key1 int, name string) stored as parquet +POSTHOOK: type: CREATETABLE +POSTHOOK: Output: database:default +POSTHOOK: Output: default@item_dim_pq +PREHOOK: query: CREATE TABLE orders_fact_pq (nokey int, key2 int, dt timestamp) stored as parquet +PREHOOK: type: CREATETABLE +PREHOOK: Output: database:default +PREHOOK: Output: default@orders_fact_pq +POSTHOOK: query: CREATE TABLE orders_fact_pq (nokey int, key2 int, dt timestamp) stored as parquet +POSTHOOK: type: CREATETABLE +POSTHOOK: Output: database:default +POSTHOOK: Output: default@orders_fact_pq +PREHOOK: query: INSERT INTO item_dim_pq values(101, "Item 101") +PREHOOK: type: QUERY +PREHOOK: Input: _dummy_database@_dummy_table +PREHOOK: Output: default@item_dim_pq +POSTHOOK: query: INSERT INTO item_dim_pq values(101, "Item 101") +POSTHOOK: type: QUERY +POSTHOOK: Input: _dummy_database@_dummy_table +POSTHOOK: Output: default@item_dim_pq +POSTHOOK: Lineage: item_dim_pq.key1 SCRIPT [] +POSTHOOK: Lineage: item_dim_pq.name SCRIPT [] +PREHOOK: query: INSERT INTO item_dim_pq values(102, "Item 102") +PREHOOK: type: QUERY +PREHOOK: Input: _dummy_database@_dummy_table +PREHOOK: Output: default@item_dim_pq +POSTHOOK: query: INSERT INTO item_dim_pq values(102, "Item 102") +POSTHOOK: type: QUERY +POSTHOOK: Input: _dummy_database@_dummy_table +POSTHOOK: Output: default@item_dim_pq +POSTHOOK: Lineage: item_dim_pq.key1 SCRIPT [] +POSTHOOK: Lineage: item_dim_pq.name SCRIPT [] +PREHOOK: query: INSERT INTO orders_fact_pq values(12345, 101, '2001-01-30 00:00:00') +PREHOOK: type: QUERY +PREHOOK: Input: _dummy_database@_dummy_table +PREHOOK: Output: default@orders_fact_pq +POSTHOOK: query: INSERT INTO orders_fact_pq values(12345, 101, '2001-01-30 00:00:00') +POSTHOOK: type: QUERY +POSTHOOK: Input: _dummy_database@_dummy_table +POSTHOOK: Output: default@orders_fact_pq +POSTHOOK: Lineage: orders_fact_pq.dt SCRIPT [] +POSTHOOK: Lineage: orders_fact_pq.key2 SCRIPT [] +POSTHOOK: Lineage: orders_fact_pq.nokey SCRIPT [] +PREHOOK: query: INSERT INTO orders_fact_pq values(23456, 104, '2002-02-30 00:00:00') +PREHOOK: type: QUERY +PREHOOK: Input: _dummy_database@_dummy_table +PREHOOK: Output: default@orders_fact_pq +POSTHOOK: query: INSERT INTO orders_fact_pq values(23456, 104, '2002-02-30 00:00:00') +POSTHOOK: type: QUERY +POSTHOOK: Input: _dummy_database@_dummy_table +POSTHOOK: Output: default@orders_fact_pq +POSTHOOK: Lineage: orders_fact_pq.dt SCRIPT [] +POSTHOOK: Lineage: orders_fact_pq.key2 SCRIPT [] +POSTHOOK: Lineage: orders_fact_pq.nokey SCRIPT [] +PREHOOK: query: INSERT INTO orders_fact_pq values(34567, 108, '2003-03-30 00:00:00') +PREHOOK: type: QUERY +PREHOOK: Input: _dummy_database@_dummy_table +PREHOOK: Output: default@orders_fact_pq +POSTHOOK: query: INSERT INTO orders_fact_pq values(34567, 108, '2003-03-30 00:00:00') +POSTHOOK: type: QUERY +POSTHOOK: Input: _dummy_database@_dummy_table +POSTHOOK: Output: default@orders_fact_pq +POSTHOOK: Lineage: orders_fact_pq.dt SCRIPT [] +POSTHOOK: Lineage: orders_fact_pq.key2 SCRIPT [] +POSTHOOK: Lineage: orders_fact_pq.nokey SCRIPT [] +PREHOOK: query: INSERT INTO orders_fact_pq values(45678, 102, '2004-04-30 00:00:00') +PREHOOK: type: QUERY +PREHOOK: Input: _dummy_database@_dummy_table +PREHOOK: Output: default@orders_fact_pq +POSTHOOK: query: INSERT INTO orders_fact_pq values(45678, 102, '2004-04-30 00:00:00') +POSTHOOK: type: QUERY +POSTHOOK: Input: _dummy_database@_dummy_table +POSTHOOK: Output: default@orders_fact_pq +POSTHOOK: Lineage: orders_fact_pq.dt SCRIPT [] +POSTHOOK: Lineage: orders_fact_pq.key2 SCRIPT [] +POSTHOOK: Lineage: orders_fact_pq.nokey SCRIPT [] +PREHOOK: query: INSERT INTO orders_fact_pq values(56789, 109, '2005-05-30 00:00:00') +PREHOOK: type: QUERY +PREHOOK: Input: _dummy_database@_dummy_table +PREHOOK: Output: default@orders_fact_pq +POSTHOOK: query: INSERT INTO orders_fact_pq values(56789, 109, '2005-05-30 00:00:00') +POSTHOOK: type: QUERY +POSTHOOK: Input: _dummy_database@_dummy_table +POSTHOOK: Output: default@orders_fact_pq +POSTHOOK: Lineage: orders_fact_pq.dt SCRIPT [] +POSTHOOK: Lineage: orders_fact_pq.key2 SCRIPT [] +POSTHOOK: Lineage: orders_fact_pq.nokey SCRIPT [] +PREHOOK: query: INSERT INTO orders_fact_pq values(67891, 110, '2006-06-30 00:00:00') +PREHOOK: type: QUERY +PREHOOK: Input: _dummy_database@_dummy_table +PREHOOK: Output: default@orders_fact_pq +POSTHOOK: query: INSERT INTO orders_fact_pq values(67891, 110, '2006-06-30 00:00:00') +POSTHOOK: type: QUERY +POSTHOOK: Input: _dummy_database@_dummy_table +POSTHOOK: Output: default@orders_fact_pq +POSTHOOK: Lineage: orders_fact_pq.dt SCRIPT [] +POSTHOOK: Lineage: orders_fact_pq.key2 SCRIPT [] +POSTHOOK: Lineage: orders_fact_pq.nokey SCRIPT [] +PREHOOK: query: select key1, key2, name, dt from orders_fact_pq join item_dim_pq on (orders_fact_pq.key2 = item_dim_pq.key1) +order by key2 +PREHOOK: type: QUERY +PREHOOK: Input: default@item_dim_pq +PREHOOK: Input: default@orders_fact_pq +#### A masked pattern was here #### +POSTHOOK: query: select key1, key2, name, dt from orders_fact_pq join item_dim_pq on (orders_fact_pq.key2 = item_dim_pq.key1) +order by key2 +POSTHOOK: type: QUERY +POSTHOOK: Input: default@item_dim_pq +POSTHOOK: Input: default@orders_fact_pq +#### A masked pattern was here #### +101 101 Item 101 2001-01-30 00:00:00 +102 102 Item 102 2004-04-30 00:00:00 +PREHOOK: query: EXPLAIN VECTORIZATION DETAIL +select key1, key2, name, dt from orders_fact_pq join item_dim_pq on (orders_fact_pq.key2 = item_dim_pq.key1) +PREHOOK: type: QUERY +PREHOOK: Input: default@item_dim_pq +PREHOOK: Input: default@orders_fact_pq +#### A masked pattern was here #### +POSTHOOK: query: EXPLAIN VECTORIZATION DETAIL +select key1, key2, name, dt from orders_fact_pq join item_dim_pq on (orders_fact_pq.key2 = item_dim_pq.key1) +POSTHOOK: type: QUERY +POSTHOOK: Input: default@item_dim_pq +POSTHOOK: Input: default@orders_fact_pq +#### A masked pattern was here #### +PLAN VECTORIZATION: + enabled: true + enabledConditionsMet: [hive.vectorized.execution.enabled IS true] + +STAGE DEPENDENCIES: + Stage-1 is a root stage + Stage-0 depends on stages: Stage-1 + +STAGE PLANS: + Stage: Stage-1 + Tez +#### A masked pattern was here #### + Edges: + Map 1 <- Map 2 (BROADCAST_EDGE) +#### A masked pattern was here #### + Vertices: + Map 1 + Map Operator Tree: + TableScan + alias: orders_fact_pq + filterExpr: key2 is not null (type: boolean) + probeDecodeDetails: cacheKey:HASH_MAP_MAPJOIN_25_container, bigKeyColName:key2, smallTablePos:1, keyRatio:1.0 + Statistics: Num rows: 6 Data size: 264 Basic stats: COMPLETE Column stats: NONE + TableScan Vectorization: + native: true + vectorizationSchemaColumns: [0:nokey:int, 1:key2:int, 2:dt:timestamp, 3:ROW__ID:struct, 4:ROW__IS__DELETED:boolean] + Filter Operator + Filter Vectorization: + className: VectorFilterOperator + native: true + predicateExpression: SelectColumnIsNotNull(col 1:int) + predicate: key2 is not null (type: boolean) + Statistics: Num rows: 6 Data size: 264 Basic stats: COMPLETE Column stats: NONE + Select Operator + expressions: key2 (type: int), dt (type: timestamp) + outputColumnNames: _col0, _col1 + Select Vectorization: + className: VectorSelectOperator + native: true + projectedOutputColumnNums: [1, 2] + Statistics: Num rows: 6 Data size: 264 Basic stats: COMPLETE Column stats: NONE + Map Join Operator + condition map: + Inner Join 0 to 1 + keys: + 0 _col0 (type: int) + 1 _col0 (type: int) + Map Join Vectorization: + bigTableKeyColumns: 1:int + bigTableRetainColumnNums: [1, 2] + bigTableValueColumns: 1:int, 2:timestamp + className: VectorMapJoinInnerLongOperator + native: true + nativeConditionsMet: hive.mapjoin.optimized.hashtable IS true, hive.vectorized.execution.mapjoin.native.enabled IS true, hive.execution.engine tez IN [tez] IS true, One MapJoin Condition IS true, No nullsafe IS true, Small table vectorizes IS true, Fast Hash Table and No Hybrid Hash Join IS true + nonOuterSmallTableKeyMapping: [] + projectedOutput: 1:int, 2:timestamp, 1:int, 5:string + smallTableValueMapping: 5:string + hashTableImplementationType: FAST + outputColumnNames: _col0, _col1, _col2, _col3 + input vertices: + 1 Map 2 + Statistics: Num rows: 6 Data size: 290 Basic stats: COMPLETE Column stats: NONE + Select Operator + expressions: _col2 (type: int), _col0 (type: int), _col3 (type: string), _col1 (type: timestamp) + outputColumnNames: _col0, _col1, _col2, _col3 + Select Vectorization: + className: VectorSelectOperator + native: true + projectedOutputColumnNums: [1, 1, 5, 2] + Statistics: Num rows: 6 Data size: 290 Basic stats: COMPLETE Column stats: NONE + File Output Operator + compressed: false + File Sink Vectorization: + className: VectorFileSinkOperator + native: false + Statistics: Num rows: 6 Data size: 290 Basic stats: COMPLETE Column stats: NONE + table: + input format: org.apache.hadoop.mapred.SequenceFileInputFormat + output format: org.apache.hadoop.hive.ql.io.HiveSequenceFileOutputFormat + serde: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe + Execution mode: vectorized, llap + LLAP IO: all inputs (cache only) + Map Vectorization: + enabled: true + enabledConditionsMet: hive.vectorized.use.vectorized.input.format IS true + inputFormatFeatureSupport: [DECIMAL_64] + featureSupportInUse: [DECIMAL_64] + inputFileFormats: org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat + allNative: false + usesVectorUDFAdaptor: false + vectorized: true + rowBatchContext: + dataColumnCount: 3 + includeColumns: [1, 2] + dataColumns: nokey:int, key2:int, dt:timestamp + partitionColumnCount: 0 + scratchColumnTypeNames: [string] + Map 2 + Map Operator Tree: + TableScan + alias: item_dim_pq + filterExpr: key1 is not null (type: boolean) + Statistics: Num rows: 2 Data size: 376 Basic stats: COMPLETE Column stats: NONE + TableScan Vectorization: + native: true + vectorizationSchemaColumns: [0:key1:int, 1:name:string, 2:ROW__ID:struct, 3:ROW__IS__DELETED:boolean] + Filter Operator + Filter Vectorization: + className: VectorFilterOperator + native: true + predicateExpression: SelectColumnIsNotNull(col 0:int) + predicate: key1 is not null (type: boolean) + Statistics: Num rows: 2 Data size: 376 Basic stats: COMPLETE Column stats: NONE + Select Operator + expressions: key1 (type: int), name (type: string) + outputColumnNames: _col0, _col1 + Select Vectorization: + className: VectorSelectOperator + native: true + projectedOutputColumnNums: [0, 1] + Statistics: Num rows: 2 Data size: 376 Basic stats: COMPLETE Column stats: NONE + Reduce Output Operator + key expressions: _col0 (type: int) + null sort order: z + sort order: + + Map-reduce partition columns: _col0 (type: int) + Reduce Sink Vectorization: + className: VectorReduceSinkLongOperator + keyColumns: 0:int + native: true + nativeConditionsMet: hive.vectorized.execution.reducesink.new.enabled IS true, hive.execution.engine tez IN [tez] IS true, No PTF TopN IS true, No DISTINCT columns IS true, BinarySortableSerDe for keys IS true, LazyBinarySerDe for values IS true + valueColumns: 1:string + Statistics: Num rows: 2 Data size: 376 Basic stats: COMPLETE Column stats: NONE + value expressions: _col1 (type: string) + Execution mode: vectorized, llap + LLAP IO: all inputs (cache only) + Map Vectorization: + enabled: true + enabledConditionsMet: hive.vectorized.use.vectorized.input.format IS true + inputFormatFeatureSupport: [DECIMAL_64] + featureSupportInUse: [DECIMAL_64] + inputFileFormats: org.apache.hadoop.hive.ql.io.parquet.MapredParquetInputFormat + allNative: true + usesVectorUDFAdaptor: false + vectorized: true + rowBatchContext: + dataColumnCount: 2 + includeColumns: [0, 1] + dataColumns: key1:int, name:string + partitionColumnCount: 0 + scratchColumnTypeNames: [] + + Stage: Stage-0 + Fetch Operator + limit: -1 + Processor Tree: + ListSink + +PREHOOK: query: select key1, key2, name, dt from orders_fact_pq join item_dim_pq on (orders_fact_pq.key2 = item_dim_pq.key1) +order by key2 +PREHOOK: type: QUERY +PREHOOK: Input: default@item_dim_pq +PREHOOK: Input: default@orders_fact_pq +#### A masked pattern was here #### +POSTHOOK: query: select key1, key2, name, dt from orders_fact_pq join item_dim_pq on (orders_fact_pq.key2 = item_dim_pq.key1) +order by key2 +POSTHOOK: type: QUERY +POSTHOOK: Input: default@item_dim_pq +POSTHOOK: Input: default@orders_fact_pq +#### A masked pattern was here #### +101 101 Item 101 2001-01-30 00:00:00 +102 102 Item 102 2004-04-30 00:00:00