【问题标题】:Update Query Intermittently Slow on PostgreSQL Database在 PostgreSQL 数据库上更新查询间歇性缓慢
【发布时间】:2019-12-27 23:21:23
【问题描述】:

我注意到我的所有查询,包括以下查询,在大多数情况下似乎都很快,但会随机变慢,我不知道为什么。

我一直在尝试追查原因,所以我添加了 auto_explain,它只记录了以下查询

START TRANSACTION
UPDATE "recipes" SET "servings" = $2, "updatedAt" = CURRENT_TIMESTAMP 
WHERE "id" IN ($1) RETURNING "updatedAt"
COMMIT
    Update on public.recipes  (cost=0.29..8.30 rows=1 width=1599) (actual time=312.687..312.693 rows=1 loops=1)
      Buffers: shared hit=122 read=2 dirtied=4
      ->  Index Scan using recipes_uid_key on public.recipes  (cost=0.29..8.30 rows=1 width=1599) (actual time=0.019..0.023 rows=1 loops=1)
            Index Cond: (recipes.id = '5f6d6875-2d32-4306-bf41-7ba541f23592'::uuid)
            Buffers: shared hit=3

我觉得奇怪的是它从8.30 跳到312.687 的实际时间并且没有解释那个时间去了哪里。有谁知道这意味着什么?也许行已被锁定,正在等待解锁?

表大小:15K 记录

【问题讨论】:

  • 看起来id 是一个主键。您需要大量流量才能使行锁定成为 IMO 因素。有人在做 DDL(ALTER TABLE 类型的事情吗?)或者可能是在您没有提交/回滚的情况下关闭提交的会话?
  • 会不会是一个在IN 子句中具有大量 id 的查询正在运行,而另一个会话正在尝试更新相同或部分相同的记录?我想这可能会使第二个查询暂停,直到第一个查询完成并解释延迟
  • 当查询速度很慢时,是否有任何其他更新/插入查询在同一张表上执行。
  • 它总是更便宜。有多个字段,您必须检查它们中的任何一个是否实际更改。使用 postgres 可以大获全胜,因为您可以避免创建多个行版本,这些版本需要稍后清除。
  • 对于单行更新,共享缓冲区命中的数量非常大。虽然是点击而不是读取,但它们不应该非常耗时。但无论如何你都应该打开 track_io_timing,以防它显示有用的东西。

标签: sql postgresql sql-update


【解决方案1】:

通过索引进行随机访问缓慢的一个原因是,如果所需的数据没有被缓存,并且磁盘必须执行多次查找操作来获取它。然而,这个查询需要 300 毫秒,这是很多寻道,尤其是在磁盘是 SSD 的情况下。所以我认为情况并非如此,除非这是一个非常大的表,其中包含大量行并且可能包含大量 TOAST 数据。

除了通常的:真空,甚至 CLUSTER,如果你有一个有意义的索引来集群......

我怀疑是锁定问题。可能有另一个事务与您的查询同时执行,更新同一行,然后在发出 COMMIT 之前需要一段时间。可能是更新多行的事务,也可能是由于外键导致的锁定等。

您可以手动梳理您的查询以寻找罪魁祸首,或者在您尝试调试此缓慢更新之前对pg_locks 进行查询以列出“recipes”表上的任何锁。然后执行更新并计时,如果速度慢则记录锁查询的结果。不过,您必须在执行更新之前检查锁。如果你先更新然后检查锁,那么更新等待的任何锁都将在更新完成时被释放,因此罪魁祸首将不再在 pg_locks 中。

【讨论】:

  • 我在配置中添加了log_lock_waits = truedeadlock_timeout = 50 以便下次捕获任何锁
  • 那么狩猎愉快!
【解决方案2】:

如果有一些通常大量的查询甚至多个查询在同一个经常更新的小表上工作(或者甚至在同一个数据库中,而不是在同一个表上),就会发生这种情况。如果可能的话,您可以尝试避免运行这些查询,让AUTOVACUUM 完成他的工作,或者尝试通过自己删除不必要的版本来使您的表格更轻。

试试

VACUUM FULL recipes;

VACUUM 回收死元组占用的存储空间。 VACUUM FULL 在表上获得排他锁,但有时其他竞争查询不会释放锁,因为它们可能需要访问死元组。 如果VACUUM FULL 没有帮助,可能的解决方案是创建某种自定义VACUUMMore on vacuum

    VACUUM recipes;
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; 
  SET LOCAL statement_timeout = '1s'; SET LOCAL lock_timeout = '1s'; 
  --we got exception if we can't get this table exclusively locked in 1 second (for stopping this transaction from over waiting of exclusive lock)
  LOCK TABLE recipes IN ACCESS EXCLUSIVE MODE; 
  --2 rows above to lock table exclusively for this transaction
  CREATE TEMPORARY TABLE temp ON COMMIT DROP AS TABLE recipes;
  TRUNCATE TABLE recipes;
  INSERT INTO recipes TABLE temp;
COMMIT;

有关此特殊案例的更多信息,请访问habr

【讨论】:

  • 如果表有 15K 条记录,我又没有海量查询,你还觉得这样吗?
  • 我仍然认为这可能是个问题。至少值得一试。
  • 好的,我试试看。我需要做些什么来设置 autovacuum 吗?
  • Autovacuum 已启用。您可以通过 ALTER TABLE x SET (autovacuum_enabled = on) 手动启用它。诀窍是使用手动真空和自定义 VACUUM FULL
  • 我不太确定它会有所帮助。当它建议对经常更新的表的索引进行多个查询时。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-16
  • 1970-01-01
  • 1970-01-01
  • 2013-11-24
  • 1970-01-01
相关资源
最近更新 更多