Может ли PostgreSQL иметь ограничение уникальности для элементов массива?
Я пытаюсь придумать схему 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 ответа
Праведный путь
Возможно, вы захотите пересмотреть нормализацию вашей схемы. Не обязательно для всех "присоединяться даже к самому простому запросу". Создать 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
список. Примечания к выпуску для версии 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)
реализует другой необходимый индекс автоматически.
В Postgres9.2+ многоколонный индекс был бы еще лучше, если бы из него можно было получить сканирование только по индексу:
CREATE INDEX hn_multi_idx ON hostname (hostname, host_id)
Начиная с Postgres9.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 с тестовым прогоном.
Используйте индекс GIN для столбца массива host.hostnames
и операторы массива для работы с ним:
Если кому-то все еще нужно то, что было в первоначальном вопросе:
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 В целом я согласен с приведенным выше ответом, но такой подход является хорошим вариантом, когда вам не нужно беспокоиться о производительности и прочем.