索引对查询性能的影响

搜索性能索引通过解决节点标签/关系类型和属性谓词的特定组合,可以实现更快、更高效的模式匹配。它们由 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 属性上创建一个相关的索引,然后再次运行上述查询,计算具有 baseball type 值的 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 行(代表数据库中具有设置为 baseballtype 值的 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值按数字顺序排列)。这可能对查询性能有重大影响,因为计划程序可能能够利用预先存在的索引顺序,因此不必在查询的后期执行昂贵的排序操作。

为了演示这种行为,以下查询将过滤掉任何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运算符访问,该运算符从关系类型索引中获取所有关系及其起点和终点节点)。

  • 因此,计划程序必须执行排序操作以按距离属性对结果进行排序(在本例中,它需要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上创建的范围索引。

  • 因此,计划不再需要执行排序操作以对结果进行排序(因为distance属性已按索引排序),这大大降低了查询的成本(查询的总内存成本现在为312字节)。

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

使用多个索引

索引主要用于查找模式的起点。如果查询包含一个MATCH子句,那么,作为一般规则,计划程序将仅选择最适合该子句中谓词的索引。但是,如果查询包含两个或多个MATCH子句,则可以使用多个索引。

为了显示在一个查询中使用的多个索引,以下示例将首先在PointOfInterest节点的lon(经度)属性上创建一个新索引。然后,它使用一个查询,该查询查找中央公园中威廉·莎士比亚雕像以北的所有PointOfInterest节点。

在经度属性上创建范围索引
CREATE INDEX range_index_lon FOR (n:PointOfInterest) ON (n.lon)
查询以查找威廉·莎士比亚雕像以北的所有PointOfInterest节点
PROFILE
MATCH (ws:PointOfInterest {name:'William Shakespeare'})
WITH ws
MATCH (poi:PointOfInterest)
WHERE poi.lon > ws.lon
RETURN poi.name AS name
执行计划
+-------------------------+----+-----------------------------------------------------------------+----------------+------+---------+----------------+------------------------+-----------+---------------------+
| 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属性上的索引查找威廉·莎士比亚节点,然后使用lon属性上的索引查找所有具有更大经度值的节点)。

索引和null

Neo4j 索引不存储null值。这意味着计划程序必须能够排除null值的可能性,以便查询使用索引。

以下查询通过计算所有name属性未设置的PointOfInterest节点来演示null值和索引之间的不相容性

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

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值的查询谓词,则可以使用索引。以下查询通过向上面的查询添加子字符串谓词来显示这一点

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

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

查询以计算PointOfInterest节点,这些节点没有null name
PROFILE
MATCH (n:PointOfInterest)
WHERE n.name IS NOT NULL
RETURN count(n) AS nodes
表 8. 结果
节点

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
查询以计算PointOfInterest节点,这些节点没有null name
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 *。有关更多信息,请参见 创建、显示和删除索引→列出索引的结果列