This is an automated email from the ASF dual-hosted git repository.

deniskuzZ pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git


The following commit(s) were added to refs/heads/master by this push:
     new c82b62b11bf HIVE-30050: Sort-merge join should size a storage-handler 
table from its statistics, not by listing its location (#6782)
c82b62b11bf is described below

commit c82b62b11bfc67bacbac65f193e7c9d276189566
Author: Denys Kuzmenko <[email protected]>
AuthorDate: Sat Oct 3 18:14:26 2026 +0200

    HIVE-30050: Sort-merge join should size a storage-handler table from its 
statistics, not by listing its location (#6782)
---
 ...rtitionSizeBasedBigTableSelectorForAutoSMJ.java |   6 +-
 .../hive/ql/optimizer/ConvertJoinMapJoin.java      |  62 ++++++------
 .../SizeBasedBigTableSelectorForAutoSMJ.java       |  49 ++++++++++
 .../TableSizeBasedBigTableSelectorForAutoSMJ.java  |   5 +-
 .../TestSizeBasedBigTableSelectorForAutoSMJ.java   | 108 +++++++++++++++++++++
 5 files changed, 190 insertions(+), 40 deletions(-)

diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/AvgPartitionSizeBasedBigTableSelectorForAutoSMJ.java
 
b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/AvgPartitionSizeBasedBigTableSelectorForAutoSMJ.java
index f157c6df145..9f8fe321572 100644
--- 
a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/AvgPartitionSizeBasedBigTableSelectorForAutoSMJ.java
+++ 
b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/AvgPartitionSizeBasedBigTableSelectorForAutoSMJ.java
@@ -29,7 +29,6 @@
 import org.apache.hadoop.hive.ql.exec.JoinOperator;
 import org.apache.hadoop.hive.ql.exec.TableScanOperator;
 import org.apache.hadoop.hive.ql.metadata.HiveException;
-import org.apache.hadoop.hive.ql.metadata.Partition;
 import org.apache.hadoop.hive.ql.metadata.Table;
 import org.apache.hadoop.hive.ql.optimizer.ppr.PartitionPruner;
 import org.apache.hadoop.hive.ql.parse.ParseContext;
@@ -82,10 +81,7 @@ public int getBigTablePosition(ParseContext parseCtx, 
JoinOperator joinOp,
           // For partitioned tables, get the size of all the partitions
           PrunedPartitionList partsList = PartitionPruner.prune(topOp, 
parseCtx, null);
           numPartitions = partsList.getNotDeniedPartns().size();
-          long totalSize = 0;
-          for (Partition part : partsList.getNotDeniedPartns()) {
-            totalSize += getSize(conf, part);
-          }
+          long totalSize = getSize(conf, table, 
partsList.getNotDeniedPartns());
           averageSize = numPartitions == 0 ? 0 : totalSize/numPartitions;
         }
 
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/ConvertJoinMapJoin.java 
b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/ConvertJoinMapJoin.java
index 5ac61d2ec22..f6a6ff49a2e 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/ConvertJoinMapJoin.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/ConvertJoinMapJoin.java
@@ -476,6 +476,24 @@ private Object 
checkAndConvertSMBJoin(OptimizeTezProcContext context, JoinOperat
       fallbackToReduceSideJoin(joinOp, context);
       return null;
     }
+    JoinDesc joinDesc = joinOp.getConf();
+    JoinCondDesc[] joinCondns = joinDesc.getConds();
+
+    Set<Integer> joinCandidates = 
MapJoinProcessor.getBigTableCandidates(joinCondns);
+    if (joinCandidates.isEmpty()) {
+      // This is a full outer join. This can never be a map-join
+      // of any type. So return false.
+      return false;
+    }
+    // establish the join's structural feasibility before electing a big table:
+    // sizing the candidates can list their tables' locations on remote storage
+    if (!checkJoinSMBFeasibility(joinOp, context)) {
+      // we are just converting to a common merge join operator. The shuffle
+      // join in map-reduce case.
+      fallbackToReduceSideJoin(joinOp, context);
+      return null;
+    }
+
     Class<? extends BigTableSelectorForAutoSMJ> bigTableMatcherClass = null;
     try {
       String selector = HiveConf.getVar(context.parseContext.getConf(),
@@ -488,14 +506,6 @@ private Object 
checkAndConvertSMBJoin(OptimizeTezProcContext context, JoinOperat
 
     BigTableSelectorForAutoSMJ bigTableMatcher =
         ReflectionUtils.newInstance(bigTableMatcherClass, null);
-    JoinDesc joinDesc = joinOp.getConf();
-    JoinCondDesc[] joinCondns = joinDesc.getConds();
-    Set<Integer> joinCandidates = 
MapJoinProcessor.getBigTableCandidates(joinCondns);
-    if (joinCandidates.isEmpty()) {
-      // This is a full outer join. This can never be a map-join
-      // of any type. So return false.
-      return false;
-    }
     int mapJoinConversionPos =
         bigTableMatcher.getBigTablePosition(context.parseContext, joinOp, 
joinCandidates);
     if (mapJoinConversionPos < 0) {
@@ -506,14 +516,15 @@ private Object 
checkAndConvertSMBJoin(OptimizeTezProcContext context, JoinOperat
       return null;
     }
 
-    if (checkConvertJoinSMBJoin(joinOp, context, mapJoinConversionPos, 
tezBucketJoinProcCtx)) {
-      convertJoinSMBJoin(joinOp, context, mapJoinConversionPos,
-          tezBucketJoinProcCtx.getNumBuckets(), true);
-    } else {
-      // we are just converting to a common merge join operator. The shuffle
-      // join in map-reduce case.
-      fallbackToReduceSideJoin(joinOp, context);
+    ReduceSinkOperator bigTableRS =
+        (ReduceSinkOperator) 
joinOp.getParentOperators().get(mapJoinConversionPos);
+    int numBuckets = 
bigTableRS.getParentOperators().get(0).getOpTraits().getNumBuckets();
+
+    if (numBuckets < 0) {
+      numBuckets = bigTableRS.getConf().getNumReducers();
     }
+    tezBucketJoinProcCtx.setNumBuckets(numBuckets);
+    convertJoinSMBJoin(joinOp, context, mapJoinConversionPos, numBuckets, 
true);
     return null;
   }
 
@@ -803,18 +814,12 @@ private void preserveOperatorInfos(Operator<?> newOp, 
Operator<?> oldOp, Optimiz
   }
 
   /*
-   * This method tries to convert a join to an SMB. This is done based on
-   * traits. If the sorted by columns are the same as the join columns then, we
-   * can convert the join to an SMB. Otherwise retain the bucket map join as it
-   * is still more efficient than a regular join.
+   * Decides from traits alone whether a join can become an SMB: the sorted by
+   * columns have to be the same as the join columns on every side. The answer
+   * does not depend on which side is elected the big table, so it is safe to
+   * consult before the election.
    */
-  private boolean checkConvertJoinSMBJoin(JoinOperator joinOp, 
OptimizeTezProcContext context,
-      int bigTablePosition, TezBucketJoinProcCtx tezBucketJoinProcCtx) throws 
SemanticException {
-
-    ReduceSinkOperator bigTableRS =
-        (ReduceSinkOperator) joinOp.getParentOperators().get(bigTablePosition);
-    int numBuckets = 
bigTableRS.getParentOperators().get(0).getOpTraits().getNumBuckets();
-
+  private boolean checkJoinSMBFeasibility(JoinOperator joinOp, 
OptimizeTezProcContext context) {
     int size = -1;
     boolean shouldCheckExternalTables =
         
context.conf.getBoolVar(HiveConf.ConfVars.HIVE_DISABLE_UNSAFE_EXTERNALTABLE_OPERATIONS);
@@ -908,11 +913,6 @@ private boolean checkConvertJoinSMBJoin(JoinOperator 
joinOp, OptimizeTezProcCont
       }
     }
 
-    if (numBuckets < 0) {
-      numBuckets = bigTableRS.getConf().getNumReducers();
-    }
-    tezBucketJoinProcCtx.setNumBuckets(numBuckets);
-
     // With bucketing using two different versions. Version 1 for exiting
     // tables and version 2 for new tables. All the inputs to the SMB must be
     // from same version. This only applies to tables read directly and not
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/SizeBasedBigTableSelectorForAutoSMJ.java
 
b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/SizeBasedBigTableSelectorForAutoSMJ.java
index a0af28976ca..e1bb3e160d3 100644
--- 
a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/SizeBasedBigTableSelectorForAutoSMJ.java
+++ 
b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/SizeBasedBigTableSelectorForAutoSMJ.java
@@ -23,6 +23,9 @@
 
 import org.apache.hadoop.fs.FileSystem;
 import org.apache.hadoop.fs.Path;
+import java.util.Map;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.hadoop.hive.common.StatsSetupConst;
 import org.apache.hadoop.hive.conf.HiveConf;
 import org.apache.hadoop.hive.ql.exec.CommonJoinOperator;
 import org.apache.hadoop.hive.ql.exec.Operator;
@@ -73,6 +76,11 @@ private long getSize(HiveConf conf, String size, Path path) {
   }
 
   protected long getSize(HiveConf conf, Table table) {
+    // a storage handler serves the size from its own metadata; the table's 
parameters do not
+    // hold it, and listing the table's location can take minutes on object 
storage
+    if (table.isNonNative()) {
+      return handlerSize(table);
+    }
     Path path = table.getPath();
     String size = table.getProperty("totalSize");
     return getSize(conf, size, path);
@@ -84,4 +92,45 @@ protected long getSize(HiveConf conf, Partition partition) {
 
     return getSize(conf, size, path);
   }
+
+  /**
+   * Sums the sizes of the given partitions. For a storage-handler table the 
partition statistics
+   * are fetched from the handler in one batch; when the handler has no 
statistics for some
+   * partition the sum would fall short, so the table-level size is used 
instead.
+   */
+  protected long getSize(HiveConf conf, Table table, List<Partition> 
partitions) {
+    if (!table.isNonNative()) {
+      long total = 0;
+      for (Partition partition : partitions) {
+        total += getSize(conf, partition);
+      }
+      return total;
+    }
+    List<String> partNames = 
partitions.stream().map(Partition::getName).toList();
+    Map<String, Map<String, String>> stats =
+        table.getStorageHandler().getAggrBasicStatsFor(table, partNames);
+    long total = 0;
+    for (String partName : partNames) {
+      Map<String, String> partStats = stats.get(partName);
+      String size = partStats != null ? 
partStats.get(StatsSetupConst.TOTAL_SIZE) : null;
+      long partSize = NumberUtils.toLong(size, -1);
+      if (partSize < 0) {
+        // a partition without statistics would make the sum an underestimate, 
so fall back to
+        // the whole-table size: it may overestimate the scan but never 
underestimates it
+        return handlerSize(table);
+      }
+      total += partSize;
+    }
+    return total;
+  }
+
+  private static long handlerSize(Table table) {
+    if (!table.getStorageHandler().canProvideBasicStatistics()) {
+      return -1;
+    }
+    Map<String, String> stats = 
table.getStorageHandler().getBasicStatistics(table);
+    return NumberUtils.toLong(
+        stats != null ? stats.get(StatsSetupConst.TOTAL_SIZE) : null,
+        -1);
+  }
 }
diff --git 
a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/TableSizeBasedBigTableSelectorForAutoSMJ.java
 
b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/TableSizeBasedBigTableSelectorForAutoSMJ.java
index a0c84daa1e4..76a81e70447 100644
--- 
a/ql/src/java/org/apache/hadoop/hive/ql/optimizer/TableSizeBasedBigTableSelectorForAutoSMJ.java
+++ 
b/ql/src/java/org/apache/hadoop/hive/ql/optimizer/TableSizeBasedBigTableSelectorForAutoSMJ.java
@@ -27,7 +27,6 @@
 import org.apache.hadoop.hive.ql.exec.JoinOperator;
 import org.apache.hadoop.hive.ql.exec.TableScanOperator;
 import org.apache.hadoop.hive.ql.metadata.HiveException;
-import org.apache.hadoop.hive.ql.metadata.Partition;
 import org.apache.hadoop.hive.ql.metadata.Table;
 import org.apache.hadoop.hive.ql.optimizer.ppr.PartitionPruner;
 import org.apache.hadoop.hive.ql.parse.ParseContext;
@@ -70,9 +69,7 @@ public int getBigTablePosition(ParseContext parseCtx, 
JoinOperator joinOp,
         else {
           // For partitioned tables, get the size of all the partitions
           PrunedPartitionList partsList = PartitionPruner.prune(topOp, 
parseCtx, null);
-          for (Partition part : partsList.getNotDeniedPartns()) {
-            currentSize += getSize(conf, part);
-          }
+          currentSize = getSize(conf, table, partsList.getNotDeniedPartns());
         }
 
         if (currentSize > maxSize) {
diff --git 
a/ql/src/test/org/apache/hadoop/hive/ql/optimizer/TestSizeBasedBigTableSelectorForAutoSMJ.java
 
b/ql/src/test/org/apache/hadoop/hive/ql/optimizer/TestSizeBasedBigTableSelectorForAutoSMJ.java
new file mode 100644
index 00000000000..a3fcde0b90e
--- /dev/null
+++ 
b/ql/src/test/org/apache/hadoop/hive/ql/optimizer/TestSizeBasedBigTableSelectorForAutoSMJ.java
@@ -0,0 +1,108 @@
+/*
+ * 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.optimizer;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.hadoop.hive.common.StatsSetupConst;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.ql.metadata.HiveStorageHandler;
+import org.apache.hadoop.hive.ql.metadata.Partition;
+import org.apache.hadoop.hive.ql.metadata.Table;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+/**
+ * The big-table choice for an automatic sort-merge join must take a handler 
table's size from
+ * the handler's statistics, and must never list the table's location for one.
+ */
+public class TestSizeBasedBigTableSelectorForAutoSMJ {
+
+  private final SizeBasedBigTableSelectorForAutoSMJ selector = new 
TableSizeBasedBigTableSelectorForAutoSMJ();
+  private final HiveConf conf = new HiveConf();
+
+  private Table handlerTable(Map<String, String> basicStats) {
+    HiveStorageHandler handler = Mockito.mock(HiveStorageHandler.class);
+    Mockito.when(handler.canProvideBasicStatistics()).thenReturn(true);
+    
Mockito.when(handler.getBasicStatistics(Mockito.any())).thenReturn(basicStats);
+    Table table = Mockito.mock(Table.class);
+    Mockito.when(table.isNonNative()).thenReturn(true);
+    Mockito.when(table.getStorageHandler()).thenReturn(handler);
+    return table;
+  }
+
+  @Test
+  public void 
handlerTableSizeComesFromItsStatisticsWithoutTouchingTheFilesystem() {
+    Table table = handlerTable(Map.of(StatsSetupConst.TOTAL_SIZE, "12345"));
+
+    Assert.assertEquals(12345, selector.getSize(conf, table));
+    // the location is only asked for on the listing fallback, which a handler 
table never takes
+    Mockito.verify(table, Mockito.never()).getPath();
+  }
+
+  @Test
+  public void handlerTableOfUnknownSizeReportsUnknownRatherThanListing() {
+    Table table = handlerTable(Map.of());
+
+    Assert.assertEquals(-1, selector.getSize(conf, table));
+    Mockito.verify(table, Mockito.never()).getPath();
+  }
+
+  @Test
+  public void handlerPartitionsAreSizedTogetherAndSummed() {
+    // the table-level size covers the whole table, not one partition, so the 
partitions
+    // are looked up by name in one batch and their sizes summed
+    Table table = handlerTable(Map.of(StatsSetupConst.TOTAL_SIZE, "777"));
+    
Mockito.when(table.getStorageHandler().getAggrBasicStatsFor(Mockito.eq(table), 
Mockito.anyList()))
+        .thenReturn(Map.of(
+            "p=a", Map.of(StatsSetupConst.TOTAL_SIZE, "100"),
+            "p=b", Map.of(StatsSetupConst.TOTAL_SIZE, "20")));
+
+    Assert.assertEquals("the partitions read, not the table once per 
partition",
+        120, selector.getSize(conf, table, List.of(partition("p=a"), 
partition("p=b"))));
+  }
+
+  @Test
+  public void handlerPartitionOfUnknownSizeFallsBackToTheTable() {
+    // summing only the sized partition would underestimate the scan, so the 
whole-table
+    // size is used instead: it may overestimate but never underestimates
+    Table table = handlerTable(Map.of(StatsSetupConst.TOTAL_SIZE, "777"));
+    
Mockito.when(table.getStorageHandler().getAggrBasicStatsFor(Mockito.eq(table), 
Mockito.anyList()))
+        .thenReturn(Map.of("p=a", Map.of(StatsSetupConst.TOTAL_SIZE, "100")));
+
+    Assert.assertEquals(777, selector.getSize(conf, table, 
List.of(partition("p=a"), partition("p=b"))));
+  }
+
+  private static Partition partition(String name) {
+    Partition partition = Mockito.mock(Partition.class);
+    Mockito.when(partition.getName()).thenReturn(name);
+    return partition;
+  }
+
+  @Test
+  public void nativeTableKeepsReadingItsParameter() {
+    Table table = Mockito.mock(Table.class);
+    Mockito.when(table.isNonNative()).thenReturn(false);
+    Mockito.when(table.getProperty("totalSize")).thenReturn("4242");
+
+    Assert.assertEquals(4242, selector.getSize(conf, table));
+  }
+}

Reply via email to