写点什么

openGauss 数据库源码解析系列文章——AI 技术(二)

作者:daydayup
  • 2023-07-20
    北京
  • 本文字数:15942 字

    阅读完需:约 52 分钟

openGauss 数据库源码解析系列文章——AI 技术(二)

Gauss 松鼠会 [openGauss](javascript:void(0);) 2023-07-10 18:00 发表于广东


openGauss数据库源码解析系列文章——AI技术(二) (qq.com)


三、慢 SQL 发现


基于历史 SQL 语句信息进行模型训练,并用训练好的模型进行 SQL 语句的预测,利用预测结果判断该 SQL 语句是否是潜在的慢 SQL。当发现潜在的慢 SQL 后,开发者便可以进行针对性优化或者风险评估,以防业务上线后发生问题。


3.1 慢 SQL 发现的功能


上线业务预检测:上线一批新业务前,使用 SQL 诊断功能评估此次上线业务的预估执行时长,便于用户参考是否应该修改上线业务。


workload 分析:能够对现有 workload 进行分析,将现有 workload 自动分为若干类别,并依次分析此类别 SQL 语句执行代价,以及各个类别之间的相似程度。


3.2 现有技术


首先,明确一下慢 SQL 发现的几个不同阶段,及其对应解决的问题。


  • 阶段 1:对用户输入的一批业务 SQL 语句进行分析,推断 SQL 语句执行时间的快慢,进而可以将评估为慢 SQL 的语句识别出来。

  • 阶段 2:对识别出的潜在慢 SQL 进行根因诊断,判断这些 SQL 语句是因为什么慢,例如比较常见的原因可能是数据量过大、SQL 语句自身过于复杂、容易产生并发的锁冲突、没有创建索引导致全表扫描等等。

  • 阶段 3:对于已经识别出来的慢 SQL 语句的可能问题源,给出针对性的解决方案,譬如可以提示用户进行 SQL 语句的改写、创建索引等。


目前 openGauss 已具备阶段 1 的能力,正在推进阶段 2 能力,同时发布了部分阶段 3 的能力,如索引推荐功能。


业内对于上述第一阶段的主要实现方法大部分是通过执行计划进行估计的,第二阶段大多是通过构建故障模式库、通过启发式规则来实现的,有了上述前两个阶段的准备,第三阶段的实现往往是比较独立的。学术界对于第一阶段的研究比较多,第二阶段采用常规的构建故障模式库的方法实现已经能取得比较好的效果了,因此并不是研究的热点,而第三阶段的工作又相对独立,可以单独作为一个领域进行研究。因此,这里仅介绍业内是如何评估 SQL 语句执行时间的,其他两部分暂不详细展开。


1. 基于执行计划的在线 SVM 模型



图 1 基于执行计划 SVM 模型的系统架构图


如图 1 所示,基于执行计划的在线 SVM(support vector machine,支持向量机)模型包含训练模块和测试模块。


训练阶段:Data Collection 模块执行作为训练集的语句,Data Extraction 模块收集执行的语句特征及执行时间,包括执行计划及算子级别的信息。Model Building 模块基于计划级别特征与算子级别信息分别训练 SVM 模型,再将两模型通过误差分布结合,生成最终的预测模型。这主要是考虑到计划级别信息具有普适性,而算子级别信息具有更高的精确性,结合两者可以在保持具有普适性的前提下,尽可能地精确预测。


测试阶段:Query Planning 模块生成待预测语句的执行计划,Feature Extraction 抽取这些计划中的特征,整合后投入训练阶段生成的模型中产生预测结果。


整个功能的流程如图 2 所示。



图 2 基于执行计划 SVM 模型的流程图


基于执行计划 SVM 技术的缺点。


(1) 如果场景不同时,当参数发生变化,系统不能很快感知,预测会有较大误差。


(2) 预测过程依赖待测语句的执行计划,加重了数据库的负荷,对于 OLTP 场景格外不适用。


(3) 每次重启都要重新训练,不能利用历史训练经验。


2. 基于执行计划的 MART 模型



图 3 基于执行计划 MART 模型的系统架构图


基于执行计划的 MART(multiple additive regression trees,多重累加回归树)模型如图 3 所示,主要包含离线训练模块和在线预测模块。他们的功能如下所示。


离线训练阶段:针对数据库每种类型的算子(如 Table Scan,Merge Join,Sort...),分别训练其对应的模型,用于估算此算子的开销。此外,使用单独的训练阶段,可为不同的算子选择适当的缩放函数。最后,形成带缩放函数的不同的回归树模型。


在线预测阶段:计算出执行计划中所有算子的特征值。然后,使用特征值为算子选择合适的模型,并使用它来估算执行时间。


整个功能的流程如图 4 所示。



图 4 基于执行计划 MART 模型的流程图


基于执行计划 MART 模型技术调优技术的缺点。


(1) 泛用性较差,强依赖训练好的算子模型,遇到例如用户自定义函数的未知语句时,预测效果会较差。


(2) 缩放函数依赖于先验结果,对于超出范围的特征值效果无法保证。


(3) 预测过程依赖待测语句的执行计划,加重了数据库的负荷,很难推广到 OLTP 场景中。


3. 基于执行计划的 DNN 模型



图 5 基于执行计划的结构化 DNN 模型的算法架构图


该技术方案的系统架构图与图 1 类似,区别在于与图 1 中的 Model Building 模块中选择的算法不同。如图 5 所示,是现有技术的算法架构图,算法的概述如下。


该算法依然是将执行计划中的算子信息输入到深度学习网络中,从而对执行时间进行预测的。对于每个算子,收集左右子树的向量化特征、优化器代价及执行时间,输入与之对应的模型中,预测该算子的向量化特征及执行时间等。图 5 中显示了一个 join 操作的预测流程,其左右子树均为 Scan 算子,将两个 Scan 算子通过对应的模型预测出的向量化特征、执行时间,以及该 join 算子的优化器评估代价作为入参,输出 join 算子模型得到该操作的向量化特征及预测出的执行时间。上述过程是个自底向上的过程。


整个功能的流程如图 6 所示。



图 6 基于深度强化学习执行时间预估流程图


上述技术的缺点。


(1) 需要通过已预测算子不断修正模型,预测过程会较慢。


(2) 对环境变化感知差,如数据库参数变化会使得原模型几乎完全失效。


(3) 预测过程依赖待测语句的执行计划,加重了数据库的负荷,对于 OLTP 场景格外不适用。


3.3 慢 SQL 发现采取的策略



图 7 慢 SQL 发现流程图


慢 SQL 发现工具 SQLDiag 的执行流程如图 7 所示,该过程可以分为两个部分,分别是基于模板化的方法和基于深度学习的方法,下面分别介绍一下。


1. 基于 SQL 模板化的流程


(1) 获取 SQL 流水数据。


(2) 检测本地是否存在对应实例的历史模板信息,如果存在,则加载该模板信息,如果不存在,则对该模板进行初始化。


(3) 基于 SQL 数据,提取 SQL 的粗粒度模板信息。粗粒度模板表示将 SQL 中表名、列名和其他敏感信息去除之后的 SQL 语句模板,该模板只保留最基本的 SQL 语句骨架。


(4) 基于 SQL 数据,提取 SQL 细粒度的模板信息。细粒度模板表示在粗粒度模板信息的基础上保留表名、列名等关键信息的 SQL 语句模板。细粒度模板相对粗粒度模板保留了更多 SQL 语句的信息。


(5) 执行训练过程时,首先构造 SQL 语句的基于粗粒度模板和细粒度模板信息,例如粗粒度模板 ID、执行平均时间、细模板执行时间序列、执行平均时间和基于滑动窗口计算出的平均执行时间等。最后将上述模板信息进行储存。


(6) 执行预测过程时,首先导入对应实例的模板信息,如果不存在该模板信息,则直接报错退出;否则继续检测是否存在该 SQL 语句的粗粒度模板信息,如果不存在,则基于模板相似度计算方法在所有粗粒度模板里面寻找最相似的 N 条模板,之后基于 KNN(k nearest neighbor,K 近邻)算法预测出执行时间;如果存在粗粒度模板,则接着检测是否存在近似的细粒度模板,如果不存在,则基于模板相似度计算方法在所有细粒度模板里面寻找最相似的 N 条模板,之后基于 KNN 预测出执行时间;如果存在匹配的细粒度模板,则基于当前模板数据,直接返回对应的执行时间。


2. 基于深度学习的执行流程


(1) 获取 SQL 流水。


(2) 在训练过程中,首先判断是否存在历史模型,如果存在,则导入模型进行增量训练;如果不存在历史模型,则首先利用 word2vector 算法对 SQL 语句进行向量化,即图 7 中的 SQL embeding 过程。而后创建深度学习模型,将该 SQL 语句向量化的结果作为输入特征。基于训练数据进行训练,并将模型保存到本地。值得一提的是,该深度学习模型的最后一个全连接层网络的输出结果作为该 SQL 语句的特征向量。


(3) 在预测过程中,首先判断是否存在模型,如果模型不存在,则直接报错退出;如果存在模型,则导入模型,并利用 word2vector 算法将待预测的 SQL 语句进行向量化,并将该向量输入到深度学习网络中,获取该神经网络的最后一个全连接层的输出结果,即为该 SQL 语句的特征向量。最后,利用余弦相似度在样本数据集中进行寻找,找到相似度最高的 SQL 语句,将该结果返回即为该待预测 SQL 语句的预估执行时间。当然,如果是基于最新 SQL 语句执行时间数据集训练出的深度学习模型,则模型的回归预测结果也可以作为预估执行时间。


3.4 关键源码解析


慢 SQL 发现工具在项目中的源代码路径为:openGauss-server/src/gausskernel/dbmind/tools/sqldiag。


1. 项目结构


慢 SQL 发现工具文件结构如表 1 所示。


表 1 慢 SQL 发现工具结构



2. 总体流程解析


算法的总体流程在 main.py 中给出,根据传来的参数实例化算法模型后,进行训练、增量训练、预测等。main 函数的核心代码如下所示。


def main(args):logging.basicConfig(level=logging.INFO)# 实例化算法模型,模板化模型或DNN模型model = SQLDiag(args.model, args.csv_file, get_config(args.config_file))# 训练模型if args.mode == 'train':    # fit训练数据,提取模板或特征        model.fit()        # 模型保存        model.save(args.model_path)    # 预测elif args.mode == 'predict':    # 加载模型        model.load(args.model_path)        # 标准化预测数据,获取结果        pred_result = model.transform()        # 保存输出结果        ResultSave().save(pred_result, args.predicted_file)        logging.info('predict result in saved in {}'.format(args.predicted_file))    # 更新模型elif args.mode == 'finetune':        model.fine_tune(args.model_path)        model.save(args.model_path)
复制代码


3. 模板化算法源码解析


通过模板化方法,实现在不获取 SQL 语句执行计划的前提下,依据语句逻辑相似度与历史执行记录,预测 SQL 语句的执行时间。主要源码如下:


class TemplateModel(AbstractModel):    # 初始化算法参数    def __init__(self, params):        super().__init__(params)        self.bias = 1e-5        self.__hash_table = dict(INSERT=dict(), UPDATE=dict(), DELETE=dict(), SELECT=dict(),                                 OTHER=dict())        self.time_list_size = params.time_list_size        self.knn_number = params.knn_number        self.similarity_algorithm = calc_sql_distance(params.similarity_algorithm)
def fit(self, data): # 对每条sql语句按照粗、细粒度进行标准化,生成模板 for sql, duration_time in data: if not self.check_illegal_sql(sql): continue fine_template, rough_template = get_sql_template(sql) sql_prefix = fine_template.split()[0] if sql_prefix not in self.__hash_table: sql_prefix = 'OTHER' # 更新粗粒度模板框架 if rough_template not in self.__hash_table[sql_prefix]: self.__hash_table[sql_prefix][rough_template] = dict() self.__hash_table[sql_prefix][rough_template]['info'] = dict() # 更新细粒度模板框架 if fine_template not in self.__hash_table[sql_prefix][rough_template]['info']: self.__hash_table[sql_prefix][rough_template]['info'][fine_template] = \ dict(time_list=[], count=0, mean_time=0.0, iter_time=0.0) # 更新每个细粒度模板的执行时间、迭代时间、sql语句的计数。
self.__hash_table[sql_prefix][rough_template]['info'][fine_template]['count'] += 1# 基于细粒度模板更新粗粒度模板信息 for sql_prefix, sql_prefix_info in self.__hash_table.items():
def transform(self, data): predict_time_list = {} for sql in data: # sql语句不属于'INSERT', 'SELECT', 'UPDATE', 'DELETE', 'CREATE', 'DROP'任何一个,预测时间默认为-1 if not self.check_illegal_sql(sql): predict_time_list[sql] = -1 continue # 若预测的sql所对应的粗粒度模板不存在,执行模板相似度计算方法获取与所有粗粒度模板的相似度 if rough_template not in self.__hash_table[sql_prefix]: for local_rough_template, local_rough_template_info in self.__hash_table[ sql_prefix].items(): similarity_info.append( (self.similarity_algorithm(rough_template, local_rough_template), local_rough_template_info['mean_time'])) # 若预测的sql所对应的细粒度模板不存在,执行模板相似度计算方法获取与所有细粒度模板的相似度 else: for local_fine_template, local_fine_template_info in \ self.__hash_table[sql_prefix][rough_template][ 'info'].items(): similarity_info.append( (self.similarity_algorithm(fine_template, local_fine_template), local_fine_template_info['iter_time'])) # 基于KNN思想计算sql执行时间 topn_similarity_info = heapq.nlargest(self.knn_number, similarity_info)
return predict_time_list
复制代码


4. DNN 算法源码解析


训练阶段先初始化 SQL 向量,之后创建深度学习模型,将模型保存到本地。


预测阶段,导入模型,向量化待预测的 SQL;基于向量相似度对 SQL 的执行时间进行预测。主要源码如下:


class KerasRegression:    # 初始化模型参数    def __init__(self, encoding_dim=1):        self.model = None        self.encoding_dim = encoding_dim# 模型定义    @staticmethod    def build_model(shape, encoding_dim):        from tensorflow.keras import Input, Model        from tensorflow.keras.layers import Dense        inputs = Input(shape=(shape,))        layer_dense1 = Dense(128, activation='relu', kernel_initializer='he_normal')(inputs)        model = Model(inputs=inputs, outputs=y_pred)        # 优化器,损失函数        model.compile(optimizer='adam', loss='mse', metrics=['mae'])        return model    # 模型训练    def fit(self, features, labels, batch_size=128, epochs=300):        self.model.fit(features, labels, epochs=epochs, batch_size=batch_size, shuffle=True, verbose=2)    # 模型预测    def predict(self, features):        predict_result = self.model.predict(features)        return predict_result    # 模型保存    def save(self, filepath):        self.model.save(filepath)    # 模型读取    def load(self, filepath):        from tensorflow.keras.models import load_model        self.model = load_model(filepath)
class DnnModel(AbstractModel, ABC): # 初始化算法参数 def __init__(self, params): self.regression = KerasRegression(encoding_dim=1) self.data = None # 把sql语句转化为vector,如果模型不存在,则直接训练w2v模型,如果模型存在则进行增量训练 def build_word2vector(self, data): self.data = list(data) if self.w2v.model: self.w2v.update(self.data) else: self.w2v.fit(self.data)
def fit(self, data): self.build_word2vector(data) # 数据归一化 self.scaler = MinMaxScaler(feature_range=(0, 1)) self.scaler.fit(labels) labels = self.scaler.transform(labels) self.regression.fit(features, labels, epochs=self.epoch)
# 利用回归模型预测执行时间def transform(self, data):
复制代码


3.5 使用示例


SQL 流水的采集方法:SQL 流水可以通过 openGauss 自带的采集工具进行采集,采集过程的性能损耗很低,一般不会超过 5%,该过程可以通过 GUC 参数设置。


(1) log_statement = all。


(2) log_statement_stats=on。


开启参数后,会向数据库日志文件中记录具体的执行语句以及其开销。


使用方法示例:使用前,可通过如下指令获取帮助。



    python main.py –help
    复制代码


    参数说明如表 2 所示。


    表 2 命令行参数说明



    使用方法示例,使用提供的训练数据进行训练,代码如下:



      python main.py train -f train.csv --model-path test/
      复制代码


      使用提供的数据进行预测,代码如下:



        python main.py predict –f predict.csv –model-path test/ --predicted-file test/result.csv
        复制代码


        使用已有的模型进行增量训练,代码如下:



          python main.py finetune –f train_new.csv –model-path test/
          复制代码


          输出样例为 SQL 语句与预测的执行时间。


          3.6 总结


          当前的慢 SQL 发现功能只是根据历史的 workload 信息,定性、定量地估计未来的 SQL 语句的执行时间。由于 SQL 语句的真实执行结果会受到多种因素影响,这为 SQL 语句的执行结果带来很大噪声,因此理论上通过本功能实现 SQL 语句的执行时间预估是存在一些偏差的,这也是本功能侧重定性判断的原因。对于更精确的 SQL 执行时间预估,可以使用后面提到的 AI 查询时间预测功能。


          四、智能索引推荐


          数据库的索引管理是一项非常普遍且重要的事情,任何数据库的性能优化都需要考虑索引的选择。为此,openGauss 支持原生的索引推荐功能,可以通过系统函数等形式进行使用。


          4.1 使用场景


          在大型关系型数据库中,索引的设计和优化对 SQL 语句的执行效率至关重要。一直以来,数据库管理人员往往基于相关理论知识和经验,进行人工设计和调整索引。这样做的缺点在于,消耗了大量的时间和人力,同时人工设计的方式往往不能确保索引是最优的。


          openGauss 提供了智能索引推荐功能,该功能将索引设计的流程自动化、标准化,可分别针对单条查询语句和工作负载推荐最优的索引,提升作业效率、减少数据库管理人员的运维操作。


          openGauss 的智能索引推荐功能可覆盖多种任务级别和使用场景,具体包含以下三个特性。


          (1) 单条查询语句的索引推荐。该特性可基于查询语句的语义信息和数据库的统计信息,对用户输入的单条查询语句生成推荐的索引。


          (2) 虚拟索引。该特性可模拟真实索引的建立,同时避免真实索引创建所需的时间和空间开销,用户可通过优化器评估虚拟索引对指定查询语句的代价影响。


          (3) 基于工作负载的索引推荐。该特性将包含有多条 DML 语句的工作负载作为任务的输入,最终生成一批可优化整体工作负载执行时间的索引。该功能适用于多种使用场景,例如,当面对一批全新的业务 SQL 且当前系统中无索引,本功能将针对该工作负载量身定制,推荐出效果最优的一批索引;当系统中已存在索引时,本功能仍可查漏补缺,对当前生产环境中运行的作业,通过获取日志来推荐可提升工作负载执行效率的索引,或者针对极个别的慢 SQL 进行单条查询语句的索引推荐。


          4.2 现有技术


          索引推荐技术按照任务级别划分,可分为基于单条查询语句的索引推荐和基于工作负载的索引推荐。对于基于单条查询语句的索引推荐,使用者每次向索引设计工具提供一个查询语句,工具会针对该语句生成最佳的索引。目前的主流算法是首先提取该查询语句的语义信息和数据库中的统计信息,然后基于相关的索引设计和优化理论,对各子句中的谓词进行分析和处理,启发式地推荐最优索引。此类任务主要是针对个别查询时间慢的 SQL 进行索引优化,应用场景较为有限。


          一般来说,更广泛使用的任务场景是基于工作负载的索引推荐,即给定一个包含多种类型 SQL 语句的工作负载,生成使得系统在该工作负载下的运行时间降至最低的索引集合。在索引选择算法中,核心是量化和估计索引对于工作负载的收益,这里的收益是指,当该索引应用于指定工作负载时,工作负载的总代价的减少量。根据代价估计的方式的不同,目前的算法可分为两大类。


          (1) 基于优化器的代价估计的方法。采用优化器的代价模型来对索引进行代价估计是较为准确的,因为优化器负责查询计划和索引的选择。同时,一些数据库系统支持虚拟索引的功能,虚拟索引并没有在存储空间中创建物理上的索引,而是通过模拟索引的效果来影响优化器的代价估计。目前的主流数据库产品均采用了该种方法,如 SQL Server 的 AutoAdmin、DB2 的 DB2 Advisor 等。


          (2) 基于机器学习的方法。上一种方法由于优化器的局限性,会导致索引收益的估计发生偏差,例如选择度的错误估算或者代价估计模型不准确。在学术界的最新进展中,一些方法采用了机器学习算法来预测和分类哪种查询计划更加有效,或者是采用基于神经网络的代价模型来缓解传统模型带来的问题。但是此类方法往往需要大量的训练数据,并不适用于全部的业务环境。


          4.3 实现原理


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


          单条查询语句的索引推荐是以数据库的系统函数形式提供的,用户可以通过调用 gs_index_advise()命令使用。其原理是利用在 SQL 引擎、优化器等处获取到的信息,使用启发式算法进行推荐。该功能可以用来对因索引配置不当而导致的慢 SQL 进行优化。


          ![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)


          图 8 单条查询语句的索引推荐流程图


          单条查询语句的索引推荐步骤如图 8 所示,该过程如下。


          (1) 对给定的查询语句进行词法和语法解析,得到解析树。


          (2) 依次对解析树中的单个或多个查询子句的结构进行分析。


          (3) 整理查询条件,分析各个子句中的谓词。


          (4) 解析 From 子句,提取其中的表信息,如果其中含有 join 子句,则解析并保存 join 关系。


          (5) 解析 Where 子句,如果是谓词表达式,则计算各谓词的选择度,并将各谓词根据选择度的大小进行倒序排列,依据最左匹配原则添加候选索引,如果是 Join 关系,则解析并保存 join 关系。


          (6) 如果是多表查询,即该语句中含有 join 关系,则将结果集最小的表作为驱动表,根据前述过程中保存的 join 关系和连接谓词为其他被驱动表添加候选索引。


          (7) 解析 Group 和 Order 子句,判断其中的谓词是否有效,如果有效则插入到候选索引的合适位置,Group 子句中的谓词优于 Order 子句,且两者只能同时存在一个;这里,候选索引的排列优先级为:join 中的谓词> Where 等值表达式中的谓词> Group 或 Order 中的谓词> Where 非等值表达式中的谓词。


          (8) 检查该索引是否在数据库中已存在,若存在则不再重复推荐。


          (9) 输出最终的索引推荐建议。


          2. 虚拟索引


          通过虚拟索引功能实现对待创建索引的效果和代价进行估计。对于给定的索引表名和列名,可以在数据库内部建立虚拟索引,该虚拟索引只具有待创建索引的元信息,而不会真正创建物理索引文件,因此避免了真实索引的创建开销。这些元信息包括:待创建索引的表名、列名和其他统计信息,虚拟索引仅用于通过 explain 语句显示优化器的可能执行路径,不能提供真正的索引扫描。


          因此,对某条 SQL 语句执行 explain 命令,可以查看到创建索引前后优化器规划出的执行计划、检验该待创建索引是否被数据库采用以及是否有性能提升。虚拟索引主要是基于数据库中的 hook(钩子机制)实现的,即通过使用全局的函数指针 get_relation_info_hook 和 explain_get_index _name_hook,来干预和改变查询计划的估计过程,让优化器在规划路径时,考虑到可能出现的索引扫描。


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


          基于工作负载的索引推荐功能的主要模块如图 9 所示。


          ![图片](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)


          图 9 基于工作负载索引推荐流程图


          主要包括以下步骤。


          (1) 对于给定的工作负载,首先对工作负载进行压缩。由于工作负载中通常存在着大量相似的语句,为了减少数据库功能的调用次数,我们会对工作负载中的 SQL 语句进行模板化和采样。


          (2) 对压缩后的工作负载,调用单条查询语句的索引推荐功能为每条语句生成推荐索引,作为候选索引集合。


          (3) 对候选索引集合中的每个索引,在数据库中创建对应的虚拟索引,根据优化器的代价估计来计算该索引对整个负载的收益。


          (4) 在候选索引集合的基础上,基于索引代价和收益进行索引的选择。openGauss 实现了两种算法进行索引优选:一种是在限定索引集大小的条件下,根据索引的收益进行排序,然后选取靠前的候选索引来最大化索引集的总收益,最后采用微调策略,基于索引间的相关性进行调整和去重,得到最终的推荐索引集合;另一种方法是采用贪心算法来迭代地进行索引集合的添加和代价推断,最终生成推荐的索引集合。两种算法各有优劣,第一种方法未充分考虑索引间的相互关系,而第二种方法会伴随较多的迭代过程。


          (5) 输出最终的索引推荐建议。


          4.4 关键源码解析


          1. 项目结构


          智能索引推荐功能在项目中的源代码路径在 openGauss-server/src/gausskernel/dbmind 中,涉及的相关文件如表 3 所示。


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



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


          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);}……}
          复制代码


          4.5 使用示例


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


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


          表 4 单 query 索引推荐功能的函数接口



          使用上述函数,获取针对该 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. 虚拟索引


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


            虚拟索引功能涉及的系统函数接口如表 5 所示。


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



            本功能涉及的 GUC 参数如表 6 所示。


            表 8-11 GUC 参数



            (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
            复制代码


            用户头像

            daydayup

            关注

            还未添加个人签名 2023-07-18 加入

            还未添加个人简介

            评论

            发布
            暂无评论
            openGauss数据库源码解析系列文章——AI技术(二)_opengauss_daydayup_InfoQ写作社区