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


Почему массивы?

Массивы в PostgreSQL позволяют хранить несколько значений одного типа в одной колонке. Это особенно удобно для:

  • Тегов и категорий — один товар может относиться к нескольким тегам

  • Хранения списков — например, дополнительные фотографии товара

  • Временных меток — история изменений

  • Любых множественных атрибутов — цвета, размеры, характеристики

Преимущества подхода:

  • Простая структура данных

  • Отсутствие лишних JOIN-операций

  • Специализированные индексы для быстрого поиска

  • Интуитивно понятный синтаксис запросов


Шаг 1. Добавляем колонку-массив

Начнём с добавления новой колонки для тегов в существующую таблицу товаров:

sql
ALTER TABLE products 
ADD COLUMN tags TEXT[] DEFAULT '{}';

Разбор команды:

  • TEXT[] — указывает, что колонка будет массивом текстовых значений

  • DEFAULT '{}' — пустой массив по умолчанию (чтобы не было NULL-значений)

После выполнения этой команды у каждого товара появится возможность хранить неограниченное количество тегов.


Шаг 2. Создаём индекс для быстрого поиска

Обычные индексы (B-tree) неэффективны для поиска внутри массивов. Для работы с массивами PostgreSQL предлагает GIN-индекс (Generalized Inverted Index):

sql
CREATE INDEX idx_products_tags ON products USING GIN (tags);

Почему GIN?

  • Строит обратный индекс: для каждого тега хранит список записей, где он встречается

  • Позволяет мгновенно находить товары по любому тегу или комбинации тегов

  • Оптимизирован для операций с массивами и полнотекстового поиска


Шаг 3. Заполняем товары тегами

Теперь добавим теги к существующим товарам. Обратите внимание на синтаксис — используется конструкция ARRAY[...] со списком значений в квадратных скобках:

sql
-- Для iPhone
UPDATE products 
SET tags = ARRAY['смартфон', 'apple', 'флагман'] 
WHERE product_name LIKE '%iPhone%';

-- Для Samsung
UPDATE products 
SET tags = ARRAY['смартфон', 'samsung', 'флагман', 'стилус'] 
WHERE product_name LIKE '%Samsung%';

-- Для беспроводных наушников
UPDATE products 
SET tags = ARRAY['наушники', 'беспроводные', 'sony'] 
WHERE product_name LIKE '%Sony%' AND product_name LIKE '%наушники%';

UPDATE products 
SET tags = ARRAY['наушники', 'беспроводные', 'apple'] 
WHERE product_name LIKE '%AirPods%';

-- Для MacBook
UPDATE products 
SET tags = ARRAY['ноутбук', 'apple', 'профессиональный'] 
WHERE product_name LIKE '%MacBook%';

Теперь наша таблица готова к экспериментам с поиском.


Шаг 4. Базовые операции поиска по массивам

4.1. Поиск по одному тегу (оператор ANY)

Найдём все товары Apple, используя оператор ANY — он проверяет, содержится ли значение в массиве:

sql
SELECT product_name, tags
FROM products
WHERE 'apple' = ANY(tags);

Результат:

  • iPhone (теги: смартфон, apple, флагман)

  • MacBook (теги: ноутбук, apple, профессиональный)

  • AirPods (теги: наушники, беспроводные, apple)

Как это работает: Для каждой записи проверяется, есть ли в массиве tags значение 'apple'. Благодаря GIN-индексу операция выполняется мгновенно даже на миллионах записей.


4.2. Поиск по нескольким тегам (оператор @>)

Оператор @> проверяет, содержит ли массив все элементы из правого массива:

sql
SELECT product_name, tags
FROM products
WHERE tags @> ARRAY['наушники', 'беспроводные'];

Результат: все беспроводные наушники (Sony и AirPods)

Синтаксис: tags @> ARRAY['значение1', 'значение2'] — массив слева должен содержать все указанные справа элементы.


4.3. Поиск по любому из тегов (оператор &&)

Оператор && проверяет пересечение массивов — есть ли хотя бы один общий элемент:

sql
SELECT product_name, tags
FROM products
WHERE tags && ARRAY['apple', 'samsung', 'sony'];

Результат: все товары указанных брендов (iPhone, Samsung, Sony, AirPods, MacBook)

Этот оператор особенно полезен, когда нужно найти товары, соответствующие любому из нескольких критериев.


Шаг 5. Поиск товаров без тегов

Для контроля качества данных полезно находить записи, у которых ещё не заполнены теги:

sql
SELECT product_name, tags
FROM products
WHERE tags = '{}' OR tags IS NULL;

Результат: товары, требующие добавления тегов (например, периферия или аксессуары без маркировки)

Это отличный способ выявить пробелы в данных и дополнить их.


Шаг 6. Продвинутые операции с массивами

6.1. Статистика использования тегов

Посмотрим, какие теги самые популярные и сколько товаров им соответствуют:

sql
SELECT 
    unnest(tags) AS tag,
    COUNT(*) AS product_count
FROM products
GROUP BY tag
ORDER BY product_count DESC;

Функция unnest «разворачивает» массивы в отдельные строки, после чего мы можем группировать и подсчитывать.

6.2. Добавление нового тега к существующему товару

Чтобы добавить тег без потери существующих, используем конкатенацию массивов:

sql
UPDATE products
SET tags = tags || ARRAY['новинка']
WHERE product_name = 'iPhone 15 Pro';

6.3. Удаление конкретного тега

Для удаления используем оператор -:

sql
UPDATE products
SET tags = tags - ARRAY['флагман']
WHERE product_name LIKE '%Samsung%';

6.4. Поиск по длине массива

Найдём товары с большим количеством тегов (например, подробно описанные):

sql
SELECT product_name, tags
FROM products
WHERE array_length(tags, 1) > 3;

Второй параметр 1 означает размерность массива (для одномерных массивов всегда 1).


Сравнение производительности: с индексом и без

Тип запроса Без GIN-индекса С GIN-индексом
ANY ('apple' = ANY(tags)) Полное сканирование Мгновенно
@> (tags @> ARRAY['x','y']) Полное сканирование Мгновенно
&& (tags && ARRAY['x','y']) Полное сканирование Мгновенно

На таблице с миллионом записей разница может составлять секунды против миллисекунд.


Когда использовать массивы, а когда — связанные таблицы?

Ситуация Массивы Отдельная таблица
Простые теги без доп. атрибутов ✅ ❌
Требуется хранить доп. информацию (дата добавления тега) ❌ ✅
Частые обновления отдельных элементов ⚠️ (перезапись всего массива) ✅
Поиск по множеству условий ✅ ✅ (но с JOIN)
Простота запросов ✅ ⚠️ (сложнее)

Общее правило: если теги — это просто набор меток без дополнительных атрибутов, массивы — отличный выбор. Если нужна детальная информация по каждому тегу (кто добавил, когда, почему) — лучше использовать нормализованную структуру.


Практические рекомендации

  1. Всегда создавайте GIN-индекс — без него поиск по массивам теряет смысл

  2. Используйте пустой массив по умолчанию — это упрощает проверки

  3. Не храните слишком много элементов — массивы оптимальны для десятков, но не тысяч значений

  4. Денормализация оправдана — иногда лучше хранить теги в массиве, чем усложнять структуру JOIN-ами

  5. Тестируйте производительность — на реальных данных проверяйте, что индекс работает


Заключение

Мы рассмотрели, как легко и эффективно работать с массивами в PostgreSQL:

✅ Добавили колонку-массив одной командой ALTER TABLE
✅ Создали специализированный GIN-индекс для быстрого поиска
✅ Научились искать товары по одному тегу (ANY), по всем тегам (@>) и по любому из тегов (&&)
✅ Освоили полезные операции: статистика, добавление, удаление, поиск пустых массивов

Массивы — это мощный инструмент, который позволяет хранить множественные значения без усложнения структуры базы данных. А в сочетании с GIN-индексами поиск остаётся молниеносным даже на огромных объёмах данных.

Попробуйте применить этот подход в своих проектах — и вы оцените, насколько он упрощает работу с тегами, категориями и другими множественными атрибутами.


Хотите увидеть все описанные операции в действии — с живыми примерами, демонстрацией скорости работы индексов и разбором неочевидных ситуаций? В нашем видео-руководстве мы:

  • Пошагово создаём таблицу и добавляем массив тегов

  • Сравниваем производительность с индексом и без

  • Показываем, как писать сложные поисковые запросы

  • Демонстрируем реальные примеры из практики


 

Статья подготовлена по материалам обучающего видео для вашего удобства.