24.【X=1.1】 在关系数据库查询优化领域,对查询时间的估计准确性直接决定了查询优化结果,进而影响到数据库整体的查询效率。但由于数据库自身的复杂性,查询时间受到数据分布、数据库负载、索引结构、数据库配置等多方面的影响,难以进行准确预测。近年来,随着AI算法的兴起,有研究者尝试使用AI模型对查询的时间进行预测并取得了一定成果。请阅读论文Plan-Structured Deep Neural Network Models for Query Performance Prediction并:
(1)提交论文阅读报告【30%分数】
(2)复现论文实验结果(可使用开源代码)【20%分数】
(3)将论文提出的模型嵌入到PostgreSQL中,使得本论文提出的代价估计方法可以被用来指导PostgreSQL的查询优化。提交代码、报告。【50%分数】
查询性能预测是预测查询延迟的任务,是数据库管理系统中最具挑战性的问题之一。现有方法依赖于人类专家设计的特征和性能模型,但通常无法捕获查询运算符和输入关系之间的复杂交互,并且通常无法自然地适应查询执行计划中的工作负载特征和模式。在本文中,我们认为深度学习可以应用于查询性能预测问题,并介绍了一种新的神经网络架构:计划结构神经网络。我们的方法消除了人工设计特征选择的需要,并在操作员和查询计划级别自动发现复杂的性能模型。我们新颖的神经网络架构可以匹配任何优化器选择的查询执行计划的结构,并高精度地预测其延迟。我们还提出了一些优化方法,可以在不牺牲有效性的情况下减少训练开销。我们在各种工作负载上评估了我们的技术,并证明我们的计划结构神经网络在查询性能预测方面可以优于最先进的技术。
- 尽管传统的深度神经网络具有许多优点,但人们很难将其应用于查询性能预测任务中。深度学习的一个直接应用将是将整个查询建模为一个单一的神经网络,并使用查询计划特征作为输入向量。然而,这种简单的方法忽略了这样一个事实,即查询计划结构、中间结果的特征和非叶操作符通常与查询执行时间相关,因此在任何预测分析任务中都很有用。此外,查询计划是不同的结构——每个计划的操作符的类型和数量不同,操作符与查询性能有不同的相关性,操作符有不同的属性集,因此有不同的预测特征集。传统的dnn具有静态的网络架构,并处理固定大小的输入向量。
- 因此,“一刀切”的神经网络体系结构不适合查询性能预测任务。最后,虽然之前在机器学习领域的工作已经研究了将深度神经网络应用于序列[14]或树结构[43,49]数据,但这些方法都不适合查询性能预测,正如我们接下来描述的。用于处理树结构数据的孤立分支神经网络架构在自然语言处理[43,49]中很流行,它是基于对树的一个分支的修改会对其他分支产生巨大影响的假设,从而允许树分支共享信息。但是,在查询执行计划的上下文中,查询执行计划树的一个分支的特性和性能与其他分支合理地隔离。具体来说,我们知道一个特定的操作符只能影响它的祖先,而永远不会影响它的兄弟姐妹。例如,考虑图1中所示的两个查询执行计划。将第一个计划中的R3更改为第二个计划中的R4不会影响R1或其滤波器的性能。
- 异构树节点传统的神经网络处理固定结构的输入向量。但是,在查询执行计划中,每种类型的操作符都具有根本不同的属性。连接运算符可以通过连接类型(例如嵌套循环连接、哈希连接)、估计需要的存储(例如,对于外部排序)等来描述。然而,过滤器操作将具有一组完全不同的属性,例如选择性估计或并行性标志。由于不同操作符的特征向量可能有不同的大小,因此简单地将它们输入相同的神经网络是不可能的。
- 解决这个问题的一个简单的解决方案可能是为每个关系操作符将向量连接在一起。例如,如果一个连接运算符有9个属性,而一个过滤器运算符有7个属性,则可以用大小为9 + 7 = 16属性的向量来表示一个连接或过滤器运算符。如果运算符是一个过滤器,那么向量的前9个条目只是0,并且向量的最后7个条目将被填充。如果运算符是一个连接,则填充向量的前9个条目,最后7个条目为空。这个解决方案的问题是稀疏性:如果一个人有许多不同的操作符类型,用于表示它们的向量将有越来越大比例的零。一般来说,这种稀疏性是统计技术[22]面临的一个主要问题,将稀疏输入转换为可用的、密集的输入仍然是[52,53]研究的一个活跃领域。换句话说,使用稀疏向量来克服异构树节点,用一个潜在的更困难的问题来代替一个问题。位置独立的操作符行为正如前面的工作[13,25]所指出的,同一操作符的两个实例(例如,连接、选择等)将共享相似的行为
1. 传统的神经网络只是将输入的变量看成一个大的输入向量,没有考虑输入变量彼此的执行顺序也会对结果产生影响。
比如我输入(连接,选择,笛卡尔积) 12 3和 13 2的执行时间是不一样的。
2. 传统的神经网络输入长度是固定的,也就是说如果输入的长度小,我们虽然可以用null值替代,但是毕竟是一个变量,这个会对预测的结果产生一定的干扰,如果长度过长的话,我们将他拆成两个输入吗?显然这是不好的。
考虑到上述观察结果,本文提出了一种新的树状神经网络结构,其网络结构与给定查询计划的结构相匹配。这个计划结构的神经网络由操作员级的神经网络(称为神经单元)组成,整个查询计划被建模为一个神经单元的树。就其本身而言,每个神经单元预计(1)预测单个操作符类型的性能——例如,对应于连接的神经单元预测连接的延迟——以及(2)关于操作符的“有趣”数据可能对神经单元的父节点有用。计划级神经网络可以预测给定查询计划的执行时间。也就是说,也就是说相同类型的节点的代价是相等的
简要说明一下 树状神经网络,允许有关联的节点相互通信,同时对不同分支的节点进行一个合理的隔离,选择树状模型,再合适不过。
这个树状神经网络是异构的树状网络,每一个节点的输入用代价表示或者其他,因为过滤操作会减少每一层的输入数量 所以不能采用传统的神经网络。
可能的一种解决方法是:比如我有9个连接和7个过滤属性,我们就可以采用长度为16的一维向量表示,但是当不同的属性变多的时候,我们可能会填充更多的0,这样就会出现稀疏矩阵,如何把稀疏矩阵变成密集型矩阵是我们研究的另一个热点。
位置独立的操作级行为正如前面的工作所指出的,同一操作级的两个实例(如连接、选择等),即使在相同的计划中或在同一计划中多次出现,也会具有相似的性能特征。例如,在哈希连接的情况下,延迟与探测关系和搜索关系的大小密切相关,并且无论操作符在查询执行计划中的位置如何,这种相关性都成立。这表明,人们可以训练一个神经网络模型来预测哈希连接操作符的性能,并且当哈希连接操作符在计划中出现时,都可以使用相同的模型。
- 例如,一个唯一的连接单元,一个唯一的选择单元,等等。这些神经单元的目标是表示足够复杂的函数,以建模在各种上下文下的关系操作符的性能。
- 虽然一个简单的多项式模型的连接算子可以仅根据估计的输入基数进行预测,但是我们的神经单元将从大量的候选输入中自动识别出最相关的特征(例如,表的底层结构、关于数据分布的统计数据、选择性估计的不确定性、可用的缓冲空间等),所有这些都没有任何手工调整。
我们假设向量x是输入的向量,每一个列表示关系代数中的一个实例,这个向量将作为该特定操作符的神经单元的输入
查询执行计划中的每个节点都被映射到与关系操作符对应的神经单元。
输出向量
操作符实例x的性能信息通常与查询执行计划中它的父操作符的性能相关。为了捕获这一点,并允许操作符级神经单元之间的信息流,一个操作符类型的每个神经单元将同时输出一个延迟预测和一个数据向量。当延迟输出预测操作符的延迟时,输出数据向量表示子操作符中与父操作符的性能相关的“有趣的”特性。例如,扫描操作符的神经单元可以产生一个数据向量,其中包含关于所产生的行的预期分布的信息。我们注意到,这些数据向量是由模型在其训练阶段自动学习的,而不需要任何人为干扰或选择出现在输出向量中的特征。
神经元:这个输入通过许多隐藏层来输入,每个隐藏层通过应用一个激活的仿射变换(如公式1所定义)来生成特征。这些复杂的变换可以通过梯度下降方法自动学习,该方法逐步调整神经单元NA的权重和偏差,以最小化其损失函数(如第2.2节所述)。最后一层将隐藏层学习到的内部表示转换为延迟预测和输出数据向量。在形式上,一个神经单元NA的输出被定义为:
其中a是操作符类型a的实例。输出向量的大小为d + 1。输出向量的第一个元素表示神经单元对算子延迟的估计,记为p→a[l]。其余的d个元素表示数据向量,记为p→a[d].我们注意到,由于不同神经单元的输入向量不会有相同的大小,每个神经单元可能有不同大小的权重和偏差向量,但它们的基本结构将是相似的。
本文使用了开源代码,引用链接
logf = open(opt.logfile, 'w+')
save_opt(opt, logf)
#qpp.test_dataset = dataset.create_test_data(opt)
qpp.test_dataset = dataset.test_dataset
total_iter = 0
for epoch in range(opt.start_epoch, opt.end_epoch):
# for epoch in range(0, opt.end_epoch):
epoch_start_time = time.time() # timer for entire epoch
iter_data_time = time.time() # timer for data loading per iteration
epoch_iter = 0 # the number of training iterations in current epoch, reset to 0 every epoch
samp_dicts = dataset.sample_data()
total_iter += opt.batch_size
qpp.set_input(samp_dicts)
qpp.optimize_parameters(epoch)
logf.write("epoch: " + str(epoch) + "; iter_num: " + str(total_iter) \
+ '; total_loss: {}; test_loss: {}; pred_err: {}; R(q): {}' \
.format(qpp.last_total_loss, qpp.last_test_loss,
qpp.last_pred_err, qpp.last_rq))
#if total_iters % opt.print_freq == 0: # print training losses and save logging information to the disk
losses = qpp.get_current_losses()
loss_str = "losses: "
for op in losses:
loss_str += str(op) + " [" + str(losses[op]) + "]; "
if epoch % 50 == 0:
print("epoch: " + str(epoch) + "; iter_num: " + str(total_iter) \
+ '; total_loss: {}; test_loss: {}; pred_err: {}; R(q): {}' \
.format(qpp.last_total_loss, qpp.last_test_loss,
qpp.last_pred_err, qpp.last_rq))
print(loss_str)
logf.write(loss_str + '\n')
if (epoch + 1) % opt.save_latest_epoch_freq == 0: # cache our latest model every iterations
print('saving the latest model (epoch %d, total_iters %d)' % (epoch + 1, total_iter))
qpp.save_units(epoch + 1)
logf.close()
环境:Linux虚拟机
vscode python3环境,左边是模型的存放位置,我们训练不同数据量以及不同的操作,为了使实验结果更准确,我们采用大量的数据进行模拟,为此光训练部分已经跑了两天。
我们的主要想法是写一个小型数据库系统并模拟查询之后我们为这个数据库建立一个接口,使用onnx模型和本实验训练得到的模型进行嵌入,观察实验的效果,指导查询执行。
输入特征向量我们用向量x表示x = F(x),这个向量将作为该特定操作符的神经单元的输入。这些向量可以从查询优化器的输出中提取,并包含以下信息:操作符的类型(例如,哈希连接或连接操作符的嵌套循环连接等),估计要产生的行数、所需的估计I/Os数等。
对psql进行一个简单的查询
EXPLAIN SELECT * FROM fake_data LIMIT 10;
将ANALYZE参数添加到查询会产生计时信息。
与EXPLAIN不同,EXPLAIN ANALYSE实际上在数据库中运行查询。这个选项对于了解计划器是否没有正确发挥其作用非常有帮助,即,从EXPLAIN和EXPLAIN ANALYSE生成的计划是否存在巨大差异。
Buffers:sharedhit=5意味着从PostgreSQL缓存本身获取了五个页面。让我们调整查询以从不同的行偏移。
更改OFFSET会导致不同的页面点击次数。
Buffers:sharedhit=7read=5显示5页来自磁盘。该read部分是显示有多少页面来自磁盘的变量,正如已经解释过的hit来自缓存。如果我们再次执行相同的查询(记住ANALYSE运行查询),那么所有数据现在都来自缓存。
再次执行查询意味着缓存现在提供所有结果。PostgreSQL使用一种称为LRU(最近最少使用)缓存的机制将经常使用的数据存储在内存中。
PostgreSQL能够以一种很好的格式给出查询计划,例如JSON,这些计划可以以一种语言中立的方式进行解释
EXPLAIN (ANALYSE,BUFFERS,VERBOSE,FORMAT JSON) SELECT * FROM fake_data LIMIT 10 OFFSET 500;
将按JSON格式打印查询计划。您可以通过复制其输出并将其插入到另一个表中来在Arctype中查看此格式,如下面的图片所示。
根据SQL查询语句,预测查询规模
- SQL类型
- 数值型数据(范围查询与等值查询)
- 最多涉及两表连接
- 提供查询计划
最后我们的目的是提供10000条训练集,需要预测2000条测试集的结果
中间结果存放在 ipynb的内容中等,老师可以阅读和运行代码。
通过刚才小型实验的机器学习算法预测查询计划,我们取得了比较不错的效果,现在我们将训练好的模型嵌入psql开源代码中。
import torch
from torchvision import models
def convert_to_onnx(model_path, output_path):
# 加载模型
model = torch.load(model_path, map_location=torch.device('cpu'))
model.eval()
# 创建虚拟输入数据
dummy_input = torch.randn(1, 3, 224, 224)
# 导出模型到ONNX格式
torch.onnx.export(model, dummy_input, output_path, verbose=True)
if __name__ == "__main__":
import os
folder_path = "./saved_model" # 替换成你的文件夹路径
output_folder = "./onnx_models" # 输出ONNX文件的文件夹
# 确保输出文件夹存在
os.makedirs(output_folder, exist_ok=True)
# 遍历文件夹中的.pth文件并转换为.onnx
for filename in os.listdir(folder_path):
if filename.endswith(".pth"):
model_path = os.path.join(folder_path, filename)
output_path = os.path.join(output_folder, f"{os.path.splitext(filename)[0]}.onnx")
convert_to_onnx(model_path, output_path)
print("转换完成!")