索引对查询性能的影响

搜索性能索引通过解决节点标签/关系类型和属性谓词的特定组合,实现更快、更高效的模式匹配。它们通常在查询开始时由 Cypher® 规划器在 MATCH 子句中自动使用,以扫描图以找到最适合开始模式匹配过程的位置。

通过检查查询执行计划,本页将解释在何种场景下使用各种搜索性能索引来提高 Cypher 查询性能。它还将提供何时使用索引的一些通用启发式方法,以及如何避免过度索引的建议。

示例图

本页示例围绕根据 OpenStreetMap 提供的数据在纽约中央公园查找路线和兴趣点展开。数据模型包含两个节点标签

  • OSMNode (Open Street Map 节点) — 一个具有地理空间属性的连接节点,连接来自特定点的路线。

  • PointOfInterest  — OSMNode 的一个子类别。除了地理空间属性外,这些节点还包含有关中央公园中特定兴趣点的信息,例如雕像、棒球场等。

数据模型还包含一种关系类型:ROUTE,它指定图中节点之间的距离(以米为单位)。

using indexes example graph

总计,该图包含 69165 个节点(其中 188 个具有 PointOfInterest 标签)和 152077 个 ROUTE 关系。

要重新创建图,请下载并导入 5.0 倾储文件到一个空的 Neo4j 数据库。倾储文件可以导入到 Aura本地实例。

令牌查找索引

创建 Neo4j 数据库时,默认存在两个令牌查找索引。它们存储数据库中所有节点标签和关系类型的副本,并且只解决节点标签和关系类型谓词。

以下查询 [1],它统计 type 属性值为 baseballPointOfInterest 节点的数量,将访问节点标签查找索引

查询
PROFILE
MATCH (n:PointOfInterest)
WHERE n.type = 'baseball'
RETURN count(n)
表 1. 结果
count(n)

26

行数: 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 值为 baseballPointOfInterest 节点的数量

创建范围索引
CREATE INDEX range_index_type FOR (n:PointOfInterest) ON (n.type)
如果在创建索引时未指定索引类型,Neo4j 将默认创建范围索引。有关创建索引的更多信息,请参阅创建、显示和删除索引 → CREATE INDEX
创建相关索引后重新运行查询
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 规划器仅在查询使用 CONTAINSENDS 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
表 2. 结果
name type

"William Shakespeare"

"statue"

"William Tecumseh Sherman"

"equestrian statue"

行数: 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 中索引大小的方法

点索引

点索引解决对空间 POINT 值进行操作的谓词。点索引针对过滤属性值之间的距离边界框内属性值的查询进行了优化。

以下示例创建一个点索引,该索引随后用于一个查询,返回给定边界框内所有 PointOfInterest 节点的 nametype

创建点索引
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
表 3. 结果
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"

行数: 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 部分指定以下任一设置来完成

  • spatial.cartesian.minspatial.cartesian.max:用于笛卡尔 2D 坐标系。

  • spatial.cartesian-3d.minspatial.cartesian-3d.max:用于笛卡尔 3D 坐标系。

  • spatial.wgs-84.minspatial.wgs-84.max:用于WGS-84 2D 坐标系。

  • spatial.wgs-84-3d.minspatial.wgs-84-3d.max:用于WGS-84 3D 坐标系。

每个设置的 minmax 定义了每个坐标系中空间数据的最小和最大边界。

例如,以下索引将仅存储中央公园北部区域的 OSMNodes

使用配置设置创建点索引
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 节点的 nametype 属性创建复合索引,然后使用 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
表 4. 结果
pathLength geographicalDistance

25

2410.4495689536334

行数: 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
表 5. 结果
name

"William Shakespeare"

行数: 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 值按字母顺序,FLOATINTEGER 值按数字顺序)。这对查询性能可能产生重要影响,因为规划器可以利用预先存在的索引顺序,从而无需在查询后期执行昂贵的 Sort 操作。

为了演示这种行为,以下查询将过滤掉所有 distance 属性小于 30ROUTE 关系,并使用 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 字节)。

同样,范围索引的顺序可以显著改善使用 max()min() 函数的查询。

多索引使用

索引主要用于查找模式的起始点。如果查询包含一个 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 值与索引之间的不兼容性

统计 name 值为 null 的节点的查询
PROFILE
MATCH (n:PointOfInterest)
WHERE n.name IS NULL
RETURN count(n) AS nodes
表 6. 结果
nodes

3

行数: 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 值存在的查询谓词,则可以使用索引。以下查询通过在上述查询中添加一个子字符串谓词来展示这一点

统计 name 值为 null 的节点或 name 属性包含 'William' 的节点的查询
PROFILE
MATCH (n:PointOfInterest)
WHERE n.name IS NULL OR n.name CONTAINS 'William'
RETURN count(n) AS nodes
表 7. 结果
nodes

5

行数: 1

查询结果现在既包括前一个查询中找到的三个 name 值未设置的节点,也包括 name 值包含 William 的两个节点(William ShakespeareWilliam 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
表 8. 结果
nodes

185

行数: 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(或其别名,在 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
执行计划
+-------------------+----+-----------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+
| 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
虽然类型谓词表达式在 Neo4j 5.9 中引入,但 IS :: STRING NOT NULL 语法仅在 Neo4j 5.15 中成为索引兼容谓词。更多信息,请参阅有关类型谓词表达式的页面。

通过 toString 函数也可以将表达式转换为 STRING 值,从而帮助规划器选择文本索引。

属性类型约束

对于仅与特定类型(即文本和点索引)兼容的索引,Cypher 规划器需要推断谓词对于不兼容的值将评估为 null,以便使用该索引。如果谓词未明确定义为所需类型(STRINGPOINT),则可能导致文本或点索引不被使用的情况。

由于属性类型约束保证属性始终是相同类型,因此它们可以用于扩展文本和点索引与谓词兼容的场景。

为了说明这一点,以下示例将首先删除 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 值,则点索引也可以以相同的方式进行扩展。

请注意,属性存在性约束目前不支持以同样的方式利用索引。

启发式:决定要索引什么

虽然不可能精确地指出搜索性能索引何时对特定用例有益,但以下几点提供了一些有用的启发式方法,以判断何时创建索引可以提高查询性能

  • 频繁的基于属性的查询:如果某些属性经常用于过滤或匹配,请考虑在其上创建索引。

  • 性能优化:如果某些查询太慢,请重新检查正在过滤的属性,并考虑为那些可能导致瓶颈的属性创建索引。

  • 高基数属性:高基数属性具有许多不同的值(例如,唯一标识符、时间戳或用户名)。旨在检索此类属性的查询可能会从索引中受益。

  • 复杂查询:如果查询遍历图中复杂的路径(例如,涉及多个跳跃和多层过滤),则为这些查询中使用的属性添加索引可以提高查询性能。

  • 实验和测试:最好尝试不同的索引和查询模式,并测量关键查询在有索引和无索引情况下的性能,以评估其有效性。

过度索引:考虑因素和解决方案

搜索性能索引可以显著提高查询性能。然而,出于以下原因,应谨慎使用它们:

  • 存储空间:因为每个索引是主数据库中数据的辅助副本,所以每个索引本质上将索引数据占用的存储空间加倍。

  • 写入查询变慢:添加索引会影响写入查询的性能。这是因为索引会在每次写入查询时更新。如果系统需要快速执行大量写入操作,那么在受影响的数据实体上拥有索引可能会适得其反。换句话说,如果写入性能对于特定用例至关重要,那么仅在读取需要时添加索引可能更有益。

由于这两点,决定索引什么(以及不索引什么)是一项重要且不简单的任务。

跟踪索引使用情况:lastReadreadCounttrackedSince

未使用的索引会占用不必要的存储空间,删除它们可能是有益的。然而,了解数据库查询中最常使用哪些索引可能很困难。从 Neo4j 5.8 开始,SHOW INDEX 命令返回的三个相关列可以帮助识别冗余索引

  • lastRead:返回索引上次用于读取的时间。

  • readCount:返回向该索引发出的读取查询数量。

  • trackedSince 返回索引使用统计跟踪开始的时间。[2]

要返回数据库中索引的这些值(以及其他相关信息),请运行以下查询

识别冗余索引的查询
SHOW INDEX YIELD name, type, entityType, labelsOrTypes, properties, lastRead, readCount, trackedSince

如果识别出任何未使用的索引,使用 DROP INDEX 命令删除它们可能会有益。

总结

  • 范围索引可用于解决大多数谓词。

  • 文本索引在 STRING 属性的 CONTAINSENDS WITH 谓词中优于范围索引,如果查询的 STRING 属性超过 8 KB 也如此。

  • 点索引用于查询根据距离和边界框进行过滤的情况。

  • 令牌查找索引只解决节点标签和关系类型谓词。它们不解决任何属性谓词。删除令牌查找索引将对查询性能产生负面影响。

  • 复合索引仅在查询过滤复合索引所索引的所有属性时使用。创建复合索引时属性的定义顺序会影响规划器解决查询谓词的方式。

  • 使用 ORDER BY 对结果进行排序的查询可以利用范围索引中预先存在的顺序,从而提高查询性能。

  • 如果规划器认为对查询性能有益,Cypher 查询可以使用多个索引。

  • Neo4j 索引不存储 null 值,规划器必须能够排除任何包含 null 值的实体,以便使用索引。有几种策略可以确保使用索引。

  • SHOW INDEX 命令返回的 lastReadreadCounttrackedSince 列可用于识别占用不必要空间的冗余索引。


1. 本页的示例查询都以 PROFILE 开头。这既运行查询又生成其执行计划。更多信息,请参阅执行计划和查询调优 → 关于 PROFILE 和 EXPLAIN 的说明
2. trackedSince 列不是 SHOW INDEXES 命令的默认返回列。要返回此列和所有其他非默认列,请使用 SHOW INDEXES YIELD *。更多信息,请参阅创建、显示和删除索引 → 列出索引的结果列
© . All rights reserved.