强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

AgensGraph 完全指南 / 第 08 章:索引与性能优化

第 08 章:索引与性能优化

8.1 索引基础

8.1.1 为什么图数据库也需要索引?

虽然图数据库的遍历操作是沿着边指针直接导航(O(1)),但起始节点的定位仍然需要索引:

查询: MATCH (p:Person {name: 'Alice'})-[:KNOWS]->(f) RETURN f

执行过程:
  1. 定位起点 "Alice"  ← 这一步需要索引!O(log N)
  2. 沿 KNOWS 边遍历  ← 直接指针导航 O(k)
  3. 获取朋友顶点      ← O(k)

没有索引:
  1. 全表扫描找 "Alice" ← O(N) 性能灾难!

8.1.2 AgensGraph 索引类型

索引类型适用场景底层实现
B-Tree等值查询、范围查询PostgreSQL B-Tree
GiST全文搜索、地理空间PostgreSQL GiST
GIN数组、JSONB 属性PostgreSQL GIN
Hash等值查询PostgreSQL Hash
Label Index标签扫描AgensGraph 专用
Graph Index图遍历优化AgensGraph 专用

8.2 创建索引

8.2.1 标签索引

标签索引是最基本的图索引,加速按标签查找顶点:

-- 为 Person 标签创建索引(通常自动创建)
CREATE INDEX ON :Person(name);

-- 查看已有索引
-- 使用 SQL 系统视图
SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = 'social_network';

8.2.2 属性索引

-- 单属性索引
CREATE INDEX ON :Person(name);
CREATE INDEX ON :Person(email);
CREATE INDEX ON :Product(price);

-- 复合属性索引
CREATE INDEX ON :Person(city, age);

-- 在边上创建索引
CREATE INDEX ON :KNOWS(since);
CREATE INDEX ON :WORKS_AT(position);

8.2.3 唯一索引(通过约束创建)

-- 唯一约束会自动创建唯一索引
CREATE CONSTRAINT person_email_unique
  ON (p:Person)
  ASSERT p.email IS UNIQUE;

-- 验证约束
SELECT conname, contype
FROM pg_constraint
WHERE conrelid = 'person'::regclass;

8.2.4 GIN 索引(JSONB/数组属性)

-- 在 SQL 层面为 JSONB 属性创建 GIN 索引
-- 适用于属性查询包含嵌套字段的场景
CREATE INDEX idx_person_props_gin
  ON person USING GIN (properties);

-- 查询嵌套属性
SELECT * FROM person
WHERE properties @> '{"address": {"city": "北京"}}';

8.2.5 全文搜索索引

-- 使用 GIN 索引支持全文搜索
CREATE INDEX idx_post_content_fts
  ON post USING GIN (to_tsvector('chinese', properties->>'content'));

8.3 查询计划分析

8.3.1 EXPLAIN 命令

-- 基本执行计划
EXPLAIN MATCH (p:Person {name: 'Alice'})-[:KNOWS]->(f:Person)
RETURN f.name;

-- 详细执行计划(包含代价和行数估算)
EXPLAIN (VERBOSE, COSTS ON)
MATCH (p:Person {name: 'Alice'})-[:KNOWS]->(f:Person)
RETURN f.name;

-- 实际执行计划(包含真实运行时间)
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
MATCH (p:Person {name: 'Alice'})-[:KNOWS]->(f:Person)
RETURN f.name;

8.3.2 执行计划解读

QUERY PLAN
────────────────────────────────────────────────────
HashAggregate  (cost=15.05..15.06 rows=1 width=32)
  → GraphScan  (cost=0.00..15.03 rows=2 width=32)
      → Nested Loop  (cost=0.56..12.00 rows=2 width=64)
          → Index Scan using person_name_idx
              on person p  (cost=0.28..8.29 rows=1)
              Index Cond: (name = 'Alice')
          → Index Scan using edge_start_idx
              on knows  (cost=0.28..3.72 rows=2)
              Index Cond: (start_id = p.id)
              Filter: (label = 'KNOWS')

关键指标

指标含义优化方向
cost估算代价越低越好
rows估算行数接近实际行数为好
actual time实际执行时间关注高耗时节点
loops循环次数嵌套循环次数多说明优化空间大
Buffers缓冲区命中hit 多为好,read 多说明 I/O 重

8.3.3 常见计划操作对比

操作说明性能何时出现
Index Scan索引扫描⭐⭐⭐⭐⭐有索引 + 精确条件
Index Only Scan索引覆盖扫描⭐⭐⭐⭐⭐查询字段都在索引中
Bitmap Index Scan位图索引扫描⭐⭐⭐⭐多条件组合
Seq Scan全表扫描无索引 / 小表
Nested Loop嵌套循环连接⭐⭐⭐小表连接
Hash Join哈希连接⭐⭐⭐⭐等值连接、中等表
Merge Join合并连接⭐⭐⭐⭐排序数据连接

8.4 索引优化策略

8.4.1 选择性分析

索引的效果取决于属性的选择性(Selectivity):

-- 查看属性值分布
SELECT properties->>'city' AS city, count(*) AS cnt
FROM person
GROUP BY city
ORDER BY cnt DESC;

-- 选择性 = 不同值数量 / 总行数
-- 选择性接近 1 → 索引效果好
-- 选择性接近 0 → 索引效果差
选择性索引效果示例
> 0.8⭐⭐⭐⭐⭐email, phone, id
0.3 - 0.8⭐⭐⭐⭐name, city
0.1 - 0.3⭐⭐age, department
< 0.1gender, status (布尔)

8.4.2 复合索引设计

-- 考虑查询模式来设计复合索引
-- 查询: WHERE city = '北京' AND age > 25

-- ✅ 正确顺序:高选择性列在前
CREATE INDEX ON :Person(city, age);

-- ❌ 错误顺序:低选择性列在前
CREATE INDEX ON :Person(age, city);

复合索引顺序规则

  1. 等值条件列放在前面
  2. 范围条件列放在后面
  3. 选择性高的列优先

8.3.3 覆盖索引

-- 创建包含查询所需字段的索引
CREATE INDEX idx_person_covering
  ON person (properties->>'name')
  INCLUDE (properties->>'age', properties->>'city');

-- 查询可以直接从索引获取数据,无需回表
SELECT properties->>'name', properties->>'age'
FROM person
WHERE properties->>'name' = 'Alice';

8.5 查询优化技巧

8.5.1 Cypher 查询优化

-- ❌ 低效:先遍历再过滤
MATCH (p:Person)-[:KNOWS]->(f:Person)
WHERE p.name = 'Alice'
RETURN f.name;

-- ✅ 高效:先过滤再遍历
MATCH (p:Person {name: 'Alice'})-[:KNOWS]->(f:Person)
RETURN f.name;

8.5.2 使用 Profile 分析

-- profile 会实际执行查询并返回统计信息
PROFILE
MATCH (p:Person)-[:KNOWS*1..3]->(fof:Person)
WHERE p.name = 'Alice'
RETURN DISTINCT fof.name;

8.5.3 优化 Checklist

检查项说明工具
✅ 起始节点是否高效定位使用索引属性定位EXPLAIN
✅ 是否避免了全表扫描检查 Seq ScanEXPLAIN
✅ 过滤条件是否尽早执行WHERE 在 MATCH 中EXPLAIN
✅ 变长路径是否有深度限制*1..N 而非 *代码审查
✅ 返回字段是否精确RETURN 特定字段而非 *代码审查
✅ 是否使用了 LIMIT避免返回过多结果代码审查
✅ 索引是否覆盖常用查询pg_indexesSQL 查询
✅ 统计信息是否最新ANALYZE 命令ANALYZE table;

8.6 统计信息管理

8.6.1 更新统计信息

-- 更新特定表的统计信息
ANALYZE person;
ANALYZE knows;

-- 更新所有表的统计信息
ANALYZE;

-- 查看统计信息
SELECT
  schemaname,
  tablename,
  n_live_tup AS live_rows,
  n_dead_tup AS dead_rows,
  last_analyze,
  last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC;

8.6.2 统计信息对优化器的影响

-- 查看某列的统计信息
SELECT
  attname AS column_name,
  n_distinct AS distinct_values,
  most_common_vals AS common_values,
  most_common_freqs AS common_freqs
FROM pg_stats
WHERE tablename = 'person' AND attname = 'properties';

8.7 业务场景:大规模社交网络优化

场景数据

实体数量说明
Person 顶点1000 万用户
KNOWS 边5000 万好友关系
FOLLOWS 边1 亿关注关系
Post 顶点2 亿动态

优化前

-- 查询:找 Alice 的二度好友(未优化)
MATCH (alice:Person)-[:KNOWS]->()-[:KNOWS]->(fof:Person)
WHERE alice.name = 'Alice'
RETURN DISTINCT fof.name;
-- 执行时间: 45 秒

优化步骤

-- 1. 创建索引
CREATE INDEX ON :Person(name);
CREATE INDEX ON :KNOWS(start_id);

-- 2. 优化查询
MATCH (alice:Person {name: 'Alice'})-[:KNOWS*2]->(fof:Person)
RETURN DISTINCT fof.name
LIMIT 100;
-- 执行时间: 0.3 秒

优化效果对比

指标优化前优化后提升
执行时间45s0.3s150x
缓冲区读取500K2K250x
CPU 时间42s0.28s150x

8.8 维护操作

8.8.1 VACUUM 与 ANALYZE

-- 清理死元组(不影响在线服务)
VACUUM (VERBOSE) person;

-- 清理并回收空间(需要排他锁)
VACUUM FULL person;

-- 更新统计信息
ANALYZE person;

-- 自动 VACUUM 设置
ALTER SYSTEM SET autovacuum = on;
ALTER SYSTEM SET autovacuum_vacuum_scale_factor = 0.1;
ALTER SYSTEM SET autovacuum_analyze_scale_factor = 0.05;
SELECT pg_reload_conf();

8.8.2 索引维护

-- 查看索引使用情况
SELECT
  indexrelname AS index_name,
  idx_scan AS times_used,
  idx_tup_read AS tuples_read,
  idx_tup_fetch AS tuples_fetched,
  pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE schemaname = 'social_network'
ORDER BY idx_scan DESC;

-- 重建索引(消除碎片)
REINDEX INDEX CONCURRENTLY person_name_idx;

-- 查找未使用的索引
SELECT indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND schemaname NOT IN ('pg_catalog', 'information_schema');

8.9 本章小结

要点说明
索引作用加速起始节点定位,遍历本身是 O(k)
核心索引B-Tree(属性查询)、GIN(JSONB/数组)
EXPLAIN理解执行计划是优化的关键
选择性高选择性列适合建索引
覆盖索引减少回表查询
统计信息定期 ANALYZE 保持优化器准确

8.10 练习

  1. 使用 EXPLAIN ANALYZE 对比有无索引时的查询性能差异。
  2. 为一个包含 10 万顶点的测试图创建合理的索引策略。
  3. 优化一个包含 WHERE 条件的变长路径查询。
  4. 编写脚本定期检查索引使用率并报告未使用的索引。

8.11 扩展阅读