索引对查询性能的影响
搜索性能索引通过解决节点标签/关系类型与属性谓词的特定组合,实现了更快、更高效的模式匹配。Cypher® 规划器在 MATCH 子句中会自动使用它们,通常在查询的起始位置,以扫描图中最合适的入口点来开始模式匹配过程。
通过检查 查询执行计划,本页将解释在何种场景下使用各种搜索性能索引来提升 Cypher 查询的性能。它还将提供一些关于何时使用索引的通用启发式方法,以及关于如何避免过度索引的建议。
示例图
本页的示例围绕查找纽约中央公园的路线和兴趣点展开,数据由 OpenStreetMap 提供。该数据模型包含两个节点标签
-
OSMNode(Open Street Map Node) — 一个枢纽节点,具有地理空间属性,连接特定点的路线。 -
PointOfInterest—OSMNode的子类别。除了地理空间属性外,这些节点还包含有关中央公园特定兴趣点的信息,例如雕像、棒球场等。
数据模型还包含一种关系类型:ROUTE,它指定了图中节点之间的距离(以米为单位)。
总计,该图包含 69165 个节点(其中 188 个具有 PointOfInterest 标签)和 152077 个 ROUTE 关系。
令牌查找索引
创建 Neo4j 数据库时,默认存在两个令牌查找索引。它们存储数据库中所有节点标签和关系类型的副本,且仅解决节点标签和关系类型谓词。
以下查询 [1](计算 type 属性值为 baseball 的 PointOfInterest 节点的数量)将访问节点标签查找索引
PROFILE
MATCH (n:PointOfInterest)
WHERE n.type = 'baseball'
RETURN count(n)
| count(n) |
|---|
|
行:1 |
+-------------------+----+------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +-------------------+----+------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | `count(n)` | 1 | 1 | 0 | 0 | 0/0 | 0.075 | In Pipeline 1 | | | +----+------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +EagerAggregation | 1 | count(n) AS `count(n)` | 1 | 1 | 0 | 32 | | | | | | +----+------------------------+----------------+------+---------+----------------+ | | | | +Filter | 2 | n.type = $autostring_0 | 9 | 26 | 376 | | | | | | | +----+------------------------+----------------+------+---------+----------------+ | | | | +NodeByLabelScan | 3 | n:PointOfInterest | 188 | 188 | 189 | 376 | 116/0 | 8.228 | Fused in Pipeline 0 | +-------------------+----+------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 565, total allocated memory: 472
执行计划中以下细节值得强调
-
NodeByLabelScan运算符访问节点标签查找索引并产生 188 行,代表数据库中具有PointOfInterest标签的 188 个节点。 -
该查询需要 565 次数据库命中(每个 数据库命中 代表查询访问数据库的一次实例)。
-
查询在 8 毫秒多一点的时间内完成。
令牌查找索引非常重要,因为它们提高了 Cypher 查询的性能以及其他索引的填充速度,删除它们将导致严重的性能下降。在上面的示例中,如果没有节点标签查找索引,NodeByLabelScan 运算符将被 AllNodesScan 替换,后者必须在返回结果之前读取数据库中的所有 69165 个节点。
虽然有用,但对于查询非平凡大小数据库的应用程序,令牌查找索引通常是不够的,因为它们无法解决任何与属性相关的谓词。
有关令牌查找索引支持的谓词的更多信息,请参阅 创建索引 → 创建令牌查找索引 → 支持的谓词。
范围索引
范围索引可以解决大多数类型的谓词,并用于基于值范围高效地检索数据。它们对于处理具有有序、可比较值的属性特别有用。
以下示例首先为 PointOfInterest 节点的 type 属性创建一个相关索引,然后再次运行上述查询,计算 type 值为 baseball 的 PointOfInterest 节点的数量
CREATE INDEX range_index_type FOR (n:PointOfInterest) ON (n.type)
| 如果在创建索引时未指定索引类型,Neo4j 将默认创建范围索引。有关创建索引的更多信息,请参阅 创建索引。 |
PROFILE
MATCH (n:PointOfInterest)
WHERE n.type = 'baseball'
RETURN count(n)
+-------------------+----+----------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +-------------------+----+----------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | `count(n)` | 1 | 1 | 0 | 0 | 0/0 | 0.057 | In Pipeline 1 | | | +----+----------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +EagerAggregation | 1 | count(n) AS `count(n)` | 1 | 1 | 0 | 32 | | | | | | +----+----------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +NodeIndexSeek | 2 | RANGE INDEX n:PointOfInterest(type) WHERE type = $autostring_0 | 5 | 26 | 27 | 376 | 0/1 | 0.945 | Fused in Pipeline 0 | +-------------------+----+----------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 27, total allocated memory: 472
将此查询计划与创建相关范围索引之前生成的计划进行比较,发生了以下变化
-
NodeByLabelScan已被 NodeIndexSeek 取代。这仅产生 26 行(代表数据库中type值设置为baseball的 26 个PointOfInterest节点)。 -
该查询现在仅需要 27 次数据库命中。
-
查询在不到 1 毫秒内完成——比没有范围索引时完成查询的速度快了近 8 倍。
这些点都说明了一个基本观点,即搜索性能索引可以显著提高 Cypher 查询的性能。
有关范围索引支持的谓词的更多信息,请参阅 创建索引 → 创建范围索引 → 支持的谓词。
文本索引
文本索引用于过滤 STRING 属性的查询。
如果给定的 STRING 属性上同时存在范围索引和文本索引,Cypher 规划器将仅在查询使用 CONTAINS 或 ENDS WITH 运算符过滤时使用文本索引。在所有其他情况下,将使用范围索引。
为了展示这种行为,有必要在同一属性上创建一个文本索引和一个范围索引
CREATE TEXT INDEX text_index_name FOR (n:PointOfInterest) ON (n.name)
CREATE INDEX range_index_name FOR (n:PointOfInterest) ON (n.name)
以下查询过滤所有 name 属性 CONTAINS(包含)'William' 的 PointOfInterest 节点
STRING 属性 CONTAINS 进行过滤的查询PROFILE
MATCH (n:PointOfInterest)
WHERE n.name CONTAINS 'William'
RETURN n.name AS name, n.type AS type
| 名称 (name) | type |
|---|---|
|
|
|
|
行:2 |
|
+------------------------+----+----------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +------------------------+----+----------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | name, type | 1 | 2 | 0 | 0 | | | | | | +----+----------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +Projection | 1 | cache[n.name] AS name, cache[n.type] AS type | 1 | 2 | 0 | | | | | | | +----+----------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +CacheProperties | 2 | cache[n.type], cache[n.name] | 1 | 2 | 6 | | | | | | | +----+----------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +NodeIndexContainsScan | 3 | TEXT INDEX n:PointOfInterest(name) WHERE name CONTAINS $autostring_0 | 1 | 2 | 3 | 248 | 4/0 | 53.297 | Fused in Pipeline 0 | +------------------------+----+----------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 9, total allocated memory: 312
该计划显示查询使用文本索引来查找所有相关节点。但是,如果将查询修改为使用 STARTS WITH 运算符而不是 CONTAINS,查询将改用范围索引
STRING 属性 STARTS WITH 进行过滤的查询PROFILE
MATCH (n:PointOfInterest)
WHERE n.name STARTS WITH 'William'
RETURN n.name, n.type
+-----------------------+----+-----------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +-----------------------+----+-----------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | `n.name`, `n.type` | 1 | 2 | 0 | 0 | | | | | | +----+-----------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +Projection | 1 | cache[n.name] AS `n.name`, n.type AS `n.type` | 1 | 2 | 4 | | | | | | | +----+-----------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +NodeIndexSeekByRange | 2 | RANGE INDEX n:PointOfInterest(name) WHERE name STARTS WITH $autostring_0, cache[n.name] | 1 | 2 | 3 | 248 | 4/1 | 1.276 | Fused in Pipeline 0 | +-----------------------+----+-----------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 7, total allocated memory: 312
这是因为范围索引按字母顺序存储 STRING 值。这意味着虽然它们对于检索 STRING 的精确匹配或前缀匹配非常高效,但对于后缀和包含搜索效率较低,因为它们必须扫描所有相关属性来过滤匹配项。文本索引不按字母顺序存储 STRING 属性,而是针对后缀和包含搜索进行了优化(有关详细信息,请参阅 创建文本索引 → 三元组索引)。话虽如此,如果 name 属性上没有范围索引,上述查询仍然能够利用文本索引。它的效率会低于范围索引,但仍然是有用的。
有关范围索引排序的更多信息,请参阅 范围索引支持的 ORDER BY 部分。
文本索引仅用于精确的查询匹配。要执行近似匹配(例如包括变体和拼写错误),以及计算 STRING 值之间的相似度得分,请改用语义 全文索引。 |
有关文本索引支持的谓词的更多信息,请参阅 创建索引 → 创建文本索引 → 支持的谓词。
确保使用文本索引
为了让规划器使用文本索引,它必须能够确认谓词中包含的属性是 STRING 值。在访问节点或关系内的属性值,或者 MAP 内的值时,这是不可能的,因为 Cypher 不存储这些值的类型信息。为确保在这些情况下使用文本索引,应使用 toString 函数。
WITH {name: 'William Shakespeare'} AS varName
MERGE (:PointOfInterest {name:varName.name})
WITH {name: 'William Shakespeare'} AS varName
MERGE (:PointOfInterest {name: toString(varName.name)})
有关在谓词可能包含 null 值时如何确保使用文本索引的信息,请参阅 索引与 null 值。
文本索引与 STRING 大小
索引 STRING 属性的大小也与规划器在范围索引和文本索引之间的选择相关。
范围索引的最大键大小限制约为 8 kb。这意味着范围索引不能用于索引大于 8 kb 的 STRING 值。另一方面,文本索引的最大键大小限制约为 32 kb。因此,它们可用于索引不超过该大小的 STRING 值。
有关计算索引大小的信息,请参阅 Neo4j 知识库 → 计算 Neo4j 中索引大小的方法。
点索引
以下示例创建了一个点索引,然后该索引被用于一个查询中,该查询返回设定边界框内所有 PointOfInterest 节点的 name 和 type
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
行:8 |
|
+-----------------------+----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +-----------------------+----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | `n.name`, `n.type` | 4 | 8 | 0 | 0 | | | | | | +----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +Projection | 1 | cache[n.name] AS `n.name`, cache[n.type] AS `n.type` | 4 | 8 | 0 | | | | | | | +----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +CacheProperties | 2 | cache[n.type], cache[n.name] | 4 | 8 | 24 | | | | | | | +----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +NodeIndexSeekByRange | 3 | POINT INDEX n:PointOfInterest(location) WHERE point.withinBBox(location, point($autoint_0, $autodoub | 4 | 8 | 10 | 248 | 302/0 | 2.619 | Fused in Pipeline 0 | | | | le_1, $autodouble_2), point($autoint_3, $autodouble_4, $autodouble_5)) | | | | | | | | +-----------------------+----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 34, total allocated memory: 312
有关点索引支持的谓词的更多信息,请参阅 创建索引 → 创建点索引 → 支持的谓词。
点索引配置设置
可以 配置点索引 以仅索引特定地理区域内的属性。这是通过在创建点索引时在 OPTIONS 子句的 indexConfig 部分中指定以下任一设置来完成的
每个设置的 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]
}
}
限制点索引的地理区域可以提高空间查询的性能。这在处理复杂、大型地理空间数据,以及当空间查询是应用程序功能的重要组成部分时特别有益。
复合索引
可以为单个属性或多个属性创建范围索引(文本索引和点索引仅支持单属性)。后者称为复合索引,如果针对数据库的查询经常过滤复合索引所索引的所有属性,则它们非常有用。
以下示例首先为 PointOfInterest 节点的 name 和 type 属性创建了一个复合索引,然后使用 shortestPath 函数 查询图,以确定 Zoo School 与其最近的 tennis pitch 之间的路径长度(就图中遍历的关系而言)和地理距离(注意图中共有 32 个唯一的 PointOfInterest tennis pitch 节点)
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
| 路径长度 | geographicalDistance |
|---|---|
|
|
行:1 |
|
+---------------------+----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+------------------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Ordered by | Pipeline | +---------------------+----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+------------------+---------------------+ | +ProduceResults | 0 | pathLength, geographicalDistance | 1 | 1 | 0 | 0 | 0/0 | 0.065 | | | | | +----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+ | | | +OrderedAggregation | 1 | length(path) AS pathLength, sum(rel.distance) AS geographicalDistance | 1 | 1 | 50 | 5140 | 31/0 | 4.097 | pathLength ASC | In Pipeline 3 | | | +----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+------------------+---------------------+ | +Unwind | 2 | relationships AS rel | 1 | 25 | 0 | 3112 | 0/0 | 0.180 | | In Pipeline 2 | | | +----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+ +---------------------+ | +Projection | 3 | relationships(path) AS relationships | 0 | 1 | 0 | | 0/0 | 0.050 | | | | | +----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+ | | | +Top | 4 | `length(path)` ASC LIMIT 1 | 0 | 1 | 0 | 57472 | 0/0 | 1.763 | length(path) ASC | In Pipeline 1 | | | +----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+------------------+---------------------+ | +Projection | 5 | length(path) AS `length(path)` | 0 | 32 | 0 | | | | | | | | +----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | +------------------+ | | +ShortestPath | 6 | path = (tennisPitch)-[anon_0:ROUTE*]-(anon_1) | 0 | 32 | 181451 | 70080 | | | | | | | +----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | +------------------+ | | +MultiNodeIndexSeek | 7 | RANGE INDEX tennisPitch:PointOfInterest(name, type) WHERE name = $autostring_0 AND type = $autostrin | 0 | 31 | 0 | 376 | 131215/1 | 188.723 | | Fused in Pipeline 0 | | | | g_1, RANGE INDEX anon_1:PointOfInterest(name) WHERE name = $autostring_2 | | | | | | | | | +---------------------+----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+------------------+---------------------+ Total database accesses: 181501, total allocated memory: 116040
查询计划显示使用了复合索引,而不是先前在 type 属性上创建的 范围索引。这是因为复合索引同时解决了查询的谓词,而单属性索引只能解决谓词的一部分。
属性顺序与查询规划
与单属性范围索引一样,复合索引支持所有谓词
-
相等性检查:
n.prop = value -
列表成员检查:
n.prop IN [value, …] -
存在性检查:
n.prop IS NOT NULL -
范围搜索:
n.prop > value -
前缀搜索:
n.prop STARTS WITH value
但是,在创建复合索引时定义属性的顺序会影响规划器如何使用索引来解决谓词。例如,在 (n.prop1, n.prop2, n.prop3) 上创建的复合索引将生成与在 (n.prop3, n.prop2, n.prop1) 上创建的复合索引不同的查询计划。
以下示例展示了在相同属性上以不同顺序定义的复合索引如何生成不同的执行计划
CREATE INDEX composite_2 FOR (n:PointOfInterest) ON (n.lat, n.name, n.type)
请注意创建索引时属性定义的顺序,lat 在第一位,name 在第二位,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) |
|---|
|
行:1 |
+-----------------+----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +-----------------+----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | name | 0 | 0 | 0 | 0 | | | | | | +----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +Projection | 1 | cache[n.name] AS name | 0 | 0 | 0 | | | | | | | +----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +NodeIndexSeek | 2 | RANGE INDEX n:PointOfInterest(lat, name, type) WHERE lat = $autodouble_0 AND name STARTS WITH $autos | 0 | 0 | 1 | 248 | 0/2 | 1.276 | Fused in Pipeline 0 | | | | tring_1 AND type IS NOT NULL, cache[n.name] | | | | | | | | +-----------------+----+------------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 1, total allocated memory: 312
该计划显示使用了最近创建的复合索引。它还显示谓词按照查询中的指定进行过滤(即对 lat 属性进行相等性检查,对 name 属性进行前缀搜索,对 type 属性进行存在性检查)。
但是,如果创建索引时更改了属性的顺序,则会生成不同的查询计划。为了演示这种行为,首先有必要删除最近创建的 composite_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
+-----------------+----+-----------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +-----------------+----+-----------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | name | 0 | 0 | 0 | 0 | | | | | | +----+-----------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +Projection | 1 | cache[n.name] AS name | 0 | 0 | 0 | | | | | | | +----+-----------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +Filter | 2 | cache[n.lat] = $autodouble_0 | 0 | 0 | 0 | | | | | | | +----+-----------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +NodeIndexSeek | 3 | RANGE INDEX n:PointOfInterest(name, type, lat) WHERE name STARTS WITH $autostring_1 AND type IS NOT | 0 | 2 | 3 | 248 | 2/0 | 0.807 | Fused in Pipeline 0 | | | | NULL AND lat IS NOT NULL, cache[n.name], cache[n.lat] | | | | | | | | +-----------------+----+-----------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 3, total allocated memory: 312
此计划现在显示,虽然已使用前缀搜索来解决 name 属性谓词,但 lat 属性谓词不再使用相等性检查来解决,而是使用存在性检查,随后显式进行 过滤 操作。请注意,如果在重新运行查询之前没有删除 composite_2 索引,规划器将使用它而不是 composite_3 索引。
这是因为,使用复合索引时,前缀搜索之后的任何谓词都将自动规划为存在性检查谓词。
复合索引规则
-
如果查询包含相等性检查或列表成员检查谓词,它们必须对应于创建复合索引时定义的第一个属性。
-
利用复合索引的查询最多可以包含一个范围搜索或前缀搜索谓词。
-
可以有任意数量的存在性检查谓词。
-
前缀搜索或存在性检查之后的任何谓词都将被规划为存在性检查。
-
后缀和子字符串搜索谓词可以使用复合索引。但是,它们总是被规划为存在性检查,并且任何后续的查询谓词也将相应地以此方式规划。请注意,如果使用这些谓词,且任何已索引的 (
STRING) 属性上还存在文本索引,规划器将使用文本索引而不是复合索引。
在创建复合索引时,这些规则可能很重要,因为某些检查比其他检查更有效。例如,规划器执行属性的相等性检查通常比执行存在性检查更高效。因此,根据查询和应用程序,考虑创建复合索引时定义属性的顺序可能是划算的。
此外,值得重申的是,仅当查询过滤复合索引所索引的所有属性时,才能使用复合索引;且复合索引只能为范围索引创建。
范围索引支持的 ORDER BY
范围索引按升序存储属性(STRING 值按字母顺序,FLOAT 和 INTEGER 值按数值顺序)。这可能对查询性能产生重要影响,因为规划器可能能够利用预先存在的索引顺序,从而不必在查询后期执行昂贵的 Sort 操作。
为了演示这种行为,以下查询将过滤掉任何 distance 属性小于 30 的 ROUTE 关系,并使用 ORDER BY 子句按数值升序返回匹配关系的 distance 属性。
PROFILE
MATCH ()-[r:ROUTE]-()
WHERE r.distance < 30
RETURN r.distance AS distance
ORDER BY distance
+---------------------------------+----+--------------------------------+----------------+-------+---------+----------------+------------------------+-----------+--------------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Ordered by | Pipeline | +---------------------------------+----+--------------------------------+----------------+-------+---------+----------------+------------------------+-----------+--------------+---------------------+ | +ProduceResults | 0 | distance | 3013 | 6744 | 0 | 0 | 0/0 | 14.397 | | | | | +----+--------------------------------+----------------+-------+---------+----------------+------------------------+-----------+ | | | +Sort | 1 | distance ASC | 3013 | 6744 | 0 | 540472 | 0/0 | 16.844 | distance ASC | In Pipeline 1 | | | +----+--------------------------------+----------------+-------+---------+----------------+------------------------+-----------+--------------+---------------------+ | +Projection | 2 | cache[r.distance] AS distance | 3013 | 6744 | 0 | | | | | | | | +----+--------------------------------+----------------+-------+---------+----------------+ | +--------------+ | | +Filter | 3 | cache[r.distance] < $autoint_0 | 3013 | 6744 | 10041 | | | | | | | | +----+--------------------------------+----------------+-------+---------+----------------+ | +--------------+ | | +UndirectedRelationshipTypeScan | 4 | (anon_0)-[r:ROUTE]-(anon_1) | 10044 | 10041 | 5023 | 376 | 84/0 | 22.397 | | Fused in Pipeline 0 | +---------------------------------+----+--------------------------------+----------------+-------+---------+----------------+------------------------+-----------+--------------+---------------------+ Total database accesses: 15064, total allocated memory: 540808
此计划显示了关于索引和结果排序的两个重要点
-
在此查询中仅使用了关系类型查找索引(由
UndirectedRelationshipTypeScan运算符访问,该运算符从关系类型索引获取所有关系及其起始和结束节点)。 -
因此,规划器必须执行
Sort操作来根据distance属性对结果进行排序(在此情况下,它需要 540472 字节的内存)。
要了解索引如何影响查询计划,首先有必要在 distance 属性上创建一个范围索引
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
+-----------------------------------------+----+--------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+----------------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Ordered by | Pipeline | +-----------------------------------------+----+--------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+----------------+---------------------+ | +ProduceResults | 0 | distance | 301 | 6744 | 0 | 0 | | | | | | | +----+--------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | | +Projection | 1 | cache[r.distance] AS distance | 301 | 6744 | 0 | | | | distance ASC | | | | +----+--------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+ | +----------------+ | | +UndirectedRelationshipIndexSeekByRange | 2 | RANGE INDEX (anon_0)-[r:ROUTE(distance)]-(anon_1) WHERE distance < $autoint_0, cache[r.distance] | 301 | 6744 | 3373 | 248 | 2361/10 | 76.542 | r.distance ASC | Fused in Pipeline 0 | +-----------------------------------------+----+--------------------------------------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+----------------+---------------------+ Total database accesses: 3373, total allocated memory: 312
聚焦于计划中的同两点,发生了以下变化
-
现在使用了最近在关系类型属性
distance上创建的范围索引。 -
结果是,计划不再需要执行
Sort操作来对结果进行排序(因为distance属性已经由索引排序),这大大降低了查询成本(查询的总内存成本现在为 312 字节)。
多索引使用
索引主要用于查找模式的起点。如果查询包含一个 MATCH 子句,那么作为一般规则,规划器只会选择最适合该子句中谓词的索引。但是,如果查询包含两个或多个 MATCH 子句,则可以使用多个索引。
为了展示在一个查询中使用多个索引,以下示例将首先为 PointOfInterest 节点的 lon(经度)属性创建一个新索引。然后,它使用一个查询来查找中央公园中 William Shakespeare 雕像以北的所有 PointOfInterest 节点。
CREATE INDEX range_index_lon FOR (n:PointOfInterest) ON (n.lon)
William Shakespeare 雕像以北所有 PointOfInterest 节点的查询PROFILE
MATCH (ws:PointOfInterest {name:'William Shakespeare'})
WITH ws
MATCH (poi:PointOfInterest)
WHERE poi.lon > ws.lon
RETURN poi.name AS name
+-------------------------+----+-----------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +-------------------------+----+-----------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | name | 9 | 143 | 0 | 0 | | | | | | +----+-----------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +Projection | 1 | poi.name AS name | 9 | 143 | 283 | | | | | | | +----+-----------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +Apply | 2 | | 9 | 143 | 0 | | | | | | |\ +----+-----------------------------------------------------------------+----------------+------+---------+----------------+ | | | | | +NodeIndexSeekByRange | 3 | RANGE INDEX poi:PointOfInterest(lon) WHERE lon > ws.lon | 9 | 143 | 146 | 2280 | 233/1 | 1.460 | Fused in Pipeline 1 | | | +----+-----------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +NodeIndexSeek | 4 | RANGE INDEX ws:PointOfInterest(name) WHERE name = $autostring_0 | 2 | 1 | 2 | 376 | 1/0 | 0.635 | In Pipeline 0 | +-------------------------+----+-----------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 431, total allocated memory: 2616
此计划显示使用了一个单独的索引来提高每个 MATCH 子句的性能(首先通过利用 name 属性上的索引找到 William Shakespeare 节点,然后使用 lon 属性上的索引找到所有具有更大经度值的节点)。
索引与 null 值
Neo4j 索引不存储 null 值。这意味着规划器必须能够排除 null 值的可能性,查询才能使用索引。
以下查询通过计算所有具有未设置 name 属性的 PointOfInterest 节点,演示了 null 值与索引之间的不兼容性
null name 值的节点的查询PROFILE
MATCH (n:PointOfInterest)
WHERE n.name IS NULL
RETURN count(n) AS nodes
| 节点 |
|---|
|
行数:1 |
+-------------------+----+-------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +-------------------+----+-------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | nodes | 1 | 1 | 0 | 0 | 0/0 | 0.012 | In Pipeline 1 | | | +----+-------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +EagerAggregation | 1 | count(n) AS nodes | 1 | 1 | 0 | 32 | | | | | | +----+-------------------+----------------+------+---------+----------------+ | | | | +Filter | 2 | n.name IS NULL | 141 | 3 | 373 | | | | | | | +----+-------------------+----------------+------+---------+----------------+ | | | | +NodeByLabelScan | 3 | n:PointOfInterest | 188 | 188 | 189 | 376 | 115/0 | 0.769 | Fused in Pipeline 0 | +-------------------+----+-------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 562, total allocated memory: 472
该计划显示,name 属性上的两个可用索引(范围索引和文本索引)均未被用于解决谓词。
但是,如果添加了一个能够排除任何 null 值存在的查询谓词,则可以使用索引。以下查询通过在上述查询中添加子字符串谓词来证明这一点
null name 值或 name 属性包含 'William' 的节点的查询PROFILE
MATCH (n:PointOfInterest)
WHERE n.name IS NULL OR n.name CONTAINS 'William'
RETURN count(n) AS nodes
| 节点 |
|---|
|
行:1 |
查询结果现在既包括上一个查询中找到的三个具有未设置 name 值的节点,也包括两个 name 值包含 William 的节点(William Shakespeare 和 William Tecumseh Sherman)。
+--------------------------+----+----------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +--------------------------+----+----------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | nodes | 1 | 1 | 0 | 0 | 0/0 | 0.010 | In Pipeline 3 | | | +----+----------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +EagerAggregation | 1 | count(n) AS nodes | 1 | 1 | 0 | 32 | | | | | | +----+----------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +Distinct | 2 | n | 141 | 5 | 0 | 352 | | | | | | +----+----------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +Union | 3 | | 142 | 5 | 0 | 352 | 0/0 | 0.220 | Fused in Pipeline 2 | | |\ +----+----------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | | +NodeIndexContainsScan | 4 | TEXT INDEX n:PointOfInterest(name) WHERE name CONTAINS $autostring_0 | 1 | 2 | 3 | 376 | 4/0 | 0.456 | In Pipeline 1 | | | +----+----------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +Filter | 5 | n.name IS NULL | 141 | 3 | 373 | | | | | | | +----+----------------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +NodeByLabelScan | 6 | n:PointOfInterest | 188 | 188 | 189 | 376 | 115/0 | 0.673 | Fused in Pipeline 0 | +--------------------------+----+----------------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 565, total allocated memory: 1352
此计划显示索引仅用于解决 WHERE 子句的第二部分,该部分排除了 null 值的存在。
因此,已索引属性中存在 null 值并不会否定索引的使用。只有当规划器无法排除匹配过程中包含任何未设置属性时,才会否定索引使用。
可能无法提前知道 null 值的存在,这可能导致索引未被使用的意外情况。但是,有一些策略可以确保使用索引。
属性存在性检查
确保使用索引的一种方法是通过在查询属性后附加 IS NOT NULL 来显式过滤掉任何 null 值。以下示例使用与上述相同的查询,但在 WHERE 子句中将 IS NULL 换成了 IS NOT NULL
null name 值的 PointOfInterest 节点的查询PROFILE
MATCH (n:PointOfInterest)
WHERE n.name IS NOT NULL
RETURN count(n) AS nodes
| 节点 |
|---|
|
行:1 |
+-------------------+----+------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +-------------------+----+------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | nodes | 1 | 1 | 0 | 0 | 0/0 | 0.013 | In Pipeline 1 | | | +----+------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +EagerAggregation | 1 | count(n) AS nodes | 1 | 1 | 0 | 32 | | | | | | +----+------------------------------------------------------------+----------------+------+---------+----------------+ | | | | +NodeIndexScan | 2 | RANGE INDEX n:PointOfInterest(name) WHERE name IS NOT NULL | 185 | 185 | 186 | 376 | 0/1 | 0.691 | Fused in Pipeline 0 | +-------------------+----+------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 186, total allocated memory: 472
该计划显示 name 属性上先前创建的范围索引现在被用于解决谓词。
文本索引与类型谓词表达式
文本索引要求谓词仅包含 STRING 属性。
为了在查询的任何属性可能是类型不兼容或 null 而不是 STRING 值的情况下使用文本索引,请将类型谓词表达式 IS :: STRING NOT NULL(或其别名 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
+-------------------+----+-----------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +-------------------+----+-----------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | nodes | 1 | 1 | 0 | 0 | 0/0 | 0.009 | In Pipeline 1 | | | +----+-----------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +EagerAggregation | 1 | count(n) AS nodes | 1 | 1 | 0 | 32 | | | | | | +----+-----------------------------------------------------------+----------------+------+---------+----------------+ | | | | +NodeIndexScan | 2 | TEXT INDEX n:PointOfInterest(name) WHERE name IS NOT NULL | 185 | 185 | 186 | 376 | 0/0 | 0.343 | Fused in Pipeline 0 | +-------------------+----+-----------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 186, total allocated memory: 472
toString 函数也可用于将表达式转换为 STRING 值,从而帮助规划器选择文本索引。
属性类型约束
对于仅与特定类型兼容的索引(即文本索引和点索引),Cypher 规划器需要推断出谓词对于非兼容值将评估为 null,才能使用索引。如果未明确将谓词定义为所需类型 (STRING 或 POINT),这可能导致不使用文本索引或点索引的情况。
由于 属性类型约束 保证属性始终具有相同的类型,因此它们可用于扩展文本索引和点索引与谓词兼容的场景。
为了展示这一点,以下示例将首先删除 name 属性上现有的范围索引(这是必要的,因为属性类型约束仅扩展类型特定索引的兼容性——范围索引不受值类型的限制)。然后,它将在创建属性类型约束之前和之后,对 name 属性运行具有 WHERE 谓词的相同查询(该属性存在先前创建的文本索引),并比较产生的执行计划。
DROP INDEX range_index_name
null name 值的 PointOfInterest 节点的查询PROFILE
MATCH (n:PointOfInterest)
WHERE n.name IS NOT NULL
RETURN count(n) AS nodes
+-------------------+----+--------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +-------------------+----+--------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | nodes | 1 | 1 | 0 | 0 | 0/0 | 0.012 | In Pipeline 1 | | | +----+--------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +EagerAggregation | 1 | count(n) AS nodes | 1 | 1 | 0 | 32 | | | | | | +----+--------------------+----------------+------+---------+----------------+ | | | | +Filter | 2 | n.name IS NOT NULL | 187 | 185 | 373 | | | | | | | +----+--------------------+----------------+------+---------+----------------+ | | | | +NodeByLabelScan | 3 | n:PointOfInterest | 188 | 188 | 189 | 376 | 259/0 | 0.363 | Fused in Pipeline 0 | +-------------------+----+--------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 562, total allocated memory: 472
此计划显示 name 属性上的可用文本索引未被用于解决谓词。这是因为规划器无法推断出所有 name 值都是 STRING 类型。
但是,如果创建了属性类型约束来确保所有 name 属性都具有 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
+-------------------+----+-----------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | Operator | Id | Details | Estimated Rows | Rows | DB Hits | Memory (Bytes) | Page Cache Hits/Misses | Time (ms) | Pipeline | +-------------------+----+-----------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +ProduceResults | 0 | nodes | 1 | 1 | 0 | 0 | 0/0 | 0.013 | In Pipeline 1 | | | +----+-----------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ | +EagerAggregation | 1 | count(n) AS nodes | 1 | 1 | 0 | 32 | | | | | | +----+-----------------------------------------------------------+----------------+------+---------+----------------+ | | | | +NodeIndexScan | 2 | TEXT INDEX n:PointOfInterest(name) WHERE name IS NOT NULL | 187 | 185 | 186 | 376 | 0/0 | 0.328 | Fused in Pipeline 0 | +-------------------+----+-----------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+ Total database accesses: 186, total allocated memory: 472
由于 name 属性上的属性类型约束,规划器现在能够推断出所有 name 属性都是 STRING 类型,因此使用了可用的文本索引。
如果创建了属性类型约束来确保所有属性都是 POINT 值,点索引也可以以相同的方式扩展。
请注意,属性存在性约束 目前不会以相同方式利用索引使用。
启发式方法:决定索引什么
虽然不可能给出关于搜索性能索引在特定用例中何时有益的确切指导,但以下几点提供了一些有用的启发式方法,说明何时创建索引可能会提高查询性能
-
频繁的基于属性的查询:如果某些属性经常用于过滤或匹配,请考虑在它们上创建索引。
-
性能优化:如果某些查询太慢,请重新检查过滤的属性,并考虑为可能导致瓶颈的属性创建索引。
-
高基数属性:高基数属性具有许多不同的值(例如唯一标识符、时间戳或用户名)。寻求检索此类属性的查询很可能会受益于索引。
-
复杂查询:如果查询遍历图中的复杂路径(例如,涉及多跳和多层过滤),则向这些查询中使用的属性添加索引可以提高查询性能。
-
实验和测试:尝试不同的索引和查询模式,并测量有和没有不同索引的关键查询的性能来评估其有效性,这是一种很好的做法。
过度索引:注意事项和解决方案
搜索性能索引可以显著提高查询性能。但是,出于以下原因,应慎重使用它们
-
存储空间:因为每个索引都是主数据库中数据的次要副本,所以每个索引基本上使索引数据占用的存储空间增加了一倍。
-
较慢的写查询:添加索引会影响写查询的性能。这是因为每次写查询时都会更新索引。如果系统需要快速执行大量写操作,在受影响的数据实体上拥有索引可能会适得其反。换句话说,如果写性能对于特定用例至关重要,则仅在出于读取目的需要它们的地方添加索引可能会有益。
由于这两点,决定索引什么(以及不索引什么)是一项重要且非平凡的任务。
跟踪索引使用情况:lastRead、readCount 和 trackedSince
未使用的索引占用了不必要的存储空间,删除它们可能会有益。然而,了解数据库查询最频繁使用哪些索引可能会很困难。SHOW INDEX 命令返回的三列相关信息可以帮助识别冗余索引
-
lastRead:返回索引上次用于读取的时间。 -
readCount:返回向索引发出的读取查询数量。 -
trackedSince返回索引的使用统计信息跟踪开始的时间。[2]
要返回数据库中索引的这些值(以及其他相关信息),请运行以下查询
SHOW INDEX YIELD name, type, entityType, labelsOrTypes, properties, lastRead, readCount, trackedSince
如果识别出任何未使用的索引,使用 DROP INDEX 命令删除它们可能会有益。
总结
-
范围索引可用于解决大多数谓词。
-
对于
STRING属性上的CONTAINS和ENDS WITH谓词,以及如果查询的STRING属性超过 8 kb,文本索引优于范围索引使用。 -
当查询过滤距离和边界框时,使用点索引。
-
令牌查找索引仅解决节点标签和关系类型谓词。它们不解决任何属性谓词。删除令牌查找索引将对查询性能产生负面影响。
-
仅当查询过滤复合索引所索引的所有属性时,才会使用复合索引。创建复合索引时定义属性的顺序会影响规划器解决查询谓词的方式。
-
使用
ORDER BY对结果进行排序的查询可以利用范围索引中预先存在的顺序,从而提高查询性能。 -
如果规划器认为对查询性能有益,Cypher 查询可以使用多个索引。
-
Neo4j 索引不存储
null值,规划器必须能够排除任何属性包含null值的实体,才能使用索引。有几种策略可以确保使用索引。 -
SHOW INDEX命令返回的lastRead、readCount和trackedSince列可用于识别占用不必要空间的冗余索引。
trackedSince 列不是 SHOW INDEXES 命令的默认返回列。要返回此列和所有其他非默认列,请使用 SHOW INDEXES YIELD *。有关更多信息,请参阅 创建、显示和删除索引 → 列出索引的结果列。