在 Cloud Spanner 数据库中,Cloud Spanner 会自动为每个表的主键创建索引。例如,您无需执行任何操作即可将 Singers
的主键编入索引,因为它会自动为您编制索引。
您还可以为其他列创建二级索引。为列添加二级索引可以提高查找该列中的数据的效率。例如,如果您需要快速查找具有指定 LastName
值范围的一组 SingerId
值,则应在 LastName
上创建二级索引,这样 Cloud Spanner 就无需扫描整个表。
如果上述示例中的查找操作在读写事务内完成,那么更高效的查找还可以避免对整个表持有锁定,从而允许对 LastName
查询范围之外的行执行并发插入和更新表操作。
除了给查询带来的好处之外,二级索引还可以帮助 Cloud Spanner 更高效地执行扫描,从而实现索引扫描而不是全表扫描。
Cloud Spanner 会将以下数据存储在每个二级索引中:
- 基表中的所有键列
- 索引中包含的所有列
- 索引定义的可选
STORING
子句中指定的所有列
随着时间的推移,Cloud Spanner 会分析表以确保您的二级索引用于适当的查询。
添加二级索引
创建二级索引的最有效方法是在创建表时。如需同时创建表及其索引,请在单个请求中向 Cloud Spanner 发送新表和新索引的 DDL 语句。
在 Cloud Spanner 中,您还可以在数据库继续处理流量时,向现有表添加新的二级索引。与 Cloud Spanner 中的任何其他架构更改一样,向现有数据库添加索引不需要使数据库离线,也不会锁定整个列或表。
每当将新索引添加到现有表时,Cloud Spanner 都会自动回填或填充索引,以反映被编入索引的数据的最新视图。Cloud Spanner 会为您管理此回填过程,它在索引回填期间会使用其他资源。
创建索引可能需要几分钟到几小时的时间。由于索引创建是架构更新,因此它受与任何其他架构更新相同的性能限制的约束。创建二级索引所需的时间取决于以下几个因素:
- 数据集的大小
- 实例的计算容量
- 实例上的负载
如需查看索引回填过程的进度,请参阅进度部分。
请注意,使用提交时间戳列作为二级索引的第一部分可以创建热点并降低写入性能。
使用 CREATE INDEX
语句在架构中定义二级索引。以下是一些示例:
如需按名字和姓氏对数据库中的所有 Singers
编制索引,请使用以下命令:
CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName)
如需按照 SongName
的值创建数据库中所有 Songs
的索引,请运行以下命令:
CREATE INDEX SongsBySongName ON Songs(SongName)
如需仅将特定歌手的歌曲编入索引,请使用 INTERLEAVE IN
子句在表 Singers
中交错索引:
CREATE INDEX SongsBySingerSongName ON Songs(SingerId, SongName),
INTERLEAVE IN Singers
要仅将特定专辑中的歌曲编入索引,请执行以下操作:
CREATE INDEX SongsBySingerAlbumSongName ON Songs(SingerId, AlbumId, SongName),
INTERLEAVE IN Albums
如需按 SongName
的降序编制索引,请使用以下命令:
CREATE INDEX SongsBySingerAlbumSongNameDesc ON Songs(SingerId, AlbumId, SongName DESC),
INTERLEAVE IN Albums
请注意,上面的 DESC
注释仅适用于 SongName
。如需按其他索引键的降序编制索引,请也使用 DESC
对其进行注解:SingerId DESC, AlbumId DESC
。
另请注意,PRIMARY_KEY
是一个保留字,不能用作索引的名称。它是创建具有主键规范的表时为伪索引指定的名称
如需详细了解选择非交错索引和交错索引的最佳做法,请参阅索引选项和对值单调递增或递减的列使用交错索引。
查看索引回填进度
查看索引回填进度的步骤
索引回填过程是架构更新长时间运行的操作的一部分,因为添加二级索引需要架构更新。您可以使用操作 ID 查看索引回填的进度。如果您没有操作 ID,请使用 gcloud spanneroperations list 查找:
gcloud spanner operations list --instance=INSTANCE --database=DATABASE
使用说明:
如需限制此命令返回的操作列表,请指定
--filter
标志。例如,使用以下过滤条件可返回架构更新操作。--filter="@TYPE:UpdateDatabaseDdlMetadata"
如需了解过滤条件语法,请参阅 gcloud topic filters。如需了解如何过滤数据库操作,请参阅 ListDatabaseOperationsRequest 中的
filter
字段。
输出示例如下:
OPERATION_ID STATEMENTS DONE @TYPE _auto_op_123456 CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName) False UpdateDatabaseDdlMetadata CREATE INDEX SongsBySingerAlbumSongName ON Songs(SingerId, AlbumId, SongName), INTERLEAVE IN Albums _auto_op_234567 True CreateDatabaseMetadata
要跟踪一个或多个辅助索引回填过程的进度,请使用 gcloud spanner actions describe:
gcloud spanner operations describe _auto_op_123456 \ --instance=INSTANCE \ --database=DATABASE
以下是一个架构更新长时间运行的操作的输出示例,其中包含两个索引回填过程:
done: true metadata: '@type': type.googleapis.com/google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata commitTimestamps: - '2021-01-22T21:58:42.912540Z' database: projects/my-project/instances/test-instance/databases/example-db progress: - endTime: '2021-01-22T21:58:42.912540Z' progressPercent: 100 startTime: '2021-01-22T21:58:11.053996Z' - progressPercent: 67 startTime: '2021-01-22T21:58:11.053996Z' statements: - CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName) - CREATE INDEX SongsBySingerAlbumSongName ON Songs(SingerId, AlbumId, SongName), INTERLEAVE IN Albums name: projects/my-project/instances/test-instance/databases/example-db/operations/_auto_op_123456 response: '@type': type.googleapis.com/google.protobuf.Empty
每个索引回填语句的进度都可以在
progress
字段中找到。对于语句数组中的每个语句,进度数组中都有对应的字段。并尽可能填充startTime
和endTime
字段。进度数组顺序与语句数组的顺序对应。
查看索引回填进度的场景
用户在尝试检查索引回填的进度时可能会遇到不同的情况。需要索引回填的索引创建语句是架构更新操作的一部分,并且可以有几个语句是架构更新操作的一部分。
第一个场景最为简单,即索引创建语句是架构更新操作中的第一个语句。索引创建语句的 startTime
字段将使用架构更新操作的开始时间进行填充,因为它是第一个语句,并且将由于执行顺序而被第一个处理和执行。当索引回填的进度超过 0% 时,系统将填充索引创建语句的 progressPercent
字段。提交语句后,系统会填充 endTime
字段。
第二种情况是,索引创建语句不是架构更新操作中的第一个语句。由于执行顺序,在提交之前的语句之前,系统不会填充与索引创建语句相关的字段。提交先前的语句后,索引创建语句的 startTime
进度字段将是第一个填充的属性。然后,当索引回填的进度超过 0% 时,系统将填充索引创建语句的 progressPercent
字段。提交语句后,系统会填充 endTime
字段。
取消索引创建
您可以使用 Google Cloud CLI 取消索引创建过程。如需检索 Cloud Spanner 数据库的架构更新操作列表,请使用 gcloud spanner operations list
命令并添加 --filter
选项:
gcloud spanner operations list \
--instance=INSTANCE \
--database=DATABASE \
--filter="@TYPE:UpdateDatabaseDdlMetadata"
找到要取消的操作的 OPERATION_ID
,然后使用 gcloud spanner operations cancel
命令取消操作:
gcloud spanner operations cancel OPERATION_ID \
--instance=INSTANCE \
--database=DATABASE
查看现有索引
如需查看数据库中现有索引的相关信息,您可以使用 Google Cloud Console 或 Google Cloud CLI:
控制台
转到 Cloud Console 中的 Cloud Spanner 实例页面。
点击您要查看的实例的名称。
在左侧窗格中,点击要查看的数据库,然后点击要查看的表。
点击索引标签页。Cloud Console 会显示索引列表。
可选:要获取有关索引的详细信息(例如其中包含的列),请点击索引的名称。
gcloud
使用 gcloud spanner databases ddl describe
命令:
gcloud spanner databases ddl describe DATABASE \
--instance=INSTANCE
gcloud
CLI 会输出数据定义语言 (DDL) 语句以创建数据库的表和索引。CREATE
INDEX
语句描述现有索引。例如:
--- |-
CREATE TABLE Singers (
SingerId INT64 NOT NULL,
FirstName STRING(1024),
LastName STRING(1024),
SingerInfo BYTES(MAX),
) PRIMARY KEY(SingerId)
---
CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName)
使用特定索引进行查询
以下部分介绍了如何在 SQL 语句中指定索引,以及如何使用 Cloud Spanner 的读取接口指定索引。这些部分中的示例假定您向 Albums
表添加了 MarketingBudget
列,并创建了名为 AlbumsByAlbumTitle
的索引:
CREATE TABLE Albums (
SingerId INT64 NOT NULL,
AlbumId INT64 NOT NULL,
AlbumTitle STRING(MAX),
MarketingBudget INT64,
) PRIMARY KEY (SingerId, AlbumId),
INTERLEAVE IN PARENT Singers ON DELETE CASCADE;
CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle);
在 SQL 语句中指定索引
当您使用 SQL 查询 Cloud Spanner 表时,Cloud Spanner 会自动使用任何可能提高查询效率的索引。因此,您不需要为 SQL 查询指定索引。但是,对于对工作负载至关重要的查询,Google 建议您在 SQL 语句中使用 FORCE_INDEX
指令,以获得更一致的性能。
在少数情况下,Cloud Spanner 可能会选择会导致查询延迟时间增加的索引。如果您已按照性能回归问题排查步骤操作,并确有必要尝试为查询使用其他索引,则可以将索引指定为查询的一部分。
如需在 SQL 语句中指定索引,请使用 FORCE_INDEX
提供索引指令。索引指令使用以下语法:
FROM MyTable@{FORCE_INDEX=MyTableIndex}
您也可以使用索引指令来指示 Cloud Spanner 扫描基表(而不是使用索引):
FROM MyTable@{FORCE_INDEX=_BASE_TABLE}
以下示例展示了指定索引的 SQL 查询:
SELECT AlbumId, AlbumTitle, MarketingBudget
FROM Albums@{FORCE_INDEX=AlbumsByAlbumTitle}
WHERE AlbumTitle >= "Aardvark" AND AlbumTitle < "Goo";
索引指令可能会强制 Cloud Spanner 的查询处理器读取查询需要但未存储在索引中的其他列。查询处理器通过联接索引和基表来检索这些列。为了避免这种额外的联接,请使用 STORING
子句将其他列存储在索引中。
例如,在上述示例中,MarketingBudget
列未存储在索引中,但 SQL 查询会选择此列。因此,Cloud Spanner 必须在基表中查找 MarketingBudget
列,然后将其与索引中的数据联接,从而返回查询结果。
如果索引指令存在以下任何问题,Cloud Spanner 会引发错误:
- 索引不存在。
- 索引位于其他基表上。
- 查询缺少
NULL_FILTERED
索引的必需的NULL
过滤表达式。
以下示例演示了如何编写和执行使用索引 AlbumsByAlbumTitle
提取 AlbumId
、AlbumTitle
和 MarketingBudget
的值的查询:
C++
C#
转到
Java
Node.js
PHP
Python
Ruby
在读取接口中指定索引
如果您使用 Cloud Spanner 的读取接口,并且希望 Cloud Spanner 使用索引,则必须指定索引。读取接口不会自动选择索引。
此外,您的索引必须包含查询结果中显示的所有数据,但主键中的列除外。存在此限制是因为读取接口不支持索引和基表之间的联接。如果需要在查询结果中包含其他列,您有以下几种选择:
- 使用
STORING
子句将其他列存储在索引中。 - 在不添加其他列的情况下进行查询,然后使用主键发送另一个读取其他列的查询。
Cloud Spanner 按索引键的升序返回索引中的值。如需按降序检索值,请完成以下步骤:
为索引键添加
DESC
注解。例如:CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle DESC);
DESC
注释适用于单个索引键。如果索引包含多个键,并且您希望根据所有键按降序显示查询结果,请为每个键添加DESC
注释。如果读取操作指定了键范围,请确保该键范围也按降序排序。换句话说,开始键的值必须大于结束键的值。
以下示例演示了如何使用索引 AlbumsByAlbumTitle
检索 AlbumId
和 AlbumTitle
的值:
C++
C#
转到
Java
Node.js
PHP
Python
Ruby
STORING 子句
(可选)您可以使用 STORING
子句将列的副本存储在索引中。此类索引可提供使用索引进行查询和读取调用的优势,但会占用额外的存储空间:
- 对于使用索引并选择存储在
STORING
子句中的列的 SQL 查询,不需要与基表进行额外联接。 - 使用索引的读取调用可以读取存储在
STORING
子句中的列。
例如,假设您创建了 AlbumsByAlbumTitle
的替代版本,将 MarketingBudget
列的副本存储在索引中(请注意,STORING
子句以粗体显示):
CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget);
使用旧的 AlbumsByAlbumTitle
索引时,Cloud Spanner 必须将索引与基表联接,然后从基表检索该列。使用新的 AlbumsByAlbumTitle2
索引后,Cloud Spanner 会直接从索引中读取该列,因此更高效。
如果您使用的是读取接口(而不是 SQL),则新的 AlbumsByAlbumTitle2
索引还允许您直接读取 MarketingBudget
列:
C++
C#
转到
Java
Node.js
PHP
Python
Ruby
将 NULL 值编入索引
默认情况下,Cloud Spanner 会将 NULL
值编入索引。例如,回顾一下表 Singers
上索引 SingersByFirstLastName
的定义:
CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);
即使 FirstName
和/或 LastName
均为 NULL
,也会将 Singers
的所有行编入索引。
将 NULL
值编入索引后,您可以执行高效的 SQL 查询并读取包含 NULL
值的数据。例如,使用以下 SQL 查询语句可查找具有 NULL
FirstName
的所有 Singers
:
SELECT s.SingerId, s.FirstName, s.LastName
FROM Singers@{FORCE_INDEX=SingersByFirstLastName} AS s
WHERE s.FirstName IS NULL;
NULL 值的排序顺序
Cloud Spanner 将 NULL
作为任何给定类型的最小值进行排序。对于按升序 (ASC
) 顺序排序的列,NULL
值排在最前面。对于按降序 (DESC
) 排序的列,NULL
值排在最后。
禁止将 NULL 值编入索引
如需禁止将 Null 编入索引,请在索引定义中添加 NULL_FILTERED
关键字。NULL_FILTERED
索引特别适用于将稀疏列编入索引,其中大多数行包含 NULL
值。在这些情况下,与包含 NULL
值的普通索引相比,NULL_FILTERED
索引要小得多,维护效率也更高。
以下是 SingersByFirstLastName
的替代定义,其不将 NULL
值编入索引:
CREATE NULL_FILTERED INDEX SingersByFirstLastNameNoNulls
ON Singers(FirstName, LastName);
NULL_FILTERED
关键字适用于所有索引键列。您不能按列指定 NULL
过滤。
将索引设为 NULL_FILTERED
可防止 Cloud Spanner 将其用于某些查询。例如,Cloud Spanner 不会为此查询使用索引,因为索引会忽略所有 LastName
为 NULL
的 Singers
行;因此,使用索引会导致查询无法返回正确的行:
FROM Singers@{FORCE_INDEX=SingersByFirstLastNameNoNulls}
WHERE FirstName = "John";
为了使 Cloud Spanner 能够使用索引,您必须重写查询,使其排除同样从索引中排除的行:
SELECT FirstName, LastName
FROM Singers@{FORCE_INDEX=SingersByFirstLastNameNoNulls}
WHERE FirstName = "John" AND LastName IS NOT NULL;
唯一索引
索引可声明为 UNIQUE
。UNIQUE
索引会为正编入索引的数据添加一个限制条件,以禁止给定索引键出现重复条目。此限制条件由 Cloud Spanner 在事务提交时强制执行。具体而言,任何会导致同一个键存在多个索引条目的事务都将无法提交。
如果表开头包含非 UNIQUE
数据,则尝试为其创建 UNIQUE
索引将失败。
关于 UNIQUE NULL_FILTERED 索引的注意事项
当至少一个索引键为 NULL 时,UNIQUE NULL_FILTERED
索引不会强制执行索引键的唯一性。
例如,假设您创建了以下表和索引:
CREATE TABLE ExampleTable (
Key1 INT64 NOT NULL,
Key2 INT64,
Key3 INT64,
Col1 INT64,
) PRIMARY KEY (Key1, Key2, Key3);
CREATE UNIQUE NULL_FILTERED INDEX ExampleIndex ON ExampleTable (Key1, Key2, Col1);
ExampleTable
中的以下两行的二级索引键 Key1
、Key2
和 Col1
具有相同的值:
1, NULL, 1, 1
1, NULL, 2, 1
由于 Key2
为 NULL
且索引为 NULL_FILTERED
,因此行不会出现在索引 ExampleIndex
中。由于系统不会将它们插入到索引中,因此索引不会因违反 (Key1, Key2,
Col1)
的唯一性而拒绝它们。
如果您希望索引强制元组(Key1
、Key2
、Col1
)的值的唯一性,则必须在表定义中使用 Key2
注释 Key2
,或者创建不带 NULL_FILTERED
的索引。
删除索引
使用 DROP INDEX
语句从架构中删除二级索引。
要删除名为 SingersByFirstLastName
的索引,请执行以下操作:
DROP INDEX SingersByFirstLastName;
编入索引以加快扫描速度
当 Cloud Spanner 需要执行表扫描(而不是索引查询)从一列或多列中提取值时,如果这些列的索引存在,并且达到查询指定的顺序,您可以更快收到结果。如果您经常执行需要扫描的查询,请考虑创建二级索引,从而提高扫描效率。
具体而言,如果您需要 Cloud Spanner 以反向顺序频繁扫描表的主键或其他索引,则可以使用二级索引来明确提高所需的顺序,从而提高其效率。
例如,即使 Cloud Spanner 需要扫描 Songs
以查找 SongId
的最低值,下面的查询也总是会返回快速结果:
SELECT SongId FROM Songs LIMIT 1;
SongId
是表的主键(与所有主键一样),按升序存储。Cloud Spanner 可以扫描该键的索引并快速找到第一个结果。
但是,如果没有二级索引,以下查询将不会快速返回,尤其是在 Songs
保存大量数据时:
SELECT SongId FROM Songs ORDER BY SongId DESC LIMIT 1;
即使 SongId
是表的主键,Cloud Spanner 也无法提取列的最高值,而无需进行全表扫描。
添加以下索引可让此查询更快地返回:
CREATE INDEX SongIdDesc On Songs(SongId DESC);
有了此索引,Cloud Spanner 将利用它更快地第二个查询的结果。