【问题标题】:WHERE clause is slower with value from CTE than with constant?WHERE 子句使用 CTE 的值比使用常量慢?
【发布时间】:2021-06-11 23:53:05
【问题描述】:

我想在 Postgres 12 上执行查询期间缓存一个变量。我遵循了如下 CTE 的方法:

-- BEGIN PART 1
with cached_vars as (
    select max(datetime) as datetime_threshold
    from locations
    where distance > 70
      and user_id = 9087
)
-- END PART 1
-- BEGIN PART 2
select *
from locations
where user_id = 9087
  and datetime > (select datetime_threshold from cached_vars)
-- END PART 2

运行上述查询会导致性能问题。我预计总运行时间大约等于(part1 runtime + part2 runtime),但它需要更长的时间。

值得注意的是,当我使用手动 datetime_threshold 仅运行第二部分时,没有性能问题。

locations表定义为:

 id | user_id | datetime | location | distance | ...
-----------------------------------------------------

有没有办法将总运行时间减少到(part1 runtime + part2 runtime) 之类的东西?

【问题讨论】:

  • 。 .我不明白你的问题。您的两个“性能估计”是同一个等式。
  • @partizaans 您是否考虑过使用临时表来保存第一次查询的日期?你可以试一试。请看我的回答。
  • 为了获得最佳解决方案,请说明查询的目的。 distance > 70 是常量过滤器吗?还有什么可变的?您需要SELECT * 还是一小部分列就足够了?请按照此处的说明分享基本信息:stackoverflow.com/tags/postgresql-performance/info

标签: sql postgresql performance postgresql-performance


【解决方案1】:

你观察到的差异背后的解释是这样的:

Postgres 具有列统计信息,并且可以根据为 datetime_threshold 提供的 constant 的值调整查询计划。使用有利的过滤器值,这可以导致更有效的查询计划。

在另一种情况下,当 datetime_threshold 必须首先在另一个 SELECT 中计算时,Postgres 必须默认为通用计划。 datetime_threshold 可以是任何东西。

EXPLAIN 输出中的差异会变得很明显。

为确保 Postgres 针对实际 datetime_threshold 值优化第二部分,您可以运行两个单独的查询(将查询 1 的结果作为常量提供给查询 2),或者使用动态 SQL 强制重新规划每次在 PL/pgSQL 函数中查询 2。

例如

CREATE OR REPLACE FUNCTION foo(_user_id int, _distance int = 70)
  RETURNS SETOF locations
  LANGUAGE plpgsql AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
     'SELECT *
      FROM   locations
      WHERE  user_id = $1
      AND    datetime > $2'
   USING _user_id
      , (SELECT max(datetime)
         FROM   locations
         WHERE  distance > _distance
         AND    user_id = _user_id);
END
$func$;

呼叫:

SELECT * FROM foo(9087);

相关:

在极端情况下,您甚至可以使用另一个动态查询来计算datetime_threshold。但我不认为这是必要的。

至于"something useful in the docs"

[...] 重要的区别在于EXECUTE 将重新计划 每次执行的命令,生成一个特定于 当前参数值;而 PL/pgSQL 可能会创建一个 通用计划并将其缓存以供重复使用。 在最好的情况下 计划强烈依赖于参数值,它可以帮助 使用EXECUTE 肯定不会选择通用计划。

我的大胆强调。

索引

完美的索引应该是:

CREATE INDEX ON locations (user_id, distance DESC NULL LAST, date_time DESC NULLS LAST); -- for query 1
CREATE INDEX ON locations (user_id, date_time);           -- for query 2

微调取决于未公开的细节。部分索引可能是一种选择。

您的查询速度慢可能有许多其他原因。没有足够的细节。

【讨论】:

  • 谢谢。那么是否有任何惯用的方式将datetime_threshold 作为常量来避免由第二部分的where 子句中的select 语句引起的此类性能问题?当然,我可以在数据库级别的应用程序层处理这个问题,但我对此感到好奇,却无法在文档中找到任何有用的东西。
  • 我添加了一个示例解决方案。
【解决方案2】:

如果您希望查询执行良好,我建议添加索引locations(user_id, distance)locations(user_id, datetime)

我也会使用窗口函数来表达查询:

select l.*
from (select l.*,
             max(datetime) filter (where distance > 70) over (partition by userid) as datetime_threshold
      from location l
      where userid = 9087
     ) l
where datetime > datetime_threshold;

窗口函数通常可以提高性能。但是,有了正确的索引,我不知道这两个版本是否会有很大的不同。

【讨论】:

  • 这也没有帮助。索引没有问题。 with 子句大约在 2 秒内运行。如果将动态部分 [我的意思是 (select datetime_threshold from cached_vars) 语句] 替换为第一部分的结果 [sth like '2021-03-14 14:40:23.000000 +00:00'],则第二部分需要 1.5 秒。这些部分的组合会破坏性能(查询需要超过 2 分钟才能执行)。
【解决方案3】:

请将查询分成两部分并将第一部分存储在临时表中(PostgreSQL 中的临时表只能在当前数据库会话中访问。)。然后将临时表与第二部分连接起来。希望它会加快处理时间。

 CREATE TEMPORARY TABLE temp_table_cached_vars (
       datetime_threshold timestamp
    );
    
    -- BEGIN PART 1
    with cached_vars as (
        select max(datetime) as datetime_threshold
        from locations
        where distance > 70
          and user_id = 9087
    )insert into temp_table_name select datetime_threshold from cached_vars 
    -- END PART 1
    -- BEGIN PART 2
    select *
    from locations
    where user_id = 9087
      and datetime > (select datetime_threshold from temp_table_cached_vars Limit 1)

-- END PART 2

【讨论】:

  • 谢谢!我测试了你的答案,问题仍然存在。我不知道为什么将datetime > (select datetime_threshold from temp_table_cached_vars ) 更改为datetime > '2021-03-14 15:17:23.000000' 之类的东西时会出现如此巨大的差异以及如何克服这一点。
  • locations 表有多少行?
  • 大约 1,300,000,000。
  • 请您在第二个查询的 where 子句中尝试使用距离
  • 我确实做到了!这也没有帮助。
【解决方案4】:

只需在子查询中添加 Limi1,就像我在下面的示例中使用的那样。

-- BEGIN PART 1
with cached_vars as (
    select max(datetime) as datetime_threshold
    from locations
    where distance > 70
      and user_id = 9087
)
-- END PART 1
-- BEGIN PART 2
select *
from locations
where user_id = 9087
  and datetime > (select datetime_threshold from cached_vars Limit 1)
-- END PART 2

【讨论】:

  • 再次感谢,我尝试了您的回答,性能问题仍然存在。我认为问题出在@erwin-brandstetter 在他的回答中提到的查询本身。感谢您的帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多