本頁面說明調整 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);
您可以使用下列 data manipulation language (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 模式來進行查詢基準測試,因為這些工具無法複製實際的正式環境網路和用戶端環境。不過,在負載測試找出效能問題後,查詢計畫 (無論是在查詢洞察中取樣,或是在 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 有時可以合併多個次要索引,避免掃描整個基礎資料表。本節範例會擴充前述範例結構定義中的 Albums 資料表,新增 ReleaseDate 資料欄和 AlbumsByReleaseDate 次要索引:
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 如何評估這個 OR 條件 (析取),方法是分別掃描每個分支,並使用 Union all 和 Hash aggregate 運算子合併結果:

接下來,假設查詢使用 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;
下列執行計畫圖表顯示重寫的查詢。請注意,計畫會使用兩個 Index scan 運算子 (on AlbumsByAlbumTitle 和 on AlbumsByReleaseDate) 找出交集鍵,然後在 Albums 基礎資料表上執行 Table scan:

如果工作負載經常需要篩選多個資料欄,建議使用多欄全文搜尋,而非次要索引。詳情請參閱「執行多欄搜尋」。
最佳化彙整作業
Spanner 執行彙整多個資料表的查詢時,查詢最佳化工具會選擇「彙整順序」 (資料表的彙整順序) 和「彙整方法」 (用於合併兩個輸入內容中資料列的演算法,例如套用彙整、雜湊彙整或合併彙整)。如果含有 join 的查詢速度比預期慢,您可以使用查詢提示,指示 Spanner 採用不同的 join 順序或方法。這會變更執行計畫,但不會變更傳回的結果。例如:
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;
以下各節提供背景資訊,協助您判斷哪些 join 順序和 join 方法有助於提升查詢效能。
加入方法和掃描
Spanner 支援多種彙整方法,您選擇的方法會直接影響 Spanner 掃描基礎資料表或索引的方式:
- 套用彙整 (在某些系統中稱為查詢彙整或巢狀迴圈彙整):針對左側 (「輸入」) 的每個資料列,執行一次右側 (「對應」)。由於每個右側查詢都會使用目前左側資料列的值,因此左側的 join 欄可做為右側的搜尋鍵。分散式套用彙整的運作方式與本機套用彙整類似,但會將資料列批次處理,以便在分散式彙整中更有效率地跨分割區通訊。
- 雜湊彙整:從一個輸入讀取資料列,在彙整鍵上建構記憶體內雜湊資料表,然後掃描另一個輸入,在雜湊資料表中探測相符項目。
- 合併彙整:按照彙整鍵的排列順序讀取兩個輸入內容,並在單一階段中合併相符的資料列。
舉例來說,請看以下彙整 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 資料表讀取一個資料列。
相較之下,在雜湊彙整或合併彙整中,左側和右側輸入內容會彼此獨立讀取,因此彙整條件不會提供從一側到另一側的搜尋鍵。在最糟的情況下,雜湊或合併彙整會對每個輸入內容執行全資料表掃描。
在互動式和交易式查詢中,大多數 join 都會使用索引,因此分散式 apply 是 Spanner 中最常見的 join 類型。不過,有時雜湊或合併彙整是更好的選擇,因為讀取連續範圍的資料列,比執行相同數量的分散單一資料列搜尋快得多。權衡之處在於,搜尋較少資料列 (使用套用彙整) 或掃描較大連續範圍的資料列 (使用雜湊或合併彙整),何者速度較快。如要進一步瞭解這項取捨,請參閱「依序讀取與分散搜尋」。
如果查詢會讀取數千個資料列,且彙整索引鍵有可用的索引,通常使用分散式套用彙整會比較安全。這是因為套用彙整成本與彙整資料列數成正比,而雜湊或合併彙整的成本則可能與資料表總大小成正比。如果是較大的查詢 (例如處理數百萬個資料列的查詢),使用雜湊或合併 join 可能會大幅提升效能。
如要進一步瞭解 join 運算子和提示,請參閱下列資源:
加入訂購
與其他關聯式資料庫管理系統 (RDBMS) 一樣,請排序彙整作業,盡可能縮減中繼結果的大小。也就是說,應盡早篩除資料,並盡可能延後任何大型比對作業。
如果您提供 FORCE_JOIN_ORDER=TRUE 提示,Spanner 會按照彙整在查詢文字中出現的順序執行彙整。在查詢中,將具有高度選擇性篩選條件的彙整作業移到前面,並將具有大量相符項目的彙整作業移到後面。
最佳化匯總
下列技術可協助您減少匯總查詢期間掃描及排序的資料列數量。
依鍵前置字串擷取一列
常見的匯總模式是擷取每個索引鍵前置字串的一列 (例如版本號碼或時間戳記最高的資料列)。在 GoogleSQL 中,最有效率的做法是搭配使用 ANY_VALUE 和 HAVING MAX,這樣就能在單一匯總階段中找出最大值的資料列,並傳回要求的資料欄,不必執行 self-join 或完整排序。舉例來說,下列查詢會依 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 和 join 重新建構查詢,讓 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,進而提高掃描效率。
這種模式常見的特殊情況是 shard 前置字元,其中 ShardId 欄會新增為前置索引鍵欄,以便在分割區之間分配寫入作業。舉例來說,下列查詢會掃描 32 個 shard (0 到 31):
SELECT *
FROM Users
WHERE ShardId BETWEEN 0 AND 31
ORDER BY LastLogin DESC
LIMIT 10;
如要最佳化這項查詢,請明確列舉 shard ID (0 到 31),並使用 GoogleSQL 中的 GENERATE_ARRAY 或 PostgreSQL 中的 generate_series,將這些 ID 與 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 會將刪除或覆寫的資料列保留一小時,以支援歷來讀取和時間點復原功能,您最多可將保留期限延長至七天。舊資料列版本超過保留期限後,系統會在背景儲存空間壓縮期間移除該版本,這項作業最多可能需要七天。在壓縮舊資料列版本之前,掃描鍵範圍的查詢可能必須讀取已刪除或覆寫的資料列版本,並將其篩除。如要進一步瞭解資料保留,請參閱「時間點復原 (PITR)」。
如要確認查詢是否掃描已刪除的資料列,請在查詢計畫視覺化工具詳細資料面板中,查看「Scan」(掃描) 運算子詳細資料檢視畫面中的「Deleted rows scanned」(掃描的已刪除資料列) 指標。
如要緩解因近期刪除大量資料列而導致掃描速度緩慢的問題,您可以手動觸發資料壓縮。詳情請參閱「手動壓縮資料」。
大型資料列
如果是小型資料列 (最多幾百個位元組),掃描費用主要取決於掃描的資料列數量。隨著資料列大小增加,資料列的位元組大小會變得更重要。在資料列導向儲存空間中,資料列的總大小會影響掃描效能,而不只是查詢投影的資料欄大小。
如要對大型資料表執行大量掃描的數據分析查詢,建議使用資料欄引擎。詳情請參閱「監控資料欄引擎」。
如果資料表同時包含大型資料欄和經常查詢的小型資料欄,且查詢範圍較窄,請考慮下列選項:
- 將較大的資料欄分隔至非預設位置群組 (將指定的資料欄與資料表的其餘部分分開儲存在磁碟上),或分隔至子資料表,這樣掃描較小的資料欄時,就不會讀取大型資料欄資料。詳情請參閱「建立及管理位置群組」。
- 如要進一步最佳化讀取時間,請使用
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 個位元組 (壓縮前)。
查看查詢使用的掃描方法
如要檢查查詢是否使用批次導向處理、資料列導向處理,或是在兩種掃描方法之間自動切換,請按照下列步驟操作:
前往 Google Cloud 控制台的 Spanner「Instances」(執行個體) 頁面。
按一下要調查的查詢所屬執行個體名稱。
在「Databases」(資料庫) 表格中,按一下要調查查詢的資料庫。
在 Spanner 導覽選單中,點選「Spanner Studio」。
按一下 「New SQL editor tab」(新增 SQL 編輯器分頁) 或 「New tab」(新增分頁)。
在查詢編輯器中輸入查詢。
按一下「執行」。
Spanner 會執行查詢並顯示結果。
在查詢編輯器窗格,點選「Explanation」(說明) 分頁標籤。
Spanner 會顯示查詢執行計畫視覺化工具。圖表上的每張資訊卡都代表一個疊代器。如要進一步瞭解執行計畫,請參閱「查詢執行計畫」。
按一下「Table scan」(資料表掃描) 疊代器資訊卡,開啟資訊面板。
資訊面板會顯示所選掃描作業的相關資訊,包括掃描方法 (「Method」(方法))。Automatic 表示由 Spanner 決定掃描方法。其他可能的值包括:Batch (批次導向處理) 和 Row (資料列導向處理),如下圖所示:

強制執行查詢使用的掃描方法
為盡可能提高查詢效能,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 運算子都支援批次導向和資料列導向的執行方法。查詢執行計畫視覺化工具會在疊代器資訊卡中,顯示每個運算子的執行方法。如果執行方法是以批次為導向,則會顯示「Batch」(批次)。如果是資料列導向,則會顯示「Row」(資料列)。如要進一步瞭解視覺化工具,請參閱「查詢執行計畫」。
如果查詢中的運算子使用不同的執行方法執行,執行方法轉接器 DataBlockToRowAdapter 和 RowToDataBlockAdapter 會顯示在運算子之間,表示執行方法有所變更。如要進一步瞭解這些運算子,請參閱 DataBlockToRowAdapter 和 RowToDataBlockAdapter。