查询执行计划

概览

本页面介绍了有关查询执行计划的概念,以及 Spanner 如何使用它们在分布式环境中执行查询。如需了解如何使用 Google Cloud 控制台检索特定查询的执行计划,请参阅了解 Spanner 如何执行查询。您还可以查看采样历史查询计划,并比较特定查询在一段时间内的查询性能。如需了解详情,请参阅示例查询计划

Spanner 使用声明式 SQL 语句来查询其数据库。SQL 语句定义用户想要的内容,而不指定如何获取结果查询执行计划是旨在获取结果的一系列步骤。对于指定的 SQL 语句,获取结果的方式可能有多种。Spanner 查询优化器会评估不同的执行计划,然后选择它认为最高效的执行计划。然后,Spanner 会使用该执行计划检索结果。

从概念上讲,执行计划是关系运算符构成的树。每个运算符从其输入读取行并生成输出行。位于执行计划根部的运算符的结果会作为 SQL 查询结果返回。

以下为一个示例查询:

SELECT s.SongName FROM Songs AS s;

上述查询会生成一个查询执行计划,该计划的可视化图表如下:

示例查询执行计划

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

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

获取高效的执行计划具有挑战性,因为 Spanner 会将数据划分为多个分块。各个分片可以彼此独立移动并被分配给不同的服务器,这些服务器可以位于不同的物理位置。为了评估分布式数据的执行计划,Spanner 根据以下因素使用执行计划:

  • 包含数据的服务器中子计划的本地执行
  • 多个远程执行的编排和聚合(通过积极的分布修剪)

Spanner 使用基元运算符 distributed union 及其变体 distributed cross applydistributed outer apply 来启用此模型。

采样查询计划

借助 Spanner 采样查询计划,您可以查看历史查询计划的示例,并比较查询在一段时间内的性能。并非所有查询都有可用的抽样查询计划。我们只可能会对消耗较高 CPU 的查询进行采样。Spanner 查询计划样本的数据保留期限为 30 天。您可以在 Google Cloud 控制台的查询数据分析页面上找到查询计划示例。如需了解相关说明,请参阅查看采样查询计划

对采样查询计划的剖析与常规查询执行计划相同。如需详细了解如何了解直观计划并使用它们来调试查询,请参阅查询计划可视化工具导览

抽样查询计划的常见用例:

抽样查询计划的一些常见使用场景包括:

如果查询的性能在一段时间内存在显著差异,或者您想要提高查询的性能,请参阅 SQL 最佳实践,以构建优化查询语句,帮助 Spanner 找到高效的执行计划。

查询生命周期

Spanner 中的 SQL 查询首先被编译为执行计划,然后发送到初始根服务器以执行。系统会选择根服务器,以尽可能减少到达要查询的数据的跃点数。然后,根服务器会执行以下操作:

  • 启动子计划的远程执行(如有必要)
  • 等待远程执行的结果
  • 处理剩余的本地执行步骤,例如聚合结果
  • 返回查询的结果

接收子计划的远程服务器充当其子计划的“根”服务器,采用与最顶层根服务器相同的模型。其结果是一个远程执行树。从概念上讲,查询执行从上到下进行,而查询结果自下而上返回。下图展示了这种模式:

概念性查询计划

以下示例更详细地说明了这种模式。

聚合查询

聚合查询实现 GROUP BY 查询。

例如,使用以下查询:

SELECT s.SingerId, COUNT(*) AS SongCount
FROM Songs AS s
WHERE s.SingerId < 100
GROUP BY s.SingerId;

以下为查询结果:

+----------+-----------+
| SingerId | SongCount |
+----------+-----------+
|        3 |         1 |
|        2 |         8 |
+----------+-----------+

从概念上讲,以下为执行计划:

聚合查询执行计划

Spanner 将执行计划发送到根服务器,该服务器协调查询执行并执行子计划的远程分布。

此执行计划从分布式联合开始,该运算符将子计划分布到分片满足 SingerId < 100 的远程服务器。单个分块扫描完成后,流聚合运算符会聚合行,以获取每个 SingerId 的计数。然后,序列化结果运算符会将结果序列化。最后,分布式联合函数会将所有结果组合在一起并返回查询结果。

如需详细了解聚合,请参阅聚合运算符

同位联接查询

交错表以物理方式与它们的相关表的行存储于同一位置。同位联接是指交错表之间的联接。与需要索引的联接或后向联接相比,同位联接具备更高的性能优势。

例如,使用以下查询:

SELECT al.AlbumTitle, so.SongName
FROM Albums AS al, Songs AS so
WHERE al.SingerId = so.SingerId AND al.AlbumId = so.AlbumId;

(此查询假定 SongsAlbums 中交错。)

以下为查询结果:

+-----------------------+--------------------------+
| AlbumTitle            | SongName                 |
+-----------------------+--------------------------+
| Nothing To Do With Me | Not About The Guitar     |
| Green                 | The Second Time          |
| Green                 | Starting Again           |
| Green                 | Nothing Is The Same      |
| Green                 | Let's Get Back Together  |
| Green                 | I Knew You Were Magic    |
| Green                 | Blue                     |
| Green                 | 42                       |
| Terrified             | Fight Story              |
+-----------------------+--------------------------+

以下为执行计划:

同位联接查询执行计划

此执行计划从分布式联合开始,该运算符将子计划分布到具有表 Albums 的分片的远程服务器。由于 SongsAlbums 的交错表,因此每台远程服务器都可以在每台远程服务器上执行整个子计划,而无需联接不同的服务器。

子计划包含交叉应用每个交叉应用都会对表 Albums 执行表扫描以检索 SingerIdAlbumIdAlbumTitle然后,交叉应用将表扫描的输出映射到索引 SongsBySingerAlbumSongNameDesc索引扫描的输出,同时需满足以下过滤器:索引中的 SingerId 与表扫描输出中的 SingerId 匹配。每个交叉应用会将其结果发送到序列化结果运算符,该运算符会将 AlbumTitleSongName 数据序列化并将结果返回给局部分布式联合。分布式联合会聚合来自局部分布式联合的结果,并将其作为查询结果返回。

索引和后向联接查询

上面的例子对两个表上使用了一个联接,两个表互相交错。当两个表(或一个表和一个索引)未交错时,执行计划会更复杂且效率更低。

请参考使用以下命令创建的索引:

CREATE INDEX SongsBySongName ON Songs(SongName)

在以下查询中使用该索引:

SELECT s.SongName, s.Duration
FROM Songs@{force_index=SongsBySongName} AS s
WHERE STARTS_WITH(s.SongName, "B");

以下为查询结果:

+----------+----------+
| SongName | Duration |
+----------+----------+
| Blue     |      238 |
+----------+----------+

以下为执行计划:

后向联接查询执行计划

生成的执行计划很复杂,因为索引 SongsBySongName 不包含 Duration 列。如需获取 Duration 值,Spanner 需要将索引结果后向联接到表 Songs这是一个非同位联接,因为 Songs 表和全局索引 SongsBySongName 不交错。生成的执行计划比同位联接示例更复杂,因为如果数据不在同一位置,Spanner 会执行优化以加快执行速度。

顶层运算符为分布式交叉应用运算符。该运算符的输入端是索引 SongsBySongName 中满足谓词 STARTS_WITH(s.SongName, "B") 的批量行。然后,分布式交叉应用将这些批次映射到分块包含 Duration 数据的远程服务器。远程服务器使用表扫描来检索 Duration 列。表扫描使用过滤条件 Condition:($Songs_key_TrackId' = $batched_Songs_key_TrackId),将 Songs 表中的 TrackId 与索引 SongsBySongName 中批处理行的 TrackId 联接在一起。

结果会聚合为最终的查询结果。接下来,输入端包含分布式联合/局部分布式联合对的分布式交叉应用会评估索引中满足 STARTS_WITH 谓词的行。

以下是一个与上述查询略有不同(即不选择 s.Duration 列)的查询:

SELECT s.SongName
FROM Songs@{force_index=SongsBySongName} AS s
WHERE STARTS_WITH(s.SongName, "B");

此查询能够充分利用以下执行计划中所示的索引:

更简单的查询执行计划

该执行计划不需要后向联接,因为查询所需的所有列都存在于索引中。

后续步骤