【问题标题】:Optimize updating first, last, and second to last ranked value优化更新第一个、最后一个、倒数第二个排名值
【发布时间】:2021-11-18 16:16:42
【问题描述】:

我需要缓存每个用户第一次、最后一次和倒数第二次发生的事情。我正在查询的历史表有数亿行(我们正在缓存以便我们可以截断它),而我正在更新的表有数千万行。

目前我正在分批执行 1000 个,以避免锁定表。查询是这样的:

with ranked as (
  select
      user_id,
      rank() over (partition by user_id order by created_at desc) as ranked_desc,
      rank() over (partition by user_id order by created_at asc) as ranked_asc,
      created_at
  from history
  where type = 'SomeType' and
        user_id between $1 and $2
)
update
  users u
set
  latest_at = (
    select created_at
    from ranked
    where ranked.ranked_desc = 1 and ranked.user_id = u.id
  ),
  previous_at = (
    select created_at
    from ranked
    where ranked.ranked_desc = 2 and ranked.user_id = u.id
  ),
  first_at = (
    select created_at
    from ranked
    where ranked.ranked_asc = 1 and ranked.user_id = u.id
  )
from ranked
where u.id = ranked.user_id

历史的相关索引是这些。都是btree索引。

  • (created_at)
  • (user_id, created_at)
  • (用户 ID,类型)
  • (类型,created_at)

这可以优化吗?我觉得这可以在没有子查询的情况下完成。

【问题讨论】:

  • 使用rank() 代替row_number() 有什么特别的原因吗?将为已使用等级中的任何重复项引发异常。另外:请始终声明您的 Postgres 版本并提供包含列类型和约束的基本表定义(CREATE TABLE ... 语句)。
  • @ErwinBrandstetter Postgres 13,已标记。我目前在移动设备上,但假设列类型是正常的(时间戳、正确声明的外键等)。感谢关于排名的建议,它不应该出现,但我会换掉它。
  • 知道(user_id, created_at) 是否为UNIQUE 会有所帮助。 (您对rank() 的使用表明了这一点!)或者可以有多个具有相同时间戳的条目?
  • @ErwinBrandstetter 这不是唯一索引,但您可以假设它实际上是唯一的。

标签: sql postgresql query-optimization postgresql-performance postgresql-13


【解决方案1】:

由于我们在(user_id, created_at) 上有最重要的索引,我建议:

UPDATE users u
SET    first_at    = h.first_at
     , latest_at   = h.latest_at
     , previous_at = h.previous_at
FROM  (
   SELECT u.id, f.first_at, l.last[1] AS latest_at, l.last[2] AS previous_at
   FROM   users u
   CROSS  JOIN LATERAL (
      SELECT ARRAY (
         SELECT h.created_at
         FROM   history h
         WHERE  h.user_id = u.id
         AND    h.type = 'SomeType'  -- ??
         ORDER  BY h.created_at DESC
         LIMIT  2
         ) AS last
      ) l
   CROSS  JOIN LATERAL (
      SELECT created_at AS first_at
      FROM   history h
      WHERE  h.user_id = u.id
      AND    h.type = 'SomeType'  -- ??
      ORDER  BY created_at
      LIMIT  1
      ) f
   WHERE  u.id BETWEEN $1 AND $2
   ) h
WHERE  u.id = h.id
AND   (u.first_at    IS DISTINCT FROM h.first_at
    OR u.latest_at   IS DISTINCT FROM h.latest_at
    OR u.previous_at IS DISTINCT FROM h.previous_at);

这也适用于每个user_id 的非唯一时间戳。

如果每个用户有很多行,它非常效率很高。它旨在避免对大表进行顺序扫描,而是大量使用(user_id, created_at) 上的索引。 相关:

假设大多数或所有用户都以这种方式更新,我们不需要users 上的索引。 (就这个UPDATE 而言,最好没有索引。)

如果表history 中只有一行用户,则previous_at 设置为NULL。 (您的原始查询具有相同的效果。)

仅在找到符合条件的历史记录行时更新用户。

这个添加的WHERE 子句跳过不会改变任何东西的更新(完全成本):

AND   (u.first_at    IS DISTINCT FROM h.first_at
    OR u.latest_at   IS DISTINCT FROM h.latest_at
    OR u.previous_at IS DISTINCT FROM h.previous_at)

见:

唯一不安全的是WHERE type = 'SomeType'。如果这是有选择性的,那么具有相同谓词的部分索引会更好。然后我们甚至可以获得仅索引扫描...

由于新查询应该快得多,您可能会一次更新更多(或所有)用户。

【讨论】:

  • 谢谢,我明天试试。 distinct from 子句的目的是什么?如果我一次完成所有操作,是否有锁定表的风险?
  • distinct from clauses?你的意思是两个横向子查询? history 表是只读的,根本没有锁定。 users 的更新行被锁定直到事务结束。
  • u.first_at IS DISTINCT FROM h.first_at 子句。他们的目的是什么?
  • 我在上面澄清了解释。链接答案中的更多详细信息:stackoverflow.com/a/12632129/939860。另外,需要明确的是:users 的更新行被写锁定直到事务结束。 (读者不会被屏蔽。)
  • 这明显更快,太棒了!这个回填现在应该在星期一早上之前完成。谢谢你,我很感激你的帮助。我要去重写其他慢速回填查询。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-07-30
  • 2021-10-21
  • 2012-06-18
  • 2016-10-10
  • 2021-04-03
  • 1970-01-01
  • 2013-09-09
相关资源
最近更新 更多