我正在尝试为当前在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属性上设置唯一性约束时,它当然会将整个名称列表视为唯一值而不是每个名称。有没有办法让每个名称在每一行都是唯一的?
如果没有,有没有人知道另一种更有意义的数据建模方法?
您可能想重新考虑规范化架构。每个人都没有必要“加入即使是最简单的查询”。为此创建一个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.hostnames
和array operators上使用GIN索引来处理它:
如果有人仍然需要原始问题中的内容:
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] && refs
比x = ANY( refs )
要快一些。
附:一般来说,我同意上面的答案,但这种方法只是一个不错的选择,当你不必真正关心性能和东西时。