neo4j學習總結--第八課 Cypher 查詢調優與執行計劃

一、查詢調優

1.1查詢如何執行

Cypher執行引擎會將每個Cypher查詢都轉爲一個執行計劃。在執行查詢時,執行計劃將告知Neo4j執行什麼樣的操作。

1.2查詢性能分析

    查看執行計劃對查詢進行分析時有兩個Cypher語句可用:

EXPLAIN

      如果只想查看查詢計劃,而不想運行該語句,可以在查詢語句中加入EXPLAIN。此時,該語句將返回空結果,對數據庫不會做出任何改變。

PROFILE

       如果想運行查詢語句並查看哪個運算符佔了大部分的工作,可以使用PROFILE。此時,該語句將被運行,並跟蹤傳遞了多少行數據給每個運算符,以及每個運算符與存儲層交互了多少以獲取必要的數據。注意,加入PROFILE的查詢語句將佔用更多的資源,所以除非真正在做性能分析,否則不要使用PROFILE。

1.3查詢調優舉例

寫一個找到'Tom Hanks'的查詢語句。比較初級的做法是按如下方式寫:

MATCH (p { name: 'Tom Hanks' })

RETURN p

這個查詢將會找到'Tom Hanks'節點,但是隨着數據庫中節點數的增加,該查詢將越來越慢。可以通過使用PROFILE來找到原因。

可以在查詢調優小節獲得更多關於查詢調優選項的信息。這裏只是在查詢前面加入一個PROFILE的前綴。

PROFILE

MATCH (p { name: 'Tom Hanks' })

RETURN p

     

    首先需要記住的是,查看執行計劃應該從底端往上看。在這個過程中,我們注意到從最後一行開始的Rows列中的數字遠高於給定的name屬性爲'Tom Hanks'的一個節點。在Operator列中我們看到AllNodeScan被使用到了,這意味着查詢計劃器掃描了數據庫中的所有節點。

      向上移動一行看Filter運算符,它將檢查由AllNodeScan傳入的每個節點的name屬性。這看起來是一種非常低效的方式來查找'Tom Hanks'。

        解決這個問題的辦法是,無論什麼時候我們查詢一個節點,都應該指定一個標籤來幫助查詢計劃器縮小搜索空間的範圍。對於這個查詢,可簡單地添加一個Person標籤。

PROFILE

MATCH (p:Person { name: 'Tom Hanks' })

RETURN p

     

這次最後一行Rows的值已經降低了,這裏沒有掃描到之前掃描到的那些節點。NodeByLabelScan運算符表明首先在數據庫中做了一個針對所有Person節點的線性掃描。一旦完成後,後續將針對所有節點執行Filter運算符,依次比較每個節點的name屬性。

    這在某些情況下看起來還可以接受,但是如果頻繁通過name屬性來查詢Person,針對帶有Person標籤的節點的name屬性創建索引將獲得更好的性能。

CREATE INDEX ON :Person(name)

現在再次運行該查詢將運行得更快。

PROFILE

MATCH (p:Person { name: 'Tom Hanks' })

RETURN p

查詢計劃下降到單一的行並使用了NodeIndexSeek運算符,它通過模式索引尋找到對應的節點。

1.4、USING語句

     當執行一個查詢時,Neo4j需要決定從查詢圖中的哪兒開始匹配。這是通過查看MATCH語句和WHERE中的條件這些信息來找到有用的索引或者其他開始節點。

      然而,系統選定的索引未必總是最好的選擇。

        可以通過USING來強制Neo4j使用一個特定的開始點。這個被稱爲計劃器提示。這裏有三種類型的計劃器提示:索引提示,掃描提示和連接(join)提示。

1.4.1、索引提示

       索引提示用於告知計劃器無論在什麼情況下都應使用指定的索引作爲開始點。對於某些特定值的查詢,索引統計信息不準確,它可能導致計劃器選擇了非最優的索引。對於這種情況,索引提示就有它的用處。使用在MATCH語句之後添加USING INDEX variable:Label(property)來補充索引提示

   也可以補充多個索引提示,但是多個開始點會在後面的查詢計劃中潛在地需要額外的連接。

使用索引提示查詢

      上面的查詢沒有選擇索引來生成計劃。這是因爲圖非常小,對於小數據庫標籤掃描很快。然而,查詢的性能通常以dbhit的值來度量。下面可以看到使用索引將獲得更好的查詢性能。

PROFILE

MATCH (p:Person { name: 'Tom Hanks' })

USING INDEX p: Person (name)

RETURN p.born AS column

使用多個索引提示查詢

PROFILE

MATCH (p:Person { name: 'Tom Hanks' })-[r]->( m:Movie {title:"You've Got Mail"

})

USING INDEX p: Person (name)

USING INDEX m: Movie (title)

RETURN p.born AS column

使用稍微更好的計劃返回'Barbara Liskov'的出生年。

1.4.2、掃描提示

      如果查詢匹配到索引的大部分數據, 那麼可以不用索引, 走標籤掃描

      如果查詢匹配到一個索引的大部分,它可以更快地掃描標籤並過濾掉不匹配的節點。通過在MATCH語句後面使用USING SCAN variable:Label可以做到這一點。它將強制Cypher不使用本應使用的索引,而採用標籤掃描。

標籤掃描提示

使用USING SCAN掃描一個標籤上的所有節點並過濾結果集將獲得最好的性能。

PROFILE

MATCH (s: Person)

USING SCAN s: Person

WHERE s.born < 1939

RETURN s.born AS column

1.4.3、連接(join)提示

      強制在特定的點進行連接。連接和後續的連接提示將強制計劃器查看額外的開始點。在那些沒有更多好的開始點的情況下,可能會選取一些很差的開始點。這將對查詢性能產生負面的效果。在其他情況下,這些提示會強制計劃器選取一些看似不好的開始點,然後它會被證明是好的。

提示在單個節點上的連接

MATCH (n1: Person { name:'Keanu Reeves' })-[: ACTED_IN]->(m1: Movie)<-[: DIRECTED]-(n2: Person {name:'Lilly Wachowski' })-[: DIRECTED]->(m2: Movie { title:'The Matrix Reloaded' })

USING INDEX n1: Person (name)

USING INDEX n2: Person (name)

USING JOIN ON m1

RETURN m1.title AS column

提示在多個節點上的連接

MATCH (n1: Person { name:'Keanu Reeves' })-[: ACTED_IN]->(m1: Movie)<-[: DIRECTED]-(n2: Person {name:'Lilly Wachowski' })-[: DIRECTED]->(m2: Movie { title:'The Matrix Reloaded' })

USING INDEX n1: Person (name)

USING JOIN ON m1,n2

RETURN m1.title AS column

 

二、執行計劃(瞭解)

    本小節主要描述執行計劃(Execution Plan)中的運算符。

       Neo4j將執行一個查詢的任務分解爲一些被稱爲運算符的小塊。每個運算符負責整個查詢中的一小部分。這些以模式(pattern)形式連接在一起的運算符被稱爲一個執行計劃。

每個運算符用如下統計信息來註解:

  1. Rows 運算符產生的行數。只有帶有profile的查詢纔有。
  2. EstimatedRows由運算符所產生的預估的行數。編譯器使用這個估值來選擇合適的執行計劃。
  3. DbHits每個運算符都會向Neo4j存儲引擎請求像檢索或者更新數據這樣的工作。數據庫命中次數就是DbHits

2.1開始點運算符

這些運算符用於找到圖的開始點。

全節點掃描

從節點庫中讀取所有節點。實參中的變量將包含所有這些節點。如果查詢中使用這個運算符,在任何大點的數據庫中就會遭遇性能問題。

MATCH (n)

RETURN n

通過id搜索有向關係

從關係庫中通過id來讀取一個或多個關係將返回關係和兩端的節點。

MATCH (n1)-[r]->()

WHERE id(r)= 0

RETURN r, n1

通過id尋找節點

從節點庫中通過id讀取一個或多個節點。

MATCH (n)

WHERE id(n)= 0

RETURN n

通過標籤掃描檢索節點

使用標籤索引 ,從節點的標籤索引中獲取擁有指定標籤的所有節點。

MATCH (person:Person)

RETURN person

通過索引檢索節點

使用索引搜索節點。節點變量和使用的索引在運算符的實參中。如果索引是一個唯一性索引,運算符將由一個被稱爲NodeUniqueIndexSeek的替代。

MATCH (location:Location { name: 'Malmo' })

RETURN location

通過索引範圍(range)尋找節點

使用索引檢索節點,節點的屬性值滿足給定的字符串前綴。這個運算符可用於STARTS WITH和比較符號,如<,>和>=。

MATCH (l:Location)

WHERE l.name STARTS WITH 'Lon'

RETURN l

通過索引包含(contains)掃描檢索節點

一個節點的包含掃描將遍歷存儲在索引中的所有值,搜索實體中是否包含指定的字符串。這個比索引檢索要慢,因爲需要檢查所有的實體,但也比直接通過標籤掃描然後過濾屬性庫要更快一些。

MATCH (l:Location)

WHERE l.name CONTAINS 'al'

RETURN l

通過索引掃描檢索節點

索引掃描將遍歷存儲在索引中的所有值,它可以找到擁有特定標籤和特定屬性的所有節點(如exists(n.prop))。

MATCH (l:Location)

WHERE exists(l.name)

RETURN l

通過id尋找無方向關係

從關係庫中通過id讀取一個或多個關係。對於每個關係將返回兩行,它們分別爲關係的開始節點和結束節點。

MATCH (n1)-[r]-()

WHERE id(r)= 1

RETURN r, n1

2.2 Expand運算符

這些運算符通過展開圖模式來探索圖。

Expand All

給定一個開始節點,expand-all將根據關係中的模式沿開始節點或者結束節點展開。它也能處理變長模式的關係。

MATCH (p:Person { name: 'me' })-[:FRIENDS_WITH]->(fof)

RETURN fof

Expand Into

當開始和結束節點都已經找到時,expand-into用於找到兩個節點之間連接的所有關係。

MATCH (p:Person { name: 'me' })-[:FRIENDS_WITH]->(fof)-->(p)

RETURN fof

可選Expand All

可選expand從一個給定節點開始遍歷關係,確保在返回結果之前斷言得到處理。如果沒有找到匹配的關係,則返回null併產生一個結束節點變量。

MATCH (p:Person)

OPTIONAL MATCH (p)-[works_in:WORKS_IN]->(l)

WHERE works_in.duration > 180

RETURN p, l

2.3組合運算符

組合運算符用於將其他運算符拼接在一起。

Apply

Apply以嵌套循環的方式工作。Apply運算符左端返回的每一行作爲右端運算符的輸入,然後Apply將產生組合的結果。

MATCH (p:Person)-[:FRIENDS_WITH]->(f)

WITH p, count(f) AS fs

WHERE fs > 2

OPTIONAL MATCH (p)-[:WORKS_IN]->(city)

RETURN city.name

找到多於兩個朋友的所有人,並返回它們所工作的城市。

SemiApply

測試一個模式斷言的存在性。SemiApply從它的子運算符中獲取一行,並將其作爲右端的葉節點運算符的輸入。如果右端運算符樹至少產生一行結果,左端的這一行由SemiApply運算符產生。這使得SemiApply成爲一個過濾運算符,可大量運用在查詢的模式斷言中。

MATCH (p:Person)

WHERE (p)-[:FRIENDS_WITH]->()

RETURN p.name

查到有朋友的所有人。

AntiSemiApply

測試一個模式斷言的存在性。AntiSemiApply具有的功能與SemiApply相反,它進行反向過濾。

MATCH (me:Person { name: "me" }),(other:Person)

WHERE NOT (me)-[:FRIENDS_WITH]->(other)

RETURN other.name

查找所有不是我朋友的人的名字。

LetSemiApply

測試模式斷言的存在性。當一個查詢包含多個模式斷言時,LetSemiApply將用於處理它們中的第一個。它會記錄斷言的評估結果,但會留下過濾器到另外一個運算符。

MATCH (other:Person)

WHERE (other)-[:FRIENDS_WITH]->() OR (other)-[:WORKS_IN]->()

RETURN other.name

找到有一個朋友或者在某地工作的所有人的名字。LetSemiApply運算符將用於檢查每個人的 FRIENDS_WITH關係。

LetAntiSemiApply

測試模式斷言的存在性。當一個查詢包含多個模式斷言時,LetAntiSemiApply將用於處理它們中的第一個。它會記錄斷言的評估結果,但會留下過濾器到另外一個運算符。下面的查詢語句查找沒有任何朋友或者在某地工作的所有人。

MATCH (other:Person)

WHERE NOT ((other)-[:FRIENDS_WITH]->()) OR (other)-[:WORKS_IN]->()

RETURN other.name

查找沒有任何朋友或者在某地工作的所有人。LetAntiSemiApply運算符將用於檢查每個人的FRIENDS_WITH關係的存在性。

SelectOrSemiApply

測試一個模式斷言的存在性並評估一個斷言。這個運算符允許將一般的斷言與檢查存在性的斷言放在一起。首先評估普通表達式,僅當它返回false時模式斷言纔會執行。

MATCH (other:Person)

WHERE other.age > 25 OR (other)-[:FRIENDS_WITH]->()

RETURN other.name

查找有朋友或者年齡大於25的所有人的名字。

SelectOrAntiSemiApply

測試一個模式斷言的存在性並評估一個斷言。

MATCH (other:Person)

WHERE other.age > 25 OR NOT (other)-[:FRIENDS_WITH]->()

RETURN other.name

查找沒有朋友或者年齡大於25的所有人的名字。

ConditionalApply

檢查一個變量是否不爲null,如果是,那麼就執行右邊的部分。

MERGE (p:Person { name: 'Andres' })

ON MATCH SET p.exists = TRUE

查看是否存在一個名爲'Andres'的人。如果找到,就設置他的exists屬性爲true。

AntiConditionalApply

檢查一個變量是否爲null,如果是,那麼就執行右邊的部分。

查詢

MERGE (p:Person { name: 'Andres' })

ON CREATE SET p.exists = TRUE

查看是否存在一個名爲'Andres'的人。如果沒找到,則創建一個並設置其exists屬性爲true。

AssertSameNode

這個運算符用於確保沒有違背唯一性約束。

MERGE (t:Team { name: 'Engineering', id: 42 })

查看Team中是否存在給定的名稱 和id的成員。如果不存在,則創建一個。由於存在:Team(name)和:Team(id)兩個唯一性約束,通過UniqueIndexSeek找到的任何節點必定是同一個 節點,否則就違背了唯一性約束。

NodeHashJoin

使用哈希表,NodeHashJoin將來自左端和右端的輸入連接起來。

MATCH (andy:Person { name:'Andreas' })-[:WORKS_IN]->(loc)<-[:WORKS_IN]-(matt:Person { name:'Mattis' })

RETURN loc.name

返回匹配到的兩個人同時都在那裏工作的地方。

三元(Triadic)

三元用於解決三元查詢,如很常用的查找我朋友的朋友中那些還不是我朋友的人。它先將所有的朋友放入一個集合,然後再檢查他們是否已經與我相連。

MATCH (me:Person)-[:FRIENDS_WITH]-()-[:FRIENDS_WITH]-(other)

WHERE NOT (me)-[:FRIENDS_WITH]-(other)

RETURN other.name

查找我朋友的朋友中那些還不是我朋友的所有人。

2.4行運算符

這些運算符將其他運算符產生的行轉換爲一個不同的行集合。

Eager

爲了隔離的目的,這個運算符確保在繼續之前將那些會影響後續操作的運算在整個數據集上被完全地執行。否則,它可能觸發無限循環。當導入數據或者遷移圖結構時,Eager運算符會引起很高的內存消耗。在這種情況下,可將操作分解爲更簡單的步驟。例如,可以分別地導入節點和關係。另外,也可以先返回要更新的數據,然後再執行更新語句。

MATCH (a)-[r]-(b)

DELETE r,a,b

MERGE ()

Distinct

移除輸入行流中重複的行。

MATCH (l:Location)<-[:WORKS_IN]-(p:Person)

RETURN DISTINCT l

Eager聚合

即時加載潛在的結果並存入哈希map中,使用分組鍵作爲map的鍵。

MATCH (l:Location)<-[:WORKS_IN]-(p:Person)

RETURN l.name AS location, collect(p.name) AS people

從計數庫獲取節點數量

從計數庫中得到節點的數量,比通過計數方式的eager聚合要快。然而,計數庫中只保存了有限範圍的組合,因此eager聚合對很多複雜的查詢依然很有用。例如,可以從計數庫中得到所有節點和擁有某個標籤的節點的數量,但無法獲取到超過一個標籤的節點的數量。

MATCH (p:Person)

RETURN count(p) AS people

從計數庫獲取關係數量

從計數庫中得到關係的數量,比通過計數方式的eager聚合要快。然而,計數庫中只保存了有限範圍的組合,因此eager聚合對很多複雜的查詢依然很有用。例如,可以從計數庫中得到所有關係,某個類型的關係的數量以及末尾節點上擁有某個標籤的關係的數量,但無法獲取到兩端節點都有標籤的關係的數量。

MATCH (p:Person)-[r:WORKS_IN]->()

RETURN count(r) AS jobs

過濾

過濾來自子運算符的每一行,僅僅讓斷言爲true的結果通過。

MATCH (p:Person)

WHERE p.name =~ '^a.*'

RETURN p

Limit

返回輸入的前n行。

MATCH (p:Person)

RETURN p

LIMIT 3

Projection

對於輸入的每一行,projection將評估表達式併產生一行表達式的結果。

RETURN 'hello' AS greeting

Skip

跳過輸入行的前n行。

MATCH (p:Person)

RETURN p

ORDER BY p.id

SKIP 1

Sort

根據給定的鍵進行排序。

MATCH (p:Person)

RETURN p

ORDER BY p.name

Top

返回根據給定鍵排序後的前n行。實際的運算符是Top,它僅保留前X行,而不像排序需要作用於整個輸入。

MATCH (p:Person)

RETURN p

ORDER BY p.name

LIMIT 2

Union

Union將左右兩個計劃的結果連接在一起。

MATCH (p:Location)

RETURN p.name

UNION ALL MATCH (p:Country)

RETURN p.name

Unwind

將列表中的值以每行一個元素的形式返回。

UNWIND range(1, 5) AS value

RETURN value;

調用過程

返回以name爲序的所有標籤。

CALL db.labels() YIELD label

RETURN *

ORDER BY label

2.5更新運算符

這些運算符用於在查詢中更新圖。

約束操作

在一對標籤和屬性上創建一個約束。下面的查詢在帶有Country標籤節點的name屬性上創建一個唯一性約束。

CREATE CONSTRAINT ON (c:Country) ASSERT c.name IS UNIQUE

EmptyResult

即時加載產生的所有結果到EmptyResult運算符並丟棄掉。

CREATE (:Person)

更新圖

對圖進行更新操作。

CYPHER planner=rule

CREATE (:Person { name: 'Alistair' })

Merge Into

當開始和結束節點都已經找到,Merge Into用於找到這兩個節點之間的所有關係或者創建一個新的關係。

CYPHER planner=rule

MATCH (p:Person { name: 'me' }),(f:Person { name: 'Andres' })

MERGE (p)-[:FRIENDS_WITH]->(f)

2.6最短路徑規劃(重點)

本小節講解Cypher中的最短路徑查找是如何規劃的。

     不同的斷言在規劃最短路徑時可能導致Cypher中產生不同的查詢計劃。如果斷言可以在搜索路徑時處理,Neo4j將使用快速的雙向廣度優先搜索算法。因此,當路徑上是普通斷言時,這個快速算法可以一直確定地返回正確的結果。例如,當搜索有Person標籤的所有節點的最短路徑時,或者沒有帶有name屬性的節點時。

      如果在決定哪條路徑是有效或者無效之前,斷言需要檢查所有路徑,那麼這個算法那就不能可靠地找到最短路徑。Neo4j可能需要求助於比較慢的窮舉深度優先搜索算法去尋找路徑

這兩種算法的運行時間可能有數量級的差異,因此,對於時間敏感性查詢確保使用的是快速算法很重要。

用快速算法檢索最短路徑

MATCH (ms:Person { name: 'Martin Sheen' }),(cs:Person { name: 'Charlie Sheen' }), p = shortestPath((ms)-[rels:ACTED_IN*]-(cs))

WHERE ALL (r IN rels WHERE exists(r.role))

RETURN p

這個查詢可以使用快速算法——因爲沒有斷言需要查看所有路徑。

需要檢查路徑上額外斷言的最短路徑規劃

慮使用窮舉搜索

在決定哪條是最短的匹配路徑之前,WHERE語句中的斷言需要應用於最短路徑模式。

MATCH (cs:Person { name: 'Charlie Sheen' }),(ms:Person { name: 'Martin Sheen' }), p = shortestPath((cs)-[*]-(ms))

WHERE length(p)> 1

RETURN p

與前面那個相反,這個查詢在知道哪條是最短路徑之前,需要檢查所有的路徑。因此,查詢計劃將使用慢一些的窮舉搜索算法。

     這種更費時的窮舉查詢計劃使用Apply/Optional來確保,當快速算法無法找到結果的時候返回一個null結果,而不是簡單地停止結果流。在查詢計劃的頂部,查詢器使用了一個AntiConditionalApply,如果路徑變量指向的是null,那麼它將運行窮舉搜索。

禁止使用窮舉搜索算法

     這個查詢與上面的查詢一樣,在知道哪條路徑是最短路徑之前需要檢查所有路徑。然而,使用WITH語句將使得查詢計劃不使用窮舉搜索算法。由快速算法找到的任何路徑接下來將被過濾掉,這可能會導致沒有結果返回。

MATCH (cs:Person { name: 'Charlie Sheen' }),(ms:Person { name: 'Martin Sheen' }), p = shortestPath((cs)-[*]-(ms))

WITH p

WHERE length(p)> 1

RETURN p

 

學習文檔出自:龐國明 老師教學視頻

給大家推薦幾個neo4j數據庫學習網站:

【1】https://www.zhihu.com/question/45401120

【2】https://www.cnblogs.com/justcooooode/p/8182376.html

【3】https://blog.csdn.net/qq_37242224/article/details/81325625

【4】https://www.cnblogs.com/qianguyihao/category/587723.html

【5】https://www.cnblogs.com/loveis715/p/5277051.html

【6】https://blog.csdn.net/u011697278/article/details/52462420

【7】https://www.w3cschool.cn/neo4j/neo4j_need_for_graph_databses.html

【8】https://blog.csdn.net/u013946356/article/details/81739079
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章