Bladeren bron

продвинутый SQL

Евгений Колесников 5 maanden geleden
bovenliggende
commit
c757f8bf7b
8 gewijzigde bestanden met toevoegingen van 551 en 1 verwijderingen
  1. 550 0
      articles/sql_advanced.md
  2. BIN
      img/groupping_by_1_field.webp
  3. BIN
      img/groupping_by_2_field.webp
  4. BIN
      img/having01.png
  5. BIN
      img/join01.png
  6. BIN
      img/join02.png
  7. BIN
      img/sql_query_order_ru.webp
  8. 1 1
      readme.md

+ 550 - 0
articles/sql_advanced.md

@@ -0,0 +1,550 @@
+Предыдущая лекция |   | Следующая лекция
+:----------------:|:----------:|:----------------:
+[Основы SQL](./sql_for_beginner.md) | [Содержание](../readme.md#проектирование-баз-данных) | [Создание ER-диаграммы](./5_1_1_1_erd_workbench.md)
+
+#  Группировка, агрегатные функции, JOIN-ы
+
+## Группировка, оператор GROUP BY
+
+>Взято [отсюда](https://sql-academy.org/ru/guide/groupping)
+
+Давайте выполним запрос:
+
+```sql
+SELECT id, home_type, has_tv, price 
+FROM Rooms;
+```
+
+id  |	home_type   |	has_tv  |	price
+:--:|--------------:|:---------:|:-------:
+1   |	Private room    |	1   |	149
+2   |	Entire home/apt |	0   |	225
+3   |   Private room    |	1   |	150
+4   |	Entire home/apt |   1   |	89
+5   |	Entire home/apt |	0   |	80
+
+Так мы получили информацию по каждому сдаваемому жилому помещению. А что если мы хотим получить информацию не о каждой записи отдельно, а о группах, которые они образуют?
+
+Например, такими группами могут выступать записи, разбитые по типу жилья:
+
+* Shared room (аренда комнаты на несколько человек)
+* Private room (аренда целой комнаты)
+* Entire home/apt (аренда целой квартиры)
+
+Эти группы включают разные записи в таблице и, соответственно, обладают разными характеристиками, которые нам могут быть весьма полезны.
+
+Такой полезной информацией о группах может быть:
+
+* средняя стоимость аренды комнаты или целого жилого помещения
+* количество сдаваемых жилых помещений каждого типа
+
+Для ответов на все эти и многие другие вопросы есть оператор `GROUP BY`.
+
+### Общая структура запроса с GROUP BY
+
+```sql
+SELECT [литералы, агрегатные_функции, поля_группировки]
+FROM имя_таблицы
+GROUP BY поля_группировки;
+```
+
+Для того, чтобы записи у нас образовали группы по типу жилья, мы должны после `GROUP BY` указать `home_type`, т.е. поле, по которому будет происходить группировка.
+
+```sql
+SELECT home_type FROM Rooms
+GROUP BY home_type
+```
+
+<table>
+<th>home_type</th>
+<tr><td>Private room</td></tr>
+<tr><td>Entire home/apt</td></tr>
+<tr><td>Shared room</td></tr>
+</table>
+
+>Следует иметь в виду, что для `GROUP BY` все значения `NULL` трактуются как равные, т.е. при группировке по полю, содержащему NULL-значения, все такие строки попадут в одну группу
+
+При использовании оператора `GROUP BY` мы перешли от работы с отдельными записями на работу с образовавшимися группами. В связи с этим мы не можем просто вывести любое поле из записи (например, `has_tv` или `price`), как мы это могли делать раньше. Так как в каждой группе может быть несколько записей и в каждой из них в этом поле может быть разное значение.
+
+При использовании `GROUP BY` мы можем выводить только:
+
+* литералы, т.е. указанное явным образом фиксированные значения.
+
+    Мы можем их выводить, так как это фиксированные значения, которые ни от чего не зависят.
+
+    Например,
+
+    ```sql
+    SELECT home_type, 'literal' 
+    FROM Rooms
+    GROUP BY home_type
+    ```
+
+    home_type   |	literal
+    ------------|-----------
+    Private room    |	literal
+    Entire home/apt |	literal
+    Shared room |	literal
+
+* результаты агрегатных функций, т.е. вычисленные значения на основании набора значений.
+
+    Более детальную информацию об агрегатных функциях мы затронем ниже. Но для примера рассмотрим агрегатную функцию `AVG`.
+
+    Функция `AVG` принимает в качестве аргумента название поля, по которому мы хотим вычислить среднее значение для каждой группы.
+
+    ```sql
+    SELECT 
+        home_type, 
+        AVG(price) as avg_price 
+    FROM Rooms
+    GROUP BY home_type
+    ```
+
+    home_type   |	avg_price
+    ------------|-------------
+    Private room    |	89.4286
+    Entire home/apt |	148.6667
+    Shared room     |	40
+
+    Так выполненный запрос сначала разбивает все записи из таблицы `Rooms` на три группы, опираясь на поле `home_type`. Далее, для каждой группы суммирует все значения, взятые из поля `price` у каждой записи, входящей в текущую группу, и затем полученный результат делится на количество записей в данной группе.
+
+* поля группировки.
+
+    Мы можем их выводить, так как в рамках одной группы поля, по которым осуществлялась группировка, одинаковые.
+
+### Группировка по 2 и более полям
+
+Мы уже рассмотрели как записи в таблице группируются по одному полю. Для дополнительной иллюстрации это выглядит примерно так, когда поле группировки `home_type`:
+
+![](../img/groupping_by_1_field.webp)
+
+При группировке по двум и более полям принцип остается такой же, только теперь образовавшиеся группы дополнительно разбиваются на более мелкие группы в зависимости от второго поля группировки.
+
+Пример группировки по `home_type` и `has_tv`:
+
+![](../img/groupping_by_2_field.webp)
+
+## Агрегатные функции
+
+Выше мы обсудили, что при использовании оператора `GROUP BY` мы можем использовать агрегатные функции. Давайте поговорим о них подробнее.
+
+>Агрегатная функция – это функция, которая выполняет вычисление на наборе значений и возвращает одиночное значение.
+
+Например, запрос с использованием агрегатной функции `AVG` может выглядеть так:
+
+```sql
+SELECT 
+    home_type, 
+    AVG(price) as avg_price 
+FROM Rooms
+GROUP BY home_type
+```
+
+home_type   |	avg_price
+------------|:------------:
+Private room    |	89.4286
+Entire home/apt |	148.6667
+Shared room     |	40
+
+### Описание агрегатных функций
+
+* `SUM(поле_таблицы)` - Возвращает сумму значений
+* `AVG(поле_таблицы)` - Возвращает среднее значение
+* `COUNT(поле_таблицы)` - Возвращает количество записей
+* `MIN(поле_таблицы)` - Возвращает минимальное значение
+* `MAX(поле_таблицы)` - Возвращает максимальное значение
+
+>Агрегатные функции применяются для значений, не равных `NULL`. Исключением является функция `COUNT(*)`.
+
+### Примеры
+
+<!-- TODO вставить большую картинку -->
+
+* Найдём количество каждого вида жилья и отсортируем полученный список по убыванию:
+
+    ```sql
+    SELECT 
+        home_type, 
+        COUNT(*) as amount 
+    FROM Rooms
+    GROUP BY home_type
+    ORDER BY amount DESC
+    ```
+
+    home_type   |	amount
+    ------------|:---:
+    Private room    |	28
+    Entire home/apt |	21
+    Shared room     |	1
+
+* Для каждого жилого помещения найдём самую позднюю дату выезда (поле `end_date`)
+
+    ```sql
+    SELECT 
+        room_id, 
+        MAX(end_date) AS last_end_date 
+    FROM Reservations
+    GROUP BY room_id
+    ```
+
+    room_id |	last_end_date
+    --------|---
+    1   |	2019-02-04T12:00:00.000Z
+    2   |	2020-03-23T09:00:00.000Z
+    13  |	2020-04-21T10:00:00.000Z
+    16  |	2019-06-24T10:00:00.000Z
+
+
+## Оператор HAVING
+
+Мы уже рассматривали запрос получения средней стоимости аренды жилых помещений в зависимости от типа жилья:
+
+![](../img/having01.png)
+
+```sql
+SELECT 
+    home_type, 
+    AVG(price) as avg_price 
+FROM Rooms
+GROUP BY home_type
+```
+
+home_type   |	avg_price
+------------|:------------:
+Private room    |	89.4286
+Entire home/apt |	148.6667
+Shared room     |	40
+
+Давайте доработаем этот запрос таким образом, чтобы в итоговой выборке отображались только те группы, чья средняя стоимость больше `50`.
+
+Обладая предыдущим опытом, есть большой соблазн попытаться использовать для этих целей оператор `WHERE`. Но при попытке выполнить такой запрос СУБД неминуемо выдаст ошибку, указав, что мы некорректно используем синтаксис `WHERE avg_price > 50`.
+
+```sql
+SELECT 
+    home_type, 
+    AVG(price) as avg_price 
+FROM Rooms
+GROUP BY home_type
+WHERE avg_price > 50
+```
+
+Говоря наперёд, для фильтрации групп мы должны использовать оператор `HAVING` (который применяется к готовой выборке):
+
+```sql
+SELECT 
+    home_type, 
+    AVG(price) as avg_price 
+FROM Rooms
+GROUP BY home_type
+HAVING avg_price > 50
+```
+
+home_type   |	avg_price
+------------|:---:
+Private room    |	89.4286
+Entire home/apt |	148.6667
+
+### Порядок выполнения SQL запроса
+
+Но почему же мы не могли использовать WHERE, и зачем нужен отдельный оператор для этой цели? Все дело в порядке выполнения SQL запроса.
+
+![](../img/sql_query_order_ru.webp)
+
+Наш первый запрос был неверный, потому что мы пытались использовать поле `avg_price` у образовавшихся групп ещё до их образования, так как выполнение оператора `WHERE` предшествует группировке.
+
+То есть оператор `WHERE` в момент его исполнения ничего не знает о последующей группировке, он работает только с записями из таблицы. Так мы, например, с его помощью можем отфильтровать записи таблицы `Rooms` по цене до применения группировки и лишь затем вычислить среднюю стоимость групп оставшегося жилья:
+
+```sql
+SELECT 
+    home_type, 
+    AVG(price) as avg_price 
+FROM Rooms
+WHERE price > 50
+GROUP BY home_type
+```
+
+home_type   |	avg_price
+------------|:---:
+Private room    |	96.875
+Entire home/apt |	148.6667
+
+>Но если присмотреться к результату, то видно, что средняя цена получилась завышенной. И это очевидно, так как мы просто выкинули низкие цены.
+
+### Пример использования HAVING
+
+Для примера давайте получим минимальную стоимость каждого типа жилья c телевизором. При этом нас интересуют только типы жилья, содержащие как минимум пять жилых помещений, относящихся к ним.
+
+Чтобы получить такой результат, мы должны:
+
+* Сначала получить все данные из таблицы
+
+    ```sql
+    SELECT ... FROM Rooms;
+    ```
+
+* Затем выбрать из всех записей таблицы `Room` только интересующие нас, т.е. только жильё с телевизором
+
+    ```sql
+    SELECT ... FROM Rooms
+    WHERE has_tv = True
+    ```
+
+* Затем сгруппировать данные записи о жилых помещениях по их типу
+
+    ```sql
+    SELECT ... FROM Rooms
+    WHERE has_tv = True
+    GROUP BY home_type
+    ```
+
+* После этого отфильтровать полученные группы по условию. Нас интересуют группы, имеющие как минимум 5 представителей
+
+    ```sql
+    SELECT ... FROM Rooms
+    WHERE has_tv = True
+    GROUP BY home_type
+    HAVING COUNT(*) >= 5
+    ```
+
+* И под конец посмотреть, что нас просят в задании и, соответственно, добавить вывод необходимой информации. В нашем случае, нам необходимо вывести название типа жилья и его минимальную стоимость.
+
+    ```sql
+    SELECT 
+        home_type, 
+        MIN(price) as min_price 
+    FROM Rooms
+    WHERE has_tv = True
+    GROUP BY home_type
+    HAVING COUNT(*) >= 5;
+    ```
+
+## Многотабличные запросы, JOIN
+
+### Многотабличные запросы
+
+В предыдущих статьях описывалась работа только с одной таблицей базы данных. В реальности же очень часто приходится делать выборку из нескольких таблиц, каким-то образом объединяя их. В данной статье вы узнаете основные способы соединения таблиц.
+
+Например, если мы хотим получить информацию о тратах на покупки, мы можем её получить следующим образом:
+
+```sql
+SELECT 
+    family_member, 
+    amount * unit_price AS price 
+FROM Payments
+```
+
+family_member   |	price
+---|:---:
+1   |	2000
+2   |	2100
+3   |	100
+4   |	350
+
+В поле `family_member` полученной выборки отображаются идентификаторы записей из таблицы `Payments`, но для нас они мало что значат.
+
+Вместо этих идентификаторов было бы гораздо нагляднее выводить имена тех, кто покупал (поле `member_name` из таблицы `FamilyMember`). Ровно для этого и существует объединение таблиц и оператор `JOIN`.
+
+![](../img/join01.png)
+
+### Общая структура многотабличного запроса
+
+```sql
+SELECT поля_таблиц
+FROM таблица_1
+[INNER] | [[LEFT | RIGHT | FULL][OUTER]] JOIN таблица_2
+    ON условие_соединения
+[[INNER] | [[LEFT | RIGHT | FULL][OUTER]] JOIN таблица_n
+    ON условие_соединения]
+```
+
+Как можно увидеть по структуре, соединение бывает:
+
+* внутренним `INNER` (по умолчанию)
+* внешним `OUTER`, при этом внешнее соединение делится на левое `LEFT`, правое `RIGHT` и полное `FULL`
+
+С более подробными деталями, чем отличается внутреннее соединение от внешнего и как они работают, мы познакомимся в следующих статьях.
+
+Пока нам достаточно лишь знать, что для вышеописанного примера с запросом на покупки нам понадобится именно запрос с внутренним соединением, который будет выглядеть следующим образом:
+
+```sql
+SELECT 
+    family_member, 
+    member_name, 
+    amount * unit_price AS price 
+FROM Payments
+JOIN FamilyMembers
+    ON Payments.family_member = FamilyMembers.member_id
+```
+
+family_member   |	member_name |	price
+:---:|---|:---:
+1   |	Headley Quincey |	2000
+2   |	Flavia Quincey  |	2100
+3   |	Andie Quincey   |	100
+4   |	Lela Quincey    |	350
+
+В данном запросе мы сопоставляем записи из таблицы `Payments` и записи из таблицы `FamilyMembers`.
+
+Чтобы сопоставление работало, мы указываем как именно записи из двух разных таблиц должны находить друг друга. Это условие указывается после `ON`:
+
+```sql
+ON Payments.family_member = FamilyMembers.member_id
+```
+
+В нашем случае поле `family_member` указывает на идентификатор в таблице `FamilyMembers` и таким образом помогает однозначному сопоставлению.
+
+>В большинстве случаев условием соединения является равенство столбцов таблиц (таблица_1.поле = таблица_2.поле), однако точно так же можно использовать и другие операторы сравнения.
+
+### Вывод всех столбцов из таблицы в многотабличном запросе
+
+Ранее, когда все запросы выполнялись на одной таблице, было достаточно указать символ `*`, чтобы вывести все поля из этой таблицы. Теперь же, когда таблиц может быть несколько, `*` будет означать "вывести все столбцы из таблиц, перечисленных в выражении `FROM`".
+
+В некоторых случаях нам может понадобиться вывести столбцы, принадлежащие только какой-то конкретной таблице. Например, дано соединение таблиц `Payments` и `FamilyMembers`, и в итоговую выборку необходимо вывести только поля из таблицы `FamilyMembers`. Как это сделать? Всё очень просто! Необходимо перед символом `*` добавить название таблицы:
+
+```sql
+SELECT FamilyMembers.* 
+FROM Payments
+JOIN FamilyMembers
+    ON Payments.family_member = FamilyMembers.member_id
+```
+
+Таким же образом можно вывести все столбцы из нескольких таблиц:
+
+```sql
+SELECT Payments.*, FamilyMembers.* 
+FROM Payments
+JOIN FamilyMembers
+    ON Payments.family_member = FamilyMembers.member_id
+```
+
+>В данном случае вместо `Payments.*`, `FamilyMembers.*` можно использовать `*`, т.к. в `FROM` перечислены только эти две таблицы. Вывод будет одинаков в обоих случаях.
+
+А что, если нужно вывести **несколько столбцов из одной таблицы и все из другой**? Это тоже возможно! Выведем поля `payment_id` и `family_member` из `Payments`, а также все поля из `FamilyMembers`:
+
+```sql
+SELECT 
+    payment_id, 
+    family_member, 
+    FamilyMembers.* 
+FROM Payments
+JOIN FamilyMembers
+    ON Payments.family_member = FamilyMembers.member_id
+```
+
+### Псевдонимы для таблиц
+
+При работе с большими многотабличными запросами рекомендуется использовать псевдонимы (алиасы) для таблиц. Это не только улучшает читаемость кода, но и помогает избежать ошибок в сложных запросах.
+
+Псевдонимы задаются после имени таблицы с помощью ключевого слова `AS`:
+
+```sql
+SELECT id, name
+FROM Passenger AS pass
+```
+
+Теперь обращаться к столбцам таблицы можно через псевдоним:
+
+```sql
+SELECT pass.id, pass.name
+FROM Passenger AS pass
+```
+
+>Ключевое слово AS является опциональным, как и у псевдонимов столбцов.
+
+Рассмотрим пример запроса с несколькими таблицами. Допустим, нам нужно вывести идентификаторы и имена пассажиров, которые хотя бы раз совершали перелёт:
+
+```sql
+SELECT
+    pass.id,
+    pass.name
+FROM Passenger AS pass
+JOIN Pass_in_trip AS pit
+    ON pit.passenger = pass.id
+```
+
+В этом примере таблицам `Passenger` и `Pass_in_trip` присваиваются псевдонимы `pass` и `pit`, соответственно, а дальше используются для вывода столбцов из этих таблиц. Обратите внимание, что в обеих таблицах есть поле с одинаковым названием – `id`. Если вы не укажете из какой таблицы его нужно брать, то СУБД сделает это за вас и отдаст значение из последней таблицы в цепочке `JOIN`. В данном случае это `Pass_in_trip`.
+
+При использовании псевдонимов всегда руководствуйтесь следующими простыми правилами, и тогда ваши запросы будут лаконичными и понятными:
+
+* Используйте логичные сокращения (например, первые буквы названия таблицы)
+* Избегайте слишком коротких (однобуквенных) или неочевидных псевдонимов
+
+## Внешнее соединение OUTER JOIN
+
+Внешнее соединение может быть трёх типов: левое (`LEFT`), правое (`RIGHT`) и полное (`FULL`) (MySQL не поддерживает полное соединение).
+
+Главным отличием внешнего соединения от внутреннего является то, что оно обязательно возвращает все строки одной (`LEFT`, `RIGHT`) или двух таблиц (`FULL`).
+
+### Внешнее левое соединение (LEFT OUTER JOIN)
+
+Соединение, которое возвращает все значения из левой таблицы, соединённые с соответствующими значениями из правой таблицы, если они удовлетворяют условию соединения, или заменяет их на `NULL` в обратном случае.
+
+Для примера получим из базы данных расписание звонков, объединённых с соответствующими занятиями в расписании занятий.
+
+![](../img/join02.png)
+
+Данные в таблице Timepair (расписание звонков):
+
+id  |	start_pair  |	end_pair
+:---:|----|---
+1   |	08:30:00    |	09:15:00
+2   |	09:20:00    |	10:05:00
+3   |	10:15:00    |	11:00:00
+
+Данные в таблице Schedule (расписание занятий):
+
+id  |	date    |	class   |	number_pair |	teacher |	subject |	classroom
+:---:|---|:---:|:-:|:-:|:-:|:-:
+1   |	2019-09-01T00:00:00.000Z    |	9   |	1   |	11  |	1   |	47
+2   |	2019-09-01T00:00:00.000Z    |	9   |	2   |	8   |	2   |	13
+3   |	2019-09-01T00:00:00.000Z    |	9   |	3   |	4   |	3   |	13
+4   |	2019-09-02T00:00:00.000Z    |	9   |	1   |	4   |	3   |	13
+
+```sql
+SELECT 
+    Timepair.id 'timepair.id', 
+    start_pair, 
+    end_pair,
+    Schedule.id 'schedule.id', 
+    date, 
+    class, 
+    number_pair, 
+    teacher, 
+    subject, 
+    classroom
+FROM Timepair
+LEFT JOIN Schedule 
+    ON Schedule.number_pair = Timepair.id;
+```
+
+timepair.id | start_pair | end_pair | schedule.id | date | class | number_pair | teacher | subject | classroom
+:-:|--|--|:-:|--|:-:|:-:|:-:|:-:|:-:
+1 | 08:30:00 | 09:15:00 | 35 | 2019-09-04T00:00:00.000Z | 8 | 1 | 1 | 11 | 4
+1 | 08:30:00 | 09:15:00 | 32 | 2019-09-03T00:00:00.000Z | 8 | 1 | 10 | 10 | 40
+1 | 08:30:00 | 09:15:00 | 21 | 2019-08-30T00:00:00.000Z | 8 | 1 | 7 | 9 | 53
+6 | 13:40:00 | 14:25:00 | `<NULL>` | `<NULL>` | `<NULL>` | `<NULL>` | `<NULL>` | `<NULL>` | `<NULL>`
+
+В выборку попали все строки из левой таблицы, дополненные данными о занятиях. Примечательно, что в конце таблицы есть строки с полями, заполненными `NULL`. Это те строки, для которых не нашлось соответствующих занятий, однако они присутствуют в левой таблице, поэтому тоже были выведены.
+
+### Внешнее правое соединение (RIGHT OUTER JOIN)
+
+Соединение, которое возвращает все значения из правой таблицы, соединённые с соответствующими значениями из левой таблицы, если они удовлетворяют условию соединения, или заменяет их на `NULL` в обратном случае.
+
+### Эмуляция полного соединения в MySQL
+
+Поскольку MySQL не поддерживает `FULL OUTER JOIN`, его можно эмулировать с помощью `UNION ALL`:
+
+```sql
+SELECT *
+FROM левая_таблица
+LEFT JOIN правая_таблица
+   ON правая_таблица.ключ = левая_таблица.ключ
+
+UNION ALL
+
+SELECT *
+FROM левая_таблица
+RIGHT JOIN правая_таблица
+ON правая_таблица.ключ = левая_таблица.ключ
+ WHERE левая_таблица.ключ IS NULL
+```

BIN
img/groupping_by_1_field.webp


BIN
img/groupping_by_2_field.webp


BIN
img/having01.png


BIN
img/join01.png


BIN
img/join02.png


BIN
img/sql_query_order_ru.webp


+ 1 - 1
readme.md

@@ -197,7 +197,7 @@ http://sergeyteplyakov.blogspot.com/2014/01/microsoft-fakes-state-verification.h
 1. [Основы проектирования баз данных. ERD.](./articles/5_1_1_1_erd2.md)
 1. [Словарь данных](articles/5_1_1_1_data_dictionary.md)
 1. [Основы SQL](./articles/sql_for_beginner.md)
-<!-- 1. [Использование MySQL Workbench для создания ER-диаграммы и DDL-скрипта](./articles/mywb01.md) -->
+1. [Группировка, агрегатные функции, JOIN-ы](./articles/sql_advanced.md)
 1. [Создание ER-диаграммы](./articles/5_1_1_1_erd_workbench.md)
 1. [Создание базы данных. Импорт данных.](./articles/sql_import.md)
 1. [Представления (View)](./articles/sql_view.md)