【问题标题】:How to secure table for avoid duplicate data如何保护表以避免重复数据
【发布时间】:2017-07-16 09:14:40
【问题描述】:

我无法解决如何保护我的表以避免重复的 attributes_positions 组合的问题。向您展示我的意思的最佳方式是下图

id_combination表示组合的数量。组合由 attributes_positions 组成。所以组合是attributes_positions的序列。

现在我将确保表格不会插入完全相同的 attributes_positions 序列。

当然,如果已经插入的组合包含一个额外的attributes_positions或少于一个插入组合是可以的

图片我展示了不同的重复和不重复组合。

有什么办法可以做到这一点吗? Meaby 之类的“更新前”。但是如何实现这个例子。我对高级sql不太好。 我试图保护表的数据库是 postgresql 9.4

我会很感激你的帮助

【问题讨论】:

  • 您想要唯一的集合,并以id_combination 为集合的标识符?
  • 是的。确切地说。我需要一组唯一的 attributes_positions,其中 id_combination 是该组的标识符
  • 必须 attributes_positions 是连续的(并且从 1 开始)?
  • 不,attributes_positions 如何连续无关紧要。它们可以混合而不是订购。它们也不需要从 1 开始。它们不会递增。它们只包含其他表的外键
  • 枚举集合非常接近关系划分。 [正在努力]

标签: sql postgresql database-design unique-constraint relational-division


【解决方案1】:
        -- The data
CREATE TABLE theset (
        set_id INTEGER NOT NULL PRIMARY KEY
        , set_name text UNIQUE
        );
INSERT INTO theset(set_id, set_name) VALUES
( 1, 'one'), ( 2, 'two'), ( 3, 'three'), ( 4, 'four');

CREATE TABLE theitem (
        item_id integer NOT NULL PRIMARY KEY
        , item_name text UNIQUE
        );
INSERT INTO theitem(item_id, item_name) VALUES
( 1, 'one'), ( 2, 'two'), ( 3, 'three'), ( 4, 'four'), ( 5, 'five');

CREATE TABLE set_item (
        set_id integer NOT NULL REFERENCES theset (set_id)
        , item_id integer NOT NULL REFERENCES theitem(item_id)
        , PRIMARY KEY (set_id,item_id)
        );
        -- swapped index is indicated for junction tables
CREATE UNIQUE INDEX ON set_item(item_id, set_id);

INSERT INTO set_item(set_id,item_id) VALUES
(1,1), (1,2), (1,3), (1,4),
(2,1), (2,2), (2,3), -- (2,4),
(3,1), (3,2), (3,3), (3,4), (3,5),
(4,1), (4,2), (4,4);

CREATE FUNCTION set_item_unique_set( ) RETURNS TRIGGER AS
$func$
BEGIN
IF EXISTS ( -- other set
        SELECT * FROM theset oth
        -- WHERE oth.set_id <> NEW.set_id -- only for insert/update
        WHERE TG_OP = 'DELETE' AND oth.set_id <> OLD.set_id
           OR TG_OP <> 'DELETE' AND oth.set_id <> NEW.set_id

        -- count (common) members in the two sets
        -- items not in common will have count=1
        AND NOT EXISTS (
                SELECT item_id FROM set_item x1
                WHERE (x1.set_id = NEW.set_id OR x1.set_id = oth.set_id )
                GROUP BY item_id
                HAVING COUNT(*) = 1
                )

        ) THEN
        RAISE EXCEPTION 'Not unique set';
        RETURN NULL;
ELSE
        RETURN NEW;
END IF;

END;
$func$ LANGUAGE 'plpgsql'
        ;

CREATE CONSTRAINT TRIGGER check_item_set_unique
        AFTER UPDATE OR INSERT OR DELETE
        -- BEFORE UPDATE OR INSERT
        ON set_item
        FOR EACH ROW
        EXECUTE PROCEDURE set_item_unique_set()
        ;

-- Test it
INSERT INTO set_item(set_id,item_id) VALUES(4,5); -- success
INSERT INTO set_item(set_id,item_id) VALUES(2,4); -- failure
DELETE FROM set_item WHERE set_id=1 AND item_id= 4; -- failure

注意:DELETE 案例也应该有一个触发器。


更新:增加了对DELETE的处理

(删除的处理并不完美;想象一下删除集合中的最后一个元素的情况)

【讨论】:

  • 可能是(TG_OP = 'DELETE' AND oth.set_id &lt;&gt; OLD.set_id OR TG_OP &lt;&gt; 'DELETE' AND oth.set_id &lt;&gt; NEW.set_id)
  • 谢谢!这正是我要找的。但我又考虑了一件事。可以在诸如 on commit 之类的东西上设置触发器?不在更新或删除每一行。因为可能会出现问题,放入包含已存在属性位置的新序列。 INSERT INTO set_item(set_id,item_id) VALUES(2,4); // 应该会失败,但这应该可以工作 INSERT INTO set_item(set_id,item_id) VALUES(2,4); b> INSERT INTO set_item(set_id,item_id) VALUES(2,5); INSERT INTO set_item(set_id,item_id) VALUES(2,6); // 在此之后3 我们有新的序列
  • @MichałZiembiński 你的意思是deferrable。将其添加到约束定义中。加号:initially deferred.
【解决方案2】:

我的回答假设目标没有被欺骗,并且我们想要插入一个新集合 - 这恰好是一个重复。我选择id_comb 为 1 的 4 人组。

您必须将 4 人组放入临时表中。然后,您必须水平旋转 staging 和 target - 以便获得 5 个名为 attr_pos1attr_pos5 的列(示例中最大的组是 5)。要进行数据透视,您需要一个序列号,我们使用 ROW_NUMBER() 获得该序列号。这适用于表、暂存和目标。然后,你同时转动两者。然后,您尝试在所有 5 个attr_pos# 列上加入旋转暂存和目标,并计算行数。如果你得到 0,你就没有重复。如果你得到 1,你就有重复。

这是整个场景:

    WITH
    -- input section: a) target table, no dupes
    target(id_comb,attr_pos) AS (
                        SELECT 2,1
    UNION ALL SELECT 2,2
    UNION ALL SELECT 2,3
    UNION ALL SELECT 2,4
    UNION ALL SELECT 3,1
    UNION ALL SELECT 3,2\
UNION ALL SELECT 3,3
UNION ALL SELECT 3,4
UNION ALL SELECT 3,5
UNION ALL SELECT 4,1
UNION ALL SELECT 4,2
UNION ALL SELECT 4,3
)
,
-- input section: b) staging, input, would be a dupe
staging(id_comb,attr_pos) AS (
          SELECT 1,1
UNION ALL SELECT 1,2
UNION ALL SELECT 1,3
UNION ALL SELECT 1,4
)
,
-- query section:
-- add sequence numbers to stage and target
target_s AS (
SELECT 
  ROW_NUMBER() OVER(PARTITION BY id_comb ORDER BY attr_pos) AS seq
, *
FROM target
)
,
staging_s AS (
SELECT 
  ROW_NUMBER() OVER(PARTITION BY id_comb ORDER BY attr_pos) AS seq
, *
FROM staging
)
,
-- horizontally pivot target, NULLS as -1 for later join
target_h AS (
SELECT 
  id_comb
, IFNULL(MAX(CASE seq WHEN 1 THEN attr_pos END),-1) AS attr_pos1
, IFNULL(MAX(CASE seq WHEN 2 THEN attr_pos END),-1) AS attr_pos2
, IFNULL(MAX(CASE seq WHEN 3 THEN attr_pos END),-1) AS attr_pos3
, IFNULL(MAX(CASE seq WHEN 4 THEN attr_pos END),-1) AS attr_pos4
, IFNULL(MAX(CASE seq WHEN 5 THEN attr_pos END),-1) AS attr_pos5
FROM target_s
GROUP BY id_comb ORDER BY id_comb
)
,
-- horizontally pivot staging, NULLS as -1 for later join
staging_h AS (
SELECT 
  id_comb
, IFNULL(MAX(CASE seq WHEN 1 THEN attr_pos END),-1) AS attr_pos1
, IFNULL(MAX(CASE seq WHEN 2 THEN attr_pos END),-1) AS attr_pos2
, IFNULL(MAX(CASE seq WHEN 3 THEN attr_pos END),-1) AS attr_pos3
, IFNULL(MAX(CASE seq WHEN 4 THEN attr_pos END),-1) AS attr_pos4
, IFNULL(MAX(CASE seq WHEN 5 THEN attr_pos END),-1) AS attr_pos5
FROM staging_s
GROUP BY id_comb ORDER BY id_comb
)
SELECT 
  COUNT(*)
FROM target_h
JOIN staging_h USING (
  attr_pos1
, attr_pos2
, attr_pos3
, attr_pos4
, attr_pos5
);

希望这会有所帮助---- 马可

【讨论】:

    【解决方案3】:

    @wildplasser 提供的有趣但不是很有用的解决方案。我创建脚本来插入示例数据:

    WITH param AS (
        SELECT 8 AS max
    ), maxarray AS (
        SELECT array_agg(i) as ma FROM (SELECT generate_series(1, max) as i FROM param) as i
    ), pre AS (
        SELECT
            *
        FROM (
        SELECT
             *, CASE WHEN (id >> mbit) & 1 = 1 THEN ma[mbit + 1] END AS item_id
         FROM (
                SELECT *,
                    generate_series(0, array_upper(ma, 1) - 1) as mbit
                FROM (
                        SELECT *,
                            generate_series(1,(2^max - 1)::int8) AS id
                        FROM param, maxarray
                    ) AS pre1
            ) AS pre2
        ) AS pre3
        WHERE item_id IS NOT NULL
    ), ins_item AS (
        INSERT INTO theitem (item_id, item_name) SELECT i, i::text FROM generate_series(1, (SELECT max FROM param)) as i RETURNING *
    ), ins_set AS (
    INSERT INTO theset (set_id, set_name)
    SELECT id, id::text FROM generate_series(1, (SELECT 2^max - 1 FROM param)::int8) as id
    RETURNING *
    ), ins_set_item AS (
    INSERT INTO set_item (set_id, item_id)
    SELECT id, item_id FROM pre WHERE (SELECT count(*) FROM ins_item) > 0 AND (SELECT count(*) FROM ins_set) > 0
    RETURNING *
    )
    SELECT
        'sets', count(*)
    FROM ins_set
    UNION ALL
    SELECT
        'items', count(*)
    FROM ins_item
    UNION ALL
    SELECT
        'sets_items', count(*)
    FROM ins_set_item
    ;
    

    当我用 8(set_item 为 1024 - 2^8 行)调用它时,它运行 21 秒。太糟糕了。当我关闭触发器时,它花费了不到 1 毫秒。

    我的建议

    在这种情况下使用数组非常有趣。不幸的是,PostgreSQL 不支持数组的外键,但它可以由 TRIGGER 完成。我删除set_item 表并为theset 添加items int[] 字段:

            -- The data
    CREATE TABLE theitem (
            item_id integer NOT NULL PRIMARY KEY
            , item_name text UNIQUE
            );
    
    CREATE TABLE theset (
            set_id INTEGER NOT NULL PRIMARY KEY
            , set_name text UNIQUE
            , items integer[] UNIQUE NOT NULL
            );
    
    CREATE INDEX i1 ON theset USING gin (items);
    
    CREATE OR REPLACE FUNCTION check_item_CU() RETURNS TRIGGER AS $sql$
    BEGIN
        IF (SELECT count(*) > 0 FROM unnest(NEW.items) AS u LEFT JOIN theitem ON (item_id = u) WHERE item_id IS NULL) THEN
            RETURN NULL;
        END IF;
    
        NEW.items = ARRAY(SELECT unnest(NEW.items) ORDER BY 1);
    
        RETURN NEW;
    END;
    $sql$ LANGUAGE plpgsql; 
    
    CREATE TRIGGER check_item_CU BEFORE INSERT OR UPDATE ON theset FOR EACH ROW EXECUTE PROCEDURE check_item_CU();
    
    CREATE OR REPLACE FUNCTION check_item_UD() RETURNS TRIGGER AS $sql$
    BEGIN
        IF (TG_OP = 'DELETE' OR TG_OP = 'UPDATE' AND NEW.item_id != OLD.item_id) AND (SELECT count(*) > 0 FROM theset WHERE OLD.item_id = ANY(items)) THEN
            RAISE EXCEPTION 'item_id % still used', OLD.item_id;
            RETURN NULL;
        END IF;
    
        RETURN NEW;
    END;
    $sql$ LANGUAGE plpgsql; 
    
    CREATE TRIGGER check_item_UD BEFORE DELETE OR UPDATE ON theitem FOR EACH ROW EXECUTE PROCEDURE check_item_UD();
    
    WITH param AS (
        SELECT 10 AS max
    ), maxarray AS (
        SELECT array_agg(i) as ma FROM (SELECT generate_series(1, max) as i FROM param) as i
    ), pre AS (
        SELECT
            *
        FROM (
        SELECT
             *, CASE WHEN (id >> mbit) & 1 = 1 THEN ma[mbit + 1] END AS item_id
         FROM (
                SELECT *,
                    generate_series(0, array_upper(ma, 1) - 1) as mbit
                FROM (
                        SELECT *,
                            generate_series(1,(2^max - 1)::int8) AS id
                        FROM param, maxarray
                    ) AS pre1
            ) AS pre2
        ) AS pre3
        WHERE item_id IS NOT NULL
    ), pre_arr AS (
    SELECT id, array_agg(item_id) AS items
    FROM pre
    GROUP BY 1
    ), ins_item AS (
        INSERT INTO theitem (item_id, item_name) SELECT i, i::text FROM generate_series(1, (SELECT max FROM param)) as i RETURNING *
    ), ins_set AS (
    INSERT INTO theset (set_id, set_name, items)
    SELECT id, id::text, items FROM pre_arr WHERE (SELECT count(*) FROM ins_item) > 0
    RETURNING *
    )
    SELECT
        'sets', count(*)
    FROM ins_set
    UNION ALL
    SELECT
        'items', count(*)
    FROM ins_item
    
    ;
    

    这个变种运行不到 1 毫秒

    【讨论】:

    • 无公关。存储 2300 个 set_items(最大值 = 9)需要 3 分 29 秒。这不允许生活在现实世界中。例如,数组存储 53000 个 set_items(最大值 = 13),但数组仍然少于 1 秒。
    猜你喜欢
    • 2021-09-23
    • 2019-08-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-06
    • 2013-01-31
    相关资源
    最近更新 更多