Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,12 @@ public ShowResultSet constructTableResultSet(TableStatsMeta tableStatistic, Tabl
newColumnsSet.add(Pair.of(Util.getTempTableDisplayName(pair.first), pair.second));
}
row.add(newColumnsSet.toString());
row.add(tableStatistic.jobType.toString());
// Bootstrap table stats may only seed row count, so job type can be absent before the first analyze task.
row.add(tableStatistic.jobType == null ? "" : tableStatistic.jobType.toString());
row.add(String.valueOf(tableStatistic.partitionChanged.get()));
row.add(String.valueOf(tableStatistic.userInjected));
row.add(table == null ? "N/A" : String.valueOf(table.autoAnalyzeEnabled()));
row.add(lastAnalyzeTime.format(formatter));
row.add(tableStatistic.lastAnalyzeTime == 0 ? "" : lastAnalyzeTime.format(formatter));
result.add(row);
return new ShowResultSet(getMetaData(), result);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,17 @@ protected void onComplete() throws UserException {
StmtExecutor.syncLoadForTablets(backendsList, allTabletIds);
}
}
// Bootstrap table-level stats only after the target data is visible to keep row-count fallback aligned.
if (txnStatus == TransactionStatus.VISIBLE
&& ctx.getSessionVariable().isEnableInsertSelectTableStatsBootstrap()) {
try {
Env.getCurrentEnv().getAnalysisManager().bootstrapTableStatsIfAbsent(olapTable, loadedRows);
} catch (Exception e) {
// Bootstrap is best-effort; failure should not fail the insert statement because the data
// has already been committed and is visible.
LOG.warn("Failed to bootstrap table stats for {} after insert", olapTable.getName(), e);
}
}
}

private void setTxnCallbackId() {
Expand Down
18 changes: 18 additions & 0 deletions fe/fe-core/src/main/java/org/apache/doris/qe/SessionVariable.java
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ public String toString() {

public static final String ENABLE_SINGLE_REPLICA_INSERT = "enable_single_replica_insert";

public static final String ENABLE_INSERT_SELECT_TABLE_STATS_BOOTSTRAP =
"enable_insert_select_table_stats_bootstrap";

public static final String SHUFFLED_AGG_NODE_IDS = "shuffled_agg_node_ids";

public static final String ENABLE_FAST_ANALYZE_INSERT_INTO_VALUES = "enable_fast_analyze_into_values";
Expand Down Expand Up @@ -2084,6 +2087,13 @@ public boolean isEnableHboNonStrictMatchingMode() {
needForward = true, varType = VariableAnnotation.EXPERIMENTAL)
public boolean enableSingleReplicaInsert = false;

@VarAttrDef.VarAttr(name = ENABLE_INSERT_SELECT_TABLE_STATS_BOOTSTRAP,
needForward = true, varType = VariableAnnotation.EXPERIMENTAL, description = {
"是否为 CTAS 和 INSERT INTO SELECT 在写入可见后补建最小表级统计基线。",
"Whether to bootstrap minimal table-level stats after CTAS and INSERT INTO SELECT become visible."
})
private boolean enableInsertSelectTableStatsBootstrap = false;

@VarAttrDef.VarAttr(name = SHUFFLED_AGG_NODE_IDS,
needForward = true, varType = VariableAnnotation.EXPERIMENTAL)
public String shuffledAggNodeIds = "";
Expand Down Expand Up @@ -4887,6 +4897,14 @@ public boolean isInsertVisibleTimeoutReturnError() {
return getInsertVisibleTimeoutReturnModeEnum() == InsertVisibleTimeoutReturnMode.ERROR;
}

public boolean isEnableInsertSelectTableStatsBootstrap() {
return enableInsertSelectTableStatsBootstrap;
}

public void setEnableInsertSelectTableStatsBootstrap(boolean enableInsertSelectTableStatsBootstrap) {
this.enableInsertSelectTableStatsBootstrap = enableInsertSelectTableStatsBootstrap;
}

public void setInsertVisibleTimeoutReturnMode(String insertVisibleTimeoutReturnMode) {
this.insertVisibleTimeoutReturnMode = parseInsertVisibleTimeoutReturnMode(insertVisibleTimeoutReturnMode)
.getOption();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,29 @@ public void updateTableStatsForAlterStats(AnalysisInfo jobInfo, TableIf tbl) {
}
}

// Bootstrap table-level row count immediately after write so the optimizer can consume
// a usable base row count before column statistics are available.
public void bootstrapTableStatsIfAbsent(OlapTable table, long loadedRows) {
if (loadedRows <= 0) {
return;
}
if (findTableStatsStatus(table.getId()) != null) {
return;
}
TableStatsMeta newStats;
synchronized (idToTblStats) {
if (idToTblStats.containsKey(table.getId())) {
return;
}
long bootstrapRowCount = resolveBootstrapRowCount(table, loadedRows);
newStats = TableStatsMeta.newBootstrapStats(table, bootstrapRowCount, loadedRows);
idToTblStats.put(newStats.tblId, newStats);
}
// Write edit log outside the lock to avoid potential deadlocks with internal locks
// held by the edit log subsystem.
logCreateTableStats(newStats);
}

public List<AutoAnalysisPendingJob> showAutoPendingJobs(TableNameInfo tblName, String priority) {
List<AutoAnalysisPendingJob> result = Lists.newArrayList();
if (priority == null || priority.isEmpty()) {
Expand All @@ -616,6 +639,19 @@ public List<AutoAnalysisPendingJob> showAutoPendingJobs(TableNameInfo tblName, S
return result;
}

private long resolveBootstrapRowCount(OlapTable table, long loadedRows) {
long bootstrapRowCount = loadedRows;
long baseIndexRowCount = table.getRowCountForIndex(table.getBaseIndexId(), true);
if (baseIndexRowCount > 0) {
bootstrapRowCount = Math.max(bootstrapRowCount, baseIndexRowCount);
}
long tableRowCount = table.getRowCount();
if (tableRowCount > 0) {
bootstrapRowCount = Math.max(bootstrapRowCount, tableRowCount);
}
return bootstrapRowCount;
}

protected List<AutoAnalysisPendingJob> getPendingJobs(Map<TableNameInfo, Set<Pair<String, String>>> jobMap,
JobPriority priority, TableNameInfo tableNameInfo) {
List<AutoAnalysisPendingJob> result = Lists.newArrayList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ public TableStatsMeta() {
idxId = 0;
}

private TableStatsMeta(TableIf table) {
this.ctlId = table.getDatabase().getCatalog().getId();
this.ctlName = table.getDatabase().getCatalog().getName();
this.dbId = table.getDatabase().getId();
this.dbName = table.getDatabase().getFullName();
this.tblId = table.getId();
this.tblName = table.getName();
this.idxId = -1;
}

// It's necessary to store these fields separately from AnalysisInfo, since the lifecycle between AnalysisInfo
// and TableStats is quite different.
public TableStatsMeta(long rowCount, AnalysisInfo analyzedJob, TableIf table) {
Expand All @@ -130,6 +140,18 @@ public TableStatsMeta(long rowCount, AnalysisInfo analyzedJob, TableIf table) {
update(analyzedJob, table);
}

// Bootstrap metadata only seeds table-level row count so the optimizer can avoid the unknown-row fallback.
public static TableStatsMeta newBootstrapStats(OlapTable table, long rowCount, long updatedRows) {
TableStatsMeta tableStats = new TableStatsMeta(table);
tableStats.rowCount = rowCount;
tableStats.updatedRows.set(updatedRows);
tableStats.indexesRowCount.put(table.getBaseIndexId(), rowCount);
// Record the time when row count was bootstrapped so show table stats displays a reasonable update time,
// but leave lastAnalyzeTime as 0 since bootstrap is not an analyze operation.
tableStats.updatedTime = System.currentTimeMillis();
return tableStats;
}

@Override
public void write(DataOutput out) throws IOException {
String json = GsonUtils.GSON.toJson(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@
import org.apache.doris.catalog.Column;
import org.apache.doris.catalog.Database;
import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.OlapTable;
import org.apache.doris.catalog.TabletInvertedIndex;
import org.apache.doris.catalog.info.PartitionNamesInfo;
import org.apache.doris.catalog.info.TableNameInfo;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.datasource.CatalogIf;
import org.apache.doris.datasource.CatalogMgr;
import org.apache.doris.datasource.InternalCatalog;
import org.apache.doris.mysql.privilege.AccessControllerManager;
import org.apache.doris.mysql.privilege.PrivPredicate;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.QueryState;
import org.apache.doris.qe.ShowResultSet;
import org.apache.doris.statistics.TableStatsMeta;

import com.google.common.collect.ImmutableList;
import org.junit.jupiter.api.AfterEach;
Expand Down Expand Up @@ -166,4 +170,35 @@ void testValidateNoPrivilege() {
Assertions.assertThrows(AnalysisException.class, () -> command2.validate(connectContext),
"Permission denied command denied to user 'null'@'null' for table 'test_db: test_tbl2'");
}

@Test
void testConstructTableResultSetForBootstrapStats() throws Exception {
runBefore();
CatalogIf catalogIf = Mockito.mock(CatalogIf.class);
Database database = Mockito.mock(Database.class);
OlapTable table = Mockito.mock(OlapTable.class);
Mockito.when(catalogIf.getId()).thenReturn(1L);
Mockito.when(catalogIf.getName()).thenReturn(internalCtl);
Mockito.when(database.getCatalog()).thenReturn(catalogIf);
Mockito.when(database.getId()).thenReturn(2L);
Mockito.when(database.getFullName()).thenReturn(CatalogMocker.TEST_DB_NAME);
Mockito.when(table.getDatabase()).thenReturn(database);
Mockito.when(table.getId()).thenReturn(CatalogMocker.TEST_TBL_ID);
Mockito.when(table.getName()).thenReturn(CatalogMocker.TEST_TBL_NAME);
Mockito.when(table.getBaseIndexId()).thenReturn(CatalogMocker.TEST_TBL_ID);
Mockito.when(table.autoAnalyzeEnabled()).thenReturn(false);
// Bootstrap stats only seed row count, so show table stats should still render without a job type.
TableStatsMeta bootstrapStats = TableStatsMeta.newBootstrapStats(table, 128L, 128L);
ShowTableStatsCommand command = new ShowTableStatsCommand(CatalogMocker.TEST_TBL_ID);
ShowResultSet resultSet = command.constructTableResultSet(bootstrapStats, table);

Assertions.assertEquals(1, resultSet.getResultRows().size());
List<String> row = resultSet.getResultRows().get(0);
Assertions.assertEquals("128", row.get(0));
Assertions.assertEquals("128", row.get(2));
Assertions.assertEquals("", row.get(5));
Assertions.assertEquals("false", row.get(7));
// last_analyze_time (index 9) should be empty for bootstrap stats.
Assertions.assertEquals("", row.get(9));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import org.apache.doris.qe.QueryState.MysqlStateType;
import org.apache.doris.qe.SessionVariable;
import org.apache.doris.qe.StmtExecutor;
import org.apache.doris.statistics.AnalysisManager;
import org.apache.doris.statistics.TableStatsMeta;
import org.apache.doris.thrift.TQueryOptions;
import org.apache.doris.thrift.TStatusCode;
import org.apache.doris.thrift.TUniqueId;
Expand Down Expand Up @@ -182,6 +184,70 @@ void testOnFailAbortsUncommittedTransaction() throws Exception {
}
}

@Test
void testExecuteSingleInsertVisibleBootstrapsTableStatsWhenAbsent() throws Exception {
ConnectContext ctx = createExecutorContext();
ctx.getSessionVariable().setEnableInsertSelectTableStatsBootstrap(true);
Coordinator coordinator = createCoordinator();
GlobalTransactionMgrIface txnMgr = Mockito.mock(GlobalTransactionMgrIface.class);
TransactionState txnState = Mockito.mock(TransactionState.class);
LoadManager loadManager = Mockito.mock(LoadManager.class);
AnalysisManager analysisManager = Mockito.spy(new AnalysisManager());
Env currentEnv = createCurrentEnv(loadManager, analysisManager);
StmtExecutor stmtExecutor = createStmtExecutor();

Mockito.doNothing().when(analysisManager).logCreateTableStats(Mockito.any(TableStatsMeta.class));
try (MockedStatic<EnvFactory> envFactoryMock = Mockito.mockStatic(EnvFactory.class);
MockedStatic<Env> envMock = Mockito.mockStatic(Env.class)) {
prepareFactoryMocks(envFactoryMock, envMock, coordinator, txnMgr, txnState, currentEnv);
ctx.setEnv(currentEnv);

Mockito.when(txnMgr.commitAndPublishTransaction(
Mockito.any(), Mockito.anyList(), Mockito.anyLong(), Mockito.anyList(), Mockito.anyLong(),
Mockito.isNull())).thenReturn(true);

OlapInsertExecutor executor = createExecutor(ctx);
executor.txnId = 10004L;
executor.executeSingleInsert(stmtExecutor);

TableStatsMeta tableStats = analysisManager.findTableStatsStatus(2L);
Assertions.assertNotNull(tableStats);
Assertions.assertEquals(12L, tableStats.rowCount);
Assertions.assertEquals(12L, tableStats.updatedRows.get());
Assertions.assertEquals(12L, tableStats.getRowCount(101L));
Assertions.assertTrue(tableStats.isColumnsStatsEmpty());
}
}

@Test
void testExecuteSingleInsertVisibleDoesNotBootstrapTableStatsWhenDisabled() throws Exception {
ConnectContext ctx = createExecutorContext();
Coordinator coordinator = createCoordinator();
GlobalTransactionMgrIface txnMgr = Mockito.mock(GlobalTransactionMgrIface.class);
TransactionState txnState = Mockito.mock(TransactionState.class);
LoadManager loadManager = Mockito.mock(LoadManager.class);
AnalysisManager analysisManager = Mockito.spy(new AnalysisManager());
Env currentEnv = createCurrentEnv(loadManager, analysisManager);
StmtExecutor stmtExecutor = createStmtExecutor();

Mockito.doNothing().when(analysisManager).logCreateTableStats(Mockito.any(TableStatsMeta.class));
try (MockedStatic<EnvFactory> envFactoryMock = Mockito.mockStatic(EnvFactory.class);
MockedStatic<Env> envMock = Mockito.mockStatic(Env.class)) {
prepareFactoryMocks(envFactoryMock, envMock, coordinator, txnMgr, txnState, currentEnv);
ctx.setEnv(currentEnv);

Mockito.when(txnMgr.commitAndPublishTransaction(
Mockito.any(), Mockito.anyList(), Mockito.anyLong(), Mockito.anyList(), Mockito.anyLong(),
Mockito.isNull())).thenReturn(true);

OlapInsertExecutor executor = createExecutor(ctx);
executor.txnId = 10005L;
executor.executeSingleInsert(stmtExecutor);

Assertions.assertNull(analysisManager.findTableStatsStatus(2L));
}
}

// Build a fresh context per case so insertResult and QueryState do not leak between tests.
private ConnectContext createExecutorContext() {
ConnectContext ctx = new ConnectContext();
Expand Down Expand Up @@ -227,6 +293,10 @@ private StmtExecutor createStmtExecutor() {

// Provide the job-manager chain needed by master-side setTxnCallbackId().
private Env createCurrentEnv(LoadManager loadManager) {
return createCurrentEnv(loadManager, Mockito.mock(AnalysisManager.class));
}

private Env createCurrentEnv(LoadManager loadManager, AnalysisManager analysisManager) {
Env currentEnv = Mockito.mock(Env.class);
// Mock the internal catalog because ConnectContext.setEnv() resolves the default catalog on master.
InternalCatalog internalCatalog = Mockito.mock(InternalCatalog.class);
Expand All @@ -236,6 +306,7 @@ private Env createCurrentEnv(LoadManager loadManager) {
Mockito.when(internalCatalog.getName()).thenReturn("internal");
Mockito.when(currentEnv.getLoadManager()).thenReturn(loadManager);
Mockito.when(currentEnv.getJobManager()).thenReturn(jobManager);
Mockito.when(currentEnv.getAnalysisManager()).thenReturn(analysisManager);
Mockito.when(jobManager.getStreamingTaskManager()).thenReturn(streamingTaskManager);
Mockito.when(streamingTaskManager.getStreamingInsertTaskById(Mockito.anyLong())).thenReturn(null);
return currentEnv;
Expand All @@ -244,14 +315,21 @@ private Env createCurrentEnv(LoadManager loadManager) {
// Create an executor with mocked table metadata because this test only validates timeout result handling.
private OlapInsertExecutor createExecutor(ConnectContext ctx) {
Database database = Mockito.mock(Database.class);
InternalCatalog catalog = Mockito.mock(InternalCatalog.class);
Mockito.when(database.getFullName()).thenReturn("test_db");
Mockito.when(database.getId()).thenReturn(1L);
Mockito.when(database.getCatalog()).thenReturn(catalog);
Mockito.when(catalog.getId()).thenReturn(3L);
Mockito.when(catalog.getName()).thenReturn("internal");

// Mock OlapTable because the master-side executor now casts the target table to OlapTable.
OlapTable table = Mockito.mock(OlapTable.class);
Mockito.when(table.getDatabase()).thenReturn(database);
Mockito.when(table.getName()).thenReturn("test_tbl");
Mockito.when(table.getId()).thenReturn(2L);
Mockito.when(table.getBaseIndexId()).thenReturn(101L);
Mockito.when(table.getRowCountForIndex(101L, true)).thenReturn(-1L);
Mockito.when(table.getRowCount()).thenReturn(0L);

return new OlapInsertExecutor(ctx, table, "label_test", Mockito.mock(NereidsPlanner.class),
Optional.empty(), false, 0L);
Expand Down
Loading
Loading