PostgreSQL可以对数组元素有唯一性约束吗?

问题描述 投票:17回答:2

我正在尝试为当前在LDAP存储中的主机数据提出PostgreSQL架构。部分数据是机器可以拥有的主机名列表,该属性通常是大多数人用来查找主机记录的关键。

我想将这些数据移动到RDBMS的一件事是能够在hostname列上设置唯一性约束,以便无法分配重复的主机名。如果主机只能有一个名称,这将很容易,但由于它们可以有多个名称,因此它更复杂。

我意识到这样做的完全规范化的方法是让一个主机名表的外键指向hosts表,但是我想避免让每个人都需要为最简单的查询做连接:

select hostnames.name,hosts.*
  from hostnames,hosts
 where hostnames.name = 'foobar'
   and hostnames.host_id = hosts.id;

我认为使用PostgreSQL数组可以为此工作,它们肯定使简单的查询变得简单:

select * from hosts where names @> '{foobar}';

但是,当我在hostnames属性上设置唯一性约束时,它当然会将整个名称列表视为唯一值而不是每个名称。有没有办法让每个名称在每一行都是唯一的?

如果没有,有没有人知道另一种更有意义的数据建模方法?

arrays postgresql database-design ldap unique-constraint
2个回答
26
投票

正义的道路

您可能想重新考虑规范化架构。每个人都没有必要“加入即使是最简单的查询”。为此创建一个VIEW

表可能如下所示:

CREATE TABLE hostname (
  hostname_id serial PRIMARY KEY
, host_id     int  REFERENCES host(host_id) ON UPDATE CASCADE ON DELETE CASCADE
, hostname    text UNIQUE
);

代理主键hostname_id是可选的。我更喜欢有一个。在你的情况下,hostname可能是主键。但是,使用简单的小型integer键,许多操作速度更快。创建外键约束以链接到表host。 创建一个这样的视图:

CREATE VIEW v_host AS
SELECT h.*
     , array_agg(hn.hostname) AS hostnames
--   , string_agg(hn.hostname, ', ') AS hostnames  -- text instead of array
FROM   host h
JOIN   hostname hn USING (host_id)
GROUP  BY h.host_id;   -- works in v9.1+

从第9.1页开始,GROUP BY中的主键覆盖了SELECT列表中该表的所有列。 release notes for version 9.1

GROUP BY子句中指定主键时,在查询目标列表中允许非GROUP BY

查询可以像表一样使用视图。以这种方式搜索主机名会快得多:

SELECT *
FROM   host h
JOIN   hostname hn USING (host_id)
WHERE  hn.hostname = 'foobar';

如果你有host(host_id)的索引,应该是这种情况,因为它应该是主键。另外,UNIQUE上的hostname(hostname)约束会自动实现其他所需的索引。

在Postgres 9.2+中,如果你能从中得到一个index-only scan,那么多列索引会更好:

CREATE INDEX hn_multi_idx ON hostname (hostname, host_id);

从Postgres 9.3开始,在情况允许的情况下,您可以使用MATERIALIZED VIEW。特别是如果你读的频率比你写的那么多。

黑暗的一面(你实际问的是什么)

如果我不能说服你正确的道路,我也会在黑暗的一面帮助你。我很灵活。 :)

这是一个如何强制主机名唯一性的演示。我使用表hostname来收集主机名和表host上的触发器以使其保持最新。唯一违规会引发异常并中止操作。

CREATE TABLE host(hostnames text[]);
CREATE TABLE hostname(hostname text PRIMARY KEY);  --  pk enforces uniqueness

触发功能:

CREATE OR REPLACE FUNCTION trg_host_insupdelbef()
  RETURNS trigger AS
$func$
BEGIN
-- split UPDATE into DELETE & INSERT
IF TG_OP = 'UPDATE' THEN
   IF OLD.hostnames IS DISTINCT FROM NEW.hostnames THEN  -- keep going
   ELSE RETURN NEW;  -- exit, nothing to do
   END IF;
END IF;

IF TG_OP IN ('DELETE', 'UPDATE') THEN
   DELETE FROM hostname h
   USING  unnest(OLD.hostnames) d(x)
   WHERE  h.hostname = d.x;

   IF TG_OP = 'DELETE' THEN RETURN OLD;  -- exit, we are done
   END IF;
END IF;

-- control only reaches here for INSERT or UPDATE (with actual changes)
INSERT INTO hostname(hostname)
SELECT h
FROM   unnest(NEW.hostnames) h;

RETURN NEW;
END
$func$ LANGUAGE plpgsql;

触发:

CREATE TRIGGER host_insupdelbef
BEFORE INSERT OR DELETE OR UPDATE OF hostnames ON host
FOR EACH ROW EXECUTE PROCEDURE trg_host_insupdelbef();

SQL Fiddle试运行。

在数组列host.hostnamesarray operators上使用GIN索引来处理它:


5
投票

如果有人仍然需要原始问题中的内容:

CREATE TABLE testtable(
    id serial PRIMARY KEY,
    refs integer[],
    EXCLUDE USING gist( refs WITH && )
);

INSERT INTO testtable( refs ) VALUES( ARRAY[100,200] );
INSERT INTO testtable( refs ) VALUES( ARRAY[200,300] );

这会给你:

ERROR:  conflicting key value violates exclusion constraint "testtable_refs_excl"
DETAIL:  Key (refs)=({200,300}) conflicts with existing key (refs)=({100,200}).

在Windows上查看Postgres 9.5。

请注意,这将使用运算符&&创建索引。因此,当你使用testtable时,由于Postgres的内部索引,检查ARRAY[x] && refsx = ANY( refs )要快一些。

附:一般来说,我同意上面的答案,但这种方法只是一个不错的选择,当你不必真正关心性能和东西时。

© www.soinside.com 2019 - 2024. All rights reserved.