iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 17
0
AI & Data

看圖說故事,讓 Neo4j 重新詮釋你的資料庫系列 第 17

Neo4j 資料庫查詢效能優化 - 起始點

上一篇文章介紹了在 Neo4j 檢視並優化執行計畫,今天會延伸這個主題,做更深入的分享。在簡單的查詢中,交給 Neo4j 決定即可;但是在非常龐大的資料庫,或是比較複雜的查詢,我們還是可以試著對執行計畫去做微調,以達到最佳的執行效能。

今天查詢的命題是:請找出 Wartian Herkku 客戶曾經購買的海鮮產品和訂單,執行計畫如下

PROFILE MATCH (c:Customer { companyName: 'Wartian Herkku' })
-[:PURCHASED]->(o:Order)
-[:ORDERS]->(p:Product)
-[:PART_OF]->(:Category { categoryName: 'Seafood' })
RETURN *

Neo4j Profile no USING 1

Neo4j Profile no USING 2

Cypher version: CYPHER 4.1, planner: COST, runtime: PIPELINED. 1363 total db hits in 390 ms.

從上面兩張圖可以看出,Neo4j 決定的執行計畫是:

  1. NodeIndexSeek
    從 :Customer(companyName) 索引找到 'Wartian Herkku'
  2. Expand(All)
    找到這位客戶的所有 :PURCHASED 關係
  3. Filter
    找到上述 :PURCHASED 關係的目的節點(訂單)
  4. Expand(All)
    找到上述訂單的所有 :ORDER 關係
  5. Filter
    找到上述 :ORDER 關係的目的節點(產品)
  6. Expand(All)
    找到上述產品的所有 :PART_OF 關係
  7. Filter
    找到上述 :PART_OF 關係的目的節點(分類),而且名稱是 'Seafood'
  8. ProduceResult
    產生最後結果

這個查詢不算複雜,只是串接了好幾層關係,接下來我們試試看是否能干涉 Neo4j 的執行計畫,以及結果是更好還是更壞~

掃描提示(USING SCAN)

使用 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 *

上述指令的執行計畫如下:

Neo4j Profile USING SCAN

Cypher version: CYPHER 4.1, planner: COST, runtime: PIPELINED. 160 total db hits in 85 ms

可以看到 Neo4j 這次有兩個查詢起始點,一個使用 Cusomter 的索引找到客戶,一個掃描 Category 節點找到海鮮分類,在中間做 JOIN,這樣比原先的依序查詢與不斷重展開關係要快得多,只花費 160 db hits!

索引提示(USING INDEX)

接著我們試著在產品分類名稱加上索引,並且同時指定為起始點

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。

連接提示(USING JOIN)

上述查詢的 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

關於效能優化與執行計畫運算子,就先分享到這,謝謝大家~


上一篇
Neo4j 資料庫查詢效能優化 - 執行計畫
下一篇
Neo4j Data Science - 評估記憶體與建立子圖
系列文
看圖說故事,讓 Neo4j 重新詮釋你的資料庫30

尚未有邦友留言

立即登入留言