【问题标题】:How to remove duplicate rows with foreign keys dependencies?如何删除具有外键依赖项的重复行?
【发布时间】:2015-09-06 15:29:40
【问题描述】:

我确定这很常见,但 Google 没有提供帮助。我正在尝试在 PostgreSQL 9.1 中编写一个简单的存储过程,它将从父 cpt 表中删除重复的条目。父表cpt被子表lab引用定义为:

CREATE TABLE lab (
 recid serial NOT NULL,
 cpt_recid integer,
  ........
 CONSTRAINT cs_cpt FOREIGN KEY (cpt_recid)
   REFERENCES cpt (recid) MATCH SIMPLE
   ON UPDATE NO ACTION ON DELETE RESTRICT,
 ...
);

我遇到的最大问题是如何获取失败的记录,以便我可以在EXCEPTION 子句中使用它来将子行从lab 移动到一个可接受的键,然后循环返回并删除cpt 表中不必要的记录。

这是(非常错误的)代码:

CREATE OR REPLACE FUNCTION h_RemoveDuplicateCPT()
  RETURNS void AS
$BODY$
BEGIN
LOOP
   BEGIN

   DELETE FROM cpt
   WHERE recid IN (
      SELECT recid
      FROM  (
         SELECT recid,
         row_number() over (partition BY cdesc ORDER BY recid) AS rnum
         FROM cpt) t
      WHERE t.rnum > 1)
   RETURNING recid;

   IF count = 0 THEN
      RETURN;
   END IF;  

   EXCEPTION WHEN foreign_key_violation THEN
      RAISE NOTICE 'fixing unique_violation';
      RAISE NOTICE 'recid is %' , recid;
   END;
END LOOP;
END;                    
$BODY$
LANGUAGE plpgsql VOLATILE;

【问题讨论】:

  • 你试过隐藏列 ctid 吗?即使所有可见列都相同,它也有助于删除重复项。

标签: database postgresql exception-handling foreign-keys plpgsql


【解决方案1】:

您可以一次选择所有重复项并使用记录变量循环遍历结果。 您将可以访问整个当前记录。下面的函数可以作为一个例子:

create or replace function show_remove_duplicates_in_cpt ()
returns setof text language plpgsql
as $$
declare
    rec record;
begin
    for rec in
        select * from (
            select 
                recid, cdesc, 
                row_number() over (partition by cdesc order by recid) as rnum
            from cpt
            ) alias
        where rnum > 1
    loop
        return next format ('fixing foreign key for %s %s %s', rec.recid, rec.cdesc, rec.rnum);
        return next format ('deleting from cpt where recid = %s', rec.recid);
    end loop;
end $$;

select * from show_remove_duplicates_in_cpt ();

【讨论】:

    【解决方案2】:

    您可以使用带有data-modifying CTEs单个 SQL 语句更有效地完成此操作。

    WITH plan AS (
       SELECT *
       FROM  (
          SELECT recid, min(recid) OVER (PARTITION BY cdesc) AS master_recid
          FROM   cpt
          ) sub
       WHERE  recid <> master_recid  -- ... <> self
       )
     , upd_lab AS (
       UPDATE lab l
       SET    cpt_recid = p.master_recid   -- link to master recid ...
       FROM   plan p
       WHERE  l.cpt_recid = p.recid
       )
    DELETE FROM cpt c
    USING  plan p
    WHERE  c.recid = p.recid
    RETURNING c.recid;
    

    dbfiddle here(第 11 页)
    SQL Fiddle(第 9.6 页)

    这应该更快更干净。循环比较昂贵,异常处理比较昂贵。
    更重要的是,lab 中的引用会自动重定向到 cpt 中的相应主行,这在您的原始代码中还没有。因此,您可以一次性删除所有的复制品

    如果您愿意,您仍然可以将其包装在 plpgsql 或 SQL 函数中。

    说明

    1. 在第一个 CTE plan 中,使用相同的 cdesc 标识每个分区中的主行。在您的情况下,recid 最小的行。

    2. 在第二个 CTE upd_lab 中,将所有引用重复的行重定向到 cpt 中的主行。

    3. 最后,删除重复数据,这不会引发异常,因为相关行实际上是同时链接到剩余的主行。

    ON DELETE RESTRICT

    语句的所有 CTE 和主查询几乎同时在基础表的同一快照上运行。他们看不到彼此对基础表的影响:

    人们可能期望带有ON DELETE RESTRICT 的 FK 约束会引发异常,因为,[根据文档][3]:

    NO ACTION 检查以外的参考操作不能被推迟, 即使约束被声明为可延迟的。

    但是,上面的语句是一个单个命令,并且,[再次使用手册][3]:

    一个不可延迟的约束将在之后立即检查 每个命令

    我的大胆强调。当然,也适用于限制较少的默认 ON DELETE NO ACTION

    但要小心并发事务写入相同的表,但这是一般考虑,并非特定于此任务。

    一个例外适用于UNIQUEPRIMARY KEY 约束,但这与这种 情况无关:

    【讨论】:

    • 哇...非常令人印象深刻,非常感谢。谢谢。
    • upd_lab 如何(何时)被执行?
    • @AlanWayne: CTE plan 首先执行,因为其他两个查询依赖于它。其他两个以任意顺序执行。但是所有查询都会看到基础表的相同快照。因此“几乎同时”。
    猜你喜欢
    • 2020-05-07
    • 1970-01-01
    • 2017-06-07
    • 2017-12-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多