基于图神经网络的数据库索引推荐:工作负载感知的自动索引
基于图神经网络的数据库索引推荐:工作负载感知的自动索引

一、索引推荐的搜索空间爆炸:从人工经验到智能搜索
数据库索引是查询性能的关键因素,但选择哪些列建索引是一个组合优化问题。一张 20 列的表,最多 3 列联合索引的搜索空间超过 7000 种。全库 100 张表的搜索空间达到 7 亿种,人工经验根本无法穷举。
生产环境中,索引推荐面临三个核心痛点:第一,搜索空间巨大——列组合的排列组合导致候选索引数量指数级增长;第二,索引间的相互影响——新建索引可能使其他索引失效,需要全局优化而非局部贪心;第三,写入代价的权衡——每个索引都会增加写入开销,索引数量需要在查询加速和写入代价之间平衡。
这个问题的本质是:索引推荐是一个组合优化问题——在候选索引空间中找到使查询总代价最小、且写入代价可接受的索引子集。图神经网络(GNN)可以将查询、表和列建模为图结构,学习它们之间的依赖关系,高效搜索最优索引组合。
二、GNN 索引推荐的底层机制
flowchart TB
subgraph 图构建["工作负载图构建"]
Q1[查询1] --> C1[列A]
Q1 --> C2[列B]
Q2[查询2] --> C2
Q2 --> C3[列C]
C1 --> T1[表1]
C2 --> T1
C3 --> T1
end
subgraph GNN推理["GNN 推理"]
GRAPH[工作负载图] --> EMBED[节点嵌入]
EMBED --> SCORE[索引评分<br/>每个候选索引的收益分数]
end
SCORE --> SELECT[索引选择<br/>约束优化]
SELECT --> |写入代价约束| RESULT[推荐索引集]
关键机制解析:
-
工作负载图:将查询工作负载建模为异构图——查询节点、列节点和表节点,边表示查询访问列、列属于表的关系。GNN 通过消息传递学习节点的嵌入表示。
-
索引评分:GNN 输出每个候选索引的收益分数——考虑查询加速效果和写入代价。分数高的索引优先推荐。
-
约束优化:在写入代价预算约束下,选择总收益最大的索引子集。这是一个 0-1 背包问题,可以用贪心或动态规划求解。
三、GNN 索引推荐的实现
3.1 工作负载图构建
import torch
from torch_geometric.data import HeteroData
class WorkloadGraphBuilder:
"""工作负载图构建器"""
def build(self, workload: list, schema: dict) -> HeteroData:
data = HeteroData()
# 查询节点特征: [执行频率, 平均耗时, 返回行数]
query_features = []
query_id_map = {}
for i, query in enumerate(workload):
query_id_map[query.id] = i
query_features.append([
query.frequency,
query.avg_latency_ms,
query.avg_rows,
])
data["query"].x = torch.tensor(query_features, dtype=torch.float)
# 列节点特征: [基数, 数据类型编码, 是否已有索引]
column_features = []
column_id_map = {}
for i, col in enumerate(schema.columns):
column_id_map[col.full_name] = i
column_features.append([
col.cardinality,
col.dtype_encoded,
1.0 if col.has_index else 0.0,
])
data["column"].x = torch.tensor(column_features, dtype=torch.float)
# 查询→列边: 查询访问了哪些列
edge_src, edge_dst = [], []
for query in workload:
for col_name in query.accessed_columns:
edge_src.append(query_id_map[query.id])
edge_dst.append(column_id_map[col_name])
data["query", "accesses", "column"].edge_index = torch.tensor(
[edge_src, edge_dst], dtype=torch.long
)
# 列→表边
edge_src, edge_dst = [], []
for i, col in enumerate(schema.columns):
edge_src.append(i)
edge_dst.append(col.table_id)
data["column", "belongs_to", "table"].edge_index = torch.tensor(
[edge_src, edge_dst], dtype=torch.long
)
return data
3.2 GNN 索引评分模型
import torch.nn as nn
from torch_geometric.nn import HeteroConv, SAGEConv
class IndexRecommendationGNN(nn.Module):
"""索引推荐GNN模型"""
def __init__(self, hidden_dim=128):
super().__init__()
self.conv1 = HeteroConv({
("query", "accesses", "column"): SAGEConv((-1, -1), hidden_dim),
("column", "belongs_to", "table"): SAGEConv((-1, -1), hidden_dim),
})
self.conv2 = HeteroConv({
("query", "accesses", "column"): SAGEConv((-1, -1), hidden_dim),
("column", "belongs_to", "table"): SAGEConv((-1, -1), hidden_dim),
})
# 索引评分头
self.index_scorer = nn.Sequential(
nn.Linear(hidden_dim * 2, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1),
)
def forward(self, data):
# 两层消息传递
x_dict = self.conv1(data.x_dict, data.edge_index_dict)
x_dict = {k: v.relu() for k, v in x_dict.items()}
x_dict = self.conv2(x_dict, data.edge_index_dict)
return x_dict
def score_index(
self, column_embeddings: torch.Tensor, query_embeddings: torch.Tensor
) -> torch.Tensor:
"""为候选索引评分"""
# 拼接列嵌入和查询嵌入
combined = torch.cat([
column_embeddings.unsqueeze(1).expand(-1, query_embeddings.size(0), -1),
query_embeddings.unsqueeze(0).expand(column_embeddings.size(0), -1, -1),
], dim=-1)
return self.index_scorer(combined).squeeze(-1)
四、GNN 索引推荐的边界分析
训练数据的获取
GNN 需要大量"查询工作负载+最优索引"的标注数据,但现实中很难获取。可以通过模拟器生成训练数据——在不同索引配置下执行查询,记录代价作为标签。
模型泛化性
GNN 在训练数据覆盖的查询模式上表现好,但对未见过的复杂查询可能推荐不佳。建议 GNN 推荐作为候选,DBA 做最终审核。
适用边界:GNN 索引推荐适合表数量 > 50、查询模式多样的复杂工作负载。小规模数据库的索引优化用规则引擎即可。
五、总结
GNN 索引推荐将索引选择从人工经验升级为智能搜索。落地路线建议:
- 起步阶段:实现工作负载图构建器,将查询和表结构建模为异构图。
- 优化阶段:训练 GNN 索引评分模型,学习查询-列-表的依赖关系。
- 强化阶段:实现约束优化求解器,在写入代价预算下选择最优索引子集。
- 精细化阶段:建立索引效果验证机制,推荐索引上线后对比查询性能变化。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)