@@ -262,4 +262,100 @@ STORING 自体の書き込みコストも考慮する必要があるが、行全
262
262
263
263
## INTERLEAVE
264
264
265
- (TODO: あとで書く)
265
+ (TODO: あとで書く)
266
+
267
+ ## 低レベル API(Read) と SQL レベル API (ExecuteSql) の違いは?
268
+
269
+ 低レベル API の Read にできることと SQL の実行計画オペレータの対応関係は下記のようになる。
270
+
271
+ * ` table ` で指定した論理テーブルのベーステーブルもしくは ` index ` で指定したセカンダリインデックスに含まれる列(` columns ` )からなる行をスキャンする。
272
+ * Scan
273
+ * (セカンダリインデックスに含まれない列を取得することができないため、暗黙の JOIN は発生しない。)
274
+ * スキャンを指定した複数のキー範囲(` key_set ` )に限定する。
275
+ * FilterScan の Seek Condition
276
+ * (Residual Condition 相当は不可能)
277
+ * ソート済のスキャン対象から ` limit ` の数だけ行を読み出し
278
+ * Local Limit
279
+ * (ソート順を指定できないため Sort Limit は不可能)
280
+ * 範囲がスプリットを跨ぐ場合、各スプリットを持つサーバーで取得した結果を1つのストリームとして得る。
281
+ * Split Range 付きの Distributed Union +
282
+ * スプリット、サーバーを跨ぐ最終的な結果も ` limit ` の数に収まるようにする。
283
+ * Global Limit
284
+ * (ソート順を指定できないため Sort Limit は不可能)
285
+ * 結果としてのデータ形式に変換する
286
+ * Serialize Result
287
+
288
+ つまり、下記の形状の実行計画が ` Read ` API でできるもっとも複雑な処理と同等となる。
289
+
290
+ ```
291
+ > EXPLAIN SELECT FirstName, LastName, SingerId FROM Singers@{FORCE_INDEX=SingersByFirstLastName} WHERE FirstName LIKE 'A%' LIMIT 10;
292
+ +----+--------------------------------------------------------------+
293
+ | ID | Query_Execution_Plan (EXPERIMENTAL) |
294
+ +----+--------------------------------------------------------------+
295
+ | 0 | Global Limit |
296
+ | *1 | +- Distributed Union |
297
+ | 2 | +- Serialize Result |
298
+ | 3 | +- Local Limit |
299
+ | 4 | +- Local Distributed Union |
300
+ | *5 | +- FilterScan |
301
+ | 6 | +- Index Scan (Index: SingersByFirstLastName) |
302
+ +----+--------------------------------------------------------------+
303
+ Predicates(identified by ID):
304
+ 1: Split Range: STARTS_WITH($FirstName, 'A')
305
+ 5: Seek Condition: STARTS_WITH($FirstName, 'A')
306
+ ```
307
+
308
+ これよりも複雑なことをサーバサイドで行いたい場合は SQL で実行する必要があり、それに応じたコストを払うことになる。
309
+ なお、 Read は SQL のように想定外のコストが高い実行計画が選ばれる危険がないため予測可能性が高いが、
310
+ クライアント側で処理できるようにデータを取得することがサーバサイドで処理するよりも Cloud Spanner インスタンスへの負荷が低いとは限らない。
311
+
312
+ 例えば ` GROUP BY ` については、 Stream Aggregate であれば Scan に対して追加のコストはかなり少ない。
313
+
314
+ 下記の ` GROUP BY ` を使ったクエリはキーの順序に対して効率的に処理できるため Stream Aggregate となり、あまりコストが掛からない。
315
+ Local Stream Aggregate よりも上では集約済の行のみを扱うため処理する量が少なくほぼ時間を使っていない。
316
+
317
+ ```
318
+ > EXPLAIN ANALYZE SELECT SongGenre, SUM(Duration) FROM Songs GROUP BY SongGenre;
319
+ +----+-----------------------------------------------------------------------------+---------------+------------+---------------+
320
+ | ID | Query_Execution_Plan | Rows_Returned | Executions | Total_Latency |
321
+ +----+-----------------------------------------------------------------------------+---------------+------------+---------------+
322
+ | 0 | Serialize Result | 4 | 1 | 313.96 msecs |
323
+ | 1 | +- Global Stream Aggregate | 4 | 1 | 313.94 msecs |
324
+ | 2 | +- Distributed Union | 4 | 1 | 313.93 msecs |
325
+ | 3 | +- Local Stream Aggregate | 4 | 1 | 313.92 msecs |
326
+ | 4 | +- Local Distributed Union | 1024000 | 1 | 285.65 msecs |
327
+ | 5 | +- Index Scan (Full scan: true, Index: SongsBySongGenreStoring) | 1024000 | 1 | 256.71 msecs |
328
+ +----+-----------------------------------------------------------------------------+---------------+------------+---------------+
329
+ 4 rows in set (318.28 msecs)
330
+ timestamp: 2020-09-29T16:26:00.69254+09:00
331
+ cpu: 314.88 msecs
332
+ scanned: 1024000 rows
333
+ optimizer: 2
334
+ ```
335
+
336
+ 同じ集計をクライアントサイドで行うには全ての行を取得する必要があり、等価な SQL クエリであれば Serialize Result や Distributed Union で時間がかかる結果となる。
337
+
338
+ ```
339
+ > EXPLAIN ANALYZE SELECT SongGenre, Duration FROM Songs@{FORCE_INDEX=SongsBySongGenreStoring};
340
+ +----+-----------------------------------------------------------------------+---------------+------------+---------------+
341
+ | ID | Query_Execution_Plan | Rows_Returned | Executions | Total_Latency |
342
+ +----+-----------------------------------------------------------------------+---------------+------------+---------------+
343
+ | 0 | Distributed Union | 1024000 | 1 | 522.63 msecs |
344
+ | 1 | +- Local Distributed Union | 1024000 | 1 | 477.35 msecs |
345
+ | 2 | +- Serialize Result | 1024000 | 1 | 453.77 msecs |
346
+ | 3 | +- Index Scan (Full scan: true, Index: SongsBySongGenreStoring) | 1024000 | 1 | 319.48 msecs |
347
+ +----+-----------------------------------------------------------------------+---------------+------------+---------------+
348
+ 1024000 rows in set (616.72 msecs)
349
+ timestamp: 2020-09-29T16:25:58.768777+09:00
350
+ cpu: 602.56 msecs
351
+ scanned: 1024000 rows
352
+ optimizer: 2
353
+ ```
354
+
355
+ このように、集計済の値のみを通信すれば良いサーバサイドでの処理の方が高速かつ Cloud Spanner インスタンスの負荷も低い場合はある。
356
+
357
+ クエリ実行時の負荷以外にも Cloud Spanner で SQL クエリを実行するには、クエリを解析してオプティマイザによって実行計画を得るオーバーヘッドがあり、これが Read では掛からないという違いがある。
358
+ 文字列として一致するクエリの実行計画はキャッシュされるため、この差は小さくなる。フィルタ条件などが可変する場合には文字列として同じにならない動的 SQL ではなく可能な限りクエリパラメータを使うと良い。
359
+
360
+ 参考
361
+ * https://cloud.google.com/spanner/docs/whitepapers/life-of-query?hl=en
0 commit comments