Как получить список авторов с их книгами и тегами в одном SQL-запросе и какие есть варианты
Что проверяют
Умеете ли вы собрать связи «один ко многим» и «многие ко многим» в одном запросе и понимаете ли, во что это выливается в Go-коде. Следом спрашивают, как избежать N+1 и что выгоднее: один большой запрос или несколько маленьких.
Как отвечать
Схема: authors, books.author_id, связующая book_tags(book_id, tag_id) и tags. Вариантов три:
- плоский JOIN:
authors LEFT JOIN books LEFT JOIN book_tags LEFT JOIN tags. Одна строка на пару книга и тег, данные автора повторяются. В Go вы сканируете строки и собираете дерево черезmap[int64]*Author, порядок держите отдельным слайсом, аNULLу автора без книг читаете вsql.NullInt64или указатель; - агрегация в базе: в PostgreSQL
array_agg(t.name)илиjson_aggсjson_build_object, в MySQLGROUP_CONCATилиJSON_ARRAYAGG. База отдаёт по строке на автора,pgxсканирует массив текста в[]string, а JSON в[]byteдляjson.Unmarshal. Для двух уровней вложенности удобенLEFT JOIN LATERALс подзапросом по книгам; - несколько запросов: авторы, затем книги
WHERE author_id = ANY($1), затем теги по списку книг. Три запроса вместо одного, зато без дублей и с простым сканированием. Это не N+1, пока вы не делаете запрос на каждого автора в цикле.
Выбор зависит от объёма: на странице из двадцати авторов агрегация в JSON удобна, на выгрузке в сотни тысяч строк лучше плоский результат и потоковое чтение через rows.Next().
Где спотыкаются
Пишут внутренний JOIN books и теряют авторов без книг. Если нужен полный список, соединение левое.
Не знают сюрпризов агрегатов: json_agg по LEFT JOIN без совпадений вернёт [null], это лечится FILTER (WHERE t.id IS NOT NULL) и COALESCE. GROUP_CONCAT в MySQL по умолчанию обрезает результат до 1024 байт.
Забывают defer rows.Close() и проверку rows.Err() после цикла. При раннем выходе соединение не возвращается в пул, а ошибка посередине выборки теряется.