为 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
,因此不会影响排名。
我正在尝试确定是否有针对以下查询的 "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
,因此不会影响排名。