1.简介
搜索性能索引通过解决节点标签/关系类型和属性谓词的特定组合,实现更快、更有效的模式匹配。通常在查询开始时,Cypher®计划器在MATCH子句中自动使用它们来扫描图形,以寻找开始模式匹配过程的最合适位置。
通过检查查询执行计划,本页将解释使用各种搜索性能索引来提高Cypher查询性能的场景。它还将提供一些关于何时使用索引的一般启发,以及关于如何避免过度索引的建议。
2.令牌查找索引(token lookup indexes)
在创建Neo4j数据库时,默认存在两个令牌查找索引。它们在数据库中存储所有节点标签和关系类型的副本,并且只解决节点标签和关系类型谓词。
2.1.使用实例
下面的查询计算类型属性值为baseball的PointOfInterest节点的数量,它将访问节点标签查找索引:
profile //profile关键字运行查询并生成其执行计划
match (n:PointOfInterest)
where n.type = 'baseball'
return count(n) //返回值26
2.2.执行计划
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline+ProduceResults0count(n)11000/00.075In Pipeline 1+EagerAggregation1count(n) AS count(n)11032116/08.228Fused in Pipeline 0+Filter2n.type = $autostring_0926376116/08.228Fused in Pipeline 0+NodeByLabelScan3n:PointOfInterest188188189376116/08.228Fused in Pipeline 0
Total database accesses: 565, total allocated memory: 472
解读及注意点:
- NodeByLabelScan操作符访问节点标签查找索引并生成188行,表示数据库中具有PointOfInterest标签的188个节点。
- 查询需要565次DB命中(每次DB命中表示查询需要访问数据库时的一个实例)。
- 查询在8毫秒多一点的时间内完成。
注意:
- 令牌查找索引非常重要,因为它们提高了Cypher查询的性能和其他索引的填充, > color=#ff00ff>删除它们将导致严重的性能下降(在上面的示例中,如果节点标签查找索引不存在,则NodeByLabelScan操作符将被AllNodesScan替换,后者在返回结果之前必须从数据库中读取所有69165个节点。)
- 不能解决任何与属性相关的谓词。
3.范围索引(range indexes)
范围索引解决了大多数类型的谓词,它们用于基于范围值有效地检索数据。它们在处理具有有序、可比较值的属性时特别有用。
3.1.使用实例
下面的示例首先在PointOfInterest节点的type属性上创建一个相关索引,然后再次运行上述查询,计算具有棒球类型值的PointOfInterest节点的数量:
//如果在创建索引时没有指定索引类型,Neo4j将默认创建范围索引。
create index range_index_type for(n:PointOfInterest) on (n.type)
创建相关索引后重新运行查询:
PROFILE
MATCH (n:PointOfInterest)
WHERE n.type = 'baseball'
RETURN count(n)
3.2.执行计划
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline+ProduceResults0
count(n)
11000/00.057In Pipeline 1+EagerAggregation1count(n) AScount(n)
110320/10.945Fused in Pipeline+NodeIndexSeek2RANGE INDEX n:PointOfInterest(type) WHERE type = $autostring_0526273760/1> 0.945Fused in Pipeline 0Total database accesses: 27, total allocated memory: 472
解读及注意点:
将此查询计划与创建相关范围索引之前生成的计划(2.2)进行比较,可以发现以下变化:
- NodeByLabelScan已被NodeIndexSeek取代。这只产生26行(表示数据库中的26个PointOfInterest节点,类型值设置为baseball)。
- 查询现在只需要27次DB命中。
- 查询在不到1毫秒的时间内完成——
几乎比不使用范围索引的查询快8倍。
这些都说明了搜索性能索引可以显著提高Cypher查询性能的基本观点。
3.3.范围索引支持ORDER BY
范围索引按升序存储属性(STRING值按字母顺序存储,FLOAT和INTEGER值按数字顺序存储)。
这可能会对查询性能产生重要影响,因为规划器可能能够利用预先存在的索引顺序,因此不必在查询后面执行昂贵的Sort操作。
实例:
为了演示这种行为,下面的查询将过滤出距离属性小于30的所有ROUTE关系,并使用order BY子句按升序返回匹配关系的距离属性。
查询返回没有相关索引的结果顺序:
PROFILE
MATCH ()-[r:ROUTE]-()
WHERE r.distance < 30
RETURN r.distance AS distance
ORDER BY distance
执行计划:
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Ordered byPipeline+ProduceResults0distance30136744000/014.397distance ASCIn Pipeline 1+Sort1distance ASC3013674405404720/016.844distance ASCIn Pipeline 1+Projection2cache[r.distance] AS distance30136744084/022.397Fused in Pipeline 0+Filter3cache[r.distance] < $autoint_0301367441004184/022.397Fused in Pipeline 0+UndirectedRelationshipTypeScan4(anon_0)-[r:ROUTE]-(anon_1)1004410041502337684/022.397Fused > in Pipeline 0
Total database accesses: 15064, total allocated memory: 540808
这个计划显示了关于索引和结果排序的两个要点:
- 在此查询中只使用了关系类型查找索引(由
UndirectedRelationshipTypeScan
操作符访问,该操作符从关系类型索引中获取所有关系及其开始和结束节点)。 - 因此,规划器必须执行Sort操作,按照距离属性对结果排序(在本例中,它需要540472字节的内存)。
要了解索引如何影响查询计划,首先需要在distance属性上创建一个范围索引:
//在关系类型属性上创建范围索引以上翻译结果来自有道神经网络翻译(YNMT)· 通用场景
CREATE INDEX range_index_relationships FOR ()-[r:ROUTE]-() ON (r.distance)
重新运行查询,它现在生成一个不同的计划:
PROFILE
MATCH ()-[r:ROUTE]-()
WHERE r.distance < 30
RETURN r.distance AS distance
ORDER BY distance
执行计划:
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Ordered byPipeline+ProduceResults0distance3016744002361/1076.542distance ASCFused in Pipeline 0+Projection1cache[r.distance] AS distance301674402361/1076.542distance ASCFused in > Pipeline 0+UndirectedRelationshipIndexSeekByRange2RANGE INDEX (anon_0)-[r:ROUTE(distance)]-(anon_1) WHERE distance < > $autoint_0, cache[r.distance]301674433732482361/1076.542r.distance ASCFused in Pipeline 0
Total database accesses: 3373, total allocated memory: 312
围绕计划中同样的两点,以下内容发生了变化:
- 现在使用最近在关系类型属性距离上创建的范围索引。
- 因此,计划不再需要执行Sort操作来对结果排序(因为距离属性已经按索引排序),这大大降低了查询的成本(查询的总内存成本现在是312字节)。
同样,使用max()和min()函数时,范围索引的顺序可以显著改善查询。
4.文本索引(text indexes)
文本索引用于对string属性进行查询过滤。
如果在给定的string属性上同时存在范围和文本索引
,则文本索引将仅由Cypher规划器用于使用Scontains或ends with操作符进行查询过滤
。在所有其他情况下,将使用范围索引。
4.1.使用实例
在同一属性上创建一个文本索引和一个范围索引:
//创建范围索引
create index range_index_name for(n:PointOfInterest) on (n.name)
//创建文本索引
create text index text_index_name for(n:PointOfInterest) on (n.name)
查询过滤包含“William”的name属性的所有PointOfInterest节点:
PROFILE
MATCH (n:PointOfInterest)
WHERE n.name CONTAINS 'William'
RETURN n.name AS name, n.type AS type
结果:
name | type |
“William Shakespeare” | “statue” |
“William Tecumseh Sherman” | “equestrian statue” |
4.2.执行计划
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline+ProduceResults0name, type12004/053.297Fused in Pipeline 0+Projection1cache[n.name] AS name, cache[n.type] AS type1204/053.297Fused in Pipeline 0+CacheProperties2cache[n.type], cache[n.name]1264/053.297Fused in Pipeline 0+NodeIndexContainsScan3TEXT INDEX n:PointOfInterest(name) WHERE name CONTAINS $autostring_0123 >2484/053.297Fused in Pipeline 0
Total database accesses: 9, total allocated memory: 312
将查询更改为使用STARTS WITH操作符而不是CONTAINS,则查询将使用范围索引:
PROFILE
MATCH (n:PointOfInterest)
WHERE n.name STARTS WITH 'William'
RETURN n.name, n.type
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline+ProduceResults0
n.name
,n.type
12004/11.276Fused in Pipeline 0+Projection1cache[n.name] ASn.name
, n.type ASn.type
1244/11.276Fused in Pipeline 0+NodeIndexSeekByRange2RANGE INDEX n:PointOfInterest(name) WHERE name STARTS WITH $autostring_0, cache[n.name]> 1232484/11.276Fused in Pipeline 0Total database accesses: 7, total allocated memory: 312
解读及注意点:
- 因为范围索引按字母顺序存储STRING值: 这意味着,虽然它们在检索STRING的精确匹配或前缀匹配方面非常有效,但在后缀和包含搜索方面效率较低,因为它们必须扫描所有相关属性以过滤任何匹配
- 文本索引不按字母顺序存储STRING属性,而是针对后缀和包含搜索进行了优化: 也就是说,如果name属性上没有范围索引,那么前面的查询仍然能够利用文本索引。虽然它的效率不如区间索引,但它仍然是有用的。
- 文本索引仅用于精确的查询匹配。要执行近似匹配(例如,包括变化和拼写错误),并计算STRING值之间的相似度评分,请使用语义全文索引。
- 范围索引的最大键大小限制在8 kb左右
- 文本索引的最大键大小限制在32 kb左右
5.点索引(point indexes)
点索引解决在空间点值上操作的谓词。点索引针对属性值之间的距离
或边界框内
的属性值的查询过滤进行了优化。
5.1.使用实例
下面的例子创建了一个点索引,然后在一个查询中使用它,返回一个设置的边界框内所有PointOfInterest节点的名称和类型:
CREATE POINT INDEX point_index_location FOR (n:PointOfInterest) ON (n.location)
使用point.withinBBox()函数查询:
PROFILE
MATCH (n:PointOfInterest)
WHERE point.withinBBox(
n.location,
point({srid: 4326, x: -73.9723702, y: 40.7697989}),
point({srid: 4326, x: -73.9725659, y: 40.770193}))
RETURN n.name AS name, n.type AS type
结果:
name | type |
“Heckscher Ballfield 3” | “baseball” |
“Heckscher Ballfield 4” | “baseball” |
“Heckscher Ballfield 1” | “baseball” |
“Robert Burns” | “statue” |
“Christopher Columbus” | “statue” |
“Walter Scott” | “statue” |
“William Shakespeare” | “statue” |
“Balto” | “statue” |
5.2.执行计划
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)> Pipeline+ProduceResults0
n.name
,n.type
4800302/02.619Fused in Pipeline 0+Projection1cache[n.name] ASn.name
, cache[n.type] ASn.type
480302/02.619Fused in Pipeline 0+CacheProperties2cache[n.type], cache[n.name]4824302/02.619Fused in Pipeline 0+NodeIndexSeekByRange3POINT INDEX n:PointOfInterest(location) WHERE point.withinBBox(location, point($autoint_0, > $autodoub4810248302/02.619Fused in Pipeline 0Total database accesses: 34, total allocated memory: 312
解读及注意点:
- 点索引配置设置可以将点索引配置为仅索引特定地理区域内的属性。这是通过在创建点索引时在OPTIONS子句的indexConfig部分指定以下设置之一来完成的。
-
spatial.cartesian.min
和spatial.cartesian.max
:用于二维直角坐标系。 -
spatial.cartesian-3d.min
和spatial.cartesian-3d.max
: 用于直角三维坐标系。 -
spatial.wgs-84.min
和spatial.wgs-84.max
: 用于WGS-84二维坐标系。 -
spatial.wgs-84-3d.min
和spatial.wgs-84-3d.max
: 用于WGS-84三维坐标系。
每个设置的min和max定义了每个坐标系中空间数据的最小和最大边界。
例如,下面的索引将只存储中央公园北半部的osmnode:
CREATE POINT INDEX central_park_north
FOR (o:OSMNode) ON (o.location)
OPTIONS {
indexConfig: {
`spatial.wgs-84.min`:[40.7714, -73.9743],
`spatial.wgs-84.max`:[40.7855, -73.9583]
}
}
限制点索引的地理区域可以提高空间查询的性能。在处理复杂的大型地理空间数据以及空间查询是应用程序功能的重要组成部分时,这一点特别有用。
6.复合索引(Composite indexes)
可以在单个属性或多个属性上创建范围索引(文本和点索引只能是单个属性)。后者称为复合索引,如果对数据库的查询经常过滤由复合索引索引的所有属性
,则这种索引非常有用。
6.1.使用实例
下面的示例首先在PointOfInterest节点上为属性名称和类型创建一个复合索引,然后使用shortestPath函数查询图,以确定路径长度(根据图中的遍历关系)和动物园学校与其最近的网球场之间的地理距离(注意图中有32个唯一的PointOfInterest网球场节点):
创建复合索引:
CREATE INDEX composite_index FOR (n:PointOfInterest) ON (n.name, n.type)
对复合索引索引的两个属性使用过滤器进行查询:
PROFILE
MATCH (tennisPitch: PointOfInterest {name: 'pitch', type: 'tennis'})
WITH tennisPitch
MATCH path = shortestPath((tennisPitch)-[:ROUTE*]-(:PointOfInterest {name: 'Zoo School'}))
WITH path, relationships(path) AS relationships
ORDER BY length(path) ASC
LIMIT 1
UNWIND relationships AS rel
RETURN length(path) AS pathLength, sum(rel.distance) AS geographicalDistance
结果:
pathLength | geographicalDistance |
25 | 2410.4495689536334 |
执行计划:
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Ordered by >Pipeline+ProduceResults0pathLength, geographicalDistance11000/00.065pathLength ASCIn Pipeline 3+OrderedAggregation1length(path) AS pathLength, sum(rel.distance) AS geographicalDistance1150> 514031/04.097pathLength ASCIn Pipeline 3+Unwind2relationships AS rel12503112> 0/00.180length(path) ASCIn Pipeline 2+Projection3relationships(path) AS relationships010> 0/00.050length(path) ASC+Top4length(path) ASC LIMIT 101057472> 0/01.763length(path) ASCIn Pipeline 1+Projection5length(path) AS length(path)0320> 131215/1188.723Fused in Pipeline 0+ShortestPath6path = (tennisPitch)-[anon_0:ROUTE*]-(anon_1)032181451> 70080131215/1188.723Fused in Pipeline 0+MultiNodeIndexSeek7RANGE INDEX tennisPitch:PointOfInterest(name, type) WHERE name = $autostring_0 AND type = $autostring_1, RANGE > > INDEX anon_1:PointOfInterest(name) WHERE name = $autostring_20310376131215/1188.723>Fused in Pipeline 0
Total database accesses: 181501, total allocated memory: 116040
解读及注意点:
- 查询计划显示正在使用的复合索引,而不是先前在type属性上创建的范围索引。这是因为复合索引同时解决查询的谓词,而单个属性索引只能解决部分谓词。
- 属性顺序和查询规划与单属性范围索引一样,复合索引支持所有谓词。但是,在创建复合索引时定义属性的顺序会影响规划器使用索引求解谓词的方式。
6.3.不同顺序定义的复合索引实例
6.3.1.实例一
例如: 在(n.p pro1, n.p pro2, n.p pro3)上创建的复合索引将生成与在(n.p pro3, n.p pro2, n.p pro1)上创建的复合索引不同的查询计划。
下面的例子展示了在相同属性上以不同顺序定义的复合索引如何生成不同的执行计划:
//请注意在创建索引时定义属性的顺序,名称在前,名称在后,类型在后
CREATE INDEX composite_2 FOR (n:PointOfInterest) ON (n.lat, n.name, n.type)
对三个索引属性使用筛选器进行查询:
PROFILE
MATCH (n:PointOfInterest)
WHERE n.lat = 40.7697989 AND n.name STARTS WITH 'William' AND n.type IS NOT NULL
RETURN n.name AS name
结果:
name |
“William Shakespeare” |
执行计划:
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline+ProduceResults0name00000/21.276+Projection1cache[n.name] AS name0000/21.276Fused in Pipeline 0+NodeIndexSeek2RANGE INDEX n:PointOfInterest(lat, name, type) WHERE lat = $autodouble_0 AND name STARTS WITH $autostring_1 AND type > IS NOT NULL, cache[n.name] 0012480/21.276Fused in Pipeline 0
Total database accesses: 1, total allocated memory: 312
该计划显示使用了最近创建的复合索引。它还显示谓词是按照查询中指定的方式过滤的(即,对lat属性进行相等性检查,对name属性进行前缀搜索,对type属性进行存在性检查)。
但是,如果在创建索引时改变了属性的顺序,则将生成不同的查询计划。为了演示此行为,首先需要删除最近创建的composite_2索引,并在以不同顺序定义的相同属性上创建一个新的复合索引。
6.3.2.实例二
//删除索引
DROP INDEX composite_2
//在以不同顺序定义的相同三个属性上创建复合索引
CREATE INDEX composite_3 FOR (n:PointOfInterest) ON (n.name, n.type, n.lat)
注意,属性的顺序已经改变:name属性现在是复合索引中定义的第一个属性,而lat属性是最后一个索引
创建不同的复合索引后重新运行查询
PROFILE
MATCH (n:PointOfInterest)
WHERE n.lat = 40.769798 AND n.name STARTS WITH 'William' AND n.type IS NOT NULL
RETURN n.name AS name
执行计划:
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline+ProduceResults0name00002/00.807Fused in Pipeline 0+Projection1cache[n.name] AS name0002/00.807Fused in Pipeline 0+Filter2cache[n.lat] = $autodouble_00002/00.807Fused in Pipeline 0+NodeIndexSeek3RANGE INDEX n:PointOfInterest(name, type, lat) WHERE name STARTS WITH $autostring_1 AND type IS NOT NULL AND lat IS > NOT NULL, cache[n.name], cache[n.lat] 0232482/00.807Fused in Pipeline 0
Total database accesses: 3, total allocated memory: 312
解读及注意点:
- 这个计划现在表明,虽然已经使用前缀搜索来解决name属性谓词,但最后的属性谓词不再使用相等性检查来解决,而是使用存在性检查和之后的显式过滤操作来解决。
- 注意,如果在重新运行查询之前没有删除composite_2索引,那么计划器将使用它而不是composite_3索引。这是因为,在使用复合索引时,前缀搜索之后的任何谓词都将自动规划为存在检查谓词。
6.4.复合索引规则
- 如果查询包含相等性检查或列表成员关系检查谓词,则它们需要用于创建复合索引时定义的
第一个属性
。 - 使用复合索引的查询
最多可以包含一个
范围搜索或前缀搜索谓词。 - 存在性检查谓词可以有
任意数量
。 - 前缀搜索或存在性检查之后的任何谓词
都将计划为存在性检查
。 - 后缀和子字符串搜索谓词可以利用复合索引。然而,它们总是被计划为存在性检查,任何后续的查询谓词也相应地被计划为
存在性检查
。注意, 如果使用了这些谓词,并且在任何已索引的(STRING)属性上也存在文本索引,那么规划器将使用文本索引
而不是复合索引。
解读及注意点:
- 在创建复合索引时,这些规则可能很重要,因为有些检查比其他检查更有效。(例如,对于计划器来说,对属性执行相等性检查通常比存在性检查更有效。) 因此,
根据查询和应用程序的不同,在创建复合索引时考虑属性定义的顺序可能更经济有效
。 - 复合索引只能在谓词过滤复合索引索引的所有属性时使用,并且复合索引只能为范围索引创建。
7.对索引使用
索引主要用于查找模式的起始点
。如果查询包含一个MATCH子句,那么作为一般规则,规划器将只选择最适合该子句
中的谓词的索引。但是,如果查询包含两个或更多MATCH子句
,则可以使用多个索引
。
使用实例:
为了显示一个查询中使用的多个索引,下面的示例将首先在PointOfInterest节点的lon(经度)属性上创建一个新索引。然后,它使用一个
查询来查找中央公园威廉·莎士比亚雕像以北的所有PointOfInterest节点。
//在longitude属性上创建范围索引
CREATE INDEX range_index_lon FOR (n:PointOfInterest) ON (n.lon)
//查询查找威廉·莎士比亚雕像以北的所有PointOfInterest节点
PROFILE
MATCH (ws:PointOfInterest {name:'William Shakespeare'})
WITH ws
MATCH (poi:PointOfInterest)
WHERE poi.lon > ws.lon
RETURN poi.name AS name
执行计划:
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline+ProduceResults0name914300233/11.460Fused in > Pipeline 1+Projection1poi.name AS name9143283233/11.460Fused in Pipeline 1+Apply291430233/11.460Fused in Pipeline 1+NodeIndexSeekByRange3RANGE INDEX poi:PointOfInterest(lon) WHERE lon > ws.lon91431462280233/11.460Fused in Pipeline 1+NodeIndexSeek4RANGE INDEX ws:PointOfInterest(name) WHERE name = $autostring_02123761/00.635In Pipeline 0
Total database accesses: 431, total allocated memory: 2616
该计划表明,先使用一个单独的索引来提高每个MATCH子句的性能
(首先利用name属性上的索引来查找William Shakespeare节点,然后使用lon属性上的索引来查找纵向值较大的所有节点)。
8.索引和null值
Neo4j索引不存储空值
。这意味着规划器必须能够排除空值的可能性,以便查询使用索引。
使用实例:
下面的查询通过计算所有具有未设置的name属性的PointOfInterest节点来演示空值和索引之间的不兼容性:
//查询名称值为空的节点计数
PROFILE
MATCH (n:PointOfInterest)
WHERE n.name IS NULL
RETURN count(n) AS nodes //返回值3
执行计划:
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline >+ProduceResults0nodes11000/00.012In Pipeline 1+EagerAggregation1count(n) AS nodes11032115/00.769Fused in Pipeline 0+Filter2n.name IS NULL1413373115/00.769Fused in Pipeline 0+NodeByLabelScan3n:PointOfInterest188188189376115/00.769Fused in Pipeline 0
Total database accesses: 562, total allocated memory: 472
该计划表明,没有使用name属性上的两个可用索引(范围和文本)来解决谓词。但是,如果添加了能够排除任何空值存在的查询谓词,则可以使用索引。
使用实例:
下面的查询通过向上面的查询添加子字符串谓词来显示这一点:
//查询计数名称值为空的节点或名称属性包含'William'的节点
PROFILE
MATCH (n:PointOfInterest)
WHERE n.name IS NULL OR n.name CONTAINS 'William'
RETURN count(n) AS nodes //返回值5
查询结果现在包括在前一个查询中找到的具有未设置名称值的三个节点,以及具有包含William (William Shakespeare和William Tecumseh Sherman)名称值的两个节点。
执行计划:
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline+ProduceResults0nodes11000/00.010In Pipeline 3+EagerAggregation1count(n) AS nodes110320/00.220Fused in Pipeline 2+Distinct2n141503520/00.220Fused in Pipeline 2+Union3142503520/00.220Fused in Pipeline 2+NodeIndexContainsScan4TEXT INDEX n:PointOfInterest(name) WHERE name CONTAINS $autostring_01233764/00.456In Pipeline 1+Filter5n.name IS NULL1413373115/00.673Fused in > Pipeline 0+NodeByLabelScan6n:PointOfInterest188188189376115/00.673Fused in > Pipeline 0
Total database accesses: 565, total allocated memory: 1352
解读及注意点:
- 该计划表明,索引仅用于解决WHERE子句的第二部分,即排除空值的存在。
- 因此,在索引属性中存在空值并不会否定索引的使用。只有当规划器无法排除在匹配过程中包含任何未设置的属性时,才会否定索引的使用。
- 空值的存在可能事先不知道,这可能导致未使用索引的意外实例。但是,有一些策略可以确保索引被使用。
8.1.属性存在性检查
确保使用索引的一种方法是通过将is not null
附加到所查询的属性,显式地过滤掉任何空值
。
使用实例:
下面的例子使用与上面相同的查询,但在WHERE子句中交换IS NULL和IS NOT NULL:
//查询名称值不为空的PointOfInterest节点计数
PROFILE
MATCH (n:PointOfInterest) WHERE n.name IS NOT NULL RETURN count(n) AS nodes //返回值185
执行计划:
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline+ProduceResults0nodes11000/00.013In Pipeline 1+EagerAggregation1count(n) AS nodes110320/10.691Fused in Pipeline 0+NodeIndexScan2RANGE INDEX n:PointOfInterest(name) WHERE name IS NOT NULL1851851863760/10.691Fused in Pipeline 0
Total database accesses: 186, total allocated memory: 472
该计划显示,以前在name属性上创建的范围索引现在用于解决谓词。
8.2.文本索引和类型谓词表达式
文本索引要求谓词只包含STRING属性
。
要在查询的任何属性可能是不兼容的类型或null而不是STRING值的情况下使用文本索引,请向查询添加类型谓词表达式IS:: STRING NOT null
(或其别名,在Neo4j 5.14中引入IS:: STRING!)这将强制属性及其STRING类型的存在,丢弃属性缺失或不是STRING类型的任何行,从而允许使用文本索引。
使用实例:
例如,如果前一个查询中的WHERE
谓词更改为附加IS:: STRING NOT NULL
,则使用文本索引而不是范围索引(范围索引不支持类型谓词表达式):
//使用类型谓词表达式进行查询
PROFILE MATCH (n:PointOfInterest) WHERE n.name IS :: STRING NOT NULL RETURN count(n) AS nodes
执行计划:
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline+ProduceResults0nodes11000/00.009In Pipeline 1+EagerAggregation1count(n) AS nodes110320/00.343Fused in Pipeline 0+NodeIndexScan2TEXT INDEX n:PointOfInterest(name) WHERE name IS NOT NULL1851851863760/00.343Fused in Pipeline 0
Total database accesses: 186, total allocated memory: 472
虽然在Neo4j 5.9中引入了类型谓词表达式,但是IS:: STRING NOT NULL语法在Neo4j 5.15中才成为与索引兼容的谓词。
toString函数还可用于将表达式转换为STRING值,从而帮助规划器选择文本索引。
8.3.类型约束(5.11版引入)
对于仅与特定类型兼容的索引
(即文本和点索引),Cypher规划器需要推断出谓词对于不兼容的值将计算为null
,以便使用索引。如果谓词没有显式定义为所需的类型(STRING或POINT),则可能导致不使用文本或点索引的情况。
由于
类型约束保证属性总是相同的类型
,因此它们可以用于扩展文本
和点索引
与谓词兼容的场景。
为了说明这一点,下面的示例将首先删除name属性上现有的范围索引(这是必要的,因为类型约束只扩展特定于类型的索引的兼容性——范围索引不受值类型的限制)。然后,它将在创建类型约束之前和之后对name属性(存在先前创建的文本索引)使用WHERE谓词运行相同的查询,并比较结果执行计划。
使用实例:
DROP INDEX range_index_name
//查询名称值不为空的PointOfInterest节点计数
PROFILE MATCH (n:PointOfInterest) WHERE n.name IS NOT NULL RETURN count(n) AS nodes
执行计划:
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline+ProduceResults0nodes11000/00.012In Pipeline 1+EagerAggregation1count(n) AS nodes11032259/00.363Fused in Pipeline 0+Filter2n.name IS NOT NULL187185373259/00.363Fused in Pipeline 0+NodeByLabelScan3n:PointOfInterest188188189376259/00.363Fused in Pipeline 0
Total database accesses: 562, total allocated memory: 472
该计划显示没有使用name属性上的可用文本索引来求解谓词。这是因为规划器无法推断出所有的名称值都是STRING类型。
但是,如果创建类型约束以确保所有名称属性都具有STRING值,则会生成不同的查询计划。
使用实例:
//在name属性上创建STRING类型约束
CREATE CONSTRAINT type_constraint FOR (n:PointOfInterest) REQUIRE n.name IS :: STRING
创建类型约束后重新运行查询:
PROFILE MATCH (n:PointOfInterest) WHERE n.name IS NOT NULL RETURN count(n) AS nodes
执行计划:
OperatorIdDetailsEstimated RowsRowsDB HitsMemory (Bytes)Page Cache Hits/MissesTime (ms)Pipeline+ProduceResults0nodes11000/00.013In Pipeline 1+EagerAggregation1count(n) AS nodes110320/00.328Fused in Pipeline 0+NodeIndexScan2TEXT INDEX n:PointOfInterest(name) WHERE name IS NOT NULL1871851863760/00.328Fused in Pipeline 0
Total database accesses: 186, total allocated memory: 472
- 由于name属性上的类型约束,规划器现在能够推断出所有name属性都是STRING类型,因此可以使用可用的文本索引。
- 如果创建类型约束以确保所有属性都是Point值,则可以以相同的方式扩展点索引。
请注意,属性存在性约束目前不以相同的方式利用索引使用。
9.索引使用启发
虽然不可能给出确切的指示,说明搜索性能索引何时可能对特定用例有益,但以下几点提供了一些有用的启发,说明创建索引何时可能提高查询性能
:
- 频繁的基于属性的查询: 如果某些属性经常用于过滤或匹配,请考虑为它们创建索引。
- 性能优化: 如果某些查询太慢,请重新检查被过滤的属性,并考虑为那些可能导致瓶颈的属性创建索引。
- 高基数属性: 高基数属性有许多不同的值(例如,唯一标识符、时间戳或用户名)。试图检索这些属性的查询可能会从索引中受益。
- 复杂查询: 如果查询遍历图中的复杂路径(例如,通过涉及多个跃点和多层过滤),则向这些查询中使用的属性添加索引可以提高查询性能。
- 实验和测试: 使用不同的索引和查询模式进行实验,并测量使用和不使用不同索引的关键查询的性能,以评估其有效性,这是一个很好的实践。
10.索引过度:注意事项和解决方案
搜索性能索引可以显著提高查询性能。然而,出于以下原因,它们应该被明智地使用:
- 存储空间: 由于每个索引都是主数据库中数据的辅助副本,因此
每个索引
实际上会使索引数据占用的存储空间增加一倍
。 - 写查询变慢:
添加索引会影响写查询的性能
。这是因为索引会随着每次写查询而更新。如果系统需要快速执行大量写操作,那么在受影响的数据实体上建立索引可能会适得其反。换句话说,如果写性能对某个特定用例至关重要,那么只在读取所需的地方添加索引可能是有益的。
由于这两点,决定索引什么(和不索引什么)是一项重要而重要的任务。
11.跟踪索引使用
未使用的索引占用不必要的存储空间,删除它们可能是有益的。然而,要知道对数据库的查询最常使用哪些索引是很困难的。从Neo4j 5.8开始,SHOW INDEX命令返回了三个相关的列,它们可以帮助识别冗余索引
。
- lastRead: 返回最后一次使用索引进行读取的时间。
- readCount: 返回对索引发出的读查询的次数。
- trackedSince: 返回索引的使用统计跟踪开始的时间。1
要返回数据库中索引的这些值(以及其他相关信息),请运行以下查询:
SHOW INDEX YIELD name, type, entityType, labelsOrTypes, properties, lastRead, readCount, trackedSince
如果识别出任何未使用的索引,使用DROP INDEX命令删除它们可能是有益的。
12.总结
- 范围索引可用于解决大多数谓词。
- 对于字符串属性上的CONTAINS和ENDS WITH谓词,以及查询的字符串属性超过8 kb,则文本索引将在范围索引之上使用。
- 当查询对距离和边界框进行过滤时,使用点索引。
- 令牌查找索引仅解决节点标签和关系类型谓词。它们不解决任何属性谓词。删除令牌查找索引将对查询性能产生负面影响。
- 只有当查询过滤由复合索引索引的所有属性时,才使用复合索引。创建复合索引时定义属性的顺序会影响规划器解决查询谓词的方式。
- 使用ORDER BY查询排序结果可以利用范围索引中已有的顺序,从而提高查询性能。
- 如果规划器认为对查询性能有利,那么Cypher查询可以使用多个索引。
- Neo4j索引不存储空值,为了使用索引,规划器必须能够排除任何具有包含空值属性的实体。有几种策略可以确保索引的使用。
- SHOW INDEX命令返回的列lastRead、readCount和trackedSince可用于标识占用不必要空间的冗余索引。
- trackedSince列不是SHOW INDEXES命令的默认返回列的一部分。要返回此列和所有其他非默认列,请使用
SHOW INDEXES YIELD *
。 ↩︎