【问题标题】:UPDATE query for 180k rows in 10M row table unexpectedly slow对 10M 行表中 180k 行的 UPDATE 查询出乎意料地慢
【发布时间】:2020-10-27 22:46:37
【问题描述】:

我的桌子太大了,我想缩小它的大小 使用 UPDATE 查询。该表中的某些数据是多余的,并且 我应该能够通过设置冗余来回收大量空间 “细胞”为 NULL。但是,我的 UPDATE 查询占用过多 完成所需的时间。

表格详情

-- table1  10M rows (estimated)
--         45 columns
--         Table size 2200 MB
--         Toast Table size 17 GB
--         Indexes Size 1500 MB
-- **columns in query**
--   id     integer      primary key
--   testid integer      foreign key
--   band   integer
--   date   timestamptz  indexed
--   data1  real[]
--   data2  real[]
--   data3  real[]

这是我第一次尝试更新查询。我把它分成了一些 临时表只是为了更新 id。此外,为了减少 查询,我选择了 2020 年 6 月的日期范围

CREATE TEMP TABLE A as
    SELECT testid
      FROM table1
     WHERE date BETWEEN '2020-06-01' AND '2020-07-01'
       AND band = 3;
 
CREATE TEMP TABLE B as    -- this table has 180k rows
    SELECT id
      FROM table1
     WHERE date BETWEEN '2020-06-01' AND '2020-07-01'
       AND testid in (SELECT testid FROM A)
       AND band > 1
       
UPDATE table1
   SET data1 = Null, data2 = Null, data3 = Null
 WHERE id in (SELECT id FROM B)

用于创建 TEMP 表的查询在 1 秒内执行。在我最终杀死它之前,我运行了 UPDATE 查询一个小时(!)。只有180k 需要更新的行。似乎不需要那么多 是时候更新那么多行了。临时表 B 准确地确定了哪个 要更新的行。

这是来自上述 UPDATE 查询的 EXPLAIN。这个解释的一个奇怪的特点是它显示了 488 万行,但只有 18 万行需要更新。

Update on table1  (cost=3212.43..4829.11 rows=4881014 width=309)
  ->  Nested Loop  (cost=3212.43..4829.11 rows=4881014 width=309)
        ->  HashAggregate  (cost=3212.00..3214.00 rows=200 width=10)
              ->  Seq Scan on b  (cost=0.00..2730.20 rows=192720 width=10)
        ->  Index Scan using table1_pkey on table1  (cost=0.43..8.07 rows=1 width=303)
              Index Cond: (id = b.id)

运行此查询的另一种方法是一次性:

WITH t as (
    SELECT id from table1
     WHERE testid in (
        SELECT testid
          from table1
         WHERE date BETWEEN '2020-06-01' AND '2020-07-01'
           AND band = 3
        )
    )
UPDATE table1 a
   SET data1 = Null, data2 = Null, data3 = Null
  FROM t
 WHERE a.id = t.id
 

在我杀死它之前,我只运行了大约 10 分钟。感觉如果我知道技巧,我应该能够在更短的时间内运行这个查询。这个查询在下面有解释。这个解释显示了 195k 行,这是更符合预期的,但成本要高得多 @ 1.3M 到 1.7M

Update on testlog a  (cost=1337986.60..1740312.98 rows=195364 width=331)
  CTE t
    ->  Hash Join  (cost=8834.60..435297.00 rows=195364 width=4)
          Hash Cond: (testlog.testid = testlog_1.testid)
          ->  Seq Scan on testlog  (cost=0.00..389801.27 rows=9762027 width=8)
          ->  Hash  (cost=8832.62..8832.62 rows=158 width=4)"
                ->  HashAggregate  (cost=8831.04..8832.62 rows=158 width=4)
                      ->  Index Scan using amptest_testlog_date_idx on testlog testlog_1  (cost=0.43..8820.18 rows=4346 width=4)
                            Index Cond: ((date >= '2020-06-01 00:00:00-07'::timestamp with time zone) AND (date <= '2020-07-01 00:00:00-07'::timestamp with time zone))
                            Filter: (band = 3)
  ->  Hash Join  (cost=902689.61..1305015.99 rows=195364 width=331)
        Hash Cond: (t.id = a.id)
        ->  CTE Scan on t  (cost=0.00..3907.28 rows=195364 width=32)
        ->  Hash  (cost=389801.27..389801.27 rows=9762027 width=303)
              ->  Seq Scan on testlog a  (cost=0.00..389801.27 rows=9762027 width=303)

编辑:已接受答案中的建议之一是在更新之前删除所有索引,然后再将它们添加回来。这就是我所采用的方法,有一个转折:我需要另一个表来保存已删除索引中的索引数据,以使 A 和 B 查询更快:

CREATE TABLE tempid AS
    SELECT id, testid, band, date
      FROM table1

我在此表上为 id、testid 和日期创建了索引。然后我将 A 和 B 查询中的 table1 替换为 tempid。它仍然比我希望的要慢,但它确实完成了工作。

【问题讨论】:

  • 代替where id in (select id from B),试试where exists (select 1 from B where B.id = table1.id)。它可能会有所作为。另外,一定要在表B中索引id
  • 请向我们展示查询的EXPLAIN 执行计划。
  • @jjanes,为两个版本的 UPDATE 查询添加了 EXPLAIN 计划。
  • 与您的问题无关,但是:Postgres 9.3 是no longer supported,您应该尽快计划升级。
  • @a_horse_with_no_name。你说得对。是时候更新了。我忽略了这个数据库,因为它工作得很好。但现在我知道是时候给它一些爱了。

标签: postgresql postgresql-9.3


【解决方案1】:

您可能有另一个表,该表的外键指向您设置为 NULL 的一个或多个列。并且这个外部表在列上没有索引。

每次将行值设置为 NULL 时,数据库都必须检查外部表 - 也许它有一行引用了您要删除的值。

如果是这种情况,您应该能够通过在此远程表上添加索引来加快速度。

例如,如果您有这样的表:

create table table2 (
  id serial primary key,
  band integer references table1(data1)
)

然后就可以创建索引create index table2_band_nnull_idx on table2(band) where band is not null了。

但是您建议您设置为 NULL 的所有列都具有数组类型。这意味着它们不太可能被引用。仍然值得检查。


另一种可能性是您的桌子上有一个工作缓慢的触发器。


另一种可能是表上有很多索引。必须为您更新的每一行更新每个索引,并且它只能使用一个处理器内核。

有时删除所有索引、进行批量更新然后重新创建它们会更快。创建索引可以使用多个核心 - 每个索引一个核心。


另一种可能性是您的查询正在等待其他查询完成并释放其锁定。您应该检查:

select now()-query_start, * from pg_stat_activity where state<>'idle' order by 1;

【讨论】:

  • 感谢您的回复。我想知道在最后一个查询中要寻找什么。大多数返回的数据看起来并不是特别有用,除了waiting 列。此表没有触发器,更新的列不是外键。更新的列是数组,我怀疑它们占 Toast Table 大小的大部分(可能几乎全部)。我会尝试删除索引
  • 如果我要删除索引(索引),是否还需要删除主键?
  • 在 pg_stat_activity 中,您只需要查找长时间运行的事务 - 由于顺序,它们将位于结果的底部。您应该离开 pk,以便 db 可以通过 id 找到要删除的行。
猜你喜欢
  • 1970-01-01
  • 2012-06-26
  • 1970-01-01
  • 2014-07-01
  • 2019-12-28
  • 1970-01-01
  • 2015-03-01
  • 2019-08-12
  • 2015-06-25
相关资源
最近更新 更多