You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug] Sampled auto analyze still runs an unsampled MIN/MAX full scan per column #68895
I had searched in the issues and found no similar issues.
Version
4.1.4; master is the same.
What's Wrong?
Automatic statistics collection with sampling still runs a full, unsampled scan per column. OlapAnalysisTask.doSample() calls collectMinMax(), which runs SELECT MIN(col), MAX(col) FROM db.tbl (BASIC_STATS_TEMPLATE) with no TABLESAMPLE. On a large table with many columns, every auto-analyze cycle issues one full-table scan per column.
On a cloud-mode table with tens of billions of rows and about 80 columns, under heavy ingestion, these scans:
read mostly uncached data, so they add a large burst of S3 GETs on top of ingestion. That pushed the storage prefixes into 503 SlowDown and curl timeouts, which also stalled concurrent loads;
In sample mode, min/max should come from the sample (or from segment zone maps or rowset statistics), not from a full scan per column. At least, the full min/max scan should be optional for huge tables.
How to Reproduce?
Create a large table (billions of rows, many columns) in cloud mode, keep auto analyze enabled, and watch the FE audit log for SELECT MIN(...), MAX(...) statements issued by statistics collection, one per column per cycle.
Anything Else?
Related: a failed auto-analyze job doesn't record an analyze time, so the scans repeat every cycle: #68896.
Source review confirms that sampled collection performs a separate, table-wide MIN/MAX query per selected column/index task. This merits statistics-collection scalability investigation. However, an unsampled SQL statement does not by itself establish a full scan of column data: eligible Doris MIN/MAX queries already use segment zone maps. The reported S3 throttling and ingestion impact still need execution evidence.
Verified against the public 4.1.4 tag (commit ad35a140c7fd0b842f18c23300bac581f7d04326) and master commit dd456ab7a894f54c6553415223c6d5c9c7d4e035:
Sampling does not constrain the preliminary MIN/MAX query.doSample() calls collectMinMax() before choosing sample tablets. BASIC_STATS_TEMPLATE contains neither a tablet/sample restriction nor a limit; only the subsequent statistics query receives TABLET(...) and, where applicable, LIMIT. The preliminary bounds are injected into that subsequent query. An exception from MIN/MAX is rethrown, preventing the subsequent sample query for that task. This applies to columns/indexes selected for collection, rather than necessarily every table column in every scheduler cycle. See 4.1.4 OlapAnalysisTask and sample parameter construction.
Zone-map collection already exists, with eligibility/fallback conditions. The internal statistics context enables MIN/MAX pushdown for unique tables and string columns. Nereids can produce a storage-layer MIN/MAX aggregate; in 4.1.4, BE uses VStatisticsIterator when pushdown is enabled and there are no delete-condition predicates. That iterator obtains two bound values per segment through next_batch_of_zone_map(), instead of reading all column values. Otherwise, BE can use SegmentIterator. See internal context, planner rules, iterator selection, and zone-map iterator. Even the metadata path traverses the table's segments and can incur remote metadata reads on misses; the actual request volume depends on segment count and cache residency.
Master retains the separate unsampled query, but its internal context now sets forcePushDownZonemapMinMax=true, with corresponding BE handling for truncated bounds and delete predicates. Thus the SQL shape is the same, while fallback behavior is not identical. See master context and master BE handling. This does not remove table-wide traversal.
Cache and timeout settings require the internal-query context. The statistics context disables page cache and sets cloud disableFileCache=true. BE maps that flag to disposable I/O/cache classification; already downloaded file-cache blocks can still be reused. It is not sufficient evidence of complete cache bypass. See BE propagation, cache classification, and cached-block handling. Query timeout is explicitly taken from global analyze_timeout; the 4.1.4 source default is 43,200 seconds. A 300-second failure therefore needs the effective setting and exact error to identify the timeout source. See timeout setting.
[Bug] Failed auto analyze jobs don't record an analyze time, so they restart every cycle #68896 can amplify this cost. Failed terminal jobs skip updateTableStats(), whereas successful jobs update column statistics metadata and lastAnalyzeTime. This supports repeated collection eligibility after failure, but does not establish the observed cadence. Also, in the inspected 4.1.4 production Java source, getHugeTableAutoAnalyzeIntervalInMillis() has no callers; the active collector filters use column eligibility/change checks. The deployed scheduling behavior should be verified rather than relying on that variable as a cooldown. See terminal job handling and collector eligibility.
To establish the expensive execution path and its effect, please provide:
Exact FE/BE build revisions, anonymized SHOW CREATE TABLE (key model, merge-on-write setting, column types, partitions and indexes), and tablet/rowset/segment counts.
One affected MIN/MAX query ID, complete SQL, audit entry, full timeout/error message, and matching FE/BE log excerpts. Include effective global analyze_timeout, sample settings, collection window, and analyze-task concurrency.
The actual internal-query plan and full profile, including pushed aggregation, rows/data pages read, segment counts, remote/local read bytes and requests, and segment-metadata load time. A manual EXPLAIN must reproduce the internal pushdown settings; ordinary session defaults differ. enable_profile_when_analyze is the existing FE switch for collecting analyze profiles.
A timestamp-aligned sample of S3 503/curl errors, request-rate metrics and load latency, plus analyze job/task history across retries. Please redact credentials and sensitive object paths. A controlled off-peak comparison with collection paused for this table would help establish attribution.
For immediate containment, the existing collection window can move future work off peak. Alternatively, temporarily exclude only this table with ALTER TABLE db.tbl SET ("auto_analyze_policy" = "disable"), restoring its previous policy afterward; this trades statistics freshness for reduced background work and does not cancel already running tasks. These gates are checked by canCollect()/column eligibility and the table policy. Reducing sample rows leaves the preliminary MIN/MAX query unchanged.
For maintainers, first distinguish missing pushdown/full data reads from expensive table-wide metadata traversal. If the latter dominates, evaluate batching bounds across columns or reusing metadata with correct version visibility, and an explicit option to omit table-wide bounds in sampled collection. Unknown bounds have an existing representation: ColumnStatistic deserialization maps absent bounds to infinities. Sample extrema should not silently become authoritative table-wide bounds, since rare extreme values can be missed and distort selectivity estimates. Any change needs coverage for outliers, nulls, long strings, deletes, key models/indexes and optimizer estimates. Handle retry backoff separately in #68896, preserving the distinction between a failed attempt and successfully refreshed statistics.
This is source-level triage; no runtime reproduction or supplied profile was available, and no code was changed.
I'd like to take this one too (see my note on #68896).
Proposal: in OlapAnalysisTask.doSample(), decide the sample algorithm before collecting min/max.
If the algorithm falls back to FULL, the analyze query already scans the whole table. So MIN/MAX can be computed inline in that query instead of in a separate collectMinMax() query. That gives the same result with one query fewer per column.
For a real sample (LINEAR/DUJ1), keep the exact collectMinMax() by default, and add a global session variable enable_sample_min_max (default false) that takes min/max from the sampled tablets instead. That trades range accuracy for far fewer reads on huge cloud tables.
Does that split sound right to you, or would you rather source min/max differently (e.g. from rowset/segment metadata)?
Activity
Doris-Breakwater commentedon Oct 10, 2026
Source review confirms that sampled collection performs a separate, table-wide MIN/MAX query per selected column/index task. This merits statistics-collection scalability investigation. However, an unsampled SQL statement does not by itself establish a full scan of column data: eligible Doris MIN/MAX queries already use segment zone maps. The reported S3 throttling and ingestion impact still need execution evidence.
Verified against the public
4.1.4tag (commitad35a140c7fd0b842f18c23300bac581f7d04326) and master commitdd456ab7a894f54c6553415223c6d5c9c7d4e035:Sampling does not constrain the preliminary MIN/MAX query.
doSample()callscollectMinMax()before choosing sample tablets.BASIC_STATS_TEMPLATEcontains neither a tablet/sample restriction nor a limit; only the subsequent statistics query receivesTABLET(...)and, where applicable,LIMIT. The preliminary bounds are injected into that subsequent query. An exception from MIN/MAX is rethrown, preventing the subsequent sample query for that task. This applies to columns/indexes selected for collection, rather than necessarily every table column in every scheduler cycle. See 4.1.4 OlapAnalysisTask and sample parameter construction.Zone-map collection already exists, with eligibility/fallback conditions. The internal statistics context enables MIN/MAX pushdown for unique tables and string columns. Nereids can produce a storage-layer MIN/MAX aggregate; in 4.1.4, BE uses
VStatisticsIteratorwhen pushdown is enabled and there are no delete-condition predicates. That iterator obtains two bound values per segment throughnext_batch_of_zone_map(), instead of reading all column values. Otherwise, BE can useSegmentIterator. See internal context, planner rules, iterator selection, and zone-map iterator. Even the metadata path traverses the table's segments and can incur remote metadata reads on misses; the actual request volume depends on segment count and cache residency.Master retains the separate unsampled query, but its internal context now sets
forcePushDownZonemapMinMax=true, with corresponding BE handling for truncated bounds and delete predicates. Thus the SQL shape is the same, while fallback behavior is not identical. See master context and master BE handling. This does not remove table-wide traversal.Cache and timeout settings require the internal-query context. The statistics context disables page cache and sets cloud
disableFileCache=true. BE maps that flag to disposable I/O/cache classification; already downloaded file-cache blocks can still be reused. It is not sufficient evidence of complete cache bypass. See BE propagation, cache classification, and cached-block handling. Query timeout is explicitly taken from globalanalyze_timeout; the 4.1.4 source default is 43,200 seconds. A 300-second failure therefore needs the effective setting and exact error to identify the timeout source. See timeout setting.[Bug] Failed auto analyze jobs don't record an analyze time, so they restart every cycle #68896 can amplify this cost. Failed terminal jobs skip
updateTableStats(), whereas successful jobs update column statistics metadata andlastAnalyzeTime. This supports repeated collection eligibility after failure, but does not establish the observed cadence. Also, in the inspected 4.1.4 production Java source,getHugeTableAutoAnalyzeIntervalInMillis()has no callers; the active collector filters use column eligibility/change checks. The deployed scheduling behavior should be verified rather than relying on that variable as a cooldown. See terminal job handling and collector eligibility.To establish the expensive execution path and its effect, please provide:
SHOW CREATE TABLE(key model, merge-on-write setting, column types, partitions and indexes), and tablet/rowset/segment counts.analyze_timeout, sample settings, collection window, and analyze-task concurrency.EXPLAINmust reproduce the internal pushdown settings; ordinary session defaults differ.enable_profile_when_analyzeis the existing FE switch for collecting analyze profiles.For immediate containment, the existing collection window can move future work off peak. Alternatively, temporarily exclude only this table with
ALTER TABLE db.tbl SET ("auto_analyze_policy" = "disable"), restoring its previous policy afterward; this trades statistics freshness for reduced background work and does not cancel already running tasks. These gates are checked by canCollect()/column eligibility and the table policy. Reducing sample rows leaves the preliminary MIN/MAX query unchanged.For maintainers, first distinguish missing pushdown/full data reads from expensive table-wide metadata traversal. If the latter dominates, evaluate batching bounds across columns or reusing metadata with correct version visibility, and an explicit option to omit table-wide bounds in sampled collection. Unknown bounds have an existing representation: ColumnStatistic deserialization maps absent bounds to infinities. Sample extrema should not silently become authoritative table-wide bounds, since rare extreme values can be missed and distort selectivity estimates. Any change needs coverage for outliers, nulls, long strings, deletes, key models/indexes and optimizer estimates. Handle retry backoff separately in #68896, preserving the distinction between a failed attempt and successfully refreshed statistics.
This is source-level triage; no runtime reproduction or supplied profile was available, and no code was changed.
Breakwater-GitHub-Analysis-Slot: slot_75984b2e62e9
Rishavbhattarai commentedon Oct 22, 2026
I'd like to take this one too (see my note on #68896).
Proposal: in
OlapAnalysisTask.doSample(), decide the sample algorithm before collecting min/max.collectMinMax()query. That gives the same result with one query fewer per column.collectMinMax()by default, and add a global session variableenable_sample_min_max(default false) that takes min/max from the sampled tablets instead. That trades range accuracy for far fewer reads on huge cloud tables.Does that split sound right to you, or would you rather source min/max differently (e.g. from rowset/segment metadata)?
@englefly @morrySnow