【问题标题】:Slow query in postgres using count distinct使用count distinct在postgres中进行慢速查询
【发布时间】:2014-09-21 11:03:31
【问题描述】:

我的目标是创建一个查询,该查询将返回在 365 天窗口内购买的唯一客户的计数。我在 postgres 中创建了下面的查询,结果查询非常慢。我的表是 812,024 行的订单日期和客户 ID。当我删除 distinct 语句时,我可以让查询在大约 60 秒内返回结果,但我还没有完成。我在 (order_date, id) 创建了一个索引。我是 SQL 的新手,这真的是我第一次用它做任何事情,在整天试图找到解决这个问题的方法之后,我找不到任何可以开始工作的东西,即使我已经看到了很多关于 distinct 的缓慢性能。

SELECT
    (d1.Ordered) AS Ordered,
    COUNT(distinct d2.ID) Users
FROM
(
    SELECT order_date AS Ordered
    FROM orders
    GROUP BY order_date
) d1 
INNER JOIN
(
    SELECT order_date AS Ordered, id
    FROM orders
) d2
ON d2.Ordered BETWEEN d1.Ordered - 364 AND d1.Ordered
GROUP BY d1.Ordered
ORDER BY d1.Ordered

"Sort  (cost=3541596.30..3541596.80 rows=200 width=29)"
"  Sort Key: orders_1.order_date"
"  ->  HashAggregate  (cost=3541586.66..3541588.66 rows=200 width=29)"
"        ->  Nested Loop  (cost=16121.73..3040838.52 rows=100149627 width=29)"
"              ->  HashAggregate  (cost=16121.30..16132.40 rows=1110 width=4)"
"                    ->  Seq Scan on orders orders_1  (cost=0.00..14091.24 rows=812024 width=4)"
"              ->  Index Only Scan using x on orders  (cost=0.43..1822.70 rows=90225 width=29)"
"                    Index Cond: ((order_date >= (orders_1.order_date - 364)) AND (order_date <= orders_1.order_date))"

【问题讨论】:

  • 不太确定我是否理解您在这里的设置...介意给我们关于订单的表创建语句并让我们知道与用户的订单关系是如何工作的吗?我在现有查询中看不到与用户有任何关系...您正在将日期加入日期,我不太明白那里有不同的用户
  • 自加入的目的是什么?
  • 我不知道我说清楚了没有,但是每一行都会有一个日期和在前365天内购买的唯一客户的数量。因此,今天的数字将是去年的唯一客户数,这将是一个滑动范围,自有订单以来的每一天都有一个条目。
  • 同一客户在同一天进行多次购买的频率如何?
  • 只统计一次去年购买过的客户。如果他们在去年购买了一次或多次,则属于活跃客户的定义,因此我只想计算一次。我想每天跟踪这个指标,看看每天的活跃客户总数是多少。

标签: sql postgresql


【解决方案1】:

不需要自加入,使用generate_series

select
    g.order_date as "Ordered",
    count(distinct o.id) as "Users"
from
    generate_series(
        (select min(order_date) from orders),
        (select max(order_date) from orders),
        '1 day'
    ) g (order_date)
    left join
    orders o on o.order_date between g.order_date - 364 and g.order_date
group by 1
order by 1

【讨论】:

    【解决方案2】:

    您还没有显示您的架构,所以这里有些猜测。根据需要更改列名等。

    SELECT 
      count(DISTINCT users.user_id)
    FROM users
    INNER JOIN order_date ON (users.user_id = orders.user_id)
    WHERE orders.order_date > current_date - INTERVAL '1' YEAR;
    

    SELECT 
      count(users.user_id)
    FROM users
    INNER JOIN order_date ON (users.user_id = orders.user_id)
    WHERE orders.order_date > current_date - INTERVAL '1' YEAR
    GROUP BY users.user_id;
    

    【讨论】:

      【解决方案3】:

      假设实际的date 类型。

      SELECT d.day, count(distinct o.id) AS users_past_year
      FROM  (
         SELECT generate_series(min(order_date), max(order_date), '1 day')::date AS day
         FROM   orders         -- single query
         ) d
      LEFT JOIN (              -- fold duplicates on same day right away
         SELECT id, order_date
         FROM   orders
         GROUP  BY 1,2
         ) o ON o.order_date >  d.day - interval '1 year' -- exclude
            AND o.order_date <= d.day                     -- include
      GROUP  BY 1
      ORDER  BY 1;
      

      只有在常见的情况下,首先折叠同一用户在同一天的多次购买才有意义。否则,省略该步骤并简单地左连接到表 orders 会更快。

      orders.id 是用户的 ID,这很奇怪。应该命名为user_id

      如果您对 SELECT 列表中的 generate_series() 不满意(效果很好),您可以在 Postgres 9.3+ 中将其替换为 LATERAL JOIN

      FROM  (SELECT min(order_date) AS a
                  , max(order_date) AS z FROM orders) x
          , generate_series(x.a, x.z, '1 day') AS d(day)
      LEFT JOIN ...
      

      请注意,在这种情况下,daytimestamp 类型。工作相同。您可能想要投射。

      一般性能提示

      我了解这是针对单个用户的只读表。这简化了事情。
      您似乎已经有了索引:

      CREATE INDEX orders_mult_idx ON orders (order_date, id);
      

      这很好。

      一些尝试:

      基础知识

      当然,通常的性能建议适用:
      https://wiki.postgresql.org/wiki/Slow_Query_Questions
      https://wiki.postgresql.org/wiki/Performance_Optimization

      流线型表

      使用此索引对您的表进行一次聚类:

      CLUSTER orders USING orders_mult_idx;
      

      这应该会有所帮助。它还有效地在表上运行VACUUM FULL,这会删除所有死行并在适用时压缩表。

      更好的统计数据

      ALTER TABLE orders ALTER COLUMN number SET STATISTICS 1000;
      ANALYZE orders;
      

      此处解释:

      分配更多内存

      确保您有充足的资源分配。特别是shared_buffers and work_mem。您可以为您的会话临时执行此操作。

      planner methods做实验

      尝试禁用嵌套循环 (enable_nestloop)(仅在您的会话中)。也许哈希连接更快。 (不过,我会感到惊讶。)

      SET enable_nestedloop = off;
      -- test ...
      
      RESET enable_nestedloop;
      

      临时表

      由于这似乎本质上是一个“临时表”,您可以尝试使其成为仅保存在 RAM 中的实际临时表。您需要足够的 RAM 来分配足够的 temp_buffers。详细说明:

      请务必手动运行ANALYZE。临时表不被 autovacuum 覆盖。

      【讨论】:

      • 对不起,这是一张奇怪的桌子。我只是想为我们的活跃客户历史生成一个一次性指标,并且第一次弄乱了 sql,所以我只是转储了一个 csv,其中包含在一列中的订单日期和在该日期订购的客户 ID其他列。我没有像您传统上那样使用单独的客户和订单表进行设置。一旦我得到我需要的数据,我就打算删除表。很抱歉造成混乱。
      • 我能够用这个查询返回一个结果,但它最终花费了 44 分钟,这可能与我之前能够提出的查询相同。我确定我创建数据库或其他东西的方式可能有问题,因为这是我第一次创建数据库,但是您知道为什么查询会花费这么长时间吗?你的头?
      • @lbollar:它一个昂贵的查询。 DISTINCT 强制 Postgres 在一年的时间范围内计算每天的值,这将是您每天 800k 行的大部分。不过,44 分钟似乎仍然过多。我添加了一些可能有助于提高性能的提示。
      • 通过提升 work_mem,我获得了超过 15 分钟的查询时间。除了禁用嵌套循环和创建临时表之外,我尝试了其余的建议,虽然我也会尝试这些,但除了增加它们的内存之外,没有任何效果。我正在考虑解决这个问题,你的查询比我原来的查询好得多,非常详细,还有很多额外的阅读可以帮助我。这是我的第一个 SO 问题,下次我会尝试提供更好的细节。谢谢。
      猜你喜欢
      • 1970-01-01
      • 2019-01-06
      • 1970-01-01
      • 2011-09-29
      • 1970-01-01
      • 1970-01-01
      • 2017-03-23
      • 2011-11-11
      • 1970-01-01
      相关资源
      最近更新 更多