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

时间:2011-11-04 23:11:20

标签: arrays postgresql database-design ldap unique-constraint

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

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

我意识到执行此操作的完全规范化方法是将主机名表与外键指向主机表,但我想避免让每个人都需要为最简单的查询进行连接:

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属性上设置唯一性约束时,它当然会将整个名称列表视为唯一值而不是每个名称。有没有办法让每个名称在每一行都是唯一的?

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

2 个答案:

答案 0 :(得分: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+

从pg 9.1 开始,GROUP BY 中的主键涵盖SELECT列表中该表的所有列。 release notes for version 9.1

  

主要时,在查询目标列表中允许非GROUP BY列   key在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索引来处理它:

答案 1 :(得分: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 )更快。

P.S。一般来说,我同意上述答案,但如果您不必非常关注性能和内容,这种方法只是一个不错的选择。