上一篇文章介紹了在 Neo4j 檢視並優化執行計畫,今天會延伸這個主題,做更深入的分享。在簡單的查詢中,交給 Neo4j 決定即可;但是在非常龐大的資料庫,或是比較複雜的查詢,我們還是可以試著對執行計畫去做微調,以達到最佳的執行效能。
今天查詢的命題是:請找出 Wartian Herkku 客戶曾經購買的海鮮產品和訂單,執行計畫如下
PROFILE MATCH (c:Customer { companyName: 'Wartian Herkku' })
-[:PURCHASED]->(o:Order)
-[:ORDERS]->(p:Product)
-[:PART_OF]->(:Category { categoryName: 'Seafood' })
RETURN *
Cypher version: CYPHER 4.1, planner: COST, runtime: PIPELINED. 1363 total db hits in 390 ms.
從上面兩張圖可以看出,Neo4j 決定的執行計畫是:
這個查詢不算複雜,只是串接了好幾層關係,接下來我們試試看是否能干涉 Neo4j 的執行計畫,以及結果是更好還是更壞~
使用 USING
可以明確要求 Neo4j 把某個節點或是索引當作起始點,以上述為例,以索引找出客戶 Wartian Herkku 是整個查詢的起始點,使用的運算子是 NodeIndexSeek。
那麼,從含有索引的節點開始搜尋,就一定是最佳策略嗎?通常都是不錯的選擇,但也許還有優化的空間!
以下敘述試著指定另一個查詢起始點,就是先找到海鮮類產品,並且是掃描所有 Category 節點。乍聽之下是個不可取的計畫,以往我們學習關聯式資料庫時,遇到大資料量也通常避免掃描整個資料表。
但事實上,以北風資料庫為例,所有產品分類也才 8 種而已!
PROFILE MATCH (c:Customer { companyName: 'Wartian Herkku' })
-[:PURCHASED]->(o:Order)
-[:ORDERS]->(p:Product)
-[:PART_OF]->(cat:Category { categoryName: 'Seafood' })
USING SCAN cat:Category
RETURN *
上述指令的執行計畫如下:
Cypher version: CYPHER 4.1, planner: COST, runtime: PIPELINED. 160 total db hits in 85 ms
可以看到 Neo4j 這次有兩個查詢起始點,一個使用 Cusomter 的索引找到客戶,一個掃描 Category 節點找到海鮮分類,在中間做 JOIN,這樣比原先的依序查詢與不斷重展開關係要快得多,只花費 160 db hits!
接著我們試著在產品分類名稱加上索引,並且同時指定為起始點
CREATE INDEX ON :Category(categoryName)
PROFILE MATCH (c:Customer { companyName: 'Wartian Herkku' })
-[:PURCHASED]->(o:Order)
-[:ORDERS]->(p:Product)
-[:PART_OF]->(cat:Category { categoryName: 'Seafood' })
USING INDEX cat:Category(categoryName)
RETURN *
Cypher version: CYPHER 4.1, planner: COST, runtime: PIPELINED. 127 total db hits in 33 ms
可以發現除了原本的雙起始點和 JOIN 之外,也微幅的降低了 db hits。
上述查詢的 JOIN,是發生在 Product 節點上,運算子是 NodeHashJoin,那是否我們可以自訂 JOIN 的時機呢?當然也可以!
先更改需求如下:請找出 Wartian Herkku 客戶曾經購買來自日本大阪供應商的海鮮產品和訂單
PROFILE MATCH (c:Customer { companyName: 'Wartian Herkku' })
-[:PURCHASED]->(o:Order)
-[:ORDERS]->(p:Product)
-[:PART_OF]->(cat:Category { categoryName: 'Seafood' }),
(p)<-[:SUPPLIES]-(s:Supplier {city: 'Osaka'} )
USING INDEX cat:Category(categoryName)
RETURN *
上述查詢的 db hits = 572
試著強制更改連接時機如下:
PROFILE MATCH (c:Customer { companyName: 'Wartian Herkku' })
-[:PURCHASED]->(o:Order)
-[:ORDERS]->(p:Product)
-[:PART_OF]->(cat:Category { categoryName: 'Seafood' }),
(p)<-[:SUPPLIES]-(s:Supplier {city: 'Osaka'} )
USING INDEX cat:Category(categoryName)
USING JOIN ON p
RETURN *
得到的執行計畫 db hits = 319,這邊就請大家自行練習,不再附圖~
Neo4j 有非常多的執行計畫運算子,上述查詢用到的只是一小部分,以下再列出一些常見的運算子給大家參考。
表格中的 Leaf 表示該運算子可以當作查詢的起始點;Eager 則表示該運算子會確保在自己的執行階段中,資料的運算完全結束後,才送往下一個運算子。
雖然執行計畫的流程圖上,是由許多運算子依序執行完成,但實際上大部分的運算子都是一邊接收資料、一邊同步運算並往下繼續送,這樣才能加快運算與查詢速度;所以 Eager 類的運算子出現時,須考慮其必要性,否則往往會是效能的瓶頸。
Execution Plan Operator | Leaf(Start point) | Eager |
---|---|---|
NodeByIdSeek | Y | |
NodeIndexSeek | Y | |
NodeUniqueIndexSeek | Y | |
NodeIndexSeekByRange | Y | |
NodeUniqueIndexSeekByRange | Y | |
NodeIndexContainsScan | Y | |
NodeIndexScan | Y | |
NodeByLabelScan | Y | |
AllNodesScan | Y | |
DirectedRelationshipByIdSeek | Y | |
UndirectedRelationshipByIdSeek | Y | |
ProduceResults | ||
Filter | ||
Expand(All), Expand(Into) | ||
OptionalExpand(All) | ||
OptionalExpand(Into) | ||
Sort | Y | |
Top | Y | |
Eager | Y | |
EagerAggregation | Y |
NodeByIdSeek
MATCH (n) WHERE id(n) = 123 RETURN n
DirectedRelationshipByIdSeek
MATCH (n)-[r]->(m) WHERE id(r) = 123 RETURN *
UndirectedRelationshipByIdSeek
MATCH (n)-[r]-(m) WHERE id(r) = 123 RETURN *
很容易理解的是,直接以 id 取得節點或關係,是最快速的!
NodeIndexSeek
NodeUniqueIndexSeek
MATCH (c:Customer { companyName: 'Wartian Herkku' }) RETURN c
如果 companyName 具有唯一性約束,則會是 NodeUniqueIndexSeek 運算子
NodeIndexSeekByRange
NodeUniqueIndexSeekByRange
MATCH (c:Customer) WHERE c.companyName STARTS WITH 'Wa' RETURN c
MATCH (c:Customer) WHERE c.companyName > 'T' RETURN c
NodeIndexContainsScan
MATCH (c:Customer) WHERE c.companyName CONTAINS 'tian' RETURN c
這個運算子需要檢查整個索引表全部的值,會比 NodeIndexSeek 還要慢,但還是比直接掃描相同標籤還要快。
NodeIndexScan
MATCH (c:Customer) WHERE exists(c.companyName) RETURN c
NodeByLabelScan
假設 :Custoemr(country) 沒有索引。
MATCH (c:Customer { country: 'Finland' }) RETURN c
AllNodesScan
掃描所有節點,請完畢避免這個運算子的出現
MATCH (c { country: 'Finland' }) RETURN c
以上只是一小部分運算子,搭配簡單的範例讓人容易理解,全部的運算子清單還是請直接看官網囉
https://neo4j.com/docs/cypher-manual/current/execution-plans/#execution-plan-operators-summary
關於效能優化與執行計畫運算子,就先分享到這,謝謝大家~