PostgreSQL дозволяє зберігати в колонці масив значень будь-якого типу: text[], int[], uuid[].
CREATE TABLE posts (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title text NOT NULL,
tags text[] NOT NULL DEFAULT '{}'
);
INSERT INTO posts (title, tags) VALUES ('Черги в Laravel', ARRAY['laravel', 'queues']);
SELECT * FROM posts WHERE 'queues' = ANY(tags); -- містить елемент
SELECT * FROM posts WHERE tags @> ARRAY['laravel']; -- містить усі
SELECT * FROM posts WHERE tags && ARRAY['vue', 'react']; -- є хоча б один спільний
SELECT unnest(tags) AS tag, count(*) FROM posts GROUP BY tag; -- розгорнути в рядки
Для швидкого пошуку - GIN-індекс: CREATE INDEX posts_tags_idx ON posts USING gin (tags); (працює з @>, &&, але не з = ANY).
Індексація з одиниці: tags[1] - перший елемент.
Коли масив доречний:
- Невеликий список простих значень, що читається й замінюється разом із рядком: теги, ролі, налаштування-прапорці.
- Значенням не потрібні власні атрибути й посилання на інші таблиці.
Коли краще окрема таблиця:
- Елементи посилаються на інші сутності - масив ID не має зовнішніх ключів, і видалення пов'язаного запису не прибере його з масиву.
- Потрібні атрибути зв'язку (хто й коли додав тег).
- Потрібно часто змінювати окремі елементи чи масив великий.
- Потрібні звичайні
JOINі агрегати - на масивах вони можливі, але громіздкіші.
Пастки: NULL і порожній масив '{}' - різні речі; = ANY(NULL) дає NULL; багатовимірні масиви мусять бути «прямокутними». Laravel не має типу масиву в схемі - колонку додають через $table->addColumn(...) чи сирий SQL, а читають з кастом.