是否可以在 spanner 中进行高效的时间戳排序查询?
Is it possible to have efficient timestamp ordered queries in spanner?
我已关注这篇文章 https://cloud.google.com/blog/products/gcp/sharding-of-timestamp-ordered-data-in-cloud-spanner 并创建了一个有点类似的架构,只是没有公司 ID:
CREATE TABLE Foo (
random_id STRING(22) NOT NULL,
shard_id INT64 NOT NULL,
timestamp_order TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
) PRIMARY KEY(random_id);
CREATE INDEX OrderIndex ON Foo(shard_id, timestamp_order);
shard_id是一个0到49的随机数。那我运行一堆selects反对它:
1: SELECT * FROM Foo@{FORCE_INDEX=OrderIndex} where shard_id=0 order by timestamp_order limit 1;
# this correctly scans 1 row
2: SELECT * FROM Foo@{FORCE_INDEX=OrderIndex} where shard_id<1 order by timestamp_order limit 1;
# this scans 192 rows
3: SELECT * FROM Foo@{FORCE_INDEX=OrderIndex} where shard_id BETWEEEN 1 AND 1 order by timestamp_order limit 1;
# this scans 185 rows
4: SELECT * FROM Foo@{FORCE_INDEX=OrderIndex} where shard_id BETWEEN 0 AND 1 order by timestamp_order limit 1;
# this scans 377 rows
我期待这样的事情:
Query #2 should scan 1 row
Query #3 should scan 1 row
Query #4 should scan 2 rows.
问题:我在这里做错了什么?是否可以在 spanner 中进行高效的时间戳排序查询?
当您指定多个分片 ID(或可以产生多个分片 ID 的表达式)时,此时的理论结果集不再按时间戳排序(而对于单个分片则为),所以它必须依靠时间戳(并且必须考虑每一行)。当您指定一个限制时,理论上的优化是从每个分片中取出前 N 个并合并,但看起来优化没有到位。
您可以通过运行在每个相关分片上并行执行 limit 1 查询并合并结果,从而在应用层实现这一点。
您可以使用 HAVING MIN 构造高效地执行此查询。
重写#2:
SELECT *
FROM
(
SELECT shard_id, ANY_VALUE(random_id HAVING MIN timestamp_order) AS random_id, MIN(timestamp_order) AS timestamp_order
FROM Foo@{FORCE_INDEX=OrderIndex}
GROUP BY shard_id
WHERE shard_id<1
)
ORDER BY timestamp_order
LIMIT 1;
效率将来自内部子查询。它应该只为每个 shard_id 扫描一行,然后从这些行中选择最小值。如果您发现情况并非如此,那么 hint 可以强制执行该行为。
SELECT *
FROM
(
SELECT shard_id, ANY_VALUE(random_id HAVING MIN timestamp_order) AS random_id, MIN(timestamp_order) AS timestamp_order
FROM Foo@{FORCE_INDEX=OrderIndex, GROUPBY_SCAN_OPTIMIZATION=true}
GROUP BY shard_id
WHERE shard_id<1
)
ORDER BY timestamp_order
LIMIT 1;
对于其他查询,只需替换内部子查询中的过滤条件即可。
我看到了关于任意 LIMIT 的评论。这对于后续评论来说太长了(不允许)所以我添加了另一个答案。
对于任意LIMIT,需要更复杂的查询才能获得最高效率。这是一个使用过滤器“shard_id < 1000”和 LIMIT x.
的模板
连接的第一端将有效地提取符合条件的 shard_id 值,非常类似于带有 LIMIT 1 的原始查询。连接的第二端将返回到 table 并获取每个 shard_id 的前 x 行。然后 parent 将 select 所有符合条件的 shard_is 值中的前 x 个。如果每个 shard_id 有很多时间戳,那么这将非常有效。
SELECT ff.*
FROM
(
SELECT shard_id
FROM Foo@{FORCE_INDEX=OrderIndex,
GROUPBY_SCAN_OPTIMIZATION=true}
WHERE shard_id < 1000
GROUP BY shard_id
) shards
JOIN UNNEST(ARRAY
(
SELECT AS STRUCT *
FROM Foo@{FORCE_INDEX=OrderIndex} AS f
WHERE f.shard_id = shards.shard_id
ORDER BY timestamp_order
LIMIT x
)) AS ff
ORDER BY ff.timestamp_order
LIMIT x;
我已关注这篇文章 https://cloud.google.com/blog/products/gcp/sharding-of-timestamp-ordered-data-in-cloud-spanner 并创建了一个有点类似的架构,只是没有公司 ID:
CREATE TABLE Foo (
random_id STRING(22) NOT NULL,
shard_id INT64 NOT NULL,
timestamp_order TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
) PRIMARY KEY(random_id);
CREATE INDEX OrderIndex ON Foo(shard_id, timestamp_order);
shard_id是一个0到49的随机数。那我运行一堆selects反对它:
1: SELECT * FROM Foo@{FORCE_INDEX=OrderIndex} where shard_id=0 order by timestamp_order limit 1;
# this correctly scans 1 row
2: SELECT * FROM Foo@{FORCE_INDEX=OrderIndex} where shard_id<1 order by timestamp_order limit 1;
# this scans 192 rows
3: SELECT * FROM Foo@{FORCE_INDEX=OrderIndex} where shard_id BETWEEEN 1 AND 1 order by timestamp_order limit 1;
# this scans 185 rows
4: SELECT * FROM Foo@{FORCE_INDEX=OrderIndex} where shard_id BETWEEN 0 AND 1 order by timestamp_order limit 1;
# this scans 377 rows
我期待这样的事情:
Query #2 should scan 1 row
Query #3 should scan 1 row
Query #4 should scan 2 rows.
问题:我在这里做错了什么?是否可以在 spanner 中进行高效的时间戳排序查询?
当您指定多个分片 ID(或可以产生多个分片 ID 的表达式)时,此时的理论结果集不再按时间戳排序(而对于单个分片则为),所以它必须依靠时间戳(并且必须考虑每一行)。当您指定一个限制时,理论上的优化是从每个分片中取出前 N 个并合并,但看起来优化没有到位。
您可以通过运行在每个相关分片上并行执行 limit 1 查询并合并结果,从而在应用层实现这一点。
您可以使用 HAVING MIN 构造高效地执行此查询。
重写#2:
SELECT *
FROM
(
SELECT shard_id, ANY_VALUE(random_id HAVING MIN timestamp_order) AS random_id, MIN(timestamp_order) AS timestamp_order
FROM Foo@{FORCE_INDEX=OrderIndex}
GROUP BY shard_id
WHERE shard_id<1
)
ORDER BY timestamp_order
LIMIT 1;
效率将来自内部子查询。它应该只为每个 shard_id 扫描一行,然后从这些行中选择最小值。如果您发现情况并非如此,那么 hint 可以强制执行该行为。
SELECT *
FROM
(
SELECT shard_id, ANY_VALUE(random_id HAVING MIN timestamp_order) AS random_id, MIN(timestamp_order) AS timestamp_order
FROM Foo@{FORCE_INDEX=OrderIndex, GROUPBY_SCAN_OPTIMIZATION=true}
GROUP BY shard_id
WHERE shard_id<1
)
ORDER BY timestamp_order
LIMIT 1;
对于其他查询,只需替换内部子查询中的过滤条件即可。
我看到了关于任意 LIMIT 的评论。这对于后续评论来说太长了(不允许)所以我添加了另一个答案。
对于任意LIMIT,需要更复杂的查询才能获得最高效率。这是一个使用过滤器“shard_id < 1000”和 LIMIT x.
的模板连接的第一端将有效地提取符合条件的 shard_id 值,非常类似于带有 LIMIT 1 的原始查询。连接的第二端将返回到 table 并获取每个 shard_id 的前 x 行。然后 parent 将 select 所有符合条件的 shard_is 值中的前 x 个。如果每个 shard_id 有很多时间戳,那么这将非常有效。
SELECT ff.*
FROM
(
SELECT shard_id
FROM Foo@{FORCE_INDEX=OrderIndex,
GROUPBY_SCAN_OPTIMIZATION=true}
WHERE shard_id < 1000
GROUP BY shard_id
) shards
JOIN UNNEST(ARRAY
(
SELECT AS STRUCT *
FROM Foo@{FORCE_INDEX=OrderIndex} AS f
WHERE f.shard_id = shards.shard_id
ORDER BY timestamp_order
LIMIT x
)) AS ff
ORDER BY ff.timestamp_order
LIMIT x;