ArangoDB生产环境查询性能远慢于本地的问题排查求助
问题:ArangoDB生产环境查询性能异常排查
环境信息
- 生产环境:裸金属服务器,Intel Xeon E-2386G 6c/12t @3.50GHz、32GB ECC 3200MHz内存、2x512GB NVME SSD RAID1,Rocky Linux 9 + ArangoDB 3.9.3
- 本地环境:Fedora 36 + ArangoDB 3.8.7
- 数据规模:生产环境
searches集合41k文档、results_linkers集合350k文档;本地对应1k、27k文档
现象
同一查询本地毫秒级完成,生产环境耗时数秒,且生产环境数据量较小时也存在慢查询。
生产环境查询执行计划
Query String (415 chars, cacheable: false): FOR entry in searches FILTER entry.session_key == '21150583' LET counters = ( FOR result in results_linkers FILTER result.search_key == entry._key AND result.validation_automatic.status == 'completed' COLLECT action = result.validation_automatic.action WITH COUNT INTO action_counter RETURN { 'action': action, 'count': action_counter } ) LIMIT 20 RETURN { 'search': entry, 'counters': counters } Execution plan: Id NodeType Calls Items Runtime [s] Comment 1 SingletonNode 1 1 0.00000 * ROOT 2 EnumerateCollectionNode 1 20 0.00737 - FOR entry IN searches /* full collection scan */ FILTER (entry.`session_key` == "21150583") /* early pruning */ 14 LimitNode 1 20 0.00001 - LIMIT 0, 20 18 SubqueryStartNode 1 40 0.00001 - LET counters = ( /* subquery begin */ 6 EnumerateCollectionNode 7136 7135580 3.33341 - FOR result IN results_linkers /* full collection scan, projections: `search_key`, `validation_automatic` */ 7 CalculationNode 7136 7135580 1.36629 - LET #9 = ((result.`search_key` == entry.`_key`) && (result.`validation_automatic`.`status` == "completed")) /* simple expression */ /* collections used: result : results_linkers, entry : searches */ 8 FilterNode 2 1854 0.12298 - FILTER #9 9 CalculationNode 2 1854 0.00024 - LET #11 = result.`validation_automatic`.`action` /* attribute expression */ /* collections used: result : results_linkers */ 10 CollectNode 1 56 0.00013 - COLLECT action = #11 AGGREGATE action_counter = LENGTH() /* hash */ 17 SortNode 1 56 0.00001 - SORT action ASC /* sorting strategy: standard */ 11 CalculationNode 1 56 0.00002 - LET #13 = { "action" : action, "count" : action_counter } /* simple expression */ 19 SubqueryEndNode 1 20 0.00001 - RETURN #13 ) /* subquery end */ 15 CalculationNode 1 20 0.00001 - LET #15 = { "search" : entry, "counters" : counters } /* simple expression */ /* collections used: entry : searches */ 16 ReturnNode 1 20 0.00000 - RETURN #15 Indexes used: none Optimization rules applied: Id RuleName 1 move-calculations-up 2 move-filters-up 3 move-calculations-up-2 4 move-filters-up-2 5 move-calculations-down 6 reduce-extraction-to-projection 7 move-filters-into-enumerate 8 splice-subqueries Query Statistics: Writes Exec Writes Ign Scan Full Scan Index Filtered Peak Mem [b] Exec Time [s] 0 0 7176820 0 7174966 655360 4.83091 Query Profile: Query Stage Duration [s] initializing 0.00000 parsing 0.00004 optimizing ast 0.00001 loading collections 0.00000 instantiating plan 0.00003 optimizing plan 0.00031 executing 4.83051 finalizing 0.00002
本地环境查询执行计划
Query String (414 chars, cacheable: false): FOR entry in searches FILTER entry.session_key == '3307542' LET counters = ( FOR result in results_linkers FILTER result.search_key == entry._key AND result.validation_automatic.status == 'completed' COLLECT action = result.validation_automatic.action WITH COUNT INTO action_counter RETURN { 'action': action, 'count': action_counter } ) LIMIT 20 RETURN { 'search': entry, 'counters': counters } Execution plan: Id NodeType Calls Items Runtime [s] Comment 1 SingletonNode 1 1 0.00001 * ROOT 2 EnumerateCollectionNode 1 1 0.00097 - FOR entry IN searches /* full collection scan */ FILTER (entry.`session_key` == "3307542") /* early pruning */ 14 LimitNode 1 1 0.00001 - LIMIT 0, 20 18 SubqueryStartNode 1 2 0.00001 - LET counters = ( /* subquery begin */ 6 EnumerateCollectionNode 28 27775 0.02148 - FOR result IN results_linkers /* full collection scan, projections: `search_key`, `validation_automatic` */ 7 CalculationNode 28 27775 0.00866 - LET #9 = ((result.`search_key` == entry.`_key`) && (result.`validation_automatic`.`status` == "completed")) /* simple expression */ /* collections used: result : results_linkers, entry : searches */ 8 FilterNode 1 97 0.00169 - FILTER #9 9 CalculationNode 1 97 0.00002 - LET #11 = result.`validation_automatic`.`action` /* attribute expression */ /* collections used: result : results_linkers */ 10 CollectNode 1 3 0.00002 - COLLECT action = #11 AGGREGATE action_counter = LENGTH() /* hash */ 17 SortNode 1 3 0.00001 - SORT action ASC /* sorting strategy: standard */ 11 CalculationNode 1 3 0.00001 - LET #13 = { "action" : action, "count" : action_counter } /* simple expression */ 19 SubqueryEndNode 1 1 0.00001 - RETURN #13 ) /* subquery end */ 15 CalculationNode 1 1 0.00001 - LET #15 = { "search" : entry, "counters" : counters } /* simple expression */ /* collections used: entry : searches */ 16 ReturnNode 1 1 0.00001 - RETURN #15 Indexes used: none Optimization rules applied: Id RuleName 1 move-calculations-up 2 move-filters-up 3 move-calculations-up-2 4 move-filters-up-2 5 move-calculations-down 6 reduce-extraction-to-projection 7 move-filters-into-enumerate 8 splice-subqueries Query Statistics: Writes Exec Writes Ign Scan Full Scan Index Filtered Peak Mem [b] Exec Time [s] 0 0 28901 0 28804 360448 0.03434 Query Profile: Query Stage Duration [s] initializing 0.00002 parsing 0.00012 optimizing ast 0.00002 loading collections 0.00001 instantiating plan 0.00007 optimizing plan 0.00117 executing 0.03293 finalizing 0.00003
额外疑问
采用类似传统关系型数据库的使用方式:单一大集合,通过key查询数据是否合理?数据持续增长时性能是否会持续下降?
解决方案
1. 索引优化(核心)
从执行计划看,两个集合均在执行全表扫描,这是性能瓶颈的核心原因:
- 给
searches集合的session_key字段创建哈希索引:
作用:直接定位符合CREATE INDEX idx_searches_session_key ON searches(session_key)session_key的文档,避免全表扫描,减少后续子查询的执行次数。 - 给
results_linkers集合创建复合哈希索引:
作用:同时过滤CREATE INDEX idx_results_linkers_search_status ON results_linkers(search_key, `validation_automatic.status`)search_key和validation_automatic.status条件,直接获取符合要求的文档,无需全表扫描后再过滤。
2. 查询改写优化
原查询中,子查询会对每个符合条件的entry执行一次全表扫描,可通过调整逻辑减少重复扫描:
FOR entry IN searches FILTER entry.session_key == '21150583' LIMIT 20 LET counters = ( FOR result IN results_linkers FILTER result.search_key == entry._key AND result.validation_automatic.status == 'completed' COLLECT action = result.validation_automatic.action WITH COUNT INTO action_counter RETURN { 'action': action, 'count': action_counter } ) RETURN { 'search': entry, 'counters': counters }
注:将LIMIT 20提前到子查询之前,减少需要执行子查询的entry数量(原计划中先扫描全表再过滤session_key,加索引后LIMIT提前能进一步降低子查询执行次数)。
3. 集合使用合理性说明
- 单一大集合通过key查询的方式在ArangoDB中是合理的,但需注意:
- 若集合数据量持续增长(如千万级以上),必须确保查询依赖的字段都有合适的索引,否则全表扫描性能会急剧下降。
- 若数据结构差异较大,拆分多个集合更利于维护和索引优化;若数据结构统一,单集合方案可行。
- 长期性能保障:
- 定期清理过期数据,避免集合无限膨胀。
- 监控索引使用率,及时删除无效索引。
- 针对高频查询优化执行计划,必要时使用视图(View)预聚合数据。
内容的提问来源于stack exchange,提问作者Kei
相关产品推荐
相关产品推荐

