Apache Calciteによるクエリオプティマイザの構築



抄録
Apacheの方解石は、SQLパーサー、オプティマイザ、エグゼクティブ、およびJDBCドライバを持つ動的なデータ管理フレームワークです.
Apacheの方解石使用の多くの例では、JDBCドライバ、いくつかの組み込み最適化規則、およびEnumerable エグゼクター.我々の顧客は、しばしば彼ら自身の実行エンジンとJDBCドライバーを持っています.それで、それがJDBCドライバーとそれなしでEnumerable エグゼクター?
このチュートリアルでは、内部Apache Calciteクラスを使用して簡単なクエリオプティマイザを作成します.

スキーマ
まず、スキーマを定義する必要があります.カスタムテーブルの実装を開始します.テーブルを作成するには、ApacheAbstractTable . 2つの情報をテーブルに渡します.
  • 表の行型を構成するために使用するフィールド名と型(式の型指定に必要な).
  • オプションStatistic クエリプランナーに役立つ情報を提供するオブジェクト.
  • 統計クラスは、行カウント情報のみを公開します.
    public class SimpleTableStatistic implements Statistic {
    
        private final long rowCount;
    
        public SimpleTableStatistic(long rowCount) {
            this.rowCount = rowCount;
        }
    
        @Override
        public Double getRowCount() {
            return (double) rowCount;
        }
    
        // Other methods no-op
    }
    
    Apache Calciteが式のデータ型を取得するために使用する行型を構成するために、列名と型をテーブルクラスに渡します.
    public class SimpleTable extends AbstractTable {
    
        private final String tableName;
        private final List<String> fieldNames;
        private final List<SqlTypeName> fieldTypes;
        private final SimpleTableStatistic statistic;
    
        private RelDataType rowType;
    
        private SimpleTable(
            String tableName, 
            List<String> fieldNames, 
            List<SqlTypeName> fieldTypes, 
            SimpleTableStatistic statistic
        ) {
            this.tableName = tableName;
            this.fieldNames = fieldNames;
            this.fieldTypes = fieldTypes;
            this.statistic = statistic;
        }
    
        @Override
        public RelDataType getRowType(RelDataTypeFactory typeFactory) {
            if (rowType == null) {
                List<RelDataTypeField> fields = new ArrayList<>(fieldNames.size());
    
                for (int i = 0; i < fieldNames.size(); i++) {
                    RelDataType fieldType = typeFactory.createSqlType(fieldTypes.get(i));
                    RelDataTypeField field = new RelDataTypeFieldImpl(fieldNames.get(i), i, fieldType);
                    fields.add(field);
                }
    
                rowType = new RelRecordType(StructKind.PEEK_FIELDS, fields, false);
            }
    
            return rowType;
        }
    
        @Override
        public Statistic getStatistic() {
            return statistic;
        }
    }
    
    私たちのテーブルもApacheScannableTable インターフェイス.我々は、我々が特定を使用するので、デモ目的のためだけにこれを行うEnumerable このインターフェースなしで失敗する私たちの例の最適化規則.Apache Calciteを使用しない場合は、このインターフェイスを実装する必要はありませんEnumerable 実行バックエンド.
    public class SimpleTable extends AbstractTable implements ScannableTable {
        ...
        @Override
        public Enumerable<Object[]> scan(DataContext root) {
            throw new UnsupportedOperationException("Not implemented");
        }
        ...
    }
    
    最後に、我々はApacheAbstractSchema 独自のスキーマを定義するクラスです.テーブル名からテーブルへマップを渡します.Apache Calciteは、このマップを使用して、セマンティック検証中にテーブルを解決します.
    public class SimpleSchema extends AbstractSchema {
    
        private final String schemaName;
        private final Map<String, Table> tableMap;
    
        private SimpleSchema(String schemaName, Map<String, Table> tableMap) {
            this.schemaName = schemaName;
            this.tableMap = tableMap;
        }
    
        @Override
        public Map<String, Table> getTableMap() {
            return tableMap;
        }
    }
    
    我々は、最適化を開始する準備が整いました.

    オプティマイザ
    最適化プロセスは以下の段階からなる.
  • クエリ文字列から抽象構文木(AST)を生成する構文解析.
  • ASTの意味解析
  • ASTの関係木への変換
  • 関係木の最適化

  • 構成
    クエリ最適化のために使用するApache Calciteクラスの多くが設定を必要とします.しかし、Apache Calciteに共通の構成クラスはありません.このため、共通の設定を単一のオブジェクトに格納し、必要に応じて設定値を他のオブジェクトにコピーします.
    この特定の例では、オブジェクト識別子の処理方法についてApache Calciteに指示します.
    Properties configProperties = new Properties();
    
    configProperties.put(CalciteConnectionProperty.CASE_SENSITIVE.camelName(), Boolean.TRUE.toString());
    configProperties.put(CalciteConnectionProperty.UNQUOTED_CASING.camelName(), Casing.UNCHANGED.toString());
    configProperties.put(CalciteConnectionProperty.QUOTED_CASING.camelName(), Casing.UNCHANGED.toString());
    
    CalciteConnectionConfig config = new CalciteConnectionConfigImpl(configProperties);
    

    構文解析
    まず、クエリ文字列を解析します.構文解析の結果は抽象構文木で、SqlNode .
    共通の設定の一部をパーサの設定に渡し、インスタンス化するSqlParser , そして最後に解析を実行します.カスタムSQLの構文を使用する場合は、カスタムパーサーファクトリクラスを構成に渡すことができます.
    public SqlNode parse(String sql) throws Exception {
        SqlParser.ConfigBuilder parserConfig = SqlParser.configBuilder();
        parserConfig.setCaseSensitive(config.caseSensitive());
        parserConfig.setUnquotedCasing(config.unquotedCasing());
        parserConfig.setQuotedCasing(config.quotedCasing());
        parserConfig.setConformance(config.conformance());
    
        SqlParser parser = SqlParser.create(sql, parserConfig.build());
    
        return parser.parseStmt();
    }
    

    意味解析
    意味解析の目標は、生成された抽象構文木が有効であることを保証することです.意味解析はオブジェクトと関数識別子の解決、データ型推論、特定のSQLGROUP BY 声明
    検証はSqlValidatorImpl クラス、最もcomplex アパッチ方解石のクラス.このクラスはいくつかのサポートオブジェクトを必要とします.まず、インスタンスを作成しますRelDataTypeFactory , SQL Count型定義を提供します.ビルトインタイプのファクトリを使用しますが、必要に応じてカスタム実装を提供することもできます.
    RelDataTypeFactory typeFactory = new JavaTypeFactoryImpl();
    
    それから、我々はPrepare.CatalogReader データベースオブジェクトへのアクセスを提供するオブジェクトです.これは、我々の以前に定義されたスキーマが遊びに来るところです.カタログリーダーは、我々の共通の設定オブジェクトを使用して、オブジェクト名解決の仕組みを、構文解析中に使用したものと一致させます.
    SimpleSchema schema = ... // Create our custom schema
    
    CalciteSchema rootSchema = CalciteSchema.createRootSchema(false, false);
    rootSchema.add(schema.getSchemaName(), schema);
    
    Prepare.CatalogReader catalogReader = new CalciteCatalogReader(
        rootSchema,
        Collections.singletonList(schema.getSchemaName()),
        typeFactory,
        config
    );
    
    それから、我々はSqlOperatorTable , SQL関数と演算子のライブラリ.我々は、組み込みのライブラリを使用します.カスタム関数を使用して実装を提供することもできます.
    SqlOperatorTable operatorTable = ChainedSqlOperatorTable.of(
        SqlStdOperatorTable.instance()
    );
    
    必要なサポートオブジェクトをすべて作成しました.今、私たちはSqlValidatorImpl . 通常のように、カスタムエラーメッセージなどのカスタム検証動作が必要な場合は、拡張することができます.
    SqlValidator.Config validatorConfig = SqlValidator.Config.DEFAULT
        .withLenientOperatorLookup(config.lenientOperatorLookup())
        .withSqlConformance(config.conformance())
        .withDefaultNullCollation(config.defaultNullCollation())
        .withIdentifierExpansion(true);
    
    SqlValidator validator = SqlValidatorUtil.newValidator(
        operatorTable, 
        catalogReader, 
        typeFactory,
        validatorConfig
    );
    
    最後に検証を行います.リレーショナルツリーにAST変換を必要とするのでバリデータのインスタンスを保持します.
    SqlNode sqlNode = parse(sqlString);
    SqlNode validatedSqlNode = validator.validate(node);
    

    関係木への変換
    ASTは、そのノードの関係セマンティクスがあまりに複雑であるので、クエリ最適化のために便利ではありません.関係演算子のツリーでクエリの最適化を行うのは、より便利ですRelNode などのサブクラスScan , Project , Filter , Join , 利用等SqlToRelConverter , もう一つmonstrous class Apache Calciteでは、元のASTをリレーショナルツリーに変換します.
    興味深いことに、コンバータを作成するには、コストベースのプランナーのインスタンスを作成する必要がありますVolcanoPlanner ファースト.これはApacheの方解石の抽象的なリークの一つです.
    作成するVolcanoPlanner , また、共通の設定とRelOptCostFactory プランナーはコストを計算するために使用されます.生産グレードオプティマイザでは、カスタムの工場を定義する可能性がありますtake in count 適切なコスト推定のためにしばしば不十分である関係のcardinalityだけ.
    また、どの物理演算子プロパティを指定する必要がありますVolcanoPlanner を追跡する必要があります.すべてのプロパティには、ApacheRelTraitDef クラス.この例では、ConventionTraitDef , これはリレーショナルノードの実行バックエンドを定義します.
    VolcanoPlanner planner = new VolcanoPlanner(
        RelOptCostImpl.FACTORY, 
        Contexts.of(config)
    );
    
    planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
    
    それから、我々はRelOptCluster , 変換と最適化の間に使用される一般的なコンテキストオブジェクト.
    RelOptCluster cluster = RelOptCluster.create(
        planner, 
        new RexBuilder(typeFactory)
    );
    
    私たちは今、コンバータを作成することができます.ここでは、サブスクリプションunnestingの2つの設定プロパティを設定します.
    SqlToRelConverter.Config converterConfig = SqlToRelConverter.configBuilder()
        .withTrimUnusedFields(true)
        .withExpand(false) 
        .build();
    
    SqlToRelConverter converter = new SqlToRelConverter(
        null,
        validator,
        catalogReader,
        cluster,
        StandardConvertletTable.INSTANCE,
        converterConfig
    );
    
    一旦コンバータを持っていれば、関係木を作ることができます.
    public RelNode convert(SqlNode validatedSqlNode) {
        RelRoot root = converter.convertQuery(validatedSqlNode, false, true);
    
        return root.rel;
    }
    
    転換の間、アパッチ方解石は木の木を生産しますlogical 関係演算子は抽象的であり、特定の実行バックエンドを対象としません.このため、論理演算子は常にConvention.NONE . 最適化中に物理演算子に変換することが期待されます.物理演算子はConvention.NONE .

    最適化
    最適化は関係木を別の関係木に変換する過程である.ヒューリスティックまたはコストベースのプランナーでルールベースの最適化を行うことができます.HepPlanner and VolcanoPlanner それぞれ.あなたは、規則なしで木のどんな手動の書き替えもするかもしれません.Apacheの方解石は、いくつかの強力な書き換えツールRelDecorrelator and RelFieldTrimmer .
    通常、リレーショナルツリーを最適化するには、ルールベースの最適化ツールと手動書換を使用して複数の最適化パスを実行します.この動画を見るdefault optimization program アパッチCalcite JDBCmulti-phase query optimization アパッチflinkで.
    この例では、VolcanoPlanner コストベースの最適化を実行します.我々はすでにVolcanoPlanner 前に.最適化するための関係木,最適化規則の集合,最適化されたツリーの親ノードが満足しなければならない特性である.
    public RelNode optimize(
        RelOptPlanner planner,
        RelNode node, 
        RelTraitSet requiredTraitSet, 
        RuleSet rules
    ) {
        Program program = Programs.of(RuleSets.ofList(rules));
    
        return program.run(
            planner,
            node,
            requiredTraitSet,
            Collections.emptyList(),
            Collections.emptyList()
        );
    }
    


    この例では、TPC - Hクエリを最適化します№6 .フルソースコードが利用可能ですhere . を実行するOptimizerTest それを行動で見ること.
    SELECT
        SUM(l.l_extendedprice * l.l_discount) AS revenue
    FROM
        lineitem
    WHERE
        l.l_shipdate >= ?
        AND l.l_shipdate < ?
        AND l.l_discount between (? - 0.01) AND (? + 0.01)
        AND l.l_quantity < ?
    
    私たちはOptimizer 作成された構成をカプセル化するクラスSqlValidator , SqlToRelConverter and VolcanoPlanner .
    public class Optimizer {
        private final CalciteConnectionConfig config;
        private final SqlValidator validator;
        private final SqlToRelConverter converter;
        private final VolcanoPlanner planner;
    
        public Optimizer(SimpleSchema schema) {
            // Create supporting objects as explained above
            ... 
        }
    }
    
    次に、lineitem 表.
    SimpleTable lineitem = SimpleTable.newBuilder("lineitem")
        .addField("l_quantity", SqlTypeName.DECIMAL)
        .addField("l_extendedprice", SqlTypeName.DECIMAL)
        .addField("l_discount", SqlTypeName.DECIMAL)
        .addField("l_shipdate", SqlTypeName.DATE)
        .withRowCount(60_000L)
        .build();
    
    SimpleSchema schema = SimpleSchema.newBuilder("tpch").addTable(lineitem).build();
    
    Optimizer optimizer = Optimizer.create(schema);
    
    今、私たちは、私たちのオプティマイザを解析し、検証し、クエリを変換します.
    SqlNode sqlTree = optimizer.parse(sql);
    SqlNode validatedSqlTree = optimizer.validate(sqlTree);
    RelNode relTree = optimizer.convert(validatedSqlTree);
    
    生成された論理ツリーはこのようになります.
    LogicalAggregate(group=[{}], revenue=[SUM($0)]): rowcount = 1.0, cumulative cost = 63751.137500047684
      LogicalProject($f0=[*($1, $2)]): rowcount = 1875.0, cumulative cost = 63750.0
        LogicalFilter(condition=[AND(>=($3, ?0), <($3, ?1), >=($2, -(?2, 0.01)), <=($2, +(?3, 0.01)), <($0, ?4))]): rowcount = 1875.0, cumulative cost = 61875.0
          LogicalTableScan(table=[[tpch, lineitem]]): rowcount = 60000.0, cumulative cost = 60000.0
    
    最後に、関係ツリーを最適化し、Enumerable コンベンション.私たちは変換してマージする論理規則を使いますLogicalProject and LogicalFilter 化合するLogicalCalc , 論理ノードを変換する物理ルールEnumerable ノード.
    RuleSet rules = RuleSets.ofList(
        CoreRules.FILTER_TO_CALC,
        CoreRules.PROJECT_TO_CALC,
        CoreRules.FILTER_CALC_MERGE,
        CoreRules.PROJECT_CALC_MERGE,
        EnumerableRules.ENUMERABLE_TABLE_SCAN_RULE,
        EnumerableRules.ENUMERABLE_PROJECT_RULE,
        EnumerableRules.ENUMERABLE_FILTER_RULE,
        EnumerableRules.ENUMERABLE_CALC_RULE,
        EnumerableRules.ENUMERABLE_AGGREGATE_RULE
    );
    
    RelNode optimizerRelTree = optimizer.optimize(
        relTree,
        relTree.getTraitSet().plus(EnumerableConvention.INSTANCE),
        rules
    );
    
    生成された物理木はこのように見えます.すべてのノードがEnumerable , そしてProject and Filter ノードはCalc .
    EnumerableAggregate(group=[{}], revenue=[SUM($0)]): rowcount = 187.5, cumulative cost = 62088.2812589407
      EnumerableCalc(expr#0..3=[{inputs}], expr#4=[*($t1, $t2)], expr#5=[?0], expr#6=[>=($t3, $t5)], expr#7=[?1], expr#8=[<($t3, $t7)], expr#9=[?2], expr#10=[0.01:DECIMAL(3, 2)], expr#11=[-($t9, $t10)], expr#12=[>=($t2, $t11)], expr#13=[?3], expr#14=[+($t13, $t10)], expr#15=[<=($t2, $t14)], expr#16=[?4], expr#17=[<($t0, $t16)], expr#18=[AND($t6, $t8, $t12, $t15, $t17)], $f0=[$t4], $condition=[$t18]): rowcount = 1875.0, cumulative cost = 61875.0
        EnumerableTableScan(table=[[tpch, lineitem]]): rowcount = 60000.0, cumulative cost = 60000.0
    

    概要
    Apacheの方解石は、クエリの最適化のための柔軟なフレームワークです.このブログの記事では、Apache Calcite - Rankパーザ、バリデータ、コンバータ、およびルールベースのオプティマイザでSQLクエリを最適化する方法を示しました.将来の投稿では、Apache Calciteの個々のコンポーネントに掘ります.ステイ!
    SQLクエリオプティマイザのデザインを常にサポートします.ジャストlet us know .