高效插入,对 Postgres 中的大表进行重复检查

Efficient inserts with duplicate checks for large tables in Postgres

我目前正在开展一个项目,从现场的无线调制解调器网络收集大量数据。我们有一个 table 'readings' 看起来像这样:

CREATE TABLE public.readings (
  id INTEGER PRIMARY KEY NOT NULL DEFAULT nextval('readings_id_seq'::regclass),
  created TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now(),
  timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL,
  modem_serial CHARACTER VARYING(255) NOT NULL,
  channel1 INTEGER NOT NULL,
  channel2 INTEGER NOT NULL,
  signal_strength INTEGER,
  battery INTEGER,
  excluded BOOLEAN NOT NULL DEFAULT false
);
CREATE UNIQUE INDEX _timestamp_modemserial_uc ON readings USING BTREE (timestamp, modem_serial);
CREATE INDEX ix_readings_timestamp ON readings USING BTREE (timestamp);
CREATE INDEX ix_readings_modem_serial ON readings USING BTREE (modem_serial);

对于系统的完整性,我们绝不会从同一调制解调器获得具有相同时间戳的两个读数,因此具有唯一索引。

我们目前的挑战是找到一种插入读数的高效方法。我们在引入历史数据时经常需要插入数百万行,而当添加到现有的 1 亿以上读数时,这可能会有点慢。

我们目前的方法是将 10,000 个读数的批次导入 temporary_readings table,这实际上是一个未索引的读数副本。然后我们 运行 以下 SQL 将其合并到主 table 并删除重复项:

INSERT INTO readings (created, timestamp, modem_serial, channel1, channel2, signal_strength, battery)
SELECT DISTINCT ON (timestamp, modem_serial) created, timestamp, modem_serial, channel1, channel2, signal_strength, battery
FROM temporary_readings
WHERE NOT EXISTS(
    SELECT * FROM readings
    WHERE timestamp=temporary_readings.timestamp
    AND modem_serial=temporary_readings.modem_serial
)
ORDER BY timestamp, modem_serial ASC;

这很好用,但每插入 10,000 行块大约需要 20 秒。我的问题是双重的:

  1. 这是解决问题的最佳方法吗?我对具有此类性能需求的项目比较陌生,所以我很想知道是否有更好的解决方案。
  2. 我可以采取哪些步骤来加快插入过程?

提前致谢!

你的查询思路没问题。我会尝试将其计时为批次中的 100,000 行,以开始了解最佳批次大小。

但是,distinct on 正在减慢速度。这里有两个想法。

首先是假设批量重复的情况很少见。如果是这样,请尝试插入不带 distinct on 的数据。如果失败,则 运行 再次使用 distinct on 代码。这会使插入逻辑复杂化,但可能会使平均插入时间更短。

第二种是在temporary_readings(timestamp, modem_serial)上建索引(不是唯一索引)。 Postgres 将利用此索引作为插入逻辑——有时构建索引并使用它比其他执行计划更快。如果这确实有效,您可以尝试更大的批量大小。

还有第三种解决方案,就是使用on conflict。这将允许插入本身忽略重复值。不过,这仅在 Postgres 9.5 中可用。

添加到已经包含 1 亿条索引记录的 table 无论如何都会很慢!您可以通过重新查看索引来加快速度。

CREATE UNIQUE INDEX _timestamp_modemserial_uc ON readings USING BTREE (timestamp, modem_serial);
CREATE INDEX ix_readings_timestamp ON readings USING BTREE (timestamp);
CREATE INDEX ix_readings_modem_serial ON readings USING BTREE (modem_serial);

目前您有三个索引,但它们位于相同的列组合上。不能仅使用唯一索引进行管理吗?

我不知道您的其他查询是什么样的,但是您的 WHERE NOT EXISTS 查询可以利用这个唯一索引。

如果您的 WHERE 子句仅过滤 modem_serial field 的查询,则不太可能使用您的唯一索引。但是,如果您翻转该索引中的列,它将是!

CREATE UNIQUE INDEX _timestamp_modemserial_uc ON readings USING BTREE (timestamp, modem_serial);

引自manual

A multicolumn B-tree index can be used with query conditions that involve any subset of the index's columns, but the index is most efficient when there are constraints on the leading (leftmost) columns.

索引中列的顺序很重要。