为 Postgres 中的竞赛存储 'Rank'

Storing 'Rank' for Contests in Postgres

我正在尝试确定是否有针对以下查询的 "low cost" 优化。我们实施了一个系统,'tickets' 可以赚取 'points' 并因此可以排名。为了支持分析类型的查询,我们将每张票(票可以绑定)的排名与票一起存储。

我发现,大规模更新此排名非常缓慢。我正在尝试 运行 下面一组 "tickets" 的场景,大约有 20k 张票。

我希望有人能帮助找出原因并提供一些帮助。

我们正在使用 postgres 9.3.6

这是一个简化的工单 table 架构:

ogs_1=> \d api_ticket
                                             Table "public.api_ticket"
            Column            |           Type           |                        Modifiers                        
------------------------------+--------------------------+---------------------------------------------------------
 id                           | integer                  | not null default nextval('api_ticket_id_seq'::regclass)
 status                       | character varying(3)     | not null
 points_earned                | integer                  | not null
 rank                         | integer                  | not null
 event_id                     | integer                  | not null
 user_id                      | integer                  | not null
Indexes:
    "api_ticket_pkey" PRIMARY KEY, btree (id)
    "api_ticket_4437cfac" btree (event_id)
    "api_ticket_e8701ad4" btree (user_id)
    "api_ticket_points_earned_idx" btree (points_earned)
    "api_ticket_rank_idx" btree ("rank")
Foreign-key constraints:
    "api_ticket_event_id_598c97289edc0e3e_fk_api_event_id" FOREIGN KEY (event_id) REFERENCES api_event(id) DEFERRABLE INITIALLY DEFERRED
(user_id) REFERENCES auth_user(id) DEFERRABLE INITIALLY DEFERRED

这是我正在执行的查询:

UPDATE api_ticket t SET rank = (
  SELECT rank
  FROM (SELECT Rank() over (
      Partition BY event_id ORDER BY points_earned DESC
    ) as rank, id
    FROM api_ticket tt
    WHERE event_id = t.event_id
      AND tt.status != 'x'
  ) as r
  WHERE r.id = t.id
)
WHERE event_id = <EVENT_ID> AND t.status != 'x';

这是对一组大约 10k 行的解释:

Update on api_ticket t  (cost=0.00..1852176.70 rows=9646 width=88) (actual time=1254035.623..1254035.623 rows=0 loops=1)
   ->  Seq Scan on api_ticket t  (cost=0.00..1852176.70 rows=9646 width=88) (actual time=121.611..1253148.416 rows=9748 loops=1)
         Filter: (((status)::text <> 'x'::text) AND (event_id = 207))
         Rows Removed by Filter: 10
         SubPlan 1
           ->  Subquery Scan on r  (cost=159.78..191.97 rows=1 width=8) (actual time=87.466..128.537 rows=1 loops=9748)
                 Filter: (r.id = t.id)
                 Rows Removed by Filter: 9747
                 ->  WindowAgg  (cost=159.78..178.55 rows=1073 width=12) (actual time=46.389..108.954 rows=9748 loops=9748)
                       ->  Sort  (cost=159.78..162.46 rows=1073 width=12) (actual time=46.370..66.163 rows=9748 loops=9748)
                             Sort Key: tt.points_earned
                             Sort Method: quicksort  Memory: 799kB
                             ->  Index Scan using api_ticket_4437cfac on api_ticket tt  (cost=0.29..105.77 rows=1073 width=12) (actual time=2.698..26.448 rows=9748 loops=9748)
                                   Index Cond: (event_id = t.event_id)
                                   Filter: ((status)::text <> 'x'::text)
 Total runtime: 1254036.583 ms

必须对每一行执行correlated subquery(在您的示例中为 20k 次)。这仅对 行数或计算需要它的地方有意义。

这个派生的 table 在我们加入它之前计算 一次 :

UPDATE api_ticket t
SET    rank = tt.rnk
FROM  (
   SELECT tt.id
        , rank() OVER (PARTITION BY tt.event_id
                       ORDER BY tt.points_earned DESC) AS rnk
   FROM   api_ticket tt
   WHERE  tt.status <> 'x'
   AND    tt.event_id = <EVENT_ID>
   ) tt
WHERE t.id = tt.id
AND   t.rank <> tt.rnk;  -- avoid empty updates

应该会快很多。 :)

其他改进

最后一个谓词排除空更新:

  • How do I (or can I) SELECT DISTINCT on multiple columns?

只有在新等级至少偶尔可以成为旧等级的情况下才有意义。否则删除它。

我们不需要在外部查询中重复 AND t.status != 'x',因为我们在 PK 列上加入 id 两边的值相同。
标准的 SQL 不等运算符是 <>,即使 Postgres 也支持 !=

也将谓词 event_id = <EVENT_ID> 下推到子查询中。无需计算任何其他 event_id 的数字。这是从你原来的外问传下来的。在重写的查询中,我们最好将它一起应用到子查询中。由于我们使用 PARTITION BY tt.event_id,因此不会影响排名。