【问题标题】:Get latest child per parent from big table - query is too slow从大表中获取每个父母的最新孩子 - 查询太慢
【发布时间】:2011-12-26 15:14:59
【问题描述】:

我有一个由 Django 的 ORM 生成的查询,需要数小时才能运行。

report_rank 表(5000 万行)与 report_profile(100k 行)存在一对多关系。我正在尝试为每个 report_profile 检索最新的 report_rank

我在一个超大的 Amazon EC2 服务器上运行 Postgres 9.1,该服务器具有大量可用 RAM(使用 2GB/15GB)。当然,磁盘 IO 很糟糕。

我在report_rank.created 以及所有外键字段上都有索引。

我可以做些什么来加快这个查询?我很乐意为查询尝试不同的方法,如果它是高性能的,或者调整所需的任何数据库配置参数。

EXPLAIN 
SELECT "report_rank"."id", "report_rank"."keyword_id", "report_rank"."site_id"
     , "report_rank"."rank", "report_rank"."url", "report_rank"."competition"
     , "report_rank"."source", "report_rank"."country", "report_rank"."created"
     , MAX(T7."created") AS "max" 
FROM "report_rank" 
LEFT OUTER JOIN "report_site" 
  ON ("report_rank"."site_id" = "report_site"."id") 
INNER JOIN "report_profile" 
  ON ("report_site"."id" = "report_profile"."site_id") 
INNER JOIN "crm_client" 
  ON ("report_profile"."client_id" = "crm_client"."id") 
INNER JOIN "auth_user" 
  ON ("crm_client"."user_id" = "auth_user"."id") 
LEFT OUTER JOIN "report_rank" T7 
  ON ("report_site"."id" = T7."site_id") 
WHERE ("auth_user"."is_active" = True  AND "crm_client"."is_deleted" = False ) 
GROUP BY "report_rank"."id", "report_rank"."keyword_id", "report_rank"."site_id"
     , "report_rank"."rank", "report_rank"."url", "report_rank"."competition"
     , "report_rank"."source", "report_rank"."country", "report_rank"."created" 
HAVING MAX(T7."created") =  "report_rank"."created";

EXPLAIN的输出:

GroupAggregate  (cost=1136244292.46..1276589375.47 rows=48133327 width=72)
  Filter: (max(t7.created) = report_rank.created)
  ->  Sort  (cost=1136244292.46..1147889577.16 rows=4658113881 width=72)
        Sort Key: report_rank.id, report_rank.keyword_id, report_rank.site_id, report_rank.rank, report_rank.url, report_rank.competition, report_rank.source, report_rank.country, report_rank.created
        ->  Hash Join  (cost=1323766.36..6107863.59 rows=4658113881 width=72)
              Hash Cond: (report_rank.site_id = report_site.id)
              ->  Seq Scan on report_rank  (cost=0.00..1076119.27 rows=48133327 width=64)
              ->  Hash  (cost=1312601.51..1312601.51 rows=893188 width=16)
                    ->  Hash Right Join  (cost=47050.38..1312601.51 rows=893188 width=16)
                          Hash Cond: (t7.site_id = report_site.id)
                          ->  Seq Scan on report_rank t7  (cost=0.00..1076119.27 rows=48133327 width=12)
                          ->  Hash  (cost=46692.28..46692.28 rows=28648 width=8)
                                ->  Nested Loop  (cost=2201.98..46692.28 rows=28648 width=8)
                                      ->  Hash Join  (cost=2201.98..5733.23 rows=28648 width=4)
                                            Hash Cond: (crm_client.user_id = auth_user.id)
                                            ->  Hash Join  (cost=2040.73..5006.71 rows=44606 width=8)
                                                  Hash Cond: (report_profile.client_id = crm_client.id)
                                                  ->  Seq Scan on report_profile  (cost=0.00..1706.09 rows=93009 width=8)
                                                  ->  Hash  (cost=1761.98..1761.98 rows=22300 width=8)
                                                        ->  Seq Scan on crm_client  (cost=0.00..1761.98 rows=22300 width=8)
                                                              Filter: (NOT is_deleted)
                                            ->  Hash  (cost=126.85..126.85 rows=2752 width=4)
                                                  ->  Seq Scan on auth_user  (cost=0.00..126.85 rows=2752 width=4)
                                                        Filter: is_active
                                      ->  Index Scan using report_site_pkey on report_site  (cost=0.00..1.42 rows=1 width=4)
                                            Index Cond: (id = report_profile.site_id)

【问题讨论】:

    标签: sql django performance postgresql aggregate-functions


    【解决方案1】:

    主要的一点很可能是你JOINGROUP 只是为了得到max(created)。单独获取此值。

    您在这里提到了所有需要的索引:report_rank.created 和外键。你在那里做得很好。 (如果您对“好吧”更感兴趣,继续阅读!)

    LEFT JOIN report_site 将被WHERE 子句强制为普通的JOIN。我替换了一个普通的JOIN。我还大大简化了你的语法。

    2015 年 7 月更新具有更简单、更快的查询和更智能的功能。

    多行解决方案

    report_rank.created 不是唯一的,您想要所有最新的行。
    在子查询中使用窗口函数rank()

    SELECT r.id, r.keyword_id, r.site_id
         , r.rank, r.url, r.competition
         , r.source, r.country, r.created  -- same as "max"
    FROM  (
       SELECT *, rank() OVER (ORDER BY created DESC NULLS LAST) AS rnk
       FROM   report_rank r
       WHERE  EXISTS (
          SELECT *
          FROM   report_site    s
          JOIN   report_profile p ON p.site_id = s.id
          JOIN   crm_client     c ON c.id      = p.client_id
          JOIN   auth_user      u ON u.id      = c.user_id
          WHERE  s.id = r.site_id
          AND    u.is_active
          AND    c.is_deleted = FALSE
          )
       ) sub
    WHERE  rnk = 1;
    

    为什么是DESC NULLS LAST

    一行的解决方案

    如果report_rank.created唯一的,或者您对max(created)任意1 行感到满意:

    SELECT id, keyword_id, site_id
         , rank, url, competition
         , source, country, created  -- same as "max"
    FROM   report_rank r
    WHERE  EXISTS (
        SELECT 1
        FROM   report_site    s
        JOIN   report_profile p ON p.site_id = s.id
        JOIN   crm_client     c ON c.id      = p.client_id
        JOIN   auth_user      u ON u.id      = c.user_id
        WHERE  s.id = r.site_id
        AND    u.is_active
        AND    c.is_deleted = FALSE
       )
    -- AND  r.created > f_report_rank_cap()
    ORDER  BY r.created DESC NULLS LAST
    LIMIT  1;
    

    应该更快,仍然。更多选择:

    动态调整部分索引的终极速度

    您可能已经注意到最后一个查询中的注释部分:

    AND  r.created > f_report_rank_cap()
    

    你提到了 50 米奥。行,很多。这是一种加快速度的方法:

    • 创建一个简单的IMMUTABLE 函数,返回一个时间戳,该时间戳保证比感兴趣的行更早,同时尽可能年轻。
    • 仅在较年轻的行上创建 partial index - 基于此函数。
    • 在与索引条件匹配的查询中使用WHERE 条件。
    • 创建另一个函数,使用动态 DDL 将这些对象更新为最新行。 (减去 安全边距,以防最新行被删除/停用 - 如果发生这种情况)
    • 在关闭时间调用此辅助功能,每个 cronjob 或按需使用最少的并发活动。随心所欲,不会造成任何伤害,它只需要在表上设置一个短的排他锁。

    这是一个完整的工作演示
    @erikcw,您必须按照以下说明激活评论部分。

    CREATE TABLE report_rank(created timestamp);
    INSERT INTO report_rank VALUES ('2011-11-11 11:11'),(now());
    
    -- initial function
    CREATE OR REPLACE FUNCTION f_report_rank_cap()
      RETURNS timestamp LANGUAGE sql COST 1 IMMUTABLE AS
    $y$SELECT timestamp '-infinity'$y$;  -- or as high as you can safely bet.
    
    -- initial index; 1st run indexes whole tbl if starting with '-infinity'
    CREATE INDEX report_rank_recent_idx ON report_rank (created DESC NULLS LAST)
    WHERE  created > f_report_rank_cap();
    
    -- function to update function & reindex
    CREATE OR REPLACE FUNCTION f_report_rank_set_cap()
      RETURNS void AS
    $func$
    DECLARE
       _secure_margin CONSTANT interval := interval '1 day';  -- adjust to your case
       _cap timestamp;  -- exclude older rows than this from partial index
    BEGIN
       SELECT max(created) - _secure_margin
       FROM   report_rank
       WHERE  created > f_report_rank_cap() + _secure_margin
       /*  not needed for the demo; @erikcw needs to activate this
       AND    EXISTS (
         SELECT *
         FROM   report_site    s
         JOIN   report_profile p ON p.site_id = s.id
         JOIN   crm_client     c ON c.id      = p.client_id
         JOIN   auth_user      u ON u.id      = c.user_id
         WHERE  s.id = r.site_id
         AND    u.is_active
         AND    c.is_deleted = FALSE)
       */
       INTO   _cap;
    
       IF FOUND THEN
         -- recreate function
         EXECUTE format('
         CREATE OR REPLACE FUNCTION f_report_rank_cap()
           RETURNS timestamp LANGUAGE sql IMMUTABLE AS
         $y$SELECT %L::timestamp$y$', _cap);
    
         -- reindex
         REINDEX INDEX report_rank_recent_idx;
       END IF;
    END
    $func$  LANGUAGE plpgsql;
    
    COMMENT ON FUNCTION f_report_rank_set_cap()
    IS 'Dynamically recreate function f_report_rank_cap()
        and reindex partial index on report_rank.';
    

    呼叫:

    SELECT f_report_rank_set_cap();
    

    见:

    SELECT f_report_rank_cap();
    

    取消注释上面查询中的子句AND r.created > f_report_rank_cap() 并观察差异。验证索引是否与EXPLAIN ANALYZE 一起使用。

    The manual on concurrency and REINDEX:

    要在不影响生产的情况下构建索引,您应该删除索引并重新发出 CREATE INDEX CONCURRENTLY 命令。

    【讨论】:

    • 我不认为这是严格等价的,但它绝对更具可读性!
    • 当然,你们俩都是对的,这是一项正在进行的工作。 @wiplasser,也许你可以牵着你的马,编辑让我很困惑。在作者可能还在的时候不要这么快编辑答案。
    • CTE 来了……抱歉,不会再发生了。我只是无法忍受水平滚动(它只是一些糟糕的依赖表达式,但你必须滚动才能找到 that ...)
    • @wildplasser:是的,我也不喜欢那个讨厌的水平滚动条。在我结案之前,我可能会清理掉它。
    • @wildplasser:您可能对我添加到答案中的“终极速度”部分感兴趣。 :)
    【解决方案2】:

    另类解释

    我正忙着优化你提出的查询,错过了你写的一段:

    我正在尝试为每个 report_profile 检索最新的 report_rank。

    这与您的查询尝试执行的操作完全不同

    首先,让我演示一下我是如何从您发布的内容中提炼出查询的。
    我删除了"" 和干扰词,使用了别名并修剪了格式,得到了这样的结果:

    SELECT r.id, r.keyword_id, r.site_id, r.rank, r.url, r.competition
          ,r.source, r.country, r.created
          ,MAX(t7.created) AS max 
    FROM   report_rank      r
    LEFT   JOIN report_site s  ON (s.id      = r.site_id) 
    JOIN   report_profile   p  ON (p.site_id = s.id) 
    JOIN   crm_client       c  ON (c.id      = p.client_id) 
    JOIN   auth_user        u  ON (u.id      = c.user_id) 
    LEFT   JOIN report_rank t7 ON (t.site_id = s.id) 
    WHERE  u.is_active
    AND    c.is_deleted = False
    GROUP  BY
           r.id
          ,r.keyword_id
          ,r.site_id
          ,r.rank
          ,r.url, r.competition
          ,r.source
          ,r.country
          ,r.created 
    HAVING MAX(t7.created) =  r.created;
    
    • 您尝试对 T7HAVING 执行的操作无法在主体上运行,我已对其进行了修剪。
    • 在这两种情况下,LEFT JOIN 将被强制为普通的 JOIN。我相应地替换了。
    • 根据您的查询,我推断report_sitereport_rankreport_profile 之间存在1:n 关系,这就是这两者之间的联系方式。因此,属于同一个report_sitereport_profile 共享同一个最新的report_rank。您不妨按report_site 分组。但我坚持提出的问题。
    • 我从查询中删除了report_site。这无关紧要,只要它存在,我断言。
    • 从 PostgreSQL 9.1 开始,GROUP BY 每个表的主键就足够了。我相应地进行了简化。
    • 为简化起见,我选择了report_rank 的所有列

    综上所述,我得到了这个基本查询

    SELECT r.*
    FROM   report_rank    r
    JOIN   report_profile p USING (site_id) 
    JOIN   crm_client     c ON (c.id = p.client_id) 
    JOIN   auth_user      u ON (u.id = c.user_id) 
    WHERE  u.is_active
    AND    c.is_deleted = FALSE
    GROUP  BY r.id;
    

    在此基础上,我用...创建了一个解决方案

    每个report_profile的最新report_rank

    WITH p AS (
        SELECT p.id AS profile_id
              ,p.site_id
        FROM   report_profile p
        WHERE  EXISTS (
            SELECT *
            FROM   crm_client c
            JOIN   auth_user  u ON u.id = c.user_id
            WHERE  c.id = p.client_id
            AND    c.is_deleted = FALSE
            AND    u.is_active
            )
        ) x AS (
        SELECT p.profile_id
              ,r.*
        FROM   p
        JOIN   report_rank r USING (site_id)
        )
    SELECT *
    FROM   x
    WHERE  NOT EXISTS (
        SELECT *
        FROM   x r
        WHERE  r.profile_id = x.profile_id
        AND    r.created > x.created
        );
    
    • 我认为有一个 report_profile.id 虽然你没有提到它。
    • 在第一次 CTE 中,我获得了一组独特的有效配置文件。
    • 在第二个 CTE 中,我加入 report_rank 以生成结果行
    • 在最后的查询中,我根据report_profile 消除了除了最新的report_rank 之外的所有内容
    • 如果created 不是唯一的,则可以是一行或多行。
    • 我的其他答案中带有部分索引的解决方案不适用于此变体。

    最后,来自 PostgreSQL wiki 的性能优化建议:

    【讨论】:

      【解决方案3】:
      -- modelled after Erwin's version
      -- does the x query really return only one row?
      
      SELECT r.id, r.keyword_id, r.site_id
          , r.rank, r.url, r.competition, r.source
          , r.country, r.created, x.max_created
      -- UPDATE3: I forgot one, too
      FROM report_rank r
      LEFT   JOIN report_site s  ON (r.site_id = s.id) 
      JOIN   report_profile   p  ON (s.id = p.site_id) 
      JOIN   crm_client       c  ON (p.client_id = c.id) 
      JOIN   auth_user        u  ON (c.user_id = u.id)
      -- UPDATE2: t7 has left the building
      WHERE  u.is_active
      AND    c.is_deleted = FALSE
      AND NOT EXISTS (SELECT * FROM report_rank x
             -- WHERE 1=1 -- uncorrelated subquery ??
             -- UPDATE1: no it's not. Erwin seems to have forgotten the t7 join
             WHERE r.id = x.site_id
             AND x.created > r.created
             ) 
      ;
      

      【讨论】:

      • 我认为这更好。我认为,uc 的连接也应该进入子查询。
      • 我仍然不确定 Erwin 的版本(或我的版本)与原始版本的等价性。致 OP:请将您的查询缩减为可读版本。
      • 您需要在子查询中重复 WHERE 条件。 (我的初稿也有这个错误。)否则,如果最新条目被删除或不活动,则不会返回任何行。除此之外,NOT EXISTS ( .. x>r) 是一个绝妙的技巧。
      • 我要辞职了。在这种情况下,最好的方法可能是将所有肉压缩成一个 CTE 并对其执行自连接。不过,max() 技巧仍然有效(大多数情况下,在子查询中不存在 WHERE_bigger 优于 max())无论如何:我已经离开这里了。哦,是的:我基本上是寄生在你的重写上。
      猜你喜欢
      • 1970-01-01
      • 2021-06-29
      • 2021-07-04
      • 2017-04-24
      • 1970-01-01
      • 1970-01-01
      • 2011-05-23
      • 1970-01-01
      相关资源
      最近更新 更多