Раздел 33 · PostgreSQL с нуля

Массивы в PostgreSQL

junior~30 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

В прошлом уроке мы искали теги внутри записки JSONB. Но если от записки нужен только список тегов, JSONB избыточен. В PostgreSQL у любого типа есть брат-массив: text[], integer[], date[]. Разберём, как с ним работать, на учебной базе магазина, и когда массив лучше связующей таблицы, а когда хуже.

Бусы на нитке

Ячейка таблицы обычно держит одно значение, как коробочка одну бусину. Массив это нитка с бусами: одна ячейка, а на ней несколько значений, и у каждого своё место по порядку.

Массив в PostgreSQL хранит упорядоченный список значений одного типа. Тип записывается как тип элемента с квадратными скобками. Заведём книгам колонку тегов:

ALTER TABLE books ADD COLUMN IF NOT EXISTS tags text[];

UPDATE books SET tags = '{классика,мистика}' WHERE id = 1;
UPDATE books SET tags = '{}' WHERE id = 2;
UPDATE books SET tags = ARRAY['фантастика', 'классика'] WHERE id = 3;
UPDATE books SET tags = ARRAY['фантастика'] WHERE id = 4;
UPDATE books SET tags = ARRAY['алгоритмы', 'python'] WHERE id = 7;
UPDATE books SET tags = ARRAY['программирование', 'классика'] WHERE id = 8;

SELECT id, title, tags FROM books WHERE tags IS NOT NULL ORDER BY id;
 id |       title        |            tags
----+--------------------+-----------------------------
  1 | Мастер и Маргарита | {классика,мистика}
  2 | Собачье сердце     | {}
  3 | Пикник на обочине  | {фантастика,классика}
  4 | Трудно быть богом  | {фантастика}
  7 | Грокаем алгоритмы  | {алгоритмы,python}
  8 | Совершенный код    | {программирование,классика}
(6 rows)

Массив можно записать двумя способами:

  • Строкой в фигурных скобках: '{классика,мистика}'. Так массивы печатает psql, и так они приходят в виде текста.
  • Конструктором ARRAY['фантастика', 'классика']. Внутри обычные значения SQL, с кавычками и выражениями.

У «Собачьего сердца» пустой массив {}: тегов нет, и это известно. У «Шинели» NULL: про теги ничего не сказано. Разница та же, что между пустой строкой и NULL.

В строковой записи свои правила: пробелы по краям элемента PostgreSQL отбрасывает, а элемент с пробелом внутри или с запятой нужно брать в двойные кавычки. psql так и печатает:

SELECT '{"научная фантастика", классика}'::text[] AS tags;
              tags
---------------------------------
 {"научная фантастика",классика}
(1 row)

Пробел перед «классикой» пропал. Конструктор ARRAY[...] избавляет от этих правил, поэтому в коде надёжнее он.

Номера с единицы

Элементы массива нумеруются с единицы, а не с нуля, как в JavaScript и Python. Номер за пределами массива не ошибка, а NULL:

SELECT title, tags[1] AS first, tags[2] AS second, tags[5] AS fifth
FROM books
WHERE id IN (1, 4)
ORDER BY id;
       title        |   first    | second  | fifth
--------------------+------------+---------+-------
 Мастер и Маргарита | классика   | мистика |
 Трудно быть богом  | фантастика |         |
(2 rows)

tags[0] тоже вернёт NULL, молча. Если привык считать с нуля, ошибку не увидишь, а первый элемент потеряешь.

Длину массива спрашивают двумя функциями, и они по-разному видят пустой массив:

SELECT title, cardinality(tags) AS n, array_length(tags, 1) AS len
FROM books
WHERE id IN (1, 2, 5)
ORDER BY id;
       title        | n | len
--------------------+---+-----
 Мастер и Маргарита | 2 |   2
 Собачье сердце     | 0 |
 Шинель             |   |
(3 rows)

cardinality честно говорит 0 для пустого массива. array_length возвращает NULL, потому что у пустого массива нет даже первого измерения. Бери cardinality.

Поиск по массиву

Четыре вопроса, которые задают массиву чаще всего:

ВопросКак спросить
есть ли элемент'классика' = ANY (tags)
есть ли все эти элементыtags @> ARRAY['классика']
все ли элементы из спискаtags <@ ARRAY['фантастика', 'классика']
есть ли хоть один общийtags && ARRAY['мистика', 'python']

ANY читается как «равно хоть одному элементу». Книги с тегом «классика»:

SELECT title FROM books WHERE 'классика' = ANY (tags) ORDER BY id;
       title
--------------------
 Мастер и Маргарита
 Пикник на обочине
 Совершенный код
(3 rows)

Операторы @>, <@ и && сравнивают массив с массивом. Порядок элементов и повторы им безразличны:

SELECT title,
       tags @> ARRAY['классика']               AS has_classic,
       tags <@ ARRAY['фантастика', 'классика'] AS only_these,
       tags && ARRAY['мистика', 'python']      AS any_of
FROM books
WHERE tags IS NOT NULL
ORDER BY id;
       title        | has_classic | only_these | any_of
--------------------+-------------+------------+--------
 Мастер и Маргарита | t           | f          | t
 Собачье сердце     | f           | t          | f
 Пикник на обочине  | t           | t          | f
 Трудно быть богом  | f           | t          | f
 Грокаем алгоритмы  | f           | f          | t
 Совершенный код    | t           | f          | f
(6 rows)

Пустой массив «Собачьего сердца» вложен в любой массив, поэтому <@ для него истинен.

Одна ловушка с отрицанием. Сколько книг без тега «классика»?

SELECT count(*) FROM books WHERE NOT ('классика' = ANY (tags));
 count
-------
     3
(1 row)

Три, а не шесть. Книги с NULL вместо тегов пропали: ANY по NULL даёт NULL, NOT NULL снова NULL, и WHERE такие строки отбрасывает. Это трёхзначная логика в новом месте. Если книги без тегов тоже нужны, допиши OR tags IS NULL.

Список id из приложения

ANY пригождается и без колонки-массива. Бэкенду нужно достать книги по списку id из корзины. Вместо того чтобы склеивать строку IN (1, 3, 7), он передаёт массив одним параметром:

SELECT title, price FROM books WHERE id = ANY ('{1,3,7}'::integer[]) ORDER BY id;
       title        |  price
--------------------+---------
 Мастер и Маргарита |  690.00
 Пикник на обочине  |  450.00
 Грокаем алгоритмы  | 1290.00
(3 rows)

В коде это выглядит как WHERE id = ANY($1), а в $1 драйвер кладёт массив. Запрос один и тот же для списка любой длины.

урок из курса

Урок из курса «PostgreSQL с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.