背景
本文基于 Starrocks 3.3.5
本文主要來探索一下Starrocks在FE端怎么實現 短路徑,從而加速點查查詢速度。
在用戶層級需要設置 enable_short_circuit 為true
分析
數據流:
直接到StatementPlanner.createQueryPlan方法:
...
OptExpression root = ShortCircuitPlanner.checkSupportShortCircuitRead(logicalPlan.getRoot(), session);
...
optimizedPlan = optimizer.optimize(session,root,mvTransformerContext,stmt,new PhysicalPropertySet(),new ColumnRefSet(logicalPlan.getOutputColumn()),columnRefFactory);
首先是通過ShortCircuitPlanner.checkSupportShortCircuitRead
來判斷該SQL是不是支持短路徑查詢:
public static OptExpression checkSupportShortCircuitRead(OptExpression root, ConnectContext connectContext) {if (!connectContext.getSessionVariable().isEnableShortCircuit()) {root.setShortCircuit(false);return root;}boolean supportShortCircuit = root.getOp().accept(new LogicalPlanChecker(), root, null);if (supportShortCircuit && OperatorType.LOGICAL_LIMIT.equals(root.getOp().getOpType())) {root = root.getInputs().get(0);}root.setShortCircuit(supportShortCircuit);return root;}
- 通過
isEnableShortCircuit
也就是enable_short_circuit
(默認是false) 來判斷是否支持短路徑查詢 - 通過visitor
LogicalPlanChecker
來判斷SQL本身是否支持短路徑查詢
通過 LogicalPlanChecker 實現看到,目前只支持 Scan Project Filter Limit 操作:public static class LogicalPlanChecker extends BaseLogicalPlanChecker {...@Overridepublic Boolean visitLogicalFilter(OptExpression optExpression, Void context) {...return visitChild(optExpression, context);}@Overridepublic Boolean visitLogicalProject(OptExpression optExpression, Void context) {...return visitChild(optExpression, context);}@Overridepublic Boolean visitLogicalLimit(OptExpression optExpression, Void context) {...return visitChild(optExpression, context);}@Overridepublic Boolean visitLogicalTableScan(OptExpression optExpression, Void context) {return createLogicalPlanChecker(optExpression, allowFilter, allowLimit, allowProject,allowSort, predicate, orderByColumns, limit).visitLogicalTableScan(optExpression, context);}protected static boolean isPointScan(Table table,List<String> keyColumns,List<ScalarOperator> conjuncts,ShortCircuitContext shortCircuitContext) {Map<String, PartitionColumnFilter> filters = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);filters.putAll(ColumnFilterConverter.convertColumnFilter(conjuncts, table));if (keyColumns == null || keyColumns.isEmpty()) {return false;}long cardinality = 1;for (String keyColumn : keyColumns) {if (filters.containsKey(keyColumn)) {PartitionColumnFilter filter = filters.get(keyColumn);if (filter.getInPredicateLiterals() != null) {cardinality *= filter.getInPredicateLiterals().size();// TODO(limit operator place fe)if (cardinality > MAX_RETURN_ROWS ||(shortCircuitContext.getMaxReturnRows() != 0 && cardinality != 1)) {return false;}} else if (!filter.isPoint()) {return false;}} else {return false;}}return true;}}}
- 直接看visitLogicalTableScan這個方法
只有是存算一體的,也就是LogicalOlapScanOperator實例,才會有短路徑查詢,最終會走到ShortCircuitPlannerHybrid.LogicalPlanChecker.visitLogicalTableScan
方法public Boolean visitLogicalTableScan(OptExpression optExpression, Void context) {LogicalScanOperator scanOp = optExpression.getOp().cast();Table table = scanOp.getTable();if (!(table instanceof OlapTable) || !(KeysType.PRIMARY_KEYS.equals(((OlapTable) table).getKeysType()))) {return false;}for (Column column : table.getFullSchema()) {if (IDictManager.getInstance().hasGlobalDict(table.getId(), column.getColumnId())) {return false;}}List<String> keyColumns = ((OlapTable) table).getKeyColumns().stream().map(Column::getName).collect(Collectors.toList());List<ScalarOperator> conjuncts = Utils.extractConjuncts(predicate);return isPointScan(table, keyColumns, conjuncts, shortCircuitContext);}
- 首先必須滿足 是主鍵模型
- 再次是 必須滿足SQL 查詢的表和字段沒有全局字典
- 最后 判斷是不是點查
滿足:1. 過濾條件要么是IN,要么是=
2. 如果是IN的話,IN中的項不能超過2024個
3. 必須包含所有的主鍵(可以額外包含其他的非主鍵)
- 直接看visitLogicalTableScan這個方法
- 如果確定可以走短路徑的話,則設置
root.setShortCircuit(true)
,否則為false
再次進行計劃級別的優化 optimizer.optimize
:
這里會調用optimizeByCost
方法,到調用 rewriteAndValidatePlan
方法:
private OptExpression rewriteAndValidatePlan(OptExpression tree,TaskContext rootTaskContext) {OptExpression result = logicalRuleRewrite(tree, rootTaskContext);OptExpressionValidator validator = new OptExpressionValidator();validator.validate(result);// skip memoif (result.getShortCircuit()) {result = new OlapScanImplementationRule().transform(result, null).get(0);result.setShortCircuit(true);}return result;}
ShortCircuit 短路徑涉及到的有兩方面:
- logicalRuleRewrite中 ruleRewriteForShortCircuit
private Optional<OptExpression> ruleRewriteForShortCircuit(OptExpression tree, TaskContext rootTaskContext) {Boolean isShortCircuit = tree.getShortCircuit();if (isShortCircuit) {deriveLogicalProperty(tree);ruleRewriteIterative(tree, rootTaskContext, RuleSetType.SHORT_CIRCUIT_SET);ruleRewriteOnlyOnce(tree, rootTaskContext, new MergeProjectWithChildRule());OptExpression result = tree.getInputs().get(0);result.setShortCircuit(true);return Optional.of(result);}return Optional.empty();}
這里會專門針對于shortCircuit做一些規則優化:
new PruneTrueFilterRule(),
new PushDownPredicateProjectRule(),
PushDownPredicateScanRule.OLAP_SCAN,
new CastToEmptyRule(),
new PruneProjectColumnsRule(),
PruneScanColumnRule.OLAP_SCAN,
new PruneProjectEmptyRule(),
new MergeTwoProjectRule(),
new PruneProjectRule(),
new PartitionPruneRule(),
new DistributionPruneRule();
new MergeProjectWithChildRule()
以上規則只是在project以及 常量優化,以及更好的過濾數據的層級進行了優化,免去了一般性的規則過濾. 正如primary_key_table所說,由于primary key模型使得謂詞下推成為了可能。
- OlapScanImplementationRule().transform
這個也是在該SQL能夠進行短路徑的情況下,才會走到的數據流
這一步的作用主要是把邏輯的scan轉換為物理的scan
經過了以上兩步以后,就直接返回了,也不會進入到memo的CBO
優化。
至此 FE端 短路徑的 優化就結束了,接下來就是生成物理計劃了。