对查询性能进行调优

本页面介绍了用于调整 Spanner 查询性能的技术。在开始调优查询之前,请先查看以下页面,其中介绍了查询调优中使用的基本概念和工具:

此页面上的查询和执行计划基于以下数据库架构:

CREATE TABLE Singers (
  SingerId   INT64 NOT NULL,
  FirstName  STRING(1024),
  LastName   STRING(1024),
  SingerInfo BYTES(MAX),
  BirthDate  DATE
) PRIMARY KEY(SingerId);

CREATE INDEX SingersByFirstLastName ON Singers(FirstName, LastName);

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);

CREATE INDEX AlbumsByAlbumTitle2 ON Albums(AlbumTitle) STORING (MarketingBudget);

CREATE TABLE Songs (
  SingerId  INT64 NOT NULL,
  AlbumId   INT64 NOT NULL,
  TrackId   INT64 NOT NULL,
  SongName  STRING(MAX),
  Duration  INT64,
  SongGenre STRING(25)
) PRIMARY KEY(SingerId, AlbumId, TrackId),
  INTERLEAVE IN PARENT Albums ON DELETE CASCADE;

CREATE INDEX SongsBySingerAlbumSongNameDesc ON Songs(SingerId, AlbumId, SongName DESC), INTERLEAVE IN Albums;

CREATE INDEX SongsBySongName ON Songs(SongName);

CREATE TABLE Concerts (
  VenueId      INT64 NOT NULL,
  SingerId     INT64 NOT NULL,
  ConcertDate  DATE NOT NULL,
  BeginTime    TIMESTAMP,
  EndTime      TIMESTAMP,
  TicketPrices ARRAY<INT64>
) PRIMARY KEY(VenueId, SingerId, ConcertDate);

您可以使用以下数据操纵语言 (DML) 语句向这些表中添加数据:

INSERT INTO Singers (SingerId, FirstName, LastName, BirthDate)
VALUES (1, "Marc", "Richards", "1970-09-03"),
       (2, "Catalina", "Smith", "1990-08-17"),
       (3, "Alice", "Trentor", "1991-10-02"),
       (4, "Lea", "Martin", "1991-11-09"),
       (5, "David", "Lomond", "1977-01-29");

INSERT INTO Albums (SingerId, AlbumId, AlbumTitle)
VALUES (1, 1, "Total Junk"),
       (1, 2, "Go, Go, Go"),
       (2, 1, "Green"),
       (2, 2, "Forever Hold Your Peace"),
       (2, 3, "Terrified"),
       (3, 1, "Nothing To Do With Me"),
       (4, 1, "Play");

INSERT INTO Songs (SingerId, AlbumId, TrackId, SongName, Duration, SongGenre)
VALUES (2, 1, 1, "Let's Get Back Together", 182, "COUNTRY"),
       (2, 1, 2, "Starting Again", 156, "ROCK"),
       (2, 1, 3, "I Knew You Were Magic", 294, "BLUES"),
       (2, 1, 4, "42", 185, "CLASSICAL"),
       (2, 1, 5, "Blue", 238, "BLUES"),
       (2, 1, 6, "Nothing Is The Same", 303, "BLUES"),
       (2, 1, 7, "The Second Time", 255, "ROCK"),
       (2, 3, 1, "Fight Story", 194, "ROCK"),
       (3, 1, 1, "Not About The Guitar", 278, "BLUES");

基准建议

最可靠的性能统计信息来自运行负载测试,该测试使用来自与目标实例位于同一区域的 Google Cloud 虚拟机 (VM) 实例的不同随机查询参数值。

避免依赖 Spanner Studio 或 QueryMode.PROFILE 模式来对查询进行基准测试,因为这些工具无法复制实际的生产网络和客户端环境。不过,在负载测试发现性能问题后,查询计划(无论是在 Query Insights 中采样还是在 Spanner Studio 中按需运行)都是调查该特定问题的有用工具。

了解查询执行计划

在查询计划可视化工具中,查询执行计划显示为运算符树,其中每个运算符从其子运算符读取行,对其进行处理,并将结果传递给其父运算符。

对于可视化计划中的大多数运算符,报告的延迟时间是累加的(仅反映在该运算符中花费的时间)。但是,对于 Distributed union 和 Distributed apply 运算符,报告的延迟时间是累积的:它既包括在分布式运算符本身中花费的时间,也包括在其子运算符中花费的时间。如需详细了解这些运算符,请参阅分布式并集和分布式应用。

例如,在以下分布式联合计划中,Distributed union 卡报告的总延迟时间为 8.46 毫秒,而其子 Stream aggregate 运算符报告 3.51 毫秒,Table scan 报告 1 毫秒。由于 Distributed union 延迟时间是累计的,因此 Distributed union 运算符本身仅增加了大约 5 毫秒的延迟时间,而不是 8 毫秒:

具有累积延迟时间的分布式联合执行计划。

在下一个示例中,Distributed union 卡片报告的延迟时间为 13.69 毫秒,而其子算子(Stream aggregate 为 <1 毫秒,Cross apply 为 1.14 毫秒,Albums 上的 Table scan 为 <1 毫秒,Filter scan 为 1 毫秒,Singers 上的 Table scan 为 10.85 毫秒)大约占 13 毫秒。因此,Distributed union 算子本身增加的延迟时间不到 1 毫秒,而不是 13 毫秒:

具有累积延迟时间的第二个分布式联合执行计划。

您还可以在算子摘要面板中查看累积延迟时间值。

合并多个索引

当查询使用 OR 或 AND 条件对多个已编入索引的列进行过滤时,Spanner 有时可以组合多个二级索引,以避免扫描整个基表。本部分中的示例通过添加 ReleaseDate 列和 AlbumsByReleaseDate 二级索引,扩展了上述示例架构中的 Albums 表:

CREATE TABLE Albums (
  SingerId        INT64 NOT NULL,
  AlbumId         INT64 NOT NULL,
  AlbumTitle      STRING(MAX),
  ReleaseDate     DATE,
  MarketingBudget INT64
) PRIMARY KEY(SingerId, AlbumId),
  INTERLEAVE IN PARENT Singers ON DELETE CASCADE;

CREATE INDEX AlbumsByAlbumTitle ON Albums(AlbumTitle);

CREATE INDEX AlbumsByReleaseDate ON Albums(ReleaseDate);

请考虑以下查询,该查询使用 OR 条件对来自两个单独二级索引(AlbumsByAlbumTitle 和 AlbumsByReleaseDate)的谓词进行过滤:

SELECT a.AlbumId, a.AlbumTitle, a.ReleaseDate
FROM Albums AS a
WHERE a.AlbumTitle = 'Play' OR a.ReleaseDate = '2023-09-07';

当查询对来自两个单独索引的谓词具有 OR 条件时,Spanner 可能会使用基于费用的优化来读取这两个索引并合并结果(索引并集)。您还可以通过向查询添加 INDEX_STRATEGY=FORCE_INDEX_UNION 表提示来强制执行此优化:

SELECT a.AlbumId, a.AlbumTitle, a.ReleaseDate
FROM Albums@{INDEX_STRATEGY=FORCE_INDEX_UNION} AS a
WHERE a.AlbumTitle = 'Play' OR a.ReleaseDate = '2023-09-07';

如需详细了解表提示,请参阅表提示。

以下执行计划图显示了 Spanner 如何通过分别扫描每个分支并使用 Union all 和 Hash aggregate 运算符组合结果来评估此 OR 条件(析取):

索引析取执行计划。

接下来,考虑使用 AND 条件对两个索引列进行过滤的查询:

SELECT a.AlbumId, a.ReleaseDate, a.AlbumTitle
FROM Albums AS a
WHERE a.AlbumTitle = 'Aardvark' AND a.ReleaseDate = '2023-09-07';

对于 AND 条件,Spanner 默认会选择单个索引,然后根据需要联接回基表,以评估 AND 子句中的其余条件。如需确定此基表反向联接是否为性能瓶颈,请在可视化工具中检查查询执行计划:如果 Index scan 运算符返回许多行,这些行在与基表上的 Table scan 运算符联接后随后被过滤掉(导致 Table scan 分支上的延迟时间或 CPU 时间相对于返回的最终行数较高),则反向联接是瓶颈。

如果基表反向联接是查询的瓶颈,建议的解决方案是将 AND 条件中的额外列添加到二级索引的 STORING 子句中,以便 Spanner 可以直接从索引评估这两个条件,而无需读取基表:

CREATE INDEX AlbumsByAlbumTitleStoringReleaseDate
  ON Albums(AlbumTitle)
  STORING (ReleaseDate);

如果无法修改索引,另一种方法是重写查询,使用 INTERSECT DISTINCT 执行两个索引的交集,然后将匹配的主键联接回 Albums 基表:

SELECT union_a.AlbumId, a.ReleaseDate, a.AlbumTitle FROM
  (SELECT a1.SingerId, a1.AlbumId
  FROM Albums @{force_index=AlbumsByReleaseDate} AS a1
  WHERE a1.ReleaseDate = '2023-09-07'
  INTERSECT DISTINCT
  SELECT a2.SingerId, a2.AlbumId
  FROM Albums @{force_index=AlbumsByAlbumTitle} AS a2
  WHERE a2.AlbumTitle = 'Aardvark') AS union_a
JOIN Albums AS a
  ON a.SingerId = union_a.SingerId AND a.AlbumId = union_a.AlbumId;

以下执行计划图显示了重写的查询。请注意,该计划在对 Albums 基表执行 Table scan 之前,如何使用两个 Index scan 运算符(on AlbumsByAlbumTitle 和 on AlbumsByReleaseDate)来查找交集键:

索引合取执行计划。

如果您的工作负载经常需要跨多列进行过滤,请考虑使用多列全文搜索,而不是二级索引。如需了解详情,请参阅执行多列搜索。

优化联接

当 Spanner 执行联接多个表的查询时,查询优化器会选择联接顺序(表联接的顺序)和联接方法(用于合并两个输入中的行的算法,例如应用联接、哈希联接或合并联接)。如果包含联接的查询比预期慢,您可以使用查询提示来指示 Spanner 使用不同的联接顺序或联接方法。这会更改执行计划,但不会更改返回的结果。例如:

GoogleSQL

强制联接顺序:

SELECT *
FROM Singers AS s JOIN@{FORCE_JOIN_ORDER=TRUE} Albums AS a
ON s.SingerId = a.SingerId
WHERE s.LastName LIKE '%x%' AND a.AlbumTitle LIKE '%love%';

更改加入方法:

SELECT *
FROM Singers AS s JOIN@{JOIN_METHOD=HASH_JOIN} Albums AS a
ON s.SingerId = a.SingerId;

PostgreSQL

强制联接顺序:

SELECT *
FROM Singers AS s JOIN/*@ FORCE_JOIN_ORDER=TRUE */ Albums AS a
ON s.SingerId = a.SingerId
WHERE s.LastName LIKE '%x%' AND a.AlbumTitle LIKE '%love%';

更改加入方法:

SELECT *
FROM Singers AS s JOIN/*@ JOIN_METHOD=HASH_JOIN */ Albums AS a
ON s.SingerId = a.SingerId;

以下部分提供了背景信息,可帮助您确定哪些联接顺序和联接方法有助于提高查询性能。

加入方法和扫描

Spanner 支持多种联接方法,您选择的联接方法会直接影响 Spanner 扫描底层表或索引的方式:

  • 应用联接(在某些系统中称为“查找联接”或“嵌套循环联接”):针对左侧(“输入”)端的每一行,执行一次右侧(“映射”)端。由于每次右侧查找都使用当前左侧行的值,因此左侧的联接列可用作右侧的查找键。分布式应用联接的工作原理与本地应用联接类似,但会将行批量处理,以便在分布式联接中跨分块进行更高效的通信。
  • 哈希联接:从一个输入中读取行,以在联接键上构建内存中哈希表,然后扫描另一个输入以探测哈希表中的匹配项。
  • 合并联接:按联接键的排列顺序读取两个输入,并在一次传递中合并匹配的行。

例如,请考虑以下联接 Singers 和 Albums 的查询:

SELECT s.*, a.AlbumTitle
FROM Singers AS s
JOIN Albums AS a ON s.SingerId = a.SingerId
WHERE s.SingerId = 2 AND a.AlbumId = 1;

通过应用联接,s.SingerId = a.SingerId 充当联接右侧的搜索条件,因此查询只需从 Albums 表中读取一行。

相比之下,在哈希联接或合并联接中,左侧和右侧输入彼此独立读取,因此联接条件不会提供从一侧到另一侧的搜索键。在最坏的情况下,哈希或合并联接会对每个输入执行全表扫描。

在交互式和事务性查询中,大多数联接都使用索引,因此分布式应用是 Spanner 中最常见的联接类型。不过,有时哈希或合并联接是更好的选择,因为读取连续的行范围比执行相同数量的分散单行查找要快得多。权衡点在于,是查找更少的行(使用应用联接)更快,还是扫描更大的连续行范围(使用哈希或合并联接)更快。如需详细了解这种权衡,请参阅顺序读取与分散式寻道。

对于读取数千行的查询,如果联接键上有可用索引,则使用分布式应用联接通常更安全。这是因为应用联接费用与联接的行数成正比,而对于哈希或合并联接,费用可能与总表大小成正比。对于较大的查询(例如处理数百万行的查询),使用哈希或合并联接可能会显著提升性能。

如需详细了解联接运算符和提示,请参阅以下资源:

加入订餐

与其他关系型数据库管理系统 (RDBMS) 一样,请对联接进行排序,以使中间结果大小尽可能小。这意味着,应尽早过滤掉数据,并尽可能晚地推迟任何大型匹配。

如果您提供 FORCE_JOIN_ORDER=TRUE 提示,Spanner 会按照联接在查询文本中出现的顺序执行联接。将具有高选择性过滤条件的联接移到查询的前面,将具有大量匹配项的联接移到查询的后面。

优化聚合

以下方法可帮助您减少在聚合查询期间扫描和排序的行数。

检索每个键前缀对应的一行

一种常见的聚合模式是检索每个键前缀的一行(例如,具有最高版本号或时间戳的行)。在 GoogleSQL 中,最有效的方法是将 ANY_VALUE 与 HAVING MAX 结合使用,这样可以在一次聚合过程中找到具有最大值的行并返回请求的列,而无需进行自联接或完全排序。例如,以下查询按 ConcertDate 对 Concerts 表进行分组,并返回每个日期具有最新 EndTime 的音乐会的 BeginTime:

SELECT ConcertDate, ANY_VALUE(BeginTime HAVING MAX(EndTime))
FROM Concerts
GROUP BY ConcertDate;

检查行是否存在

如需检查表是否包含任何行或与特定条件匹配的行,请使用 LIMIT,而不是 COUNT。例如:

SELECT 1 FROM Albums LIMIT 1;

提高并行度

Spanner 将表和索引划分为连续的主键范围(称为分块),这些分块可以在多个服务器上并行处理。通常,Spanner 会选择查询并行处理数,以平衡延迟时间和效率。例如,如果查询具有 LIMIT 子句,则可能会使用较少的并行度,以避免浪费限制舍弃的工作。查询扫描的拆分数量会限制最大并行度。

对于有限制或大型扫描的查询,您可以设置 USE_ADDITIONAL_PARALLELISM=TRUE 语句提示,以指示 Spanner 主要针对延迟时间进行优化:

GoogleSQL

@{USE_ADDITIONAL_PARALLELISM=TRUE}
SELECT *
FROM Singers
LIMIT 10;

PostgreSQL

/*@ USE_ADDITIONAL_PARALLELISM=TRUE */
SELECT *
FROM Singers
LIMIT 10;

此提示可能会使给定分块上的 CPU 达到饱和。如果查询运行时间较长,请以中等请求优先级发送,以免增加其他高优先级查询的延迟时间。

优化限制

如果查询包含 LIMIT 子句,但扫描的行数远超限制值,请检查以下潜在原因:

缺少索引

如果查询包含无法通过使用索引来满足的过滤条件,则查询计划会扫描基表或不提供所需排列顺序的索引。然后,查询会在评估 LIMIT 子句之前对匹配数据进行完全排序。

如需解决此问题,请考虑添加涵盖过滤条件列的二级索引。如果索引定义为 NULL_FILTERED(意味着它会排除索引列为 NULL 的行),请确保查询谓词包含 IS NOT NULL,以便优化器可以使用该索引。如需详细了解如何创建二级索引,请参阅二级索引。

完整排序

如果过滤后的列上存在索引,但该索引未提供 ORDER BY 子句所需的排列顺序,则 Spanner 必须读取所有匹配的行,并将其传递给查询执行计划中的 Sort 运算符,然后再应用 LIMIT。

为避免完全排序,请按以下顺序在 CREATE INDEX 语句中指定列:

  • 首先,查询中具有相等性 (=) 或 IN 条件的所有列
  • 然后,与查询的 ORDER BY 子句相同的列,顺序相同,排序方向相同(ASC 或 DESC)

例如,假设您有一个 Users 表(未在前面的示例架构中显示),并且想要优化以下查询:

SELECT * FROM Users
WHERE Name='Alex'
ORDER BY LastLogin DESC
LIMIT 10;

在 (Name, LastLogin DESC) 上创建索引,以便 Spanner 可以直接按索引顺序读取前 10 行,而无需排序:

CREATE INDEX UsersByNameLastLogin ON Users(Name, LastLogin DESC);

按键后缀排序

即使索引包含 ORDER BY 列作为索引键的后缀,如果索引的某个前缀列上有 IN、OR 或范围谓词,查询仍可能会扫描额外的行。

例如,请考虑以下查询,该查询使用上述完整排序示例中的 (Name, LastLogin DESC) 索引,该索引不属于示例架构:

SELECT *
FROM Users
WHERE Name IN ('Alex','Amal')
ORDER BY LastLogin DESC
LIMIT 10;

由于 Name 与多个值匹配,因此匹配的行不会按 LastLogin DESC 存储在单个全局排序范围内。您可以使用 UNNEST 和联接来重构查询,以便 Spanner 分别检索每个 Name 值的顶部行,然后将它们组合在一起:

SELECT *
FROM UNNEST(['Alex','Amal']) AS Name
JOIN Users AS u ON u.Name = Name
ORDER BY LastLogin DESC
LIMIT 10;

这种重写会从每个绑定的前缀键('Alex' 和 'Amal')中读取最少数量的行,然后对合并的结果进行少量排序,从而生成最终的 LIMIT,从而提高扫描效率。

此模式的一种常见特殊情况是分片前缀,其中 ShardId 列作为前导键列添加,以在拆分之间分配写入。例如,以下查询会扫描 32 个分片(0 到 31):

SELECT *
FROM Users
WHERE ShardId BETWEEN 0 AND 31
ORDER BY LastLogin DESC
LIMIT 10;

如需优化此查询,请明确枚举分片 ID(0 到 31),并使用 GoogleSQL 中的 GENERATE_ARRAY 或 PostgreSQL 中的 generate_series 将它们与 Users 表联接:

GoogleSQL

SELECT *
FROM GENERATE_ARRAY(0, 31) AS id
JOIN Users AS u ON u.ShardId = id
ORDER BY LastLogin DESC
LIMIT 10;

PostgreSQL

SELECT *
FROM generate_series(0, 31) AS id
JOIN Users AS u ON u.ShardId = id
ORDER BY LastLogin DESC
LIMIT 10;

如需详细了解这些数组和序列生成函数,请参阅适用于 GoogleSQL 的 GENERATE_ARRAY 和适用于 PostgreSQL 的 generate_series。

了解扫描性能

以下部分介绍了导致表或索引扫描速度缓慢的最常见原因以及缓解这些问题的方法。

已删除的行

默认情况下,Spanner 会将已删除或覆盖的行保留一小时,以支持历史读取和时间点恢复,您可以将此保留期限延长至 7 天。旧行版本超过保留期限后,便可在后台存储空间压缩期间移除,这可能需要额外 7 天的时间。在旧行版本被压缩之前,扫描键范围的查询可能必须读取已删除或已覆盖的行版本并将其过滤掉。如需详细了解数据保留,请参阅时间点恢复 (PITR)。

您可以在查询计划可视化工具详细信息面板的扫描运算符详细信息视图中检查已扫描的已删除行数指标,以查看查询是否扫描了已删除的行。

如需缓解因最近删除大量行而导致的扫描速度缓慢问题,您可以触发手动数据压缩。如需了解详情,请参阅手动数据压缩。

大型行

对于小行(最多几百个字节),扫描费用主要取决于扫描的行数。随着行大小的增加,行的字节大小变得更加重要。在面向行的存储中,行的总大小会影响扫描性能,而不仅仅是查询投影的列的大小。

对于针对大型表的扫描密集型分析查询,请考虑使用列式引擎。如需了解详情,请参阅监控列式引擎。

对于针对较窄行集的查询,如果表同时具有大列和经常查询的小列,请考虑以下选项:

  • 将较大的列分隔到非默认存储区域群组(该群组在磁盘上将指定列与表的其余部分分开存储)或单独的子表中,这样扫描较小的列时就不会读取较大的列数据。如需了解详情,请参阅创建和管理存放区域群组。
  • 如需进一步优化读取时间,请使用 STORING 子句将较小的列复制到与基表具有相同主键的二级索引中,以包含较小的列。

例如,假设 Documents 表(不属于上述示例架构)存储了一个小型 Metadata 字符串以及一个大型 Content JSON 对象。您可以创建一个仅存储 Metadata 的二级索引,这样不需要 Content 的查询就可以扫描较小的索引,而不是基表:

CREATE TABLE Documents (
  Id INT64,
  Metadata STRING(MAX),
  Content JSON
) PRIMARY KEY (Id);

CREATE INDEX DocumentMetadata ON Documents(Id) STORING (Metadata);

大型交错数据

当表交织时,Spanner 会将每个父行与其交织的子行一起按键顺序进行物理存储。如果父表有许多交错的子行,或者子表的行数明显少于其父表或同级表,则对较小表的范围扫描会更稀疏且效率更低,因为扫描必须跳过其他表中的交错行。

例如,请考虑以下架构(不属于上述示例架构),其中 Transaction 和 AuthorizedPerson 均交错在 Account 中:

CREATE TABLE Account (
  AccountId INT64,
  -- Additional columns omitted
) PRIMARY KEY (AccountId);

CREATE TABLE Transaction (
  AccountId INT64,
  TransactionId STRING(MAX),
  -- Additional columns omitted
) PRIMARY KEY (AccountId, TransactionId),
INTERLEAVE IN PARENT Account;

CREATE TABLE AuthorizedPerson (
  AccountId INT64,
  SignerId INT64,
  Name STRING(MAX),
  -- Additional columns omitted
) PRIMARY KEY (AccountId, SignerId),
INTERLEAVE IN PARENT Account;

如果每个 Account 行都有数千个 Transaction 行,但只有几个 AuthorizedPerson 行,则对 Account 或 AuthorizedPerson 进行范围扫描的每行费用高于对 Transaction 进行范围扫描的每行费用(即使其总费用仍然较低)。发生这种情况的原因是,Transaction 扫描会一次读取许多连续的 Transaction 行,而对 Account 或 AuthorizedPerson 的扫描具有较少同一表的相邻行,并且必须跳过交错的 Transaction 行。

如需优化对较稀疏表(Account 或 AuthorizedPerson)的范围扫描,请创建一个非交错二级索引,该索引会复制查询所需的列,类似于处理大行的方法:

CREATE INDEX AuthorizedPersonByAccount
  ON AuthorizedPerson(AccountId, SignerId)
  STORING (Name);

使用面向批处理的执行

Spanner 可以使用面向行的方法(一次处理一行)或面向批处理的方法(一次处理多个行块)执行查询扫描和查询运算符。以下部分介绍了如何检查和配置面向批处理的扫描和面向批处理的运算符执行。

面向批处理的扫描

在扫描数据时,某些 Spanner 查询可能会受益于使用面向批处理的处理方法,而不是更常见的面向行的处理方法。批量处理扫描是一种更高效的方式,可一次性处理大量数据,并使查询可实现更低的 CPU 利用率和延迟时间。

Spanner 扫描操作始终以面向行的方法开始执行。在此期间,Spanner 会收集多个运行时指标。然后,Spanner 会根据这些指标的结果应用一组启发法,以确定最佳扫描方法。在适当的情况下,Spanner 会切换到批处理方法,以帮助提高扫描吞吐量和性能。

常见使用场景

具有以下特征的查询通常可从批处理中获益:

  • 对不经常更新的数据进行的大规模扫描
  • 使用谓词对固定宽度列进行的扫描
  • 具有大量搜寻次数的扫描(搜寻使用索引来检索记录)

无法提升性能的应用场景

并非所有查询都能从批处理中受益。以下查询类型在面向行的扫描处理中表现更好:

  • 点查找查询:仅提取一行的查询。
  • 小规模扫描查询:仅扫描少量行的表扫描,除非它们具有大量搜寻次数。
  • 包含 LIMIT 子句的查询:在返回固定行数后停止扫描的查询。
  • 读取高流失率数据的查询:所读取的数据中有超过大约 10% 经常更新的查询。
  • 对包含大值的行进行的查询:对单个列中包含的值大于 32,000 字节(压缩前)的行进行的查询。

检查查询使用的扫描方法

如需检查查询是使用面向批处理的处理方法、面向行的处理方法,还是在两种扫描方法之间自动切换,请执行以下操作:

  1. 在 Google Cloud 控制台中,前往 Spanner 实例页面。

    转到“实例”页面

  2. 点击包含要调查的查询的实例的名称。

  3. 在数据库表中,点击包含要调查的查询的数据库。

  4. 在 Spanner 导航菜单中,点击 Spanner Studio。

  5. 点击 新的 SQL 编辑器标签页或 新标签页。

  6. 在查询编辑器中输入查询。

  7. 点击运行。

    Spanner 会运行查询并显示结果。

  8. 在查询编辑器窗格中,点击说明标签页。

    Spanner 会显示查询执行计划可视化工具。图表上的每个卡片都表示一个迭代器。如需详细了解执行计划,请参阅查询执行计划。

  9. 点击表扫描迭代器卡片,打开信息面板。

    信息面板会显示所选扫描的上下文信息,包括扫描方法 (Method)。自动表示 Spanner 会确定扫描方法。其他可能的值包括:批处理(表示批处理)和 行(表示行处理),如下图所示:

    表扫描卡片显示扫描方法为“自动”。

强制执行查询使用的扫描方法

为了优化查询性能,Spanner 会为您的查询选择最佳扫描方法。Google 建议您使用此默认扫描方法。 不过在某些场景中,您可能需要强制执行特定类型的扫描方法。

强制执行面向批处理的扫描

您可以在表级和语句级强制执行面向批处理的扫描。

如需在表级强制执行批处理扫描方法,请在查询中使用表提示:

GoogleSQL

SELECT *
FROM (Singers@{SCAN_METHOD=BATCH} AS s
  JOIN Albums AS a ON s.SingerId = a.SingerId);

PostgreSQL

SELECT *
FROM (Singers/*@ scan_method=batch */ AS s
  JOIN Albums AS a ON s.SingerId = a.SingerId);

如需在语句级别强制执行批处理扫描方法,请在查询中使用语句提示:

GoogleSQL

@{SCAN_METHOD=BATCH}
SELECT *
FROM Singers;

PostgreSQL

/*@ scan_method=batch */
SELECT *
FROM Singers;

使用语句级提示时,该提示会应用于整个查询。最好在表级层设置提示,这样可以提供更精细的控制。

强制执行面向行的扫描

虽然 Google 不建议停用 Spanner 设置的自动扫描方法,但您可以停用该方法并使用面向行的扫描方法以进行问题排查(例如诊断延迟时间)。

如需停用自动扫描方法选择并在表级强制执行行处理,请在查询中使用表提示:

GoogleSQL

SELECT *
FROM (Singers@{SCAN_METHOD=ROW} AS s
  JOIN Albums AS a ON s.SingerId = a.SingerId);

PostgreSQL

SELECT *
FROM (Singers/*@ scan_method=row */ AS s
  JOIN Albums AS a ON s.SingerId = a.SingerId);

如需停用自动扫描方法并在语句级强制执行行处理,请在查询中使用语句提示:

GoogleSQL

@{SCAN_METHOD=ROW}
SELECT *
FROM Singers;

PostgreSQL

/*@ scan_method=row */
SELECT *
FROM Singers;

执行方法

除了面向批处理的扫描之外,您还可以通过在语句级别强制执行相应的执行方法来优化查询执行。这仅适用于某些运算符,并且与扫描方法无关,扫描方法仅供扫描运算符使用。如需详细了解面向批处理的扫描,请参阅本文档的面向批处理的扫描部分。

默认情况下,大多数运算符以面向行的方法执行,即一次处理一行数据。向量化运算符以面向批处理的方法执行,来帮助提高执行吞吐量和性能。这些运算符一次处理一个数据块。当运算符需要处理许多行时,面向批处理的执行方法通常更高效。

执行方法与扫描方法

查询执行方法独立于查询扫描方法。您可以在查询提示中设置这两种方法中的一种、两种或都不设置。

查询执行方法是指查询运算符处理中间结果的方式以及运算符相互之间的交互方式,而扫描方法是指扫描运算符与 Spanner 存储层之间的交互方式。

强制执行查询使用的执行方法

为了优化查询性能,Spanner 会根据各种启发法为您的查询选择最佳执行方法。Google 建议您使用此默认执行方法。不过在某些场景中,您可能需要强制执行特定类型的执行方法。

您可以在语句级别强制执行您的执行方法。EXECUTION_METHOD 查询提示是建议性的,而不是指令。最终,查询优化器会决定针对每个运算符使用哪种方法。

如需在语句级别强制执行批处理执行方法,请在查询中使用语句提示:

GoogleSQL

@{EXECUTION_METHOD=BATCH}
SELECT *
FROM Singers;

PostgreSQL

/*@ EXECUTION_METHOD=BATCH */
SELECT *
FROM Singers;

虽然 Google 不建议停用 Spanner 设置的自动执行方法,但您可以停用该方法并使用面向行的执行方法以进行问题排查(例如诊断延迟时间)。

如需停用自动执行方法并在语句级强制执行面向行的执行方法,请在查询中使用语句提示:

GoogleSQL

@{EXECUTION_METHOD=ROW}
SELECT *
FROM Singers;

PostgreSQL

/*@ EXECUTION_METHOD=ROW */
SELECT *
FROM Singers;

检查已启用哪种执行方法

并非所有 Spanner 运算符都支持面向批处理和面向行的执行方法。对于每个运算符,查询执行计划可视化工具会在迭代器卡片中显示执行方法。如果执行方法面向批处理,则会显示批处理。如果是行处理,则会显示行。如需详细了解可视化工具,请参阅查询执行计划。

如果查询中的运算符使用不同的执行方法来执行,则运算符之间会出现执行方法适配器 DataBlockToRowAdapter 和 RowToDataBlockAdapter,以显示执行方法的更改。如需详细了解这些运算符,请参阅 DataBlockToRowAdapter 和 RowToDataBlockAdapter。