Postgres基于非索引列从一个巨大的表中提取数据



我们有一个生产上的表,它已经存在了很长一段时间,并且该表的容量非常大(接近3 TB),因为该表中的大多数数据都是陈旧和未使用的,我们计划删除没有任何引用的历史数据。有一列";活动的";使用类型boolean,我们可以使用它来消除这些数据,但是该列没有索引。考虑到表的数量,我不太确定创建新索引是否会有帮助,我试图一次增量删除100K个不活动的行,但数量仍然很大,需要几个月才能清除。表的主键是UUID类型,我想创建一个新表,只插入active="的值;真";作为

insert
into
mytable_active
select
*
from
mytable
where
is_active = true;   

但正如预期的那样,由于数量的原因,这种方法也失败了,并一直像往常一样运行。

欢迎提出任何建议。

当您需要快速删除大量行时,分区是很好的。。。。。。。。。当表已经分区时。

如果您需要的列上没有索引,那么至少需要一次完整的表扫描,除非您可以使用另一个索引,如";日期";或者缩小范围。

我的意思是,你可以创建一个索引";"在哪里活动";但这也需要你尽量避免的全表扫描,所以……嗯。

首先,删除。只是不要,即使是带有LIMIT的小部分也不要。它不仅会写入大部分表(3TB写入),还会将其写入WAL(3 TB),还将更新索引,并将其写入到WAL。这将需要很长时间,索引更新中的随机IO将破坏您的性能。如果它完成了,您仍然会有一个3TB的文件,其中大部分未分配。加索引。

所以,没有删除。等等。

  • 带DELETE的场景:

用视图交换表";SELECT*FROM humongous WHERE active=true";并在视图上添加触发器或规则,以将更新/插入/删除重定向到基础表。确保触发器将所有新行设置为active=true。

(同时)重新创建除主键之外的每个索引;其中active=true";。这将需要对第一个索引进行全表扫描,即使您在";"活动";,因为CREATE INDEX WHERE在指定WHERE时似乎无法使用其他索引来加快速度。

删除旧索引

注意,该视图的目的仅仅是确保绝对所有查询都具有";active=true";在WHERE中,因为否则,他们将无法使用我们刚刚创建的条件索引,因此每个查询都将是一个完整的表扫描,这是不可取的。

现在,您可以使用delete from mytable where id in ( select id from mytable where active = false limit 100000);逐点删除

这是一种权衡,您将需要大量的表扫描来重新创建索引,但您将避免由于大量删除而导致的索引更新中的随机IO,这是您说这将需要数月时间的真正原因。

  • 具有INSERT INTO new_table SELECT的场景

如果您在这个巨大的表上运行了插入和更新,那么您就有问题了,因为在操作过程中这些插入和更新不会传输到新表。因此,一个解决方案是:

  • 关闭所有运行长查询的脚本和服务
  • 锁定所有内容
  • 创建新表(_T)
  • 将huge_table重命名为huge_old
  • 创建一个huge_table和huge_old的UNION ALL视图。从应用程序的角度来看,这个视图取代了huge_table。它必须处理优先级,即如果新表中有一行,则应忽略旧表中具有相同id的行。。。所以它必须有一个JOIN。此步骤应事先仔细测试
  • 解锁

然后,让它运行一段时间,看看视图是否不会破坏您的性能。在这一点上,如果它坏了,您可以很容易地返回,方法是删除视图并将表重命名回原来的自己。我说要关闭所有运行长查询的脚本和服务,因为这些脚本和服务可能会在视图中失败,而且你不想在运行一个长查询时使用大锁,因为这会停止一切,直到完成为止。

  • 在视图上添加insert/update/delete触发器,以将写入重定向到new_table。插入将直接转到新表,更新将必须传输行,删除将必须同时命中两个表,并且UNIQUE约束将……很有趣。这会有点复杂

现在传输数据。

即使需要一段时间,谁在乎呢?它最终会结束。我想,如果你有一个3TB的表,你必须有一些不错的存储空间,即使这是我们过去用来放置数据的旧东西,如果IO不是随机的,也不应该超过几个小时所以我们的想法是只使用线性IO。

希望该表没有存储在单独的TOAST表中的大文本列,这将需要每行一次随机访问。你查过了吗?

现在,您可能真的希望它运行更长,这样它在读写,尤其是WAL写时使用更少的IO带宽。查询运行多长时间并不重要,只要它不会降低其他用户的性能即可。

Postgres可能会进行并行表扫描,以使用盒子中的所有核心和所有IO,所以可能会先禁用它。

然后,我认为你应该尽量避免这种滑稽的(对于旁观者来说)场景,即它从表中读取半天,却找不到任何匹配的行,所以磁盘可以很好地处理读取,然后它找到最后匹配的所有行,并继续向WAL和目标表写入300GB,导致巨大的写入争用,当你知道时,你必须Ctrl-C,你只知道你的直觉,它是如此接近完成。

因此:

create bogus_table just like mytable but without indices;
insert into bogus_table select * from mytable;

10%的";活动的";行仍然是300GB,所以更好地检查服务器是否可以在不放慢速度的情况下处理300GB表的写入。观察vmstat并检查iowait是否疯狂,观察每秒事务数、查询延迟、web服务器响应能力,以及常见的数据库运行状况。如果电话响了,按Ctrl-C键,然后说";修复">

在完成了几个检查点之后,Ctrl-C。是时候做真正的事情了。

现在,为了使这个查询花费更长的时间(从而破坏更少的IO带宽),您可以将其添加到您选择的列中:

pg_sleep((random()<0.000001)::INTEGER * 0.1)

这将使它平均每一百万行睡眠0.1秒。在查看vmstat时根据口味进行调整。

您还可以使用hacks监控查询进度。

它应该可以正常工作。

一旦从被诅咒的表中提取出有趣的行,您就可以将旧数据移动到数据仓库或其他地方,或者冷藏,或者如果您想运行一些分析,可以将其加载到clickhouse中。

也许在新表增长回3TB之前对其进行分区也是一个好主意。或者周期性地移动旧行。

现在,我想知道你是如何备份这个东西的。。。

--编辑

好吧,我有另一个想法,也许更简单,但你需要一个盒子。

获取第二台具有快速存储的服务器并设置逻辑复制。在此副本服务器上,创建一个大表的空UNLOGGED副本,主键上只有一个索引。逻辑复制将复制整个表,因此需要一段时间。原始服务器中的第二个网卡或一些QoS调整将有助于避免破坏您实际用于提供查询的以太网连接。

逻辑复制是基于行的,并通过主键标识行,因此您绝对需要在从属服务器上手动创建PK索引。

我现在已经在家里的盒子上测试过了,效果很好。最初的数据传输有点慢,但那可能是我的网络。暂停,然后恢复复制在暂停期间插入或更新的主机上传输的行。然而,重命名表似乎会破坏它,因此您将无法执行INSERT INTO SELECT操作,您将不得不在副本上执行DELETE操作。对于SSD,只有一个PK索引,表设置为UNLOGGED,它不应该永远存在。也许使用btrfs会将随机索引写入IO转换为线性IO,因为它具有写时复制的特性。或者,如果PK索引适合shared_buffers,只需YOLO它并将checkpoint_timeout设置为";7天";所以它实际上什么都没写。您可能需要分块进行删除,以便复制的更新能够跟上。

当我删除PK索引以加快删除速度,然后在重新启用复制之前重新创建它时,它没有赶上更新。所以你不能放弃索引。

但是,有没有一种方法可以只传输要保留的行,而不是传输所有内容并删除,同时让复制副本跟上主副本的更新?。。。对于插入(只是禁用初始数据副本)可以这样做,但不幸的是,对于更新不能这样做。你需要一个整数主键,这样你就可以在复制副本上生成伪行,然后在复制过程中更新。。。但你不能用你的UUID PK做到这一点。

不管怎样。完成此操作后,将要保留在主服务器上的WAL段数设置为一个非常高的值,以便稍后恢复复制而不会丢失更新。

现在,您可以在复制副本上运行大型DELETE。完成后,抽真空,也许是CLUSTER,重新创建所有索引等,并将表设置为LOGGED。

然后可以故障切换到新服务器。或者,如果你有冒险精神,你可以将复制副本的表复制回主表上,因为它的名称与另一个模式中的名称相同。

这应该允许非常小的停机时间,因为所有更新都是复制的,复制副本将始终是最新的。

我建议:

  1. 将活动记录复制到临时表
  2. 删除主表
  3. 将临时表重命名为主表名

相关内容

最新更新