PostgreSQL独特约束中的多个可空列

Joh*_*ard 16 sql postgresql null database-design unique-constraint

我们有一个遗留数据库模式,有一些有趣的设计决策.直到最近,我们才支持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
);
Run Code Online (Sandbox Code Playgroud)

在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');
Run Code Online (Sandbox Code Playgroud)

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

我知道以下Stack Overflow问题及其答案:使用空列创建唯一约束.根据我的理解,有两种策略可以解决这个问题:

  1. 创建描述在将空的列是这两种情况下的索引部分的索引NULLNOT NULL(其导致局部索引的数量的指数增长)
  2. COAELSCE与索引中可为空的列使用sentinel值.

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

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

Erw*_*ter 9

您正在努力与现有的OracleSQL Server实现兼容. 这是一个比较三个相关RDBS的物理行存储格式的演示文稿.

由于Oracle NULL在行存储中根本没有实现值,因此它无法区分空字符串和NULL无论如何.因此,对于这个特定的用例,使用空字符串('')而不是NULLPostgres中的值也不谨慎吗?

定义包含在唯一约束中的列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)
);
Run Code Online (Sandbox Code Playgroud)

笔记

使用它

只需省略以下的空/空字段INSERT:

INSERT INTO example(field1) VALUES ('F1_DATA');
INSERT INTO example(field1, field2, field5) VALUES ('F1_DATA', 'F2_DATA', 'F5_DATA');
Run Code Online (Sandbox Code Playgroud)

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

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

INSERT INTO example VALUES
  ('1', 'F1_DATA', DEFAULT, DEFAULT, DEFAULT, DEFAULT)
, ('2', 'F1_DATA','F2_DATA', DEFAULT, DEFAULT,'F5_DATA');
Run Code Online (Sandbox Code Playgroud)

或者干脆:

INSERT INTO example VALUES
  ('1', 'F1_DATA', '', '', '', '')
, ('2', 'F1_DATA','F2_DATA', '', '','F5_DATA');
Run Code Online (Sandbox Code Playgroud)

或者你可以写一个BEFORE INSERT OR UPDATE转换NULL为的触发器''.

替代方案

如果你需要使用实际的NULL值,我会建议使用你提到的选项(2)和@wildplasser作为他的最后一个例子的唯一索引.COALESCE

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

数组仅限于相同数据类型的列.text如果有些列没有,您可以将所有列强制转换,但通常会进一步增加存储要求.或者您可以使用众所周知的行类型来处理异构数据类型...

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


wil*_*ser 6

第三种方法:使用IS NOT DISTINCT FROMinsted =来比较关键列.(这可以利用候选自然键上的现有索引)示例(查看最后一列)

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
    ;
Run Code Online (Sandbox Code Playgroud)

下一步是将其置于触发器功能中,并在其上设置触发器.(现在没有时间,也许以后)


这里是触发器功能(它还不完美,但似乎有效):


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();
Run Code Online (Sandbox Code Playgroud)

更新:有时可以将唯一索引包装到约束中(请参阅postgres-9.4 docs,最后示例)您需要创建一个sentinel值; 我在''这里使用了空字符串.


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;
Run Code Online (Sandbox Code Playgroud)

但是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"
Run Code Online (Sandbox Code Playgroud)


小智 5

这实际上对我来说效果很好:

CREATE UNIQUE INDEX index_name ON table_name ((
   ARRAY[field1, field2, field3, field4]
));
Run Code Online (Sandbox Code Playgroud)

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