Может ли PostgreSQL иметь ограничение уникальности для элементов массива? - PullRequest
17 голосов
/ 05 ноября 2011

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

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

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

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 ]

26 голосов
/ 05 ноября 2011

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

Возможно, вы захотите пересмотреть , нормализуя вашу схему. Не обязательно для всех «присоединяться даже к самому простому запросу» . Для этого создайте 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 и операторы массива для работы с ним:

5 голосов
/ 24 января 2017

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

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.

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

...