分块 table 以将 timestamptz 批量更新为 timestamp

Chunking a table for a batch update of timestamptz to timestamp

简短版

详细版本:

我们收集数据已有一段时间了,并使用 timestamptz 字段。我犯了一个错误,我应该使用时间戳。我们所做的是从不同的位置收集大量数据,然后在将数据推送到 Postgres 之前 自己计算 UTC。据我了解,无论哪种方式,timestamp 和 timestamptz 数据都是相同的 8 个字节,timestamptz 为您提供的是神奇的(且不可见的)AT TIME ZONE 转换。意思是,数据没有不同,只是 Postgres 处理不同数据的方式不同。在我们的例子中,这意味着我们通过将数据作为 UTC 推送到 Postgres 然后再将其拉出到本地而搞砸了。我们服务器的数据没有一个时区,这就是为什么我们在内部将其设置为 UTC,就像 Postgres 一样。为了使报告更简单,分析 table 通常有一个用于 local_dts 和 utc_dts 的冗余列。这样,我们就可以 运行 比较不同时区设施中“周一早上 8 点到 11 点”的报告。不同的设施有不同的时区,因此我们使用“本地”值,即 他们 本地的此类查询。但是如果我们需要一个统一的时间线,那么我们就使用UTC。简单地说:相同 table 中的行可能来自不同时区的来源。

好的,这就是背景,我现在有数百万行要更新。结构修改看起来很简单:

-- Change the data type, this is instantaneous.
ALTER TABLE assembly
   ALTER COLUMN created_dts 
   SET DATA TYPE timestamp;
   
-- Reset the default, it's probably not necessary, but the ::timestamptz is misleading/confusing here otherwise.
ALTER TABLE assembly
   ALTER COLUMN created_dts 
   SET DEFAULT '-infinity'::timestamp

我将不得不删除并重新创建一些视图,但这只是 运行 一些备份脚本的问题。

我的问题是如何在不拖服务器的情况下有效地进行更新?我正在想象一次按 5K 行等进行批处理。为了简单起见,假设我们所有的服务器都设置为US/Central。当我们最初以 UTC 格式推送数据时,它又被 Postgres 转换了,所以现在数据因我们服务器时间和 UTC 之间的偏移而偏移。 (我认为。)如果是这样,最简单的更新可能如下所示:

SET TIME ZONE 'UTC'; -- Tell Postgres we're in UTC to line up the data with the UTC clock it's set to.
UPDATE analytic_scan 
  SET created_dts = created_dts at time zone 'US/Central' -- Tell Postgres to convert the value back to where we started.

这似乎可行 (?),忽略了处理夏令时的明显遗漏。我可以添加一个 WHERE 子句来处理这个问题,但这并没有改变我的问题。现在的问题是,我有这样的记录数:

analytic_productivity           728,708
analytic_scan                 4,296,273
analytic_sterilizer_load        136,926
analytic_sterilizer_loadinv     327,700
record_changes_log           17,949,132

所以,不是很大,但也不是什么都没有。有没有办法明智地分割 SQL 中的数据,以便

所有 table 都有一个 UUID ID PK 字段,一对夫妇有一个生成的身份列,就像从这个报告中截取的一样 table:

CREATE TABLE IF NOT EXISTS "data"."analytic_productivity" (
    "id" uuid NOT NULL DEFAULT NULL,
    "pg_con_id" integer GENERATED BY DEFAULT AS IDENTITY UNIQUE,
    "data_file_id" uuid NOT NULL DEFAULT NULL,
    "start_utc" timestamptz NOT NULL DEFAULT '-infinity',
    "start_local" timestamptz NOT NULL DEFAULT '-infinity',
    "end_utc" timestamptz NOT NULL DEFAULT '-infinity',
    "end_local" timestamptz NOT NULL DEFAULT '-infinity')

我的一个想法是使用 UUID::text 的子字符串或散列来制作较小的批次:

select * from analytic_sterilizer_loadinv 
  where left(id::text,1) = 'a'

这看起来很慢而且很糟糕。散列似乎好一点:

select abs(hashtext(id::text))  % 64,
       count(*)
       
  from analytic_sterilizer_loadinv 

存储桶的大小不是那么均匀,但可能已经足够了,如果需要,我可以增加存储桶的数量。不幸的是,我不知道如何使用存储桶 运行 我的代码在 SQL 的循环中。如果有人应该指出如何,我将不胜感激。而且,如果有一个简单的内置分块功能,我很想知道。

我没有想清楚如何处理将被修改的传入数据,而不是锁定整个 table。我也许可以做到。

如果您负担得起,请不要分批进行 UPDATE,而是一次全部进行。主要缺点是这会使表格膨胀,之后你应该 运行 VACUUM (FULL) 在表格上,这会导致停机。

我会编写客户端代码来批量更新,例如 bash:

typeset -i part=0

# PostgreSQL client time zone
export PGTZ=UTC

while [ $part -lt 64 ]
do
    psql <<-EOF
        UPDATE data.analytic_productivity
        SET created_dts = created_dts at time zone 'US/Central'
        WHERE abs(hashtext(id::text)) % 64 = '$part'
EOF
    psql -c "VACUUM data.analytic_productivity"

    part=part+1
done