Может ли PostgreSQL ограничить уникальность элементов массива?

Я пытаюсь придумать схему PostgreSQL для данных хоста, которые в настоящее время хранятся в хранилище LDAP. Часть этих данных - это список имен хостов, которые может иметь машина, и этот атрибут обычно является ключом, который большинство людей использует для поиска записей хоста.

Одна вещь, которую я хотел бы отключить от перемещения этих данных в RDBMS, - это возможность установить ограничение уникальности в столбце имени хоста, чтобы нельзя было назначить дублирование имен хостов. Это было бы легко, если бы хосты могли иметь только одно имя, но поскольку они могут иметь более одного, это более сложно.

Я понимаю, что полностью нормализованный способ сделать это состоял бы в том, чтобы таблица хостов с внешним ключом указывала обратно на таблицу хостов, но я бы хотел, чтобы все не нуждались в объединении даже для самого простого запроса

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, он, конечно, рассматривает весь список имен как уникальное значение вместо каждого имени. Есть ли способ сделать каждое имя уникальным для каждой строки?

Если нет, знает ли кто-нибудь другой подход к моделированию данных, который имел бы смысл?

Ответы

Ответ 1

Праведный путь

Возможно, вы захотите пересмотреть нормализовать вашу схему. Каждому не нужно "присоединяться даже к самому простому запросу". Создайте 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. примечания к выпуску для версии 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 + многоколоночный индекс будет еще лучше, если вы сможете получить просмотр только для индексирования:

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;

Trigger:

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

SQL Fiddle с тестовым прогоном.

Используйте индекс GIN в столбце массива host.hostnames и операторы массива для работы с ним:

Ответ 2

Если кому-то все еще нужно то, что было в исходном вопросе:

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}).

Проверено в Postgres 9.5 на Windows.

Обратите внимание, что это создаст индекс с помощью оператора &&. Поэтому, когда вы работаете с testtable, было бы быстрее проверить ARRAY[x] && refs, чем x = ANY( refs ) из-за индексирования внутренних объектов Postgres.

P.S. В общем, я согласен с вышеприведенным ответом, но этот подход - просто приятный вариант, когда вам не нужно заботиться о производительности и т.д. действительно.