【问题标题】:PostgreSQL multiple nullable columns in unique constraintPostgreSQL 唯一约束中的多个可空列
【发布时间】:2015-07-21 06:10:34
【问题描述】:

我们有一个遗留数据库架构,其中包含一些有趣的设计决策。直到最近,我们还只支持 Oracle 和 SQL Server,但我们正在尝试添加对 PostgreSQL 的支持,这带来了一个有趣的问题。我已经搜索了 Stack Overflow 和互联网的其他部分,我不认为这种特殊情况是重复的。

Oracle 和 SQL Server 在唯一约束中的可空列的行为相同,即在执行唯一检查时基本上忽略为 NULL 的列。

假设我有以下表格和约束:

CREATE TABLE EXAMPLE
(
    ID TEXT NOT NULL PRIMARY KEY,
    FIELD1 TEXT NULL,
    FIELD2 TEXT NULL,
    FIELD3 TEXT NULL,
    FIELD4 TEXT NULL,
    FIELD5 TEXT NULL,
    ...
);

CREATE UNIQUE INDEX EXAMPLE_INDEX ON EXAMPLE
(
    FIELD1 ASC,
    FIELD2 ASC,
    FIELD3 ASC,
    FIELD4 ASC,
    FIELD5 ASC
);

在 Oracle 和 SQL Server 上,保留任何可空列 NULL 将导致仅对非空列执行唯一性检查。所以下面的插入只能做一次:

INSERT INTO EXAMPLE VALUES ('1','FIELD1_DATA', NULL, NULL, NULL, NULL );
INSERT INTO EXAMPLE VALUES ('2','FIELD1_DATA','FIELD2_DATA', NULL, NULL,'FIELD5_DATA');
-- These will succeed when they should violate the unique constraint:
INSERT INTO EXAMPLE VALUES ('3','FIELD1_DATA', NULL, NULL, NULL, NULL );
INSERT INTO EXAMPLE VALUES ('4','FIELD1_DATA','FIELD2_DATA', NULL, NULL,'FIELD5_DATA');

但是,因为 PostgreSQL(正确地)遵守 SQL 标准,所以那些插入(以及任何其他值的组合,只要其中一个为 NULL)不会抛出错误并且正确插入没有问题。不幸的是,由于我们的旧架构和支持代码,我们需要 PostgreSQL 的行为与 SQL Server 和 Oracle 相同。

我知道以下 Stack Overflow 问题及其答案:Create unique constraint with null columns。据我了解,解决这个问题有两种策略:

  1. 在可空列同时为 NULLNOT NULL 的情况下创建描述索引的部分索引(这会导致部分索引的数量呈指数增长)
  2. COAELSCE 与索引中可为空的列上的标记值一起使用。

(1) 的问题在于,我们需要创建的部分索引的数量随着我们想添加到约束中的每个额外的可为空列而呈指数增长(如果我没记错的话,2^N)。 (2) 的问题是标记值减少了该列的可用值数量以及所有潜在的性能问题。

我的问题:这是解决这个问题的唯一两种方法吗?如果是这样,对于这个特定的用例,它们之间的权衡是什么?一个好的答案将讨论每个解决方案的性能、可维护性、PostgreSQL 如何在简单的SELECT 语句中利用这些索引,以及任何其他“陷阱”或需要注意的事情。请记住,5 个可为空的列仅作为示例;我们的架构中有一些表,最多 10 个(是的,我每次看到它都会哭,但它就是这样)。

【问题讨论】:

  • 还有第三种选择。您可以使用基于函数的索引,而不是实际插入“标记值”:CREATE UNIQUE INDEX u ON example(COALESCE(field1, '~~~~NULL'), COALESCE(field2, '~~~~NULL'))。会更糟,但值得考虑
  • 是的,这就是我所说的第二个选项的意思。使用COALESCE 作为功能索引。似乎这种情况下的性能会很差,并且如果另一行已经是该列的 NULL,则实际上无法插入使用的哨兵值。
  • 好的,我明白了。那么,选项 3 是实际插入标记值(例如,通过触发器或通过访问应用程序中的转换器)。那会很快,但会很难看....顺便说一句,您还有另一个兼容性问题。在 Oracle 中,'' IS NULL 为真 :)
  • 您的示例可能有问题(除了缺少分号和字符串常量的"-quoting)。如果我在 PG 中两次提交上述插入,则第二对失败。 (应该发生什么?)
  • 我将编辑问题,但在第二次插入时使用不同的键值。它会在不应该成功的时候成功。

标签: sql postgresql database-design null unique-constraint


【解决方案1】:

您正在努力实现与现有 OracleSQL Server 实施的兼容性
这是presentation comparing physical row storage formats of the three involved RDBS

由于Oracle 根本没有在行存储中实现NULL 值,因此无论如何它都无法区分空字符串和NULL。那么在 Postgres 中使用空字符串 ('') 代替 NULL 值不是谨慎的做法吗 - 对于 这个 特定用例?

将唯一约束中包含的列定义为NOT NULL DEFAULT '',问题解决:

CREATE TABLE example (
   example_id serial PRIMARY KEY
 , field1 text NOT NULL DEFAULT ''
 , field2 text NOT NULL DEFAULT ''
 , field3 text NOT NULL DEFAULT ''
 , field4 text NOT NULL DEFAULT ''
 , field5 text NOT NULL DEFAULT ''
 , CONSTRAINT example_index UNIQUE (field1, field2, field3, field4, field5)
);

注意事项

  • 您在问题中展示的是一个独特的索引

    CREATE UNIQUE INDEX ...
    

    不是您一直在谈论的独特约束。存在细微而重要的区别!

    我把它改成了一个实际的约束,就像你把它作为帖子的主题一样。

  • 关键字ASC 只是噪音,因为这是默认的排序顺序。我把它丢了。

  • 为了简单起见,使用serial PK 列是完全可选的,但通常比存储为text 的数字更好。

使用它

只需从INSERT 中省略空/空字段:

INSERT INTO example(field1) VALUES ('F1_DATA');
INSERT INTO example(field1, field2, field5) VALUES ('F1_DATA', 'F2_DATA', 'F5_DATA');

重复任何这些插入都会违反唯一性约束。

或者如果您坚持省略目标列(这在持久化的 INSERT 语句中有点反模式):
或者对于所有列的批量插入需要列出:

INSERT INTO example VALUES
  ('1', 'F1_DATA', DEFAULT, DEFAULT, DEFAULT, DEFAULT)
, ('2', 'F1_DATA','F2_DATA', DEFAULT, DEFAULT,'F5_DATA');

或者简单地说:

INSERT INTO example VALUES
  ('1', 'F1_DATA', '', '', '', '')
, ('2', 'F1_DATA','F2_DATA', '', '','F5_DATA');

或者您可以编写一个触发器BEFORE INSERT OR UPDATE,将NULL 转换为''

替代解决方案

如果您需要使用实际的 NULL 值,我建议您使用带有 COALESCE 的唯一 index,就像您提到的选项 (2) 和 @wildplasser provided as his last example.

@Rudolfo presented 这样的数组 上的索引很简单,但要昂贵得多。数组处理在 Postgres 中不是很便宜,并且存在类似于一行(24 字节)的数组开销:

数组仅限于相同数据类型的列。如果不是,您可以将所有列转换为 text,但通常会进一步增加存储需求。或者,您可以为异构数据类型使用众所周知的行类型...

一个极端情况:所有 NULL 值的数组(或行)类型都被认为是相等的 (!),因此只能有 1 行所有涉及的列都为 NULL。可能会也可能不会如您所愿。如果要禁止所有列为 NULL:

【讨论】:

  • 我们使用 ODBC 与我们的数据库进行交互,当字符串为空时我们绑定 NULL。但是因为 Oracle 已经对它们一视同仁了,所以我们基本上使用了我原来的 COALESCE 选项,其标记值为 ''。对于我们的 int 列,我们正在重写我们的业务逻辑,以允许我们通过使它们不可为空来完全从唯一索引中删除可为空的 int 列。感谢您的回答!
【解决方案2】:

第三种方法:使用IS NOT DISTINCT FROM insted of = 比较关键列。 (这可以利用候选 natural 键上的现有索引)示例(查看最后一列)

SELECT *
    , EXISTS (SELECT * FROM example x
     WHERE x.FIELD1 IS NOT DISTINCT FROM e.FIELD1
     AND x.FIELD2 IS NOT DISTINCT FROM e.FIELD2
     AND x.FIELD3 IS NOT DISTINCT FROM e.FIELD3
     AND x.FIELD4 IS NOT DISTINCT FROM e.FIELD4
     AND x.FIELD5 IS NOT DISTINCT FROM e.FIELD5
     AND x.ID <> e.ID
    ) other_exists
FROM example e
    ;

下一步是将其放入触发器函数中,并在其上放置触发器。 (现在没时间,也许以后)


这里是触发功能(还不完美,但似乎可以工作):


CREATE FUNCTION example_check() RETURNS trigger AS $func$
BEGIN
    -- Check that empname and salary are given
    IF EXISTS (
     SELECT 666 FROM example x
     WHERE x.FIELD1 IS NOT DISTINCT FROM NEW.FIELD1
     AND x.FIELD2 IS NOT DISTINCT FROM NEW.FIELD2
     AND x.FIELD3 IS NOT DISTINCT FROM NEW.FIELD3
     AND x.FIELD4 IS NOT DISTINCT FROM NEW.FIELD4
     AND x.FIELD5 IS NOT DISTINCT FROM NEW.FIELD5
     AND x.ID <> NEW.ID
            ) THEN
        RAISE EXCEPTION 'MultiLul BV';
    END IF;


    RETURN NEW;
END;
$func$ LANGUAGE plpgsql;

CREATE TRIGGER example_check BEFORE INSERT OR UPDATE ON example
  FOR EACH ROW EXECUTE PROCEDURE example_check();

更新:唯一索引有时可以包装成一个 约束(见postgres-9.4 docs, final example)你确实需要发明一个哨兵值;我在这里使用了空字符串''


CREATE UNIQUE INDEX ex_12345 ON example
        (coalesce(FIELD1, '')
        , coalesce(FIELD2, '')
        , coalesce(FIELD3, '')
        , coalesce(FIELD4, '')
        , coalesce(FIELD5, '')
        )
        ;

ALTER TABLE example
        ADD CONSTRAINT con_ex_12345
        USING INDEX ex_12345;

coalesce() 上的“功能”索引在此构造中是不允许的。 但是,唯一索引(OP 的选项 2)仍然有效:


ERROR:  index "ex_12345" contains expressions
LINE 2:  ADD CONSTRAINT con_ex_12345
             ^
DETAIL:  Cannot create a primary key or unique constraint using such an index.
INSERT 0 1
INSERT 0 1
ERROR:  duplicate key value violates unique constraint "ex_12345"

【讨论】:

  • 您的触发器实现是并发安全的吗?我认为您在这里很容易出现竞争条件问题。
  • 我想是的。触发器是当前事务的一部分,因此它应该与插入/更新本身一样并发安全。 (也许它可以通过(a)检查约束连接)它应该是一个AFTER触发器吗?
  • 好吧,据我所知,您可以同时插入 2 行违反此触发器的行:检查将同时成功通过。 “应该改为 AFTER 触发器吗?” --- 我敢肯定,您根本无法使用触发器提供适当的解决方案(除非您使用显式锁定来管理并发访问)
  • 不幸的是,我需要行为是并发安全的。否则,我很欣赏解决问题的聪明方法。添加像@zerkms 这样的显式锁定会导致性能下降吗?如果可行,最好举个例子。
  • @JohnDrouhard 这取决于吞吐量
【解决方案3】:

这实际上对我很有效:

CREATE UNIQUE INDEX index_name ON table_name ((
   ARRAY[field1, field2, field3, field4]
));

我不知道性能如何受到影响,但它应该接近理想状态(取决于优化后数组的优化程度)

【讨论】:

    【解决方案4】:

    您可以创建一个规则,将所有 NULL 值而不是原始表插入到 partition_field1_nullable、partition_fiend2_nullable 等分区中。这样您就可以仅在原始表上创建唯一索引(没有空值)。这将允许您将 not null 仅插入到 orig 表(具有唯一性),并将尽可能多的 not null(并且相应地不唯一)值插入“可为空的分区”。 并且您可以仅对可空分区应用 COALESCE 或触发方法,以避免许多分散的部分索引并触发原始表上的每个 DML...

    【讨论】:

    • @JohnDrouhard 好吧,想法是创建分区 - 一个用于不可空数据,一个用于可空数据。然后创建一个不可为空的唯一索引。和基于函数的索引,用于可空为 a_horse_with_no_name 提供。这样,DML 和选择会运行得更快
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-09-16
    • 1970-01-01
    • 1970-01-01
    • 2012-08-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多