表8-8 智能索引推荐功能源代码路径

文件路径 说明
kernel/index_advisor.cpp 单条查询语句的索引推荐。
kernel/hypopg_index.cpp 虚拟索引特性实现
tools/index_advisor/index_advisor_workload.py 基于工作负载的索引推荐

其中,单条查询语句的索引推荐功能和虚拟索引的功能通过数据库的系统函数进行调用,基于工作负载的索引推荐功能需要通过数据库外部的脚本运行。

2. 关键代码解析

单条语句索引推荐的所有实现部分都只存在于index_advisor.cpp文件中,该功能的主要入口为suggest_index函数,它通过系统函数gs_index_advise进行调用,代码如下:

SuggestedIndex *suggest_index(const char *query_string, _out_ int *len)
{
    ……
    // 对查询语句进行词法和语法解析,获得解析树
List *parse_tree_list = raw_parser(query_string);
…
    // 递归地搜索解析树中的SelectStmt结构
    Node *parsetree = (Node *)lfirst(list_head(parse_tree_list));
    find_select_stmt(parsetree);
   …

    // 依次解析和处理SelectStmt结构中的各个子句部分
    ListCell *item = NULL;

    foreach (item, g_stmt_list) {
        SelectStmt *stmt = (SelectStmt *)lfirst(item);
        /* 处理SelectStmt 结构体中涉及的FROM子句,提取涉及的表,解析和保存这些表中的join关系 */
        parse_from_clause(stmt->fromClause);
        …
        if (g_table_list) {
            // 处理WHERE子句,提取条件表达式中的谓词并添加候选索引,解析和保存其中的join关系
            parse_where_clause(stmt->whereClause);
            // 根据保存的join关系确定驱动表
            determine_driver_table();
            // 处理GROUP子句,如果满足条件,则将其中的谓词添加为候选索引
            if (parse_group_clause(stmt->groupClause, stmt->targetList)) {
                add_index_from_group_order(g_driver_table, stmt->groupClause, stmt->targetList, true);
            /* 处理ORDER子句,如果满足条件,则将其中的谓词添加为候选索引 */
            } else if (parse_order_clause(stmt->sortClause, stmt->targetList)) {
                add_index_from_group_order(g_driver_table, stmt->sortClause, stmt->targetList, false);
            }
            // 如果是多表查询,则根据保存的join关系为被驱动表添加候选索引
            if (g_table_list->length > 1 && g_driver_table) {
                add_index_for_drived_tables();
            }
            /* 对全局变量中的每个table依次进行处理,函数generate_final_index将前述过程生成的候选索引进行字符串拼接,并检查和已存在的索引是否重复 */
            ListCell *table_item = NULL;

            foreach (table_item, g_table_list) {
                TableCell *table = (TableCell *)lfirst(table_item);
                if (table->index != NIL) {
                    Oid table_oid = find_table_oid(query_tree->rtable, table->table_name);
                    if (table_oid == 0) {
                        continue;
                    }
                    generate_final_index(table, table_oid);
                }
            }
            g_driver_table = NULL;
        }
    }
……
    return array;
}

虚拟索引的核心功能全部位于hypopg_index.cpp文件中。用户通过SQL语句调用系统函数hypopg_create_index来创建虚拟索引,该系统函数主要通过调用hypo_index_store_parsetree函数来完成虚拟索引的创建。虚拟索引的结构体名为hypoIndex,该结构体的许多字段是从它涉及的表的RelOptInfo结构体中读取的,hypoIndex的结构如下:

typedef struct hypoIndex {
    Oid oid;           /* 虚拟索引的oid,该oid是唯一的 */
    Oid relid;         /* 涉及的表的oid */
    …
    char *indexname;   /* 虚拟索引名 */

    BlockNumber pages; /* 预估索引使用的磁盘页数 */
    double tuples;     /* 预估索引所涉及的元组数目 */

    /* 索引描述信息 */
    int ncolumns;         /* 涉及的总列数 */
    int nkeycolumns;      /* 涉及的关键列数 */
    … 
    Oid relam;            /* 记录索引操作回调函数的元组的oid, 从pg_am系统表中获取的 */
    … 
} hypoIndex;

函数hypo_index_store_parsetree的输入参数为创建索引的SQL语句和其语法树,依据该语句的解析结果来创建新的虚拟索引,代码如下:

hypoIndex *hypo_index_store_parsetree(IndexStmt *node, const char *queryString)
{
……
// 获得创建索引的表的oid
    relid = RangeVarGetRelid(node->relation, AccessShareLock, false);
    ……
    // 对该创建索引的语句进行语法解析
    node = transformIndexStmt(relid, node, queryString);
    ……
    // 新建虚拟索引,该虚拟索引的结构体类型hypoIndex定于位于文件openGauss-server/src/include/dbmind/hypopg_index.h,与索引结构体IndexOptInfo类似
    entry = hypo_newIndex(relid, node->accessMethod, nkeycolumns, ninccolumns, node->options);
    // 根据语法树的解析结果为虚拟索引entry内的各个成员赋值
    PG_TRY();
{
   ……
        entry->unique = node->unique;
        entry->ncolumns = nkeycolumns + ninccolumns;
        entry->nkeycolumns = nkeycolumns;
        ……
    }
    PG_CATCH();
    {        
        hypo_index_pfree(entry);
        PG_RE_THROW();
    }
    PG_END_TRY();
    // 设置虚拟索引的名字
    hypo_set_indexname(entry, indexRelationName.data);
    // 将新建的虚拟索引entry添加到虚拟索引的全局链表hypoIndexes上,该全局变量为节点类型为hypoIndex*的List链表,记录了全部创建过的虚拟索引
    hypo_addIndex(entry);

    return entry;
}
// 该函数被赋值给全局的函数指针get_relation_info_hook,当数据库执行EXPLAIN时,会通过该函数指针跳转到本函数 
void hypo_get_relation_info_hook(PlannerInfo *root, Oid relationObjectId, bool inhparent, RelOptInfo *rel)
{
    /* 判断是否开启GUC参数enable_hypo_index,当SQL语句是EXPLAIN命令时,变量isExplain的值为真 */
    if (u_sess->attr.attr_sql.enable_hypo_index && isExplain) {
        Relation relation;

        relation = heap_open(relationObjectId, AccessShareLock);

        if (relation->rd_rel->relkind == RELKIND_RELATION) {
            ListCell *lc;
            /* 遍历全局变量链表hypoIndexes中的每个创建过的虚拟索引 */
            foreach (lc, hypoIndexes) {
                hypoIndex *entry = (hypoIndex *)lfirst(lc);
                // 判断该虚拟索引和该表是否匹配
                if (hypo_index_match_table(entry, RelationGetRelid(relation))) {
                    // 如果匹配,则将该索引加入该表的indexlist中,indexlist是节点类型为IndexOptInfo的链表,是结构体类型RelOptInfo的成员,记录了表的所有的索引
                    hypo_injectHypotheticalIndex(root, relationObjectId, inhparent, rel, relation, entry);
                }
            }
        }
        heap_close(relation, AccessShareLock);
}
……
}

8.4.5 使用示例

1. 单条查询语句的索引推荐

单条查询语句的索引推荐功能支持用户在数据库中直接进行操作,本功能基于查询语句的语义信息和数据库的统计信息,对用户输入的单条查询语句生成推荐的索引。本功能涉及的函数接口如表8-9所示。

表8-9 单query索引推荐功能的函数接口

函数名 参数 返回值 功能
gs_index_advise SQL语句字符串 针对单条查询语句生成推荐索引(该版本只支持B树索引)

使用上述函数,获取针对该query生成的推荐索引,推荐结果由索引的表名和列名组成。

opengauss=> select * from gs_index_advise('SELECT c_discount from bmsql_customer where c_w_id = 10');
     table      |  column  
----------------+----------
 bmsql_customer | (c_w_id)
(1 row)

上述结果表明:应当在bmsql_customer的c_w_id列上创建索引,例如可以通过下述SQL语句创建索引。

CREATE INDEX idx on bmsql_customer(c_w_id);

某些SQL语句,也可能被推荐创建联合索引,例如:

opengauss=# select * from gs_index_advise('select name, age, sex from t1 where age >= 18 and age < 35 and sex = ''f'';');
 table | column
-------+------------
 t1    | (age, sex)
(1 row)

上述语句结果表明应该在表t1上创建一个联合索引(age, sex),可以通过下述命令创建该索引,并将其命名为idx1。

CREATE INDEX idx1 on t1(age, sex);

2. 虚拟索引

虚拟索引功能支持用户在数据库中直接进行操作,该功能模拟真实索引的建立,避免真实索引创建所需的时间和空间开销,用户基于虚拟索引,可通过优化器评估该索引对指定查询语句的代价影响。
虚拟索引功能涉及的系统函数接口如表8-10所示。

表8-10 虚拟索引功能的接口

函数名 参数 返回值 功能
hypopg_create_index 创建索引语句的字符串 创建虚拟索引
hypopg_display_index 结果集 显示所有创建的虚拟索引信息
hypopg_drop_index 索引的oid 删除指定的虚拟索引
hypopg_reset_index 清除所有虚拟索引
hypopg_estimate_size 索引的oid 整数型 估计指定索引创建所需的空间大小

本功能涉及的GUC参数如表8-11所示。

表8-11 GUC参数

参数名 级别 功能 类型 默认值
enable_hypo_index PGC_USERSET 是否开启虚拟索引功能 bool off

(1) 使用hypopg_create_index函数创建虚拟索引。例如:

opengauss=> select * from hypopg_create_index('create index on bmsql_customer(c_w_id)');
 indexrelid |              indexname              
------------+-------------------------------------
     329726 | <329726>btree_bmsql_customer_c_w_id
(1 row)

(2) 开启GUC参数enable_hypo_index,该参数控制数据库的优化器进行EXPLAIN时是否考虑创建的虚拟索引。通过对特定的查询语句执行explain,用户可根据优化器给出的执行计划评估该索引是否能够提升该查询语句的执行效率。例如:

opengauss=> set enable_hypo_index = on;
SET

开启GUC参数前,执行EXPLAIN+查询语句,如下所示:

opengauss=> explain SELECT c_discount from bmsql_customer where c_w_id = 10;
                              QUERY PLAN                              
--------------------------------------------------------------------
 Seq Scan on bmsql_customer  (cost=0.00..52963.06 rows=31224 width=4)
   Filter: (c_w_id = 10)
(2 rows)

开启GUC参数后,执行EXPLAIN+查询语句,如下所示:

opengauss=> explain SELECT c_discount from bmsql_customer where c_w_id = 10;
                              QUERY PLAN                          
--------------------------------------------------------------------
 [Bypass]
 Index Scan using <329726>btree_bmsql_customer_c_w_id on bmsql_customer  (cost=0.00..39678.69 rows=31224 width=4)
   Index Cond: (c_w_id = 10)
(3 rows)

通过对比两个执行计划可以观察到,该索引预计会降低指定查询语句的执行代价,用户可考虑创建对应的真实索引。
(3) (可选)使用hypopg_display_index函数展示所有创建过的虚拟索引。例如:

opengauss=> select * from hypopg_display_index();
                 indexname                  | indexrelid |     table      |      column      
--------------------------------------------+------------+----------------+------------------
 <329726>btree_bmsql_customer_c_w_id        |     329726 | bmsql_customer | (c_w_id)
 <329729>btree_bmsql_customer_c_d_id_c_w_id |     329729 | bmsql_customer | (c_d_id, c_w_id)
(2 rows)

(4) (可选)使用hypopg_estimate_size函数估计虚拟索引创建所需的空间大小(单位:字节)。例如:

opengauss=> select * from hypopg_estimate_size(329730);
 hypopg_estimate_size 
----------------------
             15687680
(1 row)

(5) 删除虚拟索引。
① 使用hypopg_drop_index函数删除指定oid的虚拟索引。例如:

opengauss=> select * from hypopg_drop_index(329726);
 hypopg_drop_index 
-------------------
 t
(1 row)

② 使用hypopg_reset_index函数一次性清除所有创建的虚拟索引。例如:

opengauss=> select * from hypopg_reset_index();
 hypopg_reset_index 
--------------------
    
(1 row)

3. 基于工作负载的索引推荐

对于工作负载级别的索引推荐,用户可通过运行数据库外的脚本使用此功能,本功能将包含有多条DML语句的工作负载作为输入,最终生成一批可对针对整体工作负载的索引。
(1) 准备好包含有多条DML语句的文件作为输入的工作负载,文件中每条语句占据一行。用户可从数据库的离线日志中获得历史的业务语句。
(2) 运行python脚本index_advisor_workload.py,命令如下:

python index_advisor_workload.py [p PORT] [d DATABASE] [f FILE] [--h HOST] [-U USERNAME] [-W PASSWORD]
[--max_index_num MAX_INDEX_NUM] [--multi_iter_mode]

其中的输入参数如下。
① PORT:连接数据库的端口号。
② DATABASE:连接数据库的名字。
③ FILE:包含workload语句的文件路径。
④ HOST:(可选)连接数据库的主机号。
⑤ USERNAME:(可选)连接数据库的用户名。
⑥ PASSWORD:(可选)连接数据库用户的密码。
⑦ MAX_INDEX_NUM:(可选)最大的索引推荐数目。
⑧ multi_iter_mode:(可选)算法模式,可通过是否设置该参数来切换算法。例如:

python index_advisor_workload.py 6001 opengauss tpcc_log.txt --max_index_num 10 --multi_iter_mode

推荐结果为一批索引,以多个创建索引语句的格式显示在屏幕上,结果示例如下:

create index ind0 on bmsql_stock(s_i_id,s_w_id);
create index ind1 on bmsql_customer(c_w_id,c_id,c_d_id);
create index ind2 on bmsql_order_line(ol_w_id,ol_o_id,ol_d_id);
create index ind3 on bmsql_item(i_id);
create index ind4 on bmsql_oorder(o_w_id,o_id,o_d_id);
create index ind5 on bmsql_new_order(no_w_id,no_d_id,no_o_id);
create index ind6 on bmsql_customer(c_w_id,c_d_id,c_last,c_first);
create index ind7 on bmsql_new_order(no_w_id);
Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐