优化 Redshift 查询的大 IN 条件

Posted

技术标签:

【中文标题】优化 Redshift 查询的大 IN 条件【英文标题】:Optimize large IN condition for Redshift query 【发布时间】:2016-02-19 06:40:46 【问题描述】:

我有一个约 2TB 完全清理过的 Redshift 表,其中包含一个 distkey phash(高基数,数亿个值)和复合排序键 (phash, last_seen)

当我进行如下查询时:

SELECT
    DISTINCT ret_field
FROM
    table
WHERE
    phash IN (
        '5c8615fa967576019f846b55f11b6e41',
        '8719c8caa9740bec10f914fc2434ccfd',
        '9b657c9f6bf7c5bbd04b5baf94e61dae'
    )
AND
    last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'

它很快就会返回。但是,当我将哈希数增加到 10 以上时,Redshift 会将 IN 条件从一堆 OR 转换为一个数组,每个 http://docs.aws.amazon.com/redshift/latest/dg/r_in_condition.html#r_in_condition-optimization-for-large-in-lists

问题是当我有几十个phash 值时,“优化”查询从不到一秒的响应时间变为半小时以上。换句话说,它停止使用排序键并进行全表扫描。

知道如何防止这种行为并保留使用排序键以保持快速查询吗?

这是 10 哈希之间的EXPLAIN 区别:

少于 10 个(0.4 秒):

XN Unique  (cost=0.00..157253450.20 rows=43 width=27)
    ->  XN Seq Scan on table  (cost=0.00..157253393.92 rows=22510 width=27)
                Filter: ((((phash)::text = '394e9a527f93377912cbdcf6789787f1'::text) OR ((phash)::text = '4534f9f8f68cc937f66b50760790c795'::text) OR ((phash)::text = '5c8615fa967576019f846b55f11b6e61'::text) OR ((phash)::text = '5d5743a86b5ff3d60b133c6475e7dce0'::text) OR ((phash)::text = '8719c8caa9740bec10f914fc2434cced'::text) OR ((phash)::text = '9b657c9f6bf7c5bbd04b5baf94e61d9e'::text) OR ((phash)::text = 'd7337d324be519abf6dbfd3612aad0c0'::text) OR ((phash)::text = 'ea43b04ac2f84710dd1f775efcd5ab40'::text)) AND (last_seen >= '2015-10-01 00:00:00'::timestamp without time zone) AND (last_seen <= '2015-10-31 23:59:59'::timestamp without time zone))

超过 10 个(45-60 分钟):

XN Unique  (cost=0.00..181985241.25 rows=1717530 width=27)
    ->  XN Seq Scan on table  (cost=0.00..179718164.48 rows=906830708 width=27)
                Filter: ((last_seen >= '2015-10-01 00:00:00'::timestamp without time zone) AND (last_seen <= '2015-10-31 23:59:59'::timestamp without time zone) AND ((phash)::text = ANY ('33b84c5775b6862df965a0e00478840e,394e9a527f93377912cbdcf6789787f1,3d27b96948b6905ffae503d48d75f3d1,4534f9f8f68cc937f66b50760790c795,5a63cd6686f7c7ed07a614e245da60c2,5c8615fa967576019f846b55f11b6e61,5d5743a86b5ff3d60b133c6475e7dce0,8719c8caa9740bec10f914fc2434cced,9b657c9f6bf7c5bbd04b5baf94e61d9e,d7337d324be519abf6dbfd3612aad0c0,dbf4c743832c72e9c8c3cc3b17bfae5f,ea43b04ac2f84710dd1f775efcd5ab40,fb4b83121cad6d23e6da6c7b14d2724c'::text[])))

【问题讨论】:

当您说“它停止使用排序键并进行全表扫描”时,我不明白。 Redshift 总是进行全表扫描,但它可能会使用排序键来跳过块。你能提供查询的确切解释吗? 没问题@MarkHildreth - 我刚刚编辑了主帖以包含EXPLAIN 查询。 备注,对 SO 读者和用户不太公平(但您可以在此处发布解决方案):postgresql 性能问题有一个专门的邮件列表。 向我们展示表格结构 对于这样的性能问题,显示数据类型和约束的实际表定义是必不可少的。最好是完整的CREATE TABLE 语句,以及所有相关的索引定义。 【参考方案1】:

您可以尝试创建临时表/子查询:

SELECT DISTINCT t.ret_field
FROM table t
JOIN (
   SELECT '5c8615fa967576019f846b55f11b6e41' AS phash
   UNION ALL 
   SELECT '8719c8caa9740bec10f914fc2434ccfd' AS phash
   UNION ALL
   SELECT '9b657c9f6bf7c5bbd04b5baf94e61dae' AS phash
   -- UNION ALL
) AS sub
   ON t.phash = sub.phash
WHERE t.last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59';

或者分块进行搜索(如果查询优化器将其合并为一个,则使用辅助表来存储中间结果):

SELECT ret_field
FROM table
WHERE phash IN (
        '5c8615fa967576019f846b55f11b6e41',
        '8719c8caa9740bec10f914fc2434ccfd',
        '9b657c9f6bf7c5bbd04b5baf94e61dae')
  AND last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'
UNION
SELECT ret_field
FROM table
WHERE phash IN ( ) -- more hashes)
  AND last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'
UNION 
-- ...

如果查询优化器将其合并为一个,您可以尝试使用临时表来获取中间结果

编辑:

SELECT DISTINCT t.ret_field
FROM table t
JOIN (SELECT ... AS phash
      FROM ...
) AS sub
   ON t.phash = sub.phash
WHERE t.last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59';

【讨论】:

这实际上可以继续使用索引(谢谢!),但我需要从另一个子查询返回phash 值的列表......它不是手动/编码的。有没有办法从另一个子查询的结果中使用/滥用 UNION ALL? :( @Harry 您可以将UNION ALL 更改为任何返回 phash 的内容 我之前试过EDIT,和扫表效果一样。而且我不能把它分成几块,因为散列都是从 Redshift 大批量返回的。 @lad2025,您使用SELECT DISTINCT ... UNION ALL SELECT DISTINCT ... UNION ALL ... 将散列拆分为小块的第二个变体并不等同于问题中的原始查询。原始查询在ret_field 的所有值上都有DISTINCT。您的变体可以返回重复项。看来您需要使用UNION,而不是UNION ALL。有了UNION,就不需要DISTINCTs。 @lad2025 这是一个有趣的想法,但同样我无法控制次数或手动迭代。我需要构建一个可以处理从 2-3 行到数万行的查询。有什么想法吗?【参考方案2】:

值得尝试设置sortkeys (last_seen, phash),将last_seen 放在首位。

缓慢的原因可能是因为排序键的前导列是phash,它看起来像一个随机字符。 正如 AWS redshift 开发文档所说,如果将时间戳列用于 where 条件,则时间戳列应作为排序键的前导列。

如果最近查询的数据最频繁,指定时间戳 列作为排序键的前导列。 - Choose the Best Sort Key - Amazon Redshift

使用此排序键顺序,所有列将按last_seen 排序,然后是phash。 (What does it mean to have multiple sortkey columns?)

需要注意的是,您必须重新创建表才能更改排序键。 This 将帮助您做到这一点。

【讨论】:

简单的解决方案,但这解决了它!仍然不是很快,但显然排序键在随机字符串上的效率非常低。【参考方案3】:

你真的需要DISTINCT 吗?这个运算符可能很昂贵。

我会尝试使用LATERAL JOIN。在表Hashes 下方的查询中,有一列phash - 这是您的大批量哈希。它可以是一个临时表,一个(子)查询,任何东西。

SELECT DISTINCT T.ret_field
FROM
    Hashes
    INNER JOIN LATERAL
    (
        SELECT table.ret_field
        FROM table
        WHERE
            table.phash = Hashes.phash
            AND table.last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'
    ) AS T ON true

优化器很可能将LATERAL JOIN 实现为嵌套循环。它将遍历Hashes 中的所有行,并为每一行运行SELECT FROM table。内部SELECT 应该使用您在(phash, last_seen) 上的索引。为了安全起见,将ret_field 包含到索引中,使其成为覆盖索引:(phash, last_seen, ret_field)


@Diego 的回答中有一个非常有效的观点:不要将常量 phash 值放入查询中,而是将它们放入临时表或永久表中。

我想扩展@Diego 的答案,并补充说这个带有哈希的表具有索引、唯一索引很重要。

因此,创建一个表Hashes,其中一列phash 与主table.phash 中的类型完全相同。类型匹配很重要。使该列成为具有唯一聚集索引的主键。将您的数十个 phash 值转储到 Hashes 表中。

那么查询就变成了简单的INNER JOIN,不是横向的:

SELECT DISTINCT T.ret_field
FROM
    Hashes
    INNER JOIN table ON table.phash = Hashes.phash
WHERE
    table.last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'

table(phash, last_seen, ret_field) 上有索引仍然很重要。

优化器应该能够利用两个连接表都按phash 列排序并且在Hashes 表中是唯一的这一事实。

【讨论】:

我已经尝试了横向连接的所有可能变化,但我不断收到语法错误。您确定 Redshift 支持它们吗? @Harry,不,我不确定 Redshift 是否有 LATERAL JOIN。我看到了 Postgres 标签,并没有关注 Redshift 标签。厄运。 Redshift 有存储过程和游标吗?通常,游标在执行相同操作时比声明性 SQL 慢。但是,在这种情况下,声明性 SQL 不会为每个 phash 执行索引查找,因此每个 phash 的显式循环以及将结果附加到临时表中可能总体上更快。【参考方案4】:

您可以通过将所需数据插入临时表并将其与实际表连接起来,从而摆脱“OR”。

这是一个示例(我使用的是 CTE,因为当您有多个 SQL 语句时,我使用的工具很难捕获计划 - 但如果可以的话,请使用临时表)

select * 
from <my_table>
where checksum in 
(
'd7360f1b600ae9e895e8b38262cee47936fb6ced',
'd1606f795152c73558513909cd59a8bc3ad865a8',
'bb3f6bb3d1a98d35a0f952a53d738ddec5c72c84',
'b2cad5a92575ed3868ac6e405647c2213eea74a5'
)

对比

with foo as
(
    select 'd7360f1b600ae9e895e8b38262cee47936fb6ced' as my_key union
    select 'd1606f795152c73558513909cd59a8bc3ad865a8' union
    select 'bb3f6bb3d1a98d35a0f952a53d738ddec5c72c84' union
    select 'b2cad5a92575ed3868ac6e405647c2213eea74a5'
)
select  * 
from <my_table> r 
     join foo f on r.checksum = F.my_key

这是计划,您可以看到它看起来更复杂,但这是因为 CTE,它不会在临时表上看起来那样:

【讨论】:

【参考方案5】:

您是否尝试对所有 phash 值使用联合?

就这样:

SELECT ret_field 
FROM   table 
WHERE  phash = '5c8615fa967576019f846b55f11b6e41' -- 1st phash value
and    last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'

UNION 

SELECT ret_field 
FROM   table 
WHERE  phash = '8719c8caa9740bec10f914fc2434ccfd' -- 2nd phash value
and    last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'

UNION 

SELECT ret_field 
FROM   table 
WHERE  phash = '9b657c9f6bf7c5bbd04b5baf94e61dae' -- 3rd phash value
and    last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'

-- and so on...

UNION 

SELECT ret_field 
FROM   table 
WHERE  phash = 'nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn' -- Nth phash value
and    last_seen BETWEEN '2015-10-01 00:00:00' AND '2015-10-31 23:59:59'

【讨论】:

以上是关于优化 Redshift 查询的大 IN 条件的主要内容,如果未能解决你的问题,请参考以下文章

条件包含 IN 语句的 CASE 语句 redshift

MySQL 对于千万级的大表要怎么优化

在 Redshift 中查找常用连接查询

数千个值的 Redshift IN 条件

有没有办法在 Redshift Spectrum 中使用“IN”条件检查多个列?

Redshift SQL 查询 - 优化