更新查询 10M 行中的 180k 行 table 出乎意料地慢

UPDATE query for 180k rows in 10M row table unexpectedly slow

我有一个 table 变得太大了,我想缩小它的尺寸 使用更新查询。此 table 中的某些数据是多余的,并且 我应该可以通过设置冗余来回收很多 space “细胞”为空。但是,我的 UPDATE 查询占用过多 完成时间。

Table 详情

-- 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[]

这是我第一次尝试更新查询。我把它分成一些 临时 tables 只是为了让 id 更新。此外,为了减少 查询,我选择了一个日期 运行ge 为 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 tables 的查询在 1 秒内执行。我 运行 UPDATE 查询了一个小时(!),然后才最终将其终止。只有180k 行需要更新。似乎不需要那么多 是时候更新那么多行了。 Temp table B 准确识别哪个 要更新的行。

这是来自上述 UPDATE 查询的 EXPLAIN。这个解释的一个奇怪特征是它显示了 4.88M 行,但只有 180k 行要更新。

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 分钟就杀了它。感觉如果我知道这些技巧,我应该能够在更短的时间内 运行 这个查询。此查询在下面有 EXPLAIN。这个解释显示了 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)

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

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

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

您可能有另一个 table 有一个外键到这个 table 到一个或多个您设置为 NULL 的列。而这个国外的table在列上没有索引

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

如果是这种情况,您应该可以通过在此遥控器上添加索引来加快速度 table。

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

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 的所有列都具有数组类型。这意味着它们不太可能被引用。仍然值得一试。


另一种可能性是您在 table 上有一个触发器,它工作缓慢。


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

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


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

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