如何从表中删除不需要的重复行。这些重复行之所以“不需要”,是因为同一个值在指定为主键的列中出现多次。自从 glibc 好心地改变了排序方式后,我们发现这个问题有所增加。当用户升级操作系统并修改底层 glibc 库时,这可能会导致无效索引。
唯一索引损坏的主要影响之一是允许添加本应由主键捕获的行。换句话说,对于一个在名为“id”的列上有主键的表,您可能会观察到如下情况:
-- Can you spot the problem?
SELECT id FROM mytable ORDER BY id LIMIT 5;
id
----
1
2
3
3
4
(5 rows)
在对问题一无所知的情况下,解决问题的第一步是什么?如果你说备份,那你答对了!每当你认为数据库有问题时,或者在尝试修复此类问题之前,都应该进行一次全新备份。
补充一下:我们可以简单地重新索引吗?不行——只要有重复的行,就无法创建(或重新创建)唯一索引。Postgres 会拒绝这样做,因为这违反了我们试图强制执行的唯一性。所以,我们必须从表中删除这些行。
这里有一个修复 Postgres 表中重复主键条目问题的巧妙方法。当然,你需要根据具体情况进行调整,但请慢慢操作,确保理解每个步骤。尤其是在生产环境中发生这种情况时(剧透:这种情况几乎总是在生产环境中发生)。
1. 调试辅助工具
我们要做的第一件事可能看起来有点奇怪:
-- Encourage not using indexes:
set enable_indexscan = 0;
set enable_bitmapscan = 0;
set enable_indexonlyscan = 0;
由于不良索引是此类不良行进入数据库的主要途径,因此我们不能信任它们。这些低级调试辅助工具会告诉 Postgres 规划器优先考虑其他数据获取方式。在我们的例子中,这意味着直接访问表,而不是在索引中查找
2. 进行快速健全性检查
-- Sanity check. This should return a number greater than 1. If not, stop.
set search_path = public;
select count(*) from mytable where id = 3;
在开始之前,我们需要确保拥有正确的表。search_path 是一种安全措施,就像在表上查找一样。由于 id 是表的主键,因此它应该为每个值返回 1 的计数。在我们的例子中,我们知道该表有多个 id 为“3”的条目,因此这主要是为了进行完整性检查,确保我们将要操作的患者是正确的。我们期望返回一个大于“1”的数字。对于有效的主键,返回的值应该只有 0 或 1。
3. 创建备份(始终)
-- Make a backup:
create table mytable_backup as select * from mytable;
在开始之前,我们需要将表中的所有现有行复制到新的备份表中。同样,这不会取代对整个数据库的完整备份(始终是步骤 0),但它是另一个很好的安全功能。
4.制作测试表
-- Test out the process on a subset of the data:
create table test_mytable as select * from mytable where id < 30;
create table test_mytable_duperows_20250317 (like mytable);
最好先在测试表上进行测试。在我们的例子中,使用的是实际表的较小版本。因为我们知道 ID 为 3 的行有问题,所以我们创建了一个包含这些行的新表。我们还创建了一个名为 test_mytable_20250317 的新空表,用于保存被移除的重复行。末尾的日期会告诉以后的查看者该表的创建时间。
5. 在副本会话中启动清理
从现在开始,我们将开始实际的清理工作。我们启动一个事务,然后将 session_replication_role 设置为 replica,这是一个高级(且危险)的命令,它会禁用所有触发器和规则。通常情况下,这不是一个好的做法,但我们还是希望这样做,以防万一存在外键阻止我们删除损坏的行。此外,我们这样做是SET LOCAL为了确保此设置在下次或时SET恢复正常。COMMITROLLBACK
begin;
set local session_replication_role = 'replica';
因为我们刚刚创建了这个测试表,所以我们知道它没有触发器并且没有通过外键链接到任何其他表,但我们希望使这个测试尽可能接近实际的表修改,因此我们保留 session_replication_role 修改。
6. 使用函数清理重复行
begin;
set local session_replication_role = 'replica';
with goodrows as (
select min(ctid) from TEST_mytable group by id
)
,mydelete as (
delete from TEST_mytable
where not exists (select 1 from goodrows where min=ctid)
returning *
)
insert into TEST_mytable_duperows_20250317 select * from mydelete;
reset session_replication_role;
commit;
因此,我们发出一个 begin 命令,设置 session_replication_role,运行一条 SQL 语句,重置 session_replication_role,最后提交。这条 SQL 语句执行起来比较繁重,所以我们来分解一下。
select min(ctid) from TEST_mytable group by id我们要做的第一件事就是想办法找出哪些行是重复的。由于id列应该是唯一的(所有主键列都是唯一的),我们知道任何出现超过一次的 ID 都需要一个决胜机制。Postgres 中的每一行都有一个名为“ctid”的隐藏列,它代表列元组标识符,本质上是一个指向实际物理行所在位置的指针。因此,它始终是唯一的。如果我们按 id 列分组,我们可以通过查找“最小”的 ctid 为每个唯一 ID 提取一个 ctid(使用 min() 、max() 或其他方法都没关系,只要我们只选择一个就行)。
我们将存储该信息并使用它来帮助删除,因此我们通过WITH命令启动一个 cte,并将其命名为goodrows。
delete from TEST_mytable
where not exists (select 1 from goodrows where min=ctid)
returning *
下一步是删除所有不来自我们刚刚创建的 goodrows 列表的重复行。因此,每个重复行都会有不同的 ctid,我们将删除每个 ctid 对应的所有 ctid,只保留一个。最后***RETURNING ****一步告诉 delete 函数返回所有被删除行的完整信息。
insert into test_mytable_duperows_20250317 select * from mydelete;
最后,我们将删除操作的输出存储到表中。这样,行虽然被删除了,但我们仍然拥有一个完整的被删除行列表,用于调试和取证。
此时,重复的行应该被删除,并放在“duperows”表中。最好检查一下此表和 test_mytable 表,以确保一切按预期工作。
7. 在实时表上运行该函数
准备就绪后,您可以重新运行相同的代码,但将测试表替换为实际表:
create table mytable_duperows_20250317 (like mytable);
begin;
set local session_replication_role = 'replica';
with goodrows as (
select min(ctid) from mytable group by id
)
,mydelete as (
delete from mytable
where not exists (select 1 from goodrows where min=ctid)
returning *
)
insert into mytable_duperows_20250317 select * from mydelete;
reset session_replication_role;
commit;
8. 重新索引
最后一步,我们要重建那些可疑的索引。即使重复的行已经删除,索引中仍然可能包含错误的信息。该REINDEX命令本质上是删除并重建,因此我们对表中可能存在的所有索引执行此操作:
reindex table mytable;
现在我们还可以使用、和全部设置为将 Postgres 恢复到enable_indexscan正常enable_bitmapscan计划enable_indexonlyscan设置。
以上就是所有步骤!