postgresql 将“null”作为表唯一约束中的独特值处理

lbsnaicq  于 2023-11-18  发布在  PostgreSQL
关注(0)|答案(4)|浏览(204)

我有一个表,我用它来定义客户端的默认和自定义选项。如果custom_id字段有一个值,那么它代表一个唯一的自定义作业的记录。如果它是空的,那么记录代表客户端的默认选项。
我的问题是,我想在两种情况下强制唯一性:

  1. custom_idclientoption均为非空值
  2. clientoption是非空的,但custom_id为空
    下面的表定义在第一种情况下有效,但在第二种情况下无效,因为null不被视为值。有没有一种方法可以使null被视为值?
class OptionTable(Base):
    __tablename__ = "option_table"
    __table_args__ = (
        UniqueConstraint("custom", "client", "option", name="uix_custom_client_option"),
    )

    id = Column(Integer, primary_key=True)
    custom_id = Column(Integer, ForeignKey("custom.id"), nullable=True)
    client = Column(String, nullable=False)
    option = Column(String, nullable=False)

字符串
下面是一些示例数据和按顺序添加时的结果:

+----+----------+----------+--------+---------------------------------------------+
| id | CustomID |  Client  | Option |                   result                    |
+----+----------+----------+--------+---------------------------------------------+
|  1 | 123      | MegaCorp | Apple  | OK                                          |
|  2 | 123      | MegaCorp | Apple  | not unique                                  |
|  3 | NULL     | MegaCorp | Apple  | OK                                          |
|  4 | NULL     | MegaCorp | Google | OK                                          |
|  5 | NULL     | MegaCorp | Google | this one should fail, but currently doesn't |
+----+----------+----------+--------+---------------------------------------------+


这个related answer做了我正在寻找的,使用MySQL。理想的解决方案是使用sqlalchemy。

brvekthn

brvekthn1#

根据this answer中推荐的方法,解决方案是创建两个partial indexes
使用sqlalchemy作为问题中的例子,看起来像:

class OptionTable(Base):
    __tablename__ = "option_table"

    id = Column(Integer, primary_key=True)
    custom_id = Column(Integer, ForeignKey("custom.id"), nullable=True)
    client = Column(String, nullable=False)
    option = Column(String, nullable=False)

    __table_args__ = (
        Index(
            "uix_custom_client_option", 
            "custom_id", 
            "client", 
            "option", 
            unique=True, 
            postgresql_where=custom_id.isnot(None)
        ),
        Index(
            "uix_client_option", 
            "client",  
            "option", 
            unique=True, 
            postgresql_where=custom_id.is_(None)
        ),
    )

字符串

ndh0cuux

ndh0cuux2#

我会做

CREATE UNIQUE INDEX ON atable
   (client, option, coalesce(custom_id, -42));

字符集
其中,-42是一个值,对于custom_id,该值不能出现。
它是如何产生作用的?
如果有两行具有相同的clientoptioncustom_id,并且都是NOT NULL,则它将像常规唯一索引一样工作,并将阻止添加第二行。
如果有两个具有相同clientoption的数据列,而且这两个数据列都具有custom_id IS NULL,则索引会防止加入第二个数据列,因为它会索引-42而非NULL,而且这两个索引Tuple会相同。

mrfwxfqh

mrfwxfqh3#

有没有一种方法可以使null被视为一个值?
Postgres 15开始,有一种方法是增加了子句**NULLS NOT DISTINCT**。你的索引现在可以是:

CREATE UNIQUE INDEX uix_custom_client_option ON test_table(custom, client, option) NULLS NOT DISTINCT;

字符集
注意:null在同一索引的所有索引列中被视为一个值(与另一个null冲突),而不仅仅是在option中。但这无论如何都是无效的,而customclient被定义为NOT NULL
详细信息:

  • 使用空列创建唯一约束
xlpyo6sf

xlpyo6sf4#

我为NULLS NOT DISTINCT创建了一个自定义编译器参数

from sqlalchemy.ext.compiler import compiles
from sqlalchemy import UniqueConstraint

UniqueConstraint.argument_for("postgresql", 'nulls_not_distinct', None)

@compiles(UniqueConstraint, "postgresql")
def compile_create_uc(create, compiler, **kw):
    """Add NULLS NOT DISTINCT if its in args."""
    stmt = compiler.visit_unique_constraint(create, **kw)
    postgresql_opts = create.dialect_options["postgresql"]

    if postgresql_opts.get("nulls_not_distinct"):
        return stmt.rstrip().replace("UNIQUE (", "UNIQUE NULLS NOT DISTINCT (")
    return stmt

class OptionTable(Base):
    __tablename__ = "option_table"
    __table_args__ = (
        UniqueConstraint(
            "custom", "client", "option", 
            postgresql_nulls_not_distinct=True,  # here it is.
            name="uix_custom_client_option"),
    )

字符集
请注意,Alembic处理得很好。

相关问题