Из таблицы pages мы можем узнать идентификатор темы статьи, но не её название. Чтобы добавить названия тем, требуется подключить вторую таблицу.

Исходные таблицы базы данных

Как создать единую таблицу, в которой каждая страница будет связана с названием своей темы? Для этого нужно отобразить два столбца: title из таблицы pages и name из таблицы sections.

При этом важно сопоставить данные по идентификатору темы, который в одной таблице выступает в качестве внешнего ключа, а в другой — первичного. Например, если у страницы тема указана как 1, то нужно выбрать строку из sections, где _id равен 1. Затем из этой строки извлекается name.

Для объединения данных разных таблиц используется оператор JOIN. Например, наш SQL-запрос будет выглядеть так:

sqlite> SELECT pages.title, sections.name AS theme
...> FROM pages JOIN sections
...> ON pages.theme == sections._id;

Соединение таблиц с помощью JOIN

Эксплицитное использование AS theme можно опустить, тогда названием столбца останется name.

После SELECT указаны нужные столбцы. Уточнение через имя таблицы не нужно, если оно уникально:

sqlite> SELECT title, name
   ...> FROM pages JOIN sections
   ...> ON theme = sections._id;

Только для _id необходимо уточнять имя таблицы, так как они присутствуют в обеих таблицах.

Звездочка после SELECT выведет все столбцы из каждой таблицы.

После FROM идут обе таблицы, разделённые JOIN. Порядок их указания до и после JOIN не критичен.

Ключевое слово ON вводит условие объединения, связывающее строки. Оно указывает, что к записям из pages присоединяются поля из sections, у которых _id равен theme.

Пропуск части ON приведет к полной перекрёстной комбинации строк. Так каждое соединение станет отдельной записью.

JOIN без условия

Замена части ON на WHERE с тем же условием обеспечивает те же результаты.

sqlite> SELECT pages.title, sections.name
   ...> FROM sections JOIN pages
   ...> WHERE pages.theme == sections._id;
title                name
-------------------  -------------------
What is Information  Information
Amount of Informati  Information
Binary System        Digital Sys
Boolean Lows         Boolean Alg

Это по сути фильтрация результата предыдущей команды.

JOIN можно исключить полностью, просто указав таблицы через запятую (применимо и для WHERE, и для ON):

sqlite> SELECT pages.title, sections.name
   ...> FROM pages, sections
   ...> WHERE pages.theme == sections._id;

WHERE и JOIN ON можно комбинировать. Например, вывод страниц для второй и третьей тем:

sqlite> SELECT pages.title, sections.name
   ...> FROM pages JOIN sections
   ...> ON pages.theme == sections._id
   ...> WHERE pages.theme == 2 OR pages.theme == 3;
title          name
-------------  --------------
Binary System  Digital Systems
Boolean Lows   Boolean Algebra

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

sqlite> SELECT sections.name AS theme, count() AS qty_articles
   ...> FROM pages JOIN sections ON pages.theme == sections._id
   ...> GROUP BY sections.name
   ...> ORDER BY sections._id;
theme        qty_articles
-----------  -----------
Information  2
Digital Sys  1
Boolean Alg  1

Запрос создаёт сводную таблицу, группируя данные по name и используя count() для подсчёта записей в каждой группе.

Хотите получить данные только по первой теме? Использование WHERE приведет к ошибке из-за его выполнения до агрегации:

sqlite> SELECT sections.name AS theme, count() AS qty_articles
   ...> FROM pages JOIN sections ON pages.theme == sections._id
   ...> GROUP BY sections.name
   ...> WHERE sections._id == 1;
Error: near "WHERE": syntax error

Вместо WHERE здесь нужно использовать HAVING:

sqlite> SELECT sections.name AS theme, count() AS qty_articles
   ...> FROM pages JOIN sections ON pages.theme == sections._id
   ...> GROUP BY sections.name
   ...> HAVING sections._id == 2 OR sections._id == 3;
theme            qty_articles
---------------  ------------
Boolean Algebra  1
Digital Systems  1 

Оператор JOIN бывает нескольких типов. Обычный JOIN эквивалентен INNER JOIN, который оставляет строки, удовлетворяющие pages.theme = sections._id. Но существуют и левый внешней (LEFT JOIN) и правый внешней JOIN (RIGHT JOIN).

В таблице sections есть четвертая тема, которая не связана со страницей в pages. При подсчете статей, эта тема игнорировалась, так как ни одна из строк не соответствовала условию.

JOIN в варианте пересечения

Если необходимо отобразить темы, на которые нет статей, можно сделать одну из таблиц основной. Объединённая таблица будет содержать все соответствия, плюс все строки "главной" таблицы, которые не имели соответствий. Строки основной таблицы всегда будут присутствовать в результате.

SQLite использует только LEFT JOIN, "главную" таблицу следует указывать первой. Чтобы подчеркнуть важность всех тем из sections, она становится первой.

sqlite> SELECT sections.name, count(pages.title)
   ...> FROM sections LEFT JOIN pages
   ...> ON pages.theme == sections._id
   ...> GROUP BY sections.name;
name        count(pages.title)
----------  ------------------
Algorithm   0
Boolean Al  1
Digital Sy  1
Informatio  2  

Также подсчитывается количество не NULL значений в столбце, переданном в count(). Без аргумента результат обманчив: напротив Algorithm было бы 1 из-за одной строки где тема — Algorithm.

LEFT JOIN

Используемая функция count() без аргумента считает количество строк в каждой группе, но с аргументом — только не NULL значения в столбце внутри группы.

Помимо JOIN, в SQL есть оператор UNION, который производит вертикальное объединение таблиц — соединяя строки одной таблицы с другой, чего нет в JOIN, выполняющем горизонтальное объединение — добавление столбцов. UNION применяется для схожих таблиц.

Вопросы для самопроверки:

  1. Какое условие соединения используется в SQL для сопряжения таблиц по ключам?
  2. Чем отличаются операторы INNER JOIN и LEFT JOIN? Когда их стоит применять?
  3. Какая ошибка может возникнуть при использовании WHERE после GROUP BY?
  4. Какое преимущество у использования HAVING перед WHERE при группировке данных?
  5. В каких случаях оператор UNION применим в SQL?