Gabriel39 commented on code in PR #66999:
URL: https://github.com/apache/doris/pull/66999#discussion_r3841160194
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceScanNode.java:
##########
@@ -189,6 +206,36 @@ public List<Split> getSplits(int numBackends) throws
UserException {
return createFragmentSplits(metadata, visibleFragments);
}
+ // COUNT(*)/COUNT(1) with no filter is answered from Lance metadata: emit
whole-dataset carriers
+ // holding the logical (post-deletion) row count so BE synthesizes that
many rows instead of
+ // opening any fragment scanner. Each carrier is pinned to the planned
MVCC version (not latest)
+ // so a fallback scan reads the same snapshot the count came from. BE
materializes one row per
+ // counted row, so a large count is spread over parallelExecInstanceNum *
numBackends carriers to
+ // keep the former fragment parallelism; a small count stays on one
carrier.
+ private List<Split> buildCountSplits(LanceTableMetadata metadata, int
numBackends) {
+ long rowCount = metadata.getRowCount();
+ setPushDownCount(rowCount);
+ int carrierCount = 1;
+ if (rowCount >= COUNT_WITH_PARALLEL_SPLITS) {
+ int parallelism =
sessionVariable.getParallelExecInstanceNum(scanContext.getClusterName())
+ * Math.max(numBackends, 1);
+ carrierCount = Math.max(1, parallelism);
+ }
+ List<Split> splits = new ArrayList<>(carrierCount);
+ long assigned = 0;
+ for (int i = 0; i < carrierCount; i++) {
+ // Give every carrier an even share and fold the remainder into
the last one, so the
+ // per-carrier counts sum back to exactly rowCount.
+ long carriedRows = (rowCount - assigned) / (carrierCount - i);
+ assigned += carriedRows;
+ LanceSplit countSplit = LanceSplit.wholeDatasetCountAtVersion(
+ metadata.getDatasetUri(), metadata.getVersion(),
carriedRows);
Review Comment:
[P1] Keep sharded carriers fallback-disjoint
For `rowCount >= 10000`, this loop creates multiple splits with no fragment
IDs. A new BE consumes each `table_level_row_count` share, but the pre-change
BE explicitly resets that count to `-1`; Lance then interprets the missing
fragment IDs as an unrestricted whole-dataset scan. During a rolling upgrade,
every carrier assigned to an old BE therefore scans the entire snapshot,
producing up to `carrierCount * rowCount` rows (and a current BE can take the
same path whenever its metadata-count gate declines the shortcut). Please make
the fallback inputs disjoint—e.g. carriers whose fragment groups and logical
counts match—or keep a single carrier until compatibility guarantees the scalar
path, and cover the mixed-version/fallback case.
##########
regression-test/suites/external_table_p0/lance/test_lance_optimize_count.groovy:
##########
@@ -0,0 +1,160 @@
+// 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.
+
+suite("test_lance_optimize_count", "p0,external") {
+ /*
+ * COUNT(*)/COUNT(1) with no filter is served from Lance dataset metadata:
+ * FE emits a single split carrying the logical row count and EXPLAIN shows
+ * "pushdown agg=COUNT (<rows>)". Any of the following disables that path
and
+ * falls back to a normal scan ("pushdown agg=NONE"), which must still
return
+ * the same count:
+ * 1. enable_count_push_down_for_external_table = false;
+ * 2. a WHERE filter, because Lance cannot describe COUNT with a
predicate,
+ * so the plan keeps Aggregate(Filter(FileScan)) and never folds into
a
+ * storage-layer aggregate.
+ *
+ * all_types has exactly 12 rows in a single fragment with contiguous,
unique
+ * row_id in [1, 12], so every count below is deterministic.
+ *
+ * multi_frag is the multi-split counterpart: 30 physical rows in three
fragments
+ * with one deleted row per fragment (row_id 5, 15, 25), so its logical
count is 27.
+ * It proves the metadata count reports the logical 27 rather than the
physical 30,
+ * and that a normal multi-split scan applies every fragment's deletion
vector exactly
+ * once (no fragment double-counted or skipped).
+ */
+ String enabled = context.config.otherConfigs.get("enableIcebergTest")
+ if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+ logger.info("disable Lance count pushdown test because the Iceberg
MinIO environment is disabled.")
+ return
+ }
+
+ String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+ String minioPort = context.config.otherConfigs.get("iceberg_minio_port")
+ String catalogName = "test_lance_optimize_count"
+
+ sql """DROP CATALOG IF EXISTS `${catalogName}`"""
+ try {
+ sql """
+ CREATE CATALOG `${catalogName}` PROPERTIES (
+ "type" = "lance",
+ "lance.catalog.type" = "filesystem",
+ "warehouse" = "s3://warehouse/lance",
+ "s3.endpoint" = "http://${externalEnvIp}:${minioPort}",
+ "s3.access_key" = "admin",
+ "s3.secret_key" = "password",
+ "s3.region" = "us-east-1",
+ "use_path_style" = "true"
+ )
+ """
+
+ sql """ USE `${catalogName}`.`default`; """
+ // Lance is only served by FileScannerV2, which is where the metadata
count
+ // short-circuit lives.
+ sql """ SET enable_file_scanner_v2 = true; """
+
+ String countStar = """ SELECT count(*) FROM all_types """
+ String countOne = """ SELECT count(1) FROM all_types """
+ String countStarAllRows = """ SELECT count(*) FROM all_types WHERE
row_id > 0 """
+ String countStarHalf = """ SELECT count(*) FROM all_types WHERE row_id
> 6 """
+
+ // ---- Pushdown ON (the optimization) ----
+ sql """ SET enable_count_push_down_for_external_table = true; """
+
+ // No filter: COUNT(*) and COUNT(1) both fold into the metadata count.
+ explain {
+ sql(countStar)
+ contains "pushdown agg=COUNT (12)"
+ }
+ explain {
+ sql(countOne)
+ contains "pushdown agg=COUNT (12)"
+ }
+ qt_count_star_pushdown """${countStar}"""
+ qt_count_one_pushdown """${countOne}"""
+
+ // A filter keeps the aggregate above the scan, so no metadata count.
+ explain {
+ sql(countStarHalf)
+ contains "pushdown agg=NONE"
+ }
+ qt_count_star_all_rows """${countStarAllRows}"""
+ qt_count_star_half """${countStarHalf}"""
+
+ // ---- Pushdown OFF (the baseline before the optimization) ----
+ sql """ SET enable_count_push_down_for_external_table = false; """
+
+ explain {
+ sql(countStar)
+ contains "pushdown agg=NONE"
+ }
+ // Same result whether or not the metadata count is used.
+ qt_count_star_no_pushdown """${countStar}"""
+
+ // ---- Multi-fragment table with deletions (three splits, logical
count 27) ----
+ String mfCountStar = """ SELECT count(*) FROM multi_frag """
+ String mfCountOne = """ SELECT count(1) FROM multi_frag """
+ // Filter keeps > 12 rows so it cannot be confused with all_types'
count.
+ String mfCountHalf = """ SELECT count(*) FROM multi_frag WHERE row_id
> 15 """
+ // A whole-table filter must still equal the metadata count: it
exercises the
+ // multi-split scan path (deletion vectors applied per fragment)
instead of the
+ // metadata short-circuit, and the two must agree.
+ String mfCountAll = """ SELECT count(*) FROM multi_frag WHERE row_id >
0 """
+
+ sql """ SET enable_count_push_down_for_external_table = true; """
+
+ // No filter: the single metadata split reports the logical 27, not
the physical 30.
+ explain {
+ sql(mfCountStar)
+ contains "pushdown agg=COUNT (27)"
+ }
+ explain {
+ sql(mfCountOne)
+ contains "pushdown agg=COUNT (27)"
+ }
+ qt_mf_count_star_pushdown """${mfCountStar}"""
+ qt_mf_count_one_pushdown """${mfCountOne}"""
+
+ // The metadata count carrier must pin the planned dataset version,
not latest (version 0):
+ // a fallback scan (an old BE, or a BE that declines the shortcut) has
to read the same
+ // snapshot the count came from. multi_frag is built with three
appends and three deletes,
+ // so its planned version is 4. A regression to wholeDatasetAtLatest()
would print
+ // "lanceVersion=0" here and let time-travel / concurrent-commit reads
drift.
+ explain {
+ sql(mfCountStar)
+ contains "lanceVersion=4"
Review Comment:
[P2] Assert the serialized split version, not the planner field
`getNodeExplainString()` prints `plannedVersion`, which is assigned directly
from `metadata.getVersion()` before `buildCountSplits()` runs. Consequently
this assertion still reports `lanceVersion=4` if the carrier factory regresses
to version `0`; it does not test the `TLanceFileDesc.version` consumed by the
fallback BE. Please inspect the generated range descriptor in an FE test or
execute a fixed-version query through a forced fallback path so this regression
is actually covered.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]