summaryrefslogtreecommitdiff
path: root/SQL/ШПАРГАЛКА SQLite.md
diff options
context:
space:
mode:
Diffstat (limited to 'SQL/ШПАРГАЛКА SQLite.md')
-rw-r--r--SQL/ШПАРГАЛКА SQLite.md4139
1 files changed, 4139 insertions, 0 deletions
diff --git a/SQL/ШПАРГАЛКА SQLite.md b/SQL/ШПАРГАЛКА SQLite.md
new file mode 100644
index 0000000..d6107c2
--- /dev/null
+++ b/SQL/ШПАРГАЛКА SQLite.md
@@ -0,0 +1,4139 @@
+- 09.02.2024
+- [DATA ANALYST](https://uproger.com/category/data-analyst/), [SQL](https://uproger.com/category/sql/)
+
+![Огромная шпаргалка с готовыми запросами SQL (SQLite)](https://uproger.com/wp-content/uploads/2024/02/image-2024-02-09-193350.jpg "Огромная шпаргалка с готовыми запросами SQL (SQLite)")
+
+Все примеры изначально писались для СУБД SQLite, но почти всё из этого применимо также и к другим СУБД.
+
+Вначале идут очень простые запросы, с них можно начать новичкам. Если хочется чего-то более интересного — листайте вниз. Здесь есть и примеры довольно сложных запросов с агрегирующими функциями, триггерами, длинными подзапросами, с оконными функциями. Помимо этого, часть примеров посвящена работе с SQL в Python, используя `sqlite3`, `pandas`, `polars`. Этот список запросов с комментариями можно использовать как наглядное пособие для изучения SQL.
+
+Большинство советов я публиковал в своем [канале по анализу данных](https://t.me/+dXZpK8lQ0lY4YjAy), где вы найдете большое количество советов, инструментов и примеров с кодом. А [здесь большая полезная папка,](https://t.me/addlist/qht-ouKSGaQwNjcy) которую я собрал в которой куча полезного для работы с данными.
+
+Кстати, все эти примеры SQL заботливо собраны в одном архиве, вы можете [скачать](https://github.com/gvwilson/sql-tutorial/raw/main/sql-tutorial.zip) его и экспериментировать локально. После скачивания и разархивирования, у вас будет 3 группы файлов:
+
+- `./db/*.db` — базы данных SQLite, которые используются в примерах ниже
+- `./src/*.*` — SQL-запросы, сценарии Python
+- `./out/*.*` — ожидаемый результат для примеров
+
+### Выбираем все значения из таблички
+
+```
+SELECT *
+FROM little_penguins;
+```
+
+```
+Adelie|Dream|37.2|18.1|178|3900|MALE
+Adelie|Dream|37.6|19.3|181|3300|FEMALE
+Gentoo|Biscoe|50|15.3|220|5550|MALE
+Adelie|Torgersen|37.3|20.5|199|3775|MALE
+Adelie|Biscoe|39.6|17.7|186|3500|FEMALE
+Gentoo|Biscoe|47.7|15|216|4750|FEMALE
+Adelie|Dream|36.5|18|182|3150|FEMALE
+Gentoo|Biscoe|42|13.5|210|4150|FEMALE
+Adelie|Torgersen|42.1|19.1|195|4000|MALE
+Gentoo|Biscoe|54.3|15.7|231|5650|MALE
+```
+
+- ничего особенного, выбираем все записи из таблички `little_penguins`
+
+### Дополнительные команды SQL
+
+[src/admin_commands.sql](https://gvwilson.github.io/sql-tutorial/src/admin_commands.sql)
+
+```
+.headers on
+.mode markdown
+SELECT *
+FROM little_penguins;
+```
+
+[out/admin_commands.out](https://gvwilson.github.io/sql-tutorial/out/admin_commands.out)
+
+```
+| species | island | bill_length_mm | bill_depth_mm | flipper_length_mm | body_mass_g | sex |
+|---------|-----------|----------------|---------------|-------------------|-------------|--------|
+| Adelie | Dream | 37.2 | 18.1 | 178 | 3900 | MALE |
+| Adelie | Dream | 37.6 | 19.3 | 181 | 3300 | FEMALE |
+| Gentoo | Biscoe | 50 | 15.3 | 220 | 5550 | MALE |
+| Adelie | Torgersen | 37.3 | 20.5 | 199 | 3775 | MALE |
+| Adelie | Biscoe | 39.6 | 17.7 | 186 | 3500 | FEMALE |
+| Gentoo | Biscoe | 47.7 | 15 | 216 | 4750 | FEMALE |
+| Adelie | Dream | 36.5 | 18 | 182 | 3150 | FEMALE |
+| Gentoo | Biscoe | 42 | 13.5 | 210 | 4150 | FEMALE |
+| Adelie | Torgersen | 42.1 | 19.1 | 195 | 4000 | MALE |
+| Gentoo | Biscoe | 54.3 | 15.7 | 231 | 5650 | MALE |
+```
+
+- включаем заголовки и режим markdown; в `SQLite` подобные команды начинаются с `.`, а в PostgreSQL с `\`
+- кстати, для просмотра дополнительной инфы или чтобы узнать, какие команды есть, используйте `.help`
+
+### Выбираем нужные столбцы
+
+[src/specify_columns.sql](https://gvwilson.github.io/sql-tutorial/src/specify_columns.sql)
+
+```
+SELECT species,
+ island,
+ sex
+FROM little_penguins;
+```
+
+[out/specify_columns.out](https://gvwilson.github.io/sql-tutorial/out/specify_columns.out)
+
+```
+| species | island | sex |
+|---------|-----------|--------|
+| Adelie | Dream | MALE |
+| Adelie | Dream | FEMALE |
+| Gentoo | Biscoe | MALE |
+| Adelie | Torgersen | MALE |
+| Adelie | Biscoe | FEMALE |
+| Gentoo | Biscoe | FEMALE |
+| Adelie | Dream | FEMALE |
+| Gentoo | Biscoe | FEMALE |
+| Adelie | Torgersen | MALE |
+| Gentoo | Biscoe | MALE |
+```
+
+- выбираем колонки `species`, `island`, `sex` из таблички `little_penguins`
+
+### Сортировка
+
+[src/sort.sql](https://gvwilson.github.io/sql-tutorial/src/sort.sql)
+
+```
+SELECT species,
+ sex,
+ island
+FROM little_penguins
+ORDER BY island ASC,
+ sex DESC;
+```
+
+[out/sort.out](https://gvwilson.github.io/sql-tutorial/out/sort.out)
+
+```
+| species | sex | island |
+|---------|--------|-----------|
+| Gentoo | MALE | Biscoe |
+| Gentoo | MALE | Biscoe |
+| Adelie | FEMALE | Biscoe |
+| Gentoo | FEMALE | Biscoe |
+| Gentoo | FEMALE | Biscoe |
+| Adelie | MALE | Dream |
+| Adelie | FEMALE | Dream |
+| Adelie | FEMALE | Dream |
+| Adelie | MALE | Torgersen |
+| Adelie | MALE | Torgersen |
+```
+
+- выбираем столбцы `species`, `island`, `sex` из таблички `little_penguins`
+- сортируем все значения из `island` в возрастающем порядке (от A к Z)
+- строки с одинаковыми значениями `island` дополнительно сортируем по их значениям `sex` в обратном порядке, от большего к меньшему (от Z к A)
+
+### Ограничение выводимых записей
+
+- Full dataset has 344 rows
+
+[src/limit.sql](https://gvwilson.github.io/sql-tutorial/src/limit.sql)
+
+```
+SELECT species,
+ sex,
+ island
+FROM penguins
+ORDER BY species,
+ sex,
+ island
+LIMIT 10;
+```
+
+[out/limit.out](https://gvwilson.github.io/sql-tutorial/out/limit.out)
+
+```
+| species | sex | island |
+|---------|--------|-----------|
+| Adelie | | Dream |
+| Adelie | | Torgersen |
+| Adelie | | Torgersen |
+| Adelie | | Torgersen |
+| Adelie | | Torgersen |
+| Adelie | | Torgersen |
+| Adelie | FEMALE | Biscoe |
+| Adelie | FEMALE | Biscoe |
+| Adelie | FEMALE | Biscoe |
+| Adelie | FEMALE | Biscoe |
+```
+
+- выбираем столбцы `species`, `sex`, `island` из таблички `penguins`
+- сортируем по `species` в порядке возрастания, строки с одинаковым значением `species` сортируются по `sex`, с одинаковым `sex` дополнительно сортируются по `island`
+- ну и выводим только первые 10 строк
+
+### Ещё некоторые параметры вывода
+
+[src/page.sql](https://gvwilson.github.io/sql-tutorial/src/page.sql)
+
+```
+SELECT species,
+ sex,
+ island
+FROM penguins
+ORDER BY species,
+ sex,
+ island
+LIMIT 10
+OFFSET 3;
+```
+
+[out/page.out](https://gvwilson.github.io/sql-tutorial/out/page.out)
+
+```
+| species | sex | island |
+|---------|--------|-----------|
+| Adelie | | Torgersen |
+| Adelie | | Torgersen |
+| Adelie | | Torgersen |
+| Adelie | FEMALE | Biscoe |
+| Adelie | FEMALE | Biscoe |
+| Adelie | FEMALE | Biscoe |
+| Adelie | FEMALE | Biscoe |
+| Adelie | FEMALE | Biscoe |
+| Adelie | FEMALE | Biscoe |
+| Adelie | FEMALE | Biscoe |
+```
+
+- `OFFSET` указывается после `LIMIT` и позволяет пропустить сколько-то первых строк, в данном случае пропущены 3 первых строки
+
+### Удаляем дубликаты
+
+[src/distinct.sql](https://gvwilson.github.io/sql-tutorial/src/distinct.sql)
+
+```
+SELECT DISTINCT species,
+ sex,
+ island
+FROM penguins;
+```
+
+[out/distinct.out](https://gvwilson.github.io/sql-tutorial/out/distinct.out)
+
+```
+| species | sex | island |
+|-----------|--------|-----------|
+| Adelie | MALE | Torgersen |
+| Adelie | FEMALE | Torgersen |
+| Adelie | | Torgersen |
+| Adelie | FEMALE | Biscoe |
+| Adelie | MALE | Biscoe |
+| Adelie | FEMALE | Dream |
+| Adelie | MALE | Dream |
+| Adelie | | Dream |
+| Chinstrap | FEMALE | Dream |
+| Chinstrap | MALE | Dream |
+| Gentoo | FEMALE | Biscoe |
+| Gentoo | MALE | Biscoe |
+| Gentoo | | Biscoe |
+```
+
+- `SELECT DISTINCT` — выбираем уникальные комбинации из столбцов `species`, `sex`, `island`
+
+### Фильтруем результаты
+
+[src/filter.sql](https://gvwilson.github.io/sql-tutorial/src/filter.sql)
+
+```
+SELECT DISTINCT species,
+ sex,
+ island
+FROM penguins
+WHERE island = 'Biscoe';
+```
+
+[out/filter.out](https://gvwilson.github.io/sql-tutorial/out/filter.out)
+
+```
+| species | sex | island |
+|---------|--------|--------|
+| Adelie | FEMALE | Biscoe |
+| Adelie | MALE | Biscoe |
+| Gentoo | FEMALE | Biscoe |
+| Gentoo | MALE | Biscoe |
+| Gentoo | | Biscoe |
+```
+
+- выбираем уникальные комбинации значений `species`, `sex`, `island` из `penguins`, где значения поля `island` равно `Biscoe`
+
+### Более сложные условия фильтрации
+
+[src/filter_and.sql](https://gvwilson.github.io/sql-tutorial/src/filter_and.sql)
+
+```
+SELECT DISTINCT species,
+ sex,
+ island
+FROM penguins
+WHERE island = 'Biscoe'
+ AND sex != 'MALE';
+```
+
+[out/filter_and.out](https://gvwilson.github.io/sql-tutorial/out/filter_and.out)
+
+```
+| species | sex | island |
+|---------|--------|--------|
+| Adelie | FEMALE | Biscoe |
+| Gentoo | FEMALE | Biscoe |
+```
+
+- выбираем уникальные комбинации значений `species`, `sex`, `island` из `penguins`, где значения поля `island` равно `Biscoe`, а значения поля `sex` не равно `MALE`
+
+### Некоторые математические действия
+
+[src/calculations.sql](https://gvwilson.github.io/sql-tutorial/src/calculations.sql)
+
+```
+SELECT flipper_length_mm / 10.0,
+ body_mass_g / 1000.0
+FROM penguins
+LIMIT 3;
+```
+
+[out/calculations.out](https://gvwilson.github.io/sql-tutorial/out/calculations.out)
+
+```
+| flipper_length_mm / 10.0 | body_mass_g / 1000.0 |
+|--------------------------|----------------------|
+| 18.1 | 3.75 |
+| 18.6 | 3.8 |
+| 19.5 | 3.25 |
+```
+
+- выводим 3 первых строки значений `flipper_length_mm`, делённых на 10.0, и значений `body_mass_g`, делённых на 1000.0
+
+### Переименовываем столбцы
+
+[src/rename_columns.sql](https://gvwilson.github.io/sql-tutorial/src/rename_columns.sql)
+
+```
+SELECT flipper_length_mm / 10.0 AS flipper_cm,
+ body_mass_g / 1000.0 AS weight_kg,
+ island AS where_found
+FROM penguins
+LIMIT 3;
+```
+
+[out/rename_columns.out](https://gvwilson.github.io/sql-tutorial/out/rename_columns.out)
+
+```
+| flipper_cm | weight_kg | where_found |
+|------------|-----------|-------------|
+| 18.1 | 3.75 | Torgersen |
+| 18.6 | 3.8 | Torgersen |
+| 19.5 | 3.25 | Torgersen |
+```
+
+- делим значения `flipper_length_mm` на 10.0, делим значения `body_mass_g` на 1000.0
+- переименовываем столбцы `flipper_length_mm` — в `flipper_cm`, `body_mass_g` — в `weight_kg`, `island` — в `where_found`
+- выводим первые 3 строки
+
+**Взаимосвязь рассмотренных понятий SQL можно показать так:**
+
+![concept map: selection](https://habrastorage.org/getpro/habr/upload_files/806/9f9/4f0/8069f94f0a448b711e810ebe05ee69d1.png "Огромная шпаргалка с готовыми запросами SQL (SQLite)")
+
+concept map: selection
+
+### Подсчёт с пропущенными значениями
+
+[src/show_missing_values.sql](https://gvwilson.github.io/sql-tutorial/src/show_missing_values.sql)
+
+```
+SELECT flipper_length_mm / 10.0 AS flipper_cm,
+ body_mass_g / 1000.0 AS weight_kg,
+ island AS where_found
+FROM penguins
+LIMIT 5;
+```
+
+[out/show_missing_values.out](https://gvwilson.github.io/sql-tutorial/out/show_missing_values.out)
+
+```
+| flipper_cm | weight_kg | where_found |
+|------------|-----------|-------------|
+| 18.1 | 3.75 | Torgersen |
+| 18.6 | 3.8 | Torgersen |
+| 19.5 | 3.25 | Torgersen |
+| | | Torgersen |
+| 19.3 | 3.45 | Torgersen |
+```
+
+- делим значения из `flipper_length_mm` на 10, затем присваиваем результаты новому столбцу `flipper_cm`
+- делим значения из столбца `body_mass_g` на 1000 и затем присваивание результатов новому столбцу `weight_kg`
+- переименовываем `island` в `where_found`
+
+### Вывод с условием при помощи WHERE
+
+- Repeated from above so it doesn’t count against our query limit
+
+[src/filter.sql](https://gvwilson.github.io/sql-tutorial/src/filter.sql)
+
+```
+SELECT DISTINCT species,
+ sex,
+ island
+FROM penguins
+WHERE island = 'Biscoe';
+```
+
+[out/filter.out](https://gvwilson.github.io/sql-tutorial/out/filter.out)
+
+```
+| species | sex | island |
+|---------|--------|--------|
+| Adelie | FEMALE | Biscoe |
+| Adelie | MALE | Biscoe |
+| Gentoo | FEMALE | Biscoe |
+| Gentoo | MALE | Biscoe |
+| Gentoo | | Biscoe |
+```
+
+- выбираем столбцы `species`, `sex`, `island`
+- выводим все записи из `penguins`, где значение `island` равно `'Biscoe'`
+
+[src/null_equality.sql](https://gvwilson.github.io/sql-tutorial/src/null_equality.sql)
+
+```
+SELECT DISTINCT species,
+ sex,
+ island
+FROM penguins
+WHERE island = 'Biscoe'
+ AND sex = 'FEMALE';
+```
+
+[out/null_equality.out](https://gvwilson.github.io/sql-tutorial/out/null_equality.out)
+
+```
+| species | sex | island |
+|---------|--------|--------|
+| Adelie | FEMALE | Biscoe |
+| Gentoo | FEMALE | Biscoe |
+```
+
+- выводим все записи из `penguins`, где значение `island` равно `'Biscoe'` и значение `sex` равно `'FEMALE'`
+
+### Условие с отрицанием
+
+- условие с оператором отрицания `!=` тоже без проблем работает
+
+[src/null_inequality.sql](https://gvwilson.github.io/sql-tutorial/src/null_inequality.sql)
+
+```
+SELECT DISTINCT species,
+ sex,
+ island
+FROM penguins
+WHERE island = 'Biscoe'
+ AND sex != 'FEMALE';
+```
+
+[out/null_inequality.out](https://gvwilson.github.io/sql-tutorial/out/null_inequality.out)
+
+```
+| species | sex | island |
+|---------|------|--------|
+| Adelie | MALE | Biscoe |
+| Gentoo | MALE | Biscoe |
+```
+
+### Выбираем NULL значения
+
+[src/safe_null_equality.sql](https://gvwilson.github.io/sql-tutorial/src/safe_null_equality.sql)
+
+```
+SELECT species,
+ sex,
+ island
+FROM penguins
+WHERE sex IS NULL;
+```
+
+[out/safe_null_equality.out](https://gvwilson.github.io/sql-tutorial/out/safe_null_equality.out)
+
+```
+| species | sex | island |
+|---------|-----|-----------|
+| Adelie | | Torgersen |
+| Adelie | | Torgersen |
+| Adelie | | Torgersen |
+| Adelie | | Torgersen |
+| Adelie | | Torgersen |
+| Adelie | | Dream |
+| Gentoo | | Biscoe |
+| Gentoo | | Biscoe |
+| Gentoo | | Biscoe |
+| Gentoo | | Biscoe |
+| Gentoo | | Biscoe |
+```
+
+- выбираем строки со значениями `species`, `sex`, `island` из таблички `penguins`, где значения `sex` нет (`NULL`)
+
+**Вот так можно показать связь понятий SQL, которые мы рассмотрели выше:**
+
+![concept map: null](https://habrastorage.org/getpro/habr/upload_files/cd2/072/2cb/cd20722cb7505cf071a36076435c746f.png "Огромная шпаргалка с готовыми запросами SQL (SQLite)")
+
+concept map: null
+
+### Агрегирование в SQL
+
+[src/simple_sum.sql](https://gvwilson.github.io/sql-tutorial/src/simple_sum.sql)
+
+```
+SELECT sum(body_mass_g) AS total_mass
+FROM penguins;
+```
+
+[out/simple_sum.out](https://gvwilson.github.io/sql-tutorial/out/simple_sum.out)
+
+```
+| total_mass |
+|------------|
+| 1437000 |
+```
+
+- суммируем все значения колонки `body_mass_g`, сохраняем в новый столбец `total_mass`
+
+### Распространённые агрегирующие функции в SQL
+
+[src/common_aggregations.sql](https://gvwilson.github.io/sql-tutorial/src/common_aggregations.sql)
+
+```
+SELECT MAX(bill_length_mm) AS longest_bill,
+ MIN(flipper_length_mm) AS shortest_flipper,
+ AVG(bill_length_mm) / AVG(bill_depth_mm) AS weird_ratio
+FROM penguins;
+```
+
+[out/common_aggregations.out](https://gvwilson.github.io/sql-tutorial/out/common_aggregations.out)
+
+```
+| longest_bill | shortest_flipper | weird_ratio |
+|--------------|------------------|------------------|
+| 59.6 | 172 | 2.56087082530644 |
+```
+
+- находим максимальное значение из столбца `bill_length_mm`, записываем это значение как `longest_bill`
+- аналогично находим минимальное из `flipper_length_mm`, находим среднее из `bill_length_mm`, среднее из `bill_depth_mm`
+
+### Подсчёт значений при помощи COUNT
+
+[src/count_behavior.sql](https://gvwilson.github.io/sql-tutorial/src/count_behavior.sql)
+
+```
+SELECT COUNT(*) AS count_star,
+ COUNT(sex) AS count_specific,
+ COUNT(DISTINCT sex) AS count_distinct
+FROM penguins;
+```
+
+[out/count_behavior.out](https://gvwilson.github.io/sql-tutorial/out/count_behavior.out)
+
+```
+| count_star | count_specific | count_distinct |
+|------------|----------------|----------------|
+| 344 | 333 | 2 |
+```
+
+- `COUNT(*)` — считаем все значения из `count_star`
+- `COUNT(sex)` — считаем все значения из столбца `sex`
+- `COUNT(DISTINCT sex)` — считаем уникальные значения из `sex` (очевидно их 2: `MALE`, `FEMALE`)
+- записываем эти 3 числа как `count_star`, `count_specific`, `count_distinct` соответственно
+
+### Группировка
+
+[src/simple_group.sql](https://gvwilson.github.io/sql-tutorial/src/simple_group.sql)
+
+```
+SELECT AVG(body_mass_g) AS average_mass_g
+FROM penguins
+GROUP BY sex;
+```
+
+[out/simple_group.out](https://gvwilson.github.io/sql-tutorial/out/simple_group.out)
+
+```
+| average_mass_g |
+|------------------|
+| 4005.55555555556 |
+| 3862.27272727273 |
+| 4545.68452380952 |
+```
+
+- из таблички `penguins` находим среднее всех значений `body_mass_g`, сохраняем как `average_mass_g`
+- группируем по значениям `sex` (группы `FEMALE`, `MALE`, `NULL`)
+
+### Как себя ведут неагрегированные столбцы
+
+[src/unaggregated_columns.sql](https://gvwilson.github.io/sql-tutorial/src/unaggregated_columns.sql)
+
+```
+SELECT sex,
+ AVG(body_mass_g) AS average_mass_g
+FROM penguins
+GROUP BY sex;
+```
+
+[out/unaggregated_columns.out](https://gvwilson.github.io/sql-tutorial/out/unaggregated_columns.out)
+
+```
+| sex | average_mass_g |
+|--------|------------------|
+| | 4005.55555555556 |
+| FEMALE | 3862.27272727273 |
+| MALE | 4545.68452380952 |
+```
+
+- для того, чтобы было видно названия отдельных групп, выбираем не только среднее `AVG(body_mass_g)`, но и `sex`
+- видим 3 группы: `NULL`, `FEMALE`, `MALE`
+
+### Выбор нужных столбцов для агрегирования
+
+[src/arbitrary_in_aggregation.sql](https://gvwilson.github.io/sql-tutorial/src/arbitrary_in_aggregation.sql)
+
+```
+SELECT sex,
+ body_mass_g
+FROM penguins
+GROUP BY sex;
+```
+
+[out/arbitrary_in_aggregation.out](https://gvwilson.github.io/sql-tutorial/out/arbitrary_in_aggregation.out)
+
+```
+| sex | body_mass_g |
+|--------|-------------|
+| | |
+| FEMALE | 3800 |
+| MALE | 3750 |
+```
+
+- здесь у нас популярная ошибка, мы просто выбираем `body_mass_g`, а не находим среднее, поэтому SQL выбирает любые значения из `body_mass_g`. Аккуратнее)
+
+### Фильтрация агрегированных значений
+
+[src/filter_aggregation.sql](https://gvwilson.github.io/sql-tutorial/src/filter_aggregation.sql)
+
+```
+SELECT sex,
+ AVG(body_mass_g) AS average_mass_g
+FROM penguins
+GROUP BY sex
+HAVING average_mass_g > 4000.0;
+```
+
+[out/filter_aggregation.out](https://gvwilson.github.io/sql-tutorial/out/filter_aggregation.out)
+
+```
+| sex | average_mass_g |
+|------|------------------|
+| | 4005.55555555556 |
+| MALE | 4545.68452380952 |
+```
+
+- здесь мы используем `HAVING` вместо `WHERE` (эффект тот же самый), оставляем только те значения из `average_mass_g`, которые больше 4000
+
+### Читабельный вывод
+
+[src/readable_aggregation.sql](https://gvwilson.github.io/sql-tutorial/src/readable_aggregation.sql)
+
+```
+SELECT sex,
+ ROUND(AVG(body_mass_g), 1) AS average_mass_g
+FROM penguins
+GROUP BY sex
+HAVING average_mass_g > 4000.0;
+```
+
+[out/readable_aggregation.out](https://gvwilson.github.io/sql-tutorial/out/readable_aggregation.out)
+
+```
+| sex | average_mass_g |
+|------|----------------|
+| | 4005.6 |
+| MALE | 4545.7 |
+```
+
+- округляем среднее `AVG(body_mass_g` до 1 знака после запятой, используя `ROUND`
+
+### Фильтрация входных данных
+
+[src/filter_aggregate_inputs.sql](https://gvwilson.github.io/sql-tutorial/src/filter_aggregate_inputs.sql)
+
+```
+SELECT sex,
+ ROUND(
+ AVG(body_mass_g) FILTER (WHERE body_mass_g < 4000.0),
+ 1)
+ AS average_mass_g
+FROM penguins
+GROUP BY sex;
+```
+
+[out/filter_aggregate_inputs.out](https://gvwilson.github.io/sql-tutorial/out/filter_aggregate_inputs.out)
+
+```
+| sex | average_mass_g |
+|--------|----------------|
+| | 3362.5 |
+| FEMALE | 3417.3 |
+| MALE | 3752.5 |
+```
+
+- при помощи `FILTER` мы находим среднее только тех значений `body_mass_g`, которые меньше 4000
+- округляем до 1 знака после запятой, сохраняем в столбец `average_mass_g`
+- группируем по `sex`
+
+**Вот так выглядит связь основных понятий, которые мы только что обсуждали:**
+
+![concept map: aggregation](https://habrastorage.org/getpro/habr/upload_files/65f/bf3/79b/65fbf379b530f5bc0afc31cb7bc8051d.png "Огромная шпаргалка с готовыми запросами SQL (SQLite)")
+
+concept map: aggregation
+
+**Кстати, вот так выглядит создание БД в оперативной памяти:**
+
+[src/in_memory_db.sh](https://gvwilson.github.io/sql-tutorial/src/in_memory_db.sh)
+
+```
+sqlite3 :memory:
+```
+
+- запускаем интерактивную оболочку SQLite, создаём новую базу данных в оперативной памяти для более быстрой работы
+
+### Создание табличек
+
+[src/create_work_job.sql](https://gvwilson.github.io/sql-tutorial/src/create_work_job.sql)
+
+```
+CREATE TABLE job (name text NOT NULL,
+ billable real NOT NULL);
+
+CREATE TABLE work (person text NOT NULL,
+ job text NOT NULL);
+```
+
+- создаём таблицу `job` со столбцами: `name` — столбец текстовых значений, не может быть пустым (`NOT NULL`), `billable` — содержит вещественные числа, не может быть пустым
+- создаём табличку `work` со столбцами: `person` — текстовый, не может быть пустым, `job` — текстовый, не может быть пустым
+
+### Вставляем данные
+
+[src/populate_work_job.sql](https://gvwilson.github.io/sql-tutorial/src/populate_work_job.sql)
+
+```
+INSERT INTO job
+VALUES ('calibrate', 1.5),
+ ('clean', 0.5);
+
+INSERT INTO work
+VALUES ('mik', 'calibrate'),
+ ('mik', 'clean'),
+ ('mik', 'complain'),
+ ('po', 'clean'),
+ ('po', 'complain'),
+ ('tay', 'complain');
+```
+
+[out/insert_values.out](https://gvwilson.github.io/sql-tutorial/out/insert_values.out)
+
+```
+| name | billable |
+|-----------|----------|
+| calibrate | 1.5 |
+| clean | 0.5 |
+
+| person | job |
+|--------|-----------|
+| mik | calibrate |
+| mik | clean |
+| mik | complain |
+| po | clean |
+| po | complain |
+| tay | complain |
+```
+
+- ничего особенного, заполняем табличку `job` парами `name`–`billable`, и так же заполняем табличку `work` парами `person`–`job`
+
+### Обновляем строки
+
+[src/update_work_job.sql](https://gvwilson.github.io/sql-tutorial/src/update_work_job.sql)
+
+```
+UPDATE work
+SET person = "tae"
+WHERE person = "tay";
+```
+
+[out/update_rows.out](https://gvwilson.github.io/sql-tutorial/out/update_rows.out)
+
+```
+| person | job |
+|--------|-----------|
+| mik | calibrate |
+| mik | clean |
+| mik | complain |
+| po | clean |
+| po | complain |
+| tae | complain |
+```
+
+- меняем все записи `"tay"` на `"tae"`
+
+### Удаляем строки
+
+[src/delete_rows.sql](https://gvwilson.github.io/sql-tutorial/src/delete_rows.sql)
+
+```
+DELETE FROM work
+WHERE person = "tae";
+SELECT *
+FROM work;
+```
+
+[out/delete_rows.out](https://gvwilson.github.io/sql-tutorial/out/delete_rows.out)
+
+```
+| person | job |
+|--------|-----------|
+| mik | calibrate |
+| mik | clean |
+| mik | complain |
+| po | clean |
+| po | complain |
+```
+
+- удаляем все строки, где значение `person` равно `"tae"`
+
+### Резервное копирование
+
+[src/backing_up.sql](https://gvwilson.github.io/sql-tutorial/src/backing_up.sql)
+
+```
+CREATE TABLE backup (person text NOT NULL,
+ job text NOT NULL);
+
+INSERT INTO backup
+SELECT person,
+ job
+FROM work
+WHERE person = 'tae';
+
+DELETE FROM work
+WHERE person = 'tae';
+
+SELECT *
+FROM backup;
+```
+
+[out/backing_up.out](https://gvwilson.github.io/sql-tutorial/out/backing_up.out)
+
+```
+| person | job |
+|--------|----------|
+| tae | complain |
+```
+
+- создаём табличку `backup` c текстовыми столбцами `person` и `job`
+- помещаем внутрь `backup` значения столбцов `person` и `job` из таблицы `work`, где значения столбца `person` равно `'tae'`
+- удаляем из `work` все записи со значением `person` равным `'tae'`
+- отображаем записи таблички `backup`
+
+**Вот так выглядит связь основных понятий, которые мы только что обсуждали:**
+
+![concept map: data definition and modification](https://habrastorage.org/getpro/habr/upload_files/0c6/324/9e5/0c63249e54e3b837d6a4736d80f7f12c.png "Огромная шпаргалка с готовыми запросами SQL (SQLite)")
+
+concept map: data definition and modification
+
+### Объединение табличек при помощи JOIN
+
+[src/cross_join.sql](https://gvwilson.github.io/sql-tutorial/src/cross_join.sql)
+
+```
+SELECT *
+FROM work
+CROSS JOIN job;
+```
+
+[out/cross_join.out](https://gvwilson.github.io/sql-tutorial/out/cross_join.out)
+
+```
+| person | job | name | billable |
+|--------|-----------|-----------|----------|
+| mik | calibrate | calibrate | 1.5 |
+| mik | calibrate | clean | 0.5 |
+| mik | clean | calibrate | 1.5 |
+| mik | clean | clean | 0.5 |
+| mik | complain | calibrate | 1.5 |
+| mik | complain | clean | 0.5 |
+| po | clean | calibrate | 1.5 |
+| po | clean | clean | 0.5 |
+| po | complain | calibrate | 1.5 |
+| po | complain | clean | 0.5 |
+| tay | complain | calibrate | 1.5 |
+| tay | complain | clean | 0.5 |
+```
+
+- делаем `CROSS JOIN` для 2 таблиц `work` и `job` — все возможные комбинации строк из этих таблиц (если в `work` 3 строки, а в `job` 4 строки, то результат будет иметь 4 ⋅ 3 = 12 строк)
+
+### INNER JOIN
+
+[src/inner_join.sql](https://gvwilson.github.io/sql-tutorial/src/inner_join.sql)
+
+```
+SELECT *
+FROM work
+INNER JOIN job ON work.job = job.name;
+```
+
+[out/inner_join.out](https://gvwilson.github.io/sql-tutorial/out/inner_join.out)
+
+```
+| person | job | name | billable |
+|--------|-----------|-----------|----------|
+| mik | calibrate | calibrate | 1.5 |
+| mik | clean | clean | 0.5 |
+| po | clean | clean | 0.5 |
+```
+
+- объединяем 2 таблицы `work` и `job` — берём те записи, где значение `job` из `work` совпадает со значением `name` из `job`
+
+### Агрегирование объединённых через JOIN записей
+
+[src/aggregate_join.sql](https://gvwilson.github.io/sql-tutorial/src/aggregate_join.sql)
+
+```
+SELECT work.person,
+ SUM(job.billable) AS pay
+FROM work
+INNER JOIN job ON work.job = job.name
+GROUP BY work.person;
+```
+
+[out/aggregate_join.out](https://gvwilson.github.io/sql-tutorial/out/aggregate_join.out)
+
+```
+| person | pay |
+|--------|-----|
+| mik | 2.0 |
+| po | 0.5 |
+```
+
+- объединяем те строки таблиц `work` и `job`, где значение `job` в таблице `work` соответствует значению `name` в `job`
+- суммируем значения `billable` из таблицы `job` для каждого значения `person` из таблицы `work`
+- группируем результаты по значениям `person` из `work`
+
+### LEFT JOIN
+
+[src/left_join.sql](https://gvwilson.github.io/sql-tutorial/src/left_join.sql)
+
+```
+SELECT *
+FROM work
+LEFT JOIN job ON work.job = job.name;
+```
+
+[out/left_join.out](https://gvwilson.github.io/sql-tutorial/out/left_join.out)
+
+```
+| person | job | name | billable |
+|--------|-----------|-----------|----------|
+| mik | calibrate | calibrate | 1.5 |
+| mik | clean | clean | 0.5 |
+| mik | complain | | |
+| po | clean | clean | 0.5 |
+| po | complain | | |
+| tay | complain | | |
+```
+
+- склеиваем таблицы `work` и `job` по соответствующим значениям столбца `job`
+- если в таблице `work` есть строки, для которых нет совпадений в таблице `job`, то они все равно будут включены в результат с пустыми (`NULL`) значениями
+- использование `LEFT JOIN` гарантирует, что все строки из левой таблицы `work` будут включены в результат, независимо от наличия совпадающих строк в правой таблице `job`
+
+### Агрегирование данных, собранных через LEFT JOIN
+
+[src/aggregate_left_join.sql](https://gvwilson.github.io/sql-tutorial/src/aggregate_left_join.sql)
+
+```
+SELECT work.person,
+ sum(job.billable) AS pay
+FROM work
+LEFT JOIN job ON work.job = job.name
+GROUP BY work.person;
+```
+
+[out/aggregate_left_join.out](https://gvwilson.github.io/sql-tutorial/out/aggregate_left_join.out)
+
+```
+| person | pay |
+|--------|-----|
+| mik | 2.0 |
+| po | 0.5 |
+| tay | |
+```
+
+- вычисляем сумму значений столбца `billable` из `job`, сохраняем как `pay`
+- используем `LEFT JOIN`, чтобы гарантированно включить все строки из `work` в `job`
+- группируем по столбцу `person` из `work`
+
+**Вот так выглядит связь основных понятий, которые мы только что обсуждали:**
+
+![concept map: join](https://habrastorage.org/getpro/habr/upload_files/fa9/d5d/d80/fa9d5dd80111e82f67d5e6ba9f249bf0.png "Огромная шпаргалка с готовыми запросами SQL (SQLite)")
+
+concept map: join
+
+### Объединение значений
+
+[src/coalesce.sql](https://gvwilson.github.io/sql-tutorial/src/coalesce.sql)
+
+```
+SELECT work.person,
+ COALESCE(SUM(job.billable), 0.0) AS pay
+FROM work
+LEFT JOIN job ON work.job = job.name
+GROUP BY work.person;
+```
+
+[out/coalesce.out](https://gvwilson.github.io/sql-tutorial/out/coalesce.out)
+
+```
+| person | pay |
+|--------|-----|
+| mik | 2.0 |
+| po | 0.5 |
+| tay | 0.0 |
+```
+
+- `COALESCE` используется для замены `NULL` на 0.0, если сумма `billable` для данного `person` равна `NULL`
+- `LEFT JOIN` включает все записи из `work` и только соответствующие записи из `job`
+- группируем по значениям столбца `person` из `work`
+
+### SELECT DISTINCT и условие WHERE
+
+[src/negate_incorrectly.sql](https://gvwilson.github.io/sql-tutorial/src/negate_incorrectly.sql)
+
+```
+SELECT DISTINCT person
+FROM work
+WHERE job != 'calibrate';
+```
+
+[out/negate_incorrectly.out](https://gvwilson.github.io/sql-tutorial/out/negate_incorrectly.out)
+
+```
+| person |
+|--------|
+| mik |
+| po |
+| tay |
+```
+
+- выбираем уникальные значения из столбца `person`, где поле `job` не равно `calibrate`
+
+### Использование набора в условии WHERE при помощи IN
+
+[src/set_membership.sql](https://gvwilson.github.io/sql-tutorial/src/set_membership.sql)
+
+```
+SELECT *
+FROM work
+WHERE person NOT IN ('mik',
+ 'tay');
+```
+
+[out/set_membership.out](https://gvwilson.github.io/sql-tutorial/out/set_membership.out)
+
+```
+| person | job |
+|--------|----------|
+| po | clean |
+| po | complain |
+```
+
+- выбираем все строки из `work`, где `person` не равно `'mik'` и не равно `'tay'`
+
+### Подзапросы
+
+[src/subquery_set.sql](https://gvwilson.github.io/sql-tutorial/src/subquery_set.sql)
+
+```
+SELECT DISTINCT person
+FROM work
+WHERE person not in
+ (SELECT DISTINCT person
+ FROM work
+ WHERE job = 'calibrate');
+```
+
+[out/subquery_set.out](https://gvwilson.github.io/sql-tutorial/out/subquery_set.out)
+
+```
+| person |
+|--------|
+| po |
+| tay |
+```
+
+- внутренний подзапрос выбирает уникальные значения столбца `person` из `work`, где в поле `job` стоит `'calibrate'`
+- внешний, главный запрос выбирает те уникальные значения `person`, где `person` не равно значениям из внутренного подзапроса
+
+### Автоикремент и PRIMARY KEY
+
+[src/autoincrement.sql](https://gvwilson.github.io/sql-tutorial/src/autoincrement.sql)
+
+```
+CREATE TABLE person (ident integer PRIMARY KEY autoincrement,
+ name text NOT NULL);
+INSERT INTO person
+VALUES (NULL, 'mik'),
+ (NULL, 'po'),
+ (NULL, 'tay');
+
+SELECT *
+FROM person;
+INSERT INTO person
+VALUES (1, "prevented");
+```
+
+[out/autoincrement.out](https://gvwilson.github.io/sql-tutorial/out/autoincrement.out)
+
+```
+| ident | name |
+|-------|------|
+| 1 | mik |
+| 2 | po |
+| 3 | tay |
+Runtime error near line 12: UNIQUE constraint failed: person.ident (19)
+```
+
+- создаём табличку `person` с 2 столбцами: `ident` с целочисленными значениями, `name` с текстовыми значениями; столбец `ident` устанавливаем как `PRIMARY KEY`, включаем автоматическое инкрементирование значений
+- помещаем в таблицу `person` 3 пары `ident`–`name`
+- при попытке добавить ещё одну пару `(1, "prevented")` возникает ошибка, поскольку уже существует строка с `indent` равным 1
+
+**Внутренняя табличка:**
+
+[src/sequence_table.sql](https://gvwilson.github.io/sql-tutorial/src/sequence_table.sql)
+
+```
+SELECT *
+FROM sqlite_sequence;
+```
+
+[out/sequence_table.out](https://gvwilson.github.io/sql-tutorial/out/sequence_table.out)
+
+```
+| name | seq |
+|--------|-----|
+| person | 3 |
+```
+
+- выводим все текущие значения автоинкрементных счетчиков для таблиц в БД SQLite
+
+### Изменение таблички при помощи ALTER
+
+[src/alter_tables.sql](https://gvwilson.github.io/sql-tutorial/src/alter_tables.sql)
+
+```
+ALTER TABLE job ADD ident integer NOT NULL DEFAULT -1;
+
+UPDATE job
+SET ident = 1
+WHERE name = 'calibrate';
+
+UPDATE job
+SET ident = 2
+WHERE name = 'clean';
+
+SELECT *
+FROM job;
+```
+
+[out/alter_tables.out](https://gvwilson.github.io/sql-tutorial/out/alter_tables.out)
+
+```
+| name | billable | ident |
+|-----------|----------|-------|
+| calibrate | 1.5 | 1 |
+| clean | 0.5 | 2 |
+```
+
+- добавляем новый столбец `ident` в табличку `job`; столбец заполняется целыми числами, не может быть пустым; ставим значение по умолчанию `-1` для этого столбца
+- делаем значение столбца `ident` равным 1 там, где `name` равен `'calibrate'`
+- устанавливаем значение `ident` равным 2 для строки, где `name` равен `clean`
+
+### Создание новой таблички на базе старой
+
+[src/insert_select.sql](https://gvwilson.github.io/sql-tutorial/src/insert_select.sql)
+
+```
+CREATE TABLE new_work (person_id integer NOT NULL,
+ job_id integer NOT NULL,
+ FOREIGN key(person_id) REFERENCES person(ident),
+ FOREIGN key(job_id) REFERENCES job(ident));
+
+INSERT INTO new_work
+SELECT person.ident AS person_id,
+ job.ident AS job_id
+FROM (person
+ JOIN work
+ ON person.name = work.person)
+
+JOIN job ON job.name = work.job;
+
+SELECT *
+FROM new_work;
+```
+
+[out/insert_select.out](https://gvwilson.github.io/sql-tutorial/out/insert_select.out)
+
+```
+| person_id | job_id |
+|-----------|--------|
+| 1 | 1 |
+| 1 | 2 |
+| 2 | 2 |
+```
+
+- создаём таблицу `new_work` с 2 целочисленными столбцами: `person_id` и `job_id`; оба столбца не могут быть пустыми
+- 2 `FOREIGN KEY` ограничения добавляются, чтобы связать столбцы `person_id` и `job_id` новой таблицы `new_work` с соответствующими столбцами `ident` в таблицах `person` и `job`
+- добавляем данные в таблицу `new_work`, используя результат запроса `SELECT`
+- `FROM (person JOIN work ON person.name = work.person)` — данные будут выбраны из результатов соединения таблиц `person` и `work` по условию равенства значений столбца `name` в таблице `person` и столбца `person` в таблице `work`
+- `JOIN job ON job.name = work.job` — результаты предыдущего соединения будут дополнительно соединены с таблицей `job` по условию равенства значений столбца `name` в таблице `job` и столбца `job`в `work`
+
+### Удаление таблички
+
+[src/drop_table.sql](https://gvwilson.github.io/sql-tutorial/src/drop_table.sql)
+
+```
+DROP TABLE work;
+ALTER TABLE new_work RENAME TO work;
+```
+
+- удаляем `work` из БД
+- изменяем имя таблички `new_work` на `work`
+
+[out/drop_table.out](https://gvwilson.github.io/sql-tutorial/out/drop_table.out)
+
+```
+CREATE TABLE job (ident integer PRIMARY KEY autoincrement,
+ name text NOT NULL,
+ billable real NOT NULL);
+CREATE TABLE sqlite_sequence(name,
+ seq);
+
+CREATE TABLE person (ident integer PRIMARY KEY autoincrement,
+ name text NOT NULL);
+
+CREATE TABLE IF NOT EXISTS "work" (person_id integer NOT NULL,
+ job_id integer NOT NULL,
+ FOREIGN key(person_id) REFERENCES person(ident),
+ FOREIGN key(job_id) REFERENCES job(ident));
+```
+
+- создаём таблицу `job` с 3 колонками: `ident` хранит целые числа, используется в качестве первичного ключа (`PRIMARY KEY`) и автоматически увеличивается (autoincrement);`name` текстовый столбец, не может быть пустым (`NOT NULL`);`billable` — столбец вещественных чисел, не может быть пустым
+- создаём `sqlite_sequence` с 2 колонками: `name` и `seq`
+- создаём таблицу `person` с 2 колонками: `ident` — хранит целые числа, используется в качестве первичного ключа и автоматически увеличивается (autoincrement), `name` — хранит текст, не может быть пустым
+- создаём `work` с 4 колонками: `person_id` – хранит целые числа, не может быть пустым; аналогичный столбец `job_id`
+- устанавливаем внешние ключи, связывающие `person_id` с `ident` в таблице `person` и `job_id` с `ident` в таблице `job`
+
+### Сравнение отдельных значений с агрегированными
+
+[src/compare_individual_aggregate.sql](https://gvwilson.github.io/sql-tutorial/src/compare_individual_aggregate.sql)
+
+```
+SELECT body_mass_g
+FROM penguins
+WHERE body_mass_g > (SELECT AVG(body_mass_g)
+ FROM penguins)
+LIMIT 5;
+```
+
+[out/compare_individual_aggregate.out](https://gvwilson.github.io/sql-tutorial/out/compare_individual_aggregate.out)
+
+```
+| body_mass_g |
+|-------------|
+| 4675 |
+| 4250 |
+| 4400 |
+| 4500 |
+| 4650 |
+```
+
+- выбираем только те строки, где значение в столбце `body_mass_g` больше, чем среднее значение `body_mass_g` по всем строкам в таблице `penguins`
+- ну и выводим только первые 5 строк
+
+### Сравнение отдельных значений с агрегированными внутри групп
+
+[src/compare_within_groups.sql](https://gvwilson.github.io/sql-tutorial/src/compare_within_groups.sql)
+
+```
+SELECT penguins.species,
+ penguins.body_mass_g,
+ Round(averaged.avg_mass_g, 1) AS avg_mass_g
+FROM penguins
+ JOIN (SELECT species,
+ Avg(body_mass_g) AS avg_mass_g
+ FROM penguins
+ GROUP BY species) AS averaged
+ ON penguins.species = averaged.species
+WHERE penguins.body_mass_g > averaged.avg_mass_g
+LIMIT 5;
+```
+
+[out/compare_within_groups.out](https://gvwilson.github.io/sql-tutorial/out/compare_within_groups.out)
+
+```
+| species | body_mass_g | avg_mass_g |
+|---------|-------------|------------|
+| Adelie | 3750 | 3700.7 |
+| Adelie | 3800 | 3700.7 |
+| Adelie | 4675 | 3700.7 |
+| Adelie | 4250 | 3700.7 |
+| Adelie | 3800 | 3700.7 |
+```
+
+- выбираем столбцы `species` и `body_mass_g` из таблицы `penguins`
+- вычисляем среднюю массу для каждого вида пингвина, округляем до 1 знака после запятой, используя подзапрос, который связывается с исходной таблицей `penguins` по полю `species`
+- используя результаты подзапроса, фильтруем только те записи, где масса пингвина больше средней массы для его вида
+- выводим только первые 5 записей
+
+### CTE — табличные выражения
+
+[src/common_table_expressions.sql](https://gvwilson.github.io/sql-tutorial/src/common_table_expressions.sql)
+
+```
+WITH grouped AS
+ (SELECT species,
+ avg(body_mass_g) AS avg_mass_g
+ FROM penguins
+ GROUP BY species)
+SELECT penguins.species,
+ penguins.body_mass_g,
+ round(grouped.avg_mass_g, 1) AS avg_mass_g
+FROM penguins
+JOIN grouped
+WHERE penguins.body_mass_g > grouped.avg_mass_g
+LIMIT 5;
+```
+
+[out/common_table_expressions.out](https://gvwilson.github.io/sql-tutorial/out/common_table_expressions.out)
+
+```
+| species | body_mass_g | avg_mass_g |
+|---------|-------------|------------|
+| Adelie | 3750 | 3700.7 |
+| Adelie | 3800 | 3700.7 |
+| Adelie | 4675 | 3700.7 |
+| Adelie | 4250 | 3700.7 |
+| Adelie | 3800 | 3700.7 |
+```
+
+- создаём табличку `grouped` (с помощью `WITH`), которая содержит среднюю массу тела пингвинов (`AVG(body_mass_g)`) для каждого вида из `penguins` (`GROUP BY species`)
+- из `penguins` выбираем такие столбцы: `species`, `body_mass_g`; и из из общей таблицы `grouped`выбираем `avg_mass_g`, округлённое до 1 знака
+- объединяем `penguins` с общей таблицей `grouped` (через `JOIN`); для каждого пингвина будет найдена соответствующая средняя масса тела для его вида
+- `WHERE` — фильтруем; оставляем только тех, у которых масса тела больше средней массы их вида
+- выводим только первые 5 строк
+
+### Смотрим план запроса с помощью EXPLAIN
+
+[src/explain_query_plan.sql](https://gvwilson.github.io/sql-tutorial/src/explain_query_plan.sql)
+
+```
+EXPLAIN query PLAN
+SELECT species,
+ AVG(body_mass_g)
+FROM penguins
+GROUP BY species;
+```
+
+[out/explain_query_plan.out](https://gvwilson.github.io/sql-tutorial/out/explain_query_plan.out)
+
+```
+QUERY PLAN
+|--SCAN penguins
+`--USE TEMP B-TREE FOR GROUP BY
+```
+
+- `EXPLAIN query PLAN` — получаем план выполнения запроса, как будет выполнен запрос в базе данных
+- выбираем столбец `species`, вычисляем среднее значение столбца body_mass_g для каждого вида из `penguins`
+- `GROUP BY species` — группируем результаты по столбцу `species`
+
+### Нумеруем строки
+
+- каждая таблица имеет специальный столбец `rowid` с уникальными числовыми идентификаторами
+
+[src/rowid.sql](https://gvwilson.github.io/sql-tutorial/src/rowid.sql)
+
+```
+SELECT rowid, species, island
+FROM penguins
+LIMIT 5;
+```
+
+[out/rowid.out](https://gvwilson.github.io/sql-tutorial/out/rowid.out)
+
+```
+| rowid | species | island |
+|-------|---------|-----------|
+| 1 | Adelie | Torgersen |
+| 2 | Adelie | Torgersen |
+| 3 | Adelie | Torgersen |
+| 4 | Adelie | Torgersen |
+| 5 | Adelie | Torgersen |
+```
+
+### Условия if-else
+
+[src/if_else.sql](https://gvwilson.github.io/sql-tutorial/src/if_else.sql)
+
+```
+WITH sized_penguins AS
+ (SELECT species,
+ iif(body_mass_g < 3500, 'small', 'large') AS size
+ FROM penguins)
+SELECT species,
+ size,
+ count(*) AS num
+FROM sized_penguins
+GROUP BY species,
+ size
+ORDER BY species,
+ num;
+```
+
+[out/if_else.out](https://gvwilson.github.io/sql-tutorial/out/if_else.out)
+
+```
+| species | size | num |
+|-----------|-------|-----|
+| Adelie | small | 54 |
+| Adelie | large | 98 |
+| Chinstrap | small | 17 |
+| Chinstrap | large | 51 |
+| Gentoo | large | 124 |
+```
+
+- создаём временную таблицу `sized_penguins`, которая содержит два столбца: `species` и `size`
+- `size` определяется на основе условия: если `body_mass_g` меньше 3500, то он считается `'small'`, в противном случае – `'large'`
+- выбираем столбцы `species` и `size` из временной таблицы `sized_penguins`, а подсчитываем количество записей для каждой комбинации `species` и `size`, используя функцию `count(*)`
+- группируем данные (`GROUP BY`) по `species` и `size`
+
+### Выбираем с помощью SELECT и CASE
+
+А если нам нужны маленькие, средние и большие?
+
+Можно вложить if, но он быстро становится нечитаемым
+
+[src/case_when.sql](https://gvwilson.github.io/sql-tutorial/src/case_when.sql)
+
+```
+WITH sized_penguins AS
+ (SELECT species,
+ CASE
+ WHEN body_mass_g < 3500 THEN 'small'
+ WHEN body_mass_g < 5000 THEN 'medium'
+ ELSE 'large'
+ END AS SIZE
+ FROM penguins)
+SELECT species,
+ SIZE,
+ count(*) AS num
+FROM sized_penguins
+GROUP BY species,
+ SIZE
+ORDER BY species,
+ num;
+```
+
+[out/case_when.out](https://gvwilson.github.io/sql-tutorial/out/case_when.out)
+
+```
+| species | size | num |
+|-----------|--------|-----|
+| Adelie | large | 1 |
+| Adelie | small | 54 |
+| Adelie | medium | 97 |
+| Chinstrap | small | 17 |
+| Chinstrap | medium | 51 |
+| Gentoo | medium | 56 |
+| Gentoo | large | 68 |
+```
+
+- в блоке `WITH` создаём набор данных с именем `sized_penguins`, где находится `species` и `size`, определенные на `body_mass_g`
+- `CASE` разделяет пингвинов на 3 категории: `'small'`, `'medium'` и `'large'` в зависимости от их массы
+- в основном блоке `SELECT` выбираются вид пингвина, его размер и количество пингвинов каждого размера (`num`) из набора `sized_penguins`
+- результаты группируются по виду пингвина и их размеру с помощью `GROUP BY`
+- в конце запроса результаты сортируются сначала по `species` в алфавитном порядке, а затем по `num`
+
+### Работаем с диапазоном значений
+
+[src/check_range.sql](https://gvwilson.github.io/sql-tutorial/src/check_range.sql)
+
+```
+WITH sized_penguins AS
+ (SELECT species,
+ CASE
+ WHEN body_mass_g BETWEEN 3500 AND 5000 THEN 'normal'
+ ELSE 'abnormal'
+ END AS SIZE
+ FROM penguins)
+SELECT species,
+ SIZE,
+ count(*) AS num
+FROM sized_penguins
+GROUP BY species,
+ SIZE
+ORDER BY species,
+ num;
+```
+
+[out/check_range.out](https://gvwilson.github.io/sql-tutorial/out/check_range.out)
+
+```
+| species | size | num |
+|-----------|----------|-----|
+| Adelie | abnormal | 55 |
+| Adelie | normal | 97 |
+| Chinstrap | abnormal | 17 |
+| Chinstrap | normal | 51 |
+| Gentoo | abnormal | 62 |
+| Gentoo | normal | 62 |
+```
+
+- создаём общую таблицу выражений (CTE) `sized_penguins`, она выбирает вид пингвина и определяет его размер в зависимости от массы тела; если масса в диапазоне от 3500 до 5000 г, это размер `normal`, в противном случае – `abnormal`
+- затем из этой CTE извлекаем данные с указанием видов пингвинов, их размеров и количества пингвинов каждого вида и размера, используя `SELECT` с агрегирующей функцией `COUNT(*)`
+- группируем по виду и размеру пингвина с помощью `GROUP BY`
+- сортируем результат по виду и количеству пингвинов в порядке возрастания с помощью `ORDER BY`
+
+**Ещё одна БД: **
+
+ER-диаграмма показывает отношения между отдельными табличками и выглядит так:
+
+![assay database table diagram](https://habrastorage.org/getpro/habr/upload_files/648/684/cd2/648684cd2e1abbf5bde4c04ac8d416d1.png "Огромная шпаргалка с готовыми запросами SQL (SQLite)")
+
+assay database table diagram
+
+![assay ER diagram](https://habrastorage.org/getpro/habr/upload_files/63a/122/f00/63a122f00a285e28e49eac6483666c7f.png "Огромная шпаргалка с готовыми запросами SQL (SQLite)")
+
+assay ER diagram
+
+[src/assay_staff.sql](https://gvwilson.github.io/sql-tutorial/src/assay_staff.sql)
+
+```
+SELECT *
+FROM staff;
+```
+
+[out/assay_staff.out](https://gvwilson.github.io/sql-tutorial/out/assay_staff.out)
+
+```
+| ident | personal | family | dept | age |
+|-------|----------|-----------|------|-----|
+| 1 | Kartik | Gupta | | 46 |
+| 2 | Divit | Dhaliwal | hist | 34 |
+| 3 | Indrans | Sridhar | mb | 47 |
+| 4 | Pranay | Khanna | mb | 51 |
+| 5 | Riaan | Dua | | 23 |
+| 6 | Vedika | Rout | hist | 45 |
+| 7 | Abram | Chokshi | gen | 23 |
+| 8 | Romil | Kapoor | hist | 38 |
+| 9 | Ishaan | Ramaswamy | mb | 35 |
+| 10 | Nitya | Lal | gen | 52 |
+```
+
+### Ищем по фрагменту с помощью LIKE
+
+[src/like_glob.sql](https://gvwilson.github.io/sql-tutorial/src/like_glob.sql)
+
+```
+SELECT personal,
+ family
+FROM staff
+WHERE personal LIKE '%ya%'
+ OR family GLOB '*De*';
+```
+
+[out/like_glob.out](https://gvwilson.github.io/sql-tutorial/out/like_glob.out)
+
+```
+| personal | family |
+|----------|--------|
+| Nitya | Lal |
+```
+
+- `SELECT personal, family` — хотим выбрать столбцы `personal` и `family` из таблицы `staff`
+- `FROM staff` — ну понятно, запрос будет выполнен в таблице `staff`
+- `'%ya%'` — хотим выбрать строки, в которых значение столбца `personal` содержит подстроку `ya` (с помощью `LIKE`) или значение столбца `family` содержит `De` (с помощью `GLOB`)
+
+### Выбираем первую и последнюю строки
+
+[src/union_all.sql](https://gvwilson.github.io/sql-tutorial/src/union_all.sql)
+
+```
+SELECT *
+FROM
+ (SELECT *
+ FROM
+ (SELECT *
+ FROM experiment
+ ORDER BY started ASC
+ LIMIT 5)
+ UNION ALL SELECT *
+ FROM
+ (SELECT *
+ FROM experiment
+ ORDER BY started DESC
+ LIMIT 5))
+ORDER BY started ASC ;
+```
+
+[out/union_all.out](https://gvwilson.github.io/sql-tutorial/out/union_all.out)
+
+```
+| ident | kind | started | ended |
+|-------|-------------|------------|------------|
+| 17 | trial | 2023-01-29 | 2023-01-30 |
+| 35 | calibration | 2023-01-30 | 2023-01-30 |
+| 36 | trial | 2023-02-02 | 2023-02-03 |
+| 25 | trial | 2023-02-12 | 2023-02-14 |
+| 2 | calibration | 2023-02-14 | 2023-02-14 |
+| 40 | calibration | 2024-01-21 | 2024-01-21 |
+| 12 | trial | 2024-01-26 | 2024-01-28 |
+| 44 | trial | 2024-01-27 | 2024-01-29 |
+| 34 | trial | 2024-02-01 | 2024-02-02 |
+| 14 | calibration | 2024-02-03 | 2024-02-03 |
+```
+
+- выбираем 5 самых старых записей из таблицы `experiment`, отсортированных по возрастанию даты начала (`started ASC`) с помощью подзапроса (внутренний `SELECT`)
+- выбираем 5 самых новых записей из `experiment`, отсортированных по убыванию даты начала (`started DESC`) с помощью другого подзапроса
+- объединяем эти 2 подзапроса с помощью `UNION ALL`, так мы получаем временную таблицу, содержащую 10 записей (5 самых старых и 5 самых новых)
+- из временной таблицы выбираем все столбцы для каждой записи (`SELECT *`) и окончательно сортируем записи по возрастанию даты начала (`started ASC`) с помощью внешнего `ORDER BY`
+
+### Пересечение отдельных табличек
+
+[src/intersect.sql](https://gvwilson.github.io/sql-tutorial/src/intersect.sql)
+
+```
+SELECT personal,
+ family,
+ dept,
+ age
+FROM staff
+WHERE dept = 'mb' INTERSECT
+ SELECT personal,
+ family,
+ dept,
+ age
+ FROM staff WHERE age < 50 ;
+```
+
+[out/intersect.out](https://gvwilson.github.io/sql-tutorial/out/intersect.out)
+
+```
+| personal | family | dept | age |
+|----------|-----------|------|-----|
+| Indrans | Sridhar | mb | 47 |
+| Ishaan | Ramaswamy | mb | 35 |
+```
+
+- здесь мы используем `INTERSECT` для объединения результатов двух отдельных запросов
+- вначале выбираем данные из таблицы `staff`, в которых значение поля `dept` равно `'mb'`
+- потом выбираем данные из таблицы `staff`, в которых значение поля `age` меньше 50
+- с помощью `INTERSECT` объединяем результаты этих двух запросов
+- в результате будут выбраны строки, которые присутствуют в обоих результатах, то есть записи из `staff`, где значение `dept` равно `'mb'` и значение `age` меньше 50
+
+### Исключение
+
+[src/except.sql](https://gvwilson.github.io/sql-tutorial/src/except.sql)
+
+```
+SELECT personal,
+ family,
+ dept,
+ age
+FROM staff
+WHERE dept = 'mb'
+EXCEPT
+SELECT personal,
+ family,
+ dept,
+ age
+FROM staff
+WHERE age < 50 ;
+```
+
+[out/except.out](https://gvwilson.github.io/sql-tutorial/out/except.out)
+
+```
+| personal | family | dept | age |
+|----------|--------|------|-----|
+| Pranay | Khanna | mb | 51 |
+```
+
+- при помощи `SELECT` извлекаем 4 поля из `staff`: `personal`, `family`, `dept` и `age`
+- затем используем `WHERE`, чтобы отфильтровать только те строки, в которых значение `dept` равно `'mb'`
+- после этого при помощи `EXCEPT` удаляем из исходного результата любые строки, которые также присутствуют в результате второго запроса
+- второй запрос `SELECT` также извлекает четыре поля из `staff`: `personal`, `family`, `dept` и `age`
+- используем `WHERE`, чтобы отфильтровать только те строки, где значение `age` меньше 50
+
+### Случайные значения в SQL
+
+[src/random_numbers.sql](https://gvwilson.github.io/sql-tutorial/src/random_numbers.sql)
+
+```
+WITH decorated AS
+ (SELECT random() AS rand,
+ personal || ' ' || family AS name
+ FROM staff)
+SELECT rand,
+ abs(rand) % 10 AS selector,
+ name
+FROM decorated
+WHERE selector < 5;
+```
+
+[out/random_numbers.out](https://gvwilson.github.io/sql-tutorial/out/random_numbers.out)
+
+```
+| rand | selector | name |
+|----------------------|----------|-----------------|
+| 7176652035743196310 | 0 | Divit Dhaliwal |
+| -2243654635505630380 | 2 | Indrans Sridhar |
+| -6940074802089166303 | 5 | Pranay Khanna |
+| 8882650891091088193 | 9 | Riaan Dua |
+| -45079732302991538 | 5 | Vedika Rout |
+| -8973877087806386134 | 2 | Abram Chokshi |
+| 3360598450426870356 | 9 | Romil Kapoor |
+```
+
+- создаём временную таблицу `decorated`
+
+- в этой таблице извлекается случайное число с помощью `random()`
+- конкатенируем значения `personal` и `family` под именем `name` с помощью `' '` для разделения
+- таким образом создаём временную таблицу, содержащую столбцы `rand` с случайными числами и `name` со значениями из столбцов `personal` и `family` таблицы `staff`
+- делаем выборку из временной таблицы `decorated`; в выборку включаем столбцы `rand`, `name`
+- `abs(rand) % 10` — это мы вычисляем остаток от деления абсолютного значения `rand` на 10
+- ну и в конце оставляем только строки, где `selector` меньше 5
+
+### Создание индексов
+
+[src/create_use_index.sql](https://gvwilson.github.io/sql-tutorial/src/create_use_index.sql)
+
+```
+EXPLAIN query PLAN
+SELECT filename
+FROM plate
+WHERE filename like '%07%';
+
+CREATE INDEX plate_file ON plate(filename);
+
+EXPLAIN query PLAN
+SELECT filename
+FROM plate
+WHERE filename like '%07%';
+```
+
+[out/create_use_index.out](https://gvwilson.github.io/sql-tutorial/out/create_use_index.out)
+
+```
+QUERY PLAN
+`--SCAN plate USING COVERING INDEX sqlite_autoindex_plate_1
+QUERY PLAN
+`--SCAN plate USING COVERING INDEX plate_file
+```
+
+- выбираем все значения столбца `filename` из таблицы `plate`, где значение столбца `filename` содержит подстроку `07`
+- создаём индекс с именем `plate_file` для столбца `filename` в таблице `plate`
+- запрашиваем план выполнения запроса (`EXPLAIN query PLAN`)
+
+### Генерация последовательности значений
+
+[src/generate_sequence.sql](https://gvwilson.github.io/sql-tutorial/src/generate_sequence.sql)
+
+```
+SELECT value
+FROM generate_series(1, 5);
+```
+
+[out/generate_sequence.out](https://gvwilson.github.io/sql-tutorial/out/generate_sequence.out)
+
+```
+| value |
+|-------|
+| 1 |
+| 2 |
+| 3 |
+| 4 |
+| 5 |
+```
+
+- `generate_series(1, 5)` — генерируем ряд чисел от 1 до 5
+- `SELECT value` — выбираем этот столбец `value` со сгенерированными числами от 1 до 5
+
+### Генерируем последовательность на основе данных
+
+[src/data_range_sequence.sql](https://gvwilson.github.io/sql-tutorial/src/data_range_sequence.sql)
+
+```
+CREATE TABLE temp (num integer NOT NULL);
+
+INSERT INTO temp
+VALUES (1),
+ (5);
+
+SELECT value
+FROM generate_series ((SELECT min(num)
+ FROM TEMP),
+ (SELECT max(num)
+ FROM TEMP));
+```
+
+[out/data_range_sequence.out](https://gvwilson.github.io/sql-tutorial/out/data_range_sequence.out)
+
+```
+| value |
+|-------|
+| 1 |
+| 2 |
+| 3 |
+| 4 |
+| 5 |
+```
+
+- создаём временную таблицу `temp`, которая содержит 1 столбец с именем `num` типа integer; этот столбец не может быть пустым
+- помещаем в `temp` значения 1 и 5 в столбец `num`
+- используем `generate_series` для создания последовательности чисел между минимальным и максимальным значениями из столбца `num` в таблице `temp`
+
+### Генерация последовательностей дат
+
+[src/date_sequence.sql](https://gvwilson.github.io/sql-tutorial/src/date_sequence.sql)
+
+```
+SELECT date(
+ (SELECT julianday(min(started))
+ FROM experiment) + value) AS some_day
+FROM
+ (SELECT value
+ FROM generate_series(
+ (SELECT 0),
+ (SELECT count(*) - 1
+ FROM experiment)))
+LIMIT 5;
+```
+
+[out/date_sequence.out](https://gvwilson.github.io/sql-tutorial/out/date_sequence.out)
+
+```
+| some_day |
+|------------|
+| 2023-01-29 |
+| 2023-01-30 |
+| 2023-01-31 |
+| 2023-02-01 |
+| 2023-02-02 |
+```
+
+- `SELECT julianday(min(started)) FROM experiment` — находим минимальную дату в столбце `started` из `experiment`, преобразуем её в `Julian` день (числовое представление даты) и возвращаем этот `Julian` день
+- внешним подзапросом вычисляем разницу между этим минимальным `Julian` днем и каждым `value` из `generate_series`
+- затем складываем эти разницы с минимальным `Julian` днем, и конвертируем обратно в дату с помощью `date()`
+- ну и выбираем только первые 5 результатов этого вычисления с помощью `LIMIT 5`
+
+### Подсчитываем количество значений за день, без пропусков
+
+[src/experiments_per_day.sql](https://gvwilson.github.io/sql-tutorial/src/experiments_per_day.sql)
+
+```
+ WITH -- complete sequence of days with 0 as placeholder for number of experiments
+ all_days AS (
+ SELECT DATE (
+ (
+ SELECT julianday (MIN(started))
+ FROM experiment
+ ) + VALUE
+ ) AS some_day,
+ 0 AS zeroes
+ FROM (
+ SELECT VALUE
+ FROM generate_series (
+ (
+ SELECT 0
+ ),
+ (
+ SELECT COUNT(*) - 1
+ FROM experiment
+ )
+ )
+ )
+ ), -- sequence of actual days with actual number of experiments started
+ actual_days AS (
+ SELECT started,
+ COUNT(started) AS num_exp
+ FROM experiment
+ GROUP BY started
+ ) -- combined by joining on day and taking actual number (if available) or zero
+ SELECT all_days.some_day AS DAY,
+ COALESCE(actual_days.num_exp, all_days.zeroes) AS num_exp
+ FROM all_days
+LEFT JOIN actual_days ON all_days.some_day = actual_days.started
+ LIMIT 5;
+```
+
+[out/experiments_per_day.out](https://gvwilson.github.io/sql-tutorial/out/experiments_per_day.out)
+
+```
+| day | num_exp |
+|------------|---------|
+| 2023-01-29 | 1 |
+| 2023-01-30 | 1 |
+| 2023-01-31 | 0 |
+| 2023-02-01 | 0 |
+| 2023-02-02 | 1 |
+```
+
+- создаём последовательность всех дней с нулевым значением в качестве заполнителя для количества экспериментов
+- создаём последовательность фактических дней с реальным числом экспериментов
+- объединяем эти последовательности, соединяя их по дням и беря реальное количество (если доступно) или ноль
+- выводит результат, показывая дни (`all_days.some_day`) и соответствующее количество экспериментов (`COALESCE(actual_days.num_exp, all_days.zeroes) AS num_exp`), при этом используется функция `COALESCE`, чтобы использовать фактическое количество экспериментов, если оно доступно, или ноль, если нет
+
+### JOIN таблички с собой же
+
+[src/self_join.sql](https://gvwilson.github.io/sql-tutorial/src/self_join.sql)
+
+```
+ WITH person AS (
+ SELECT ident,
+ personal || ' ' || family AS name
+ FROM staff
+ )
+ SELECT LEFT.name,
+ RIGHT.name
+ FROM person AS
+LEFT JOIN person AS RIGHT
+ LIMIT 10;
+```
+
+[out/self_join.out](https://gvwilson.github.io/sql-tutorial/out/self_join.out)
+
+```
+| name | name |
+|--------------|------------------|
+| Kartik Gupta | Kartik Gupta |
+| Kartik Gupta | Divit Dhaliwal |
+| Kartik Gupta | Indrans Sridhar |
+| Kartik Gupta | Pranay Khanna |
+| Kartik Gupta | Riaan Dua |
+| Kartik Gupta | Vedika Rout |
+| Kartik Gupta | Abram Chokshi |
+| Kartik Gupta | Romil Kapoor |
+| Kartik Gupta | Ishaan Ramaswamy |
+| Kartik Gupta | Nitya Lal |
+```
+
+- создаём временную общую таблицу `person` с помощью WITH
+- объединяем столбцы `personal` и `family` в один столбец `name`
+- при помощи `SELECT` выбираем из `person` значения столбца `name` через алиасы `left` и `right`
+- после этого происходит объединение `person` с собой с помощью оператора `LEFT JOIN`, при этом таблица алиасируется как `RIGHT`
+
+Этот SQL-код, однако, содержит ошибку, правильный синтаксис должен быть следующим:
+
+```
+ WITH person AS (
+ SELECT ident,
+ personal || ' ' || family AS name
+ FROM staff
+ )
+ SELECT LEFT.name,
+ RIGHT.name
+ FROM person AS LEFT
+LEFT JOIN person AS RIGHT ON < условие соединения >
+ LIMIT 10;
+```
+
+В исходном примере условие соединения (`ON`) не было указано
+
+### Генерируем уникальные пары значений
+
+[src/unique_pairs.sql](https://gvwilson.github.io/sql-tutorial/src/unique_pairs.sql)
+
+```
+ WITH person AS (
+ SELECT ident,
+ personal || ' ' || family AS name
+ FROM staff
+ )
+ SELECT LEFT.name,
+ RIGHT.name
+ FROM person AS
+LEFT JOIN person AS RIGHT ON LEFT.ident < RIGHT.ident
+ WHERE LEFT.ident <= 4
+ AND RIGHT.ident <= 4;
+```
+
+[out/unique_pairs.out](https://gvwilson.github.io/sql-tutorial/out/unique_pairs.out)
+
+```
+| name | name |
+|-----------------|-----------------|
+| Kartik Gupta | Divit Dhaliwal |
+| Kartik Gupta | Indrans Sridhar |
+| Kartik Gupta | Pranay Khanna |
+| Divit Dhaliwal | Indrans Sridhar |
+| Divit Dhaliwal | Pranay Khanna |
+| Indrans Sridhar | Pranay Khanna |
+```
+
+- создаём временную таблицу `person`, которая содержит результат выбора из таблицы `staff`
+- из `person` выбираем значения `left.name` и `right.name` с использованием операции слияния (`JOIN`). В этом случае происходит слияние `person` с собой, причем каждая копия `person` используется в качестве левой и правой таблиц соответственно. Слияние выполняется по условию, что идентификатор слева меньше идентификатора справа.
+- затем применяем дополнительное условие с помощью `WHERE`, которое фильтрует результаты `JOIN`-операции. Это условие проверяет, что идентификаторы слева и справа меньше или равны 4
+
+### Фильтрация пар
+
+[src/filter_pairs.sql](https://gvwilson.github.io/sql-tutorial/src/filter_pairs.sql)
+
+```
+ WITH person AS (
+ SELECT ident,
+ personal || ' ' || family AS name
+ FROM staff
+ ),
+ together AS (
+ SELECT LEFT.staff AS left_staff,
+ RIGHT.staff AS right_staff
+ FROM performed AS
+ LEFT JOIN performed AS RIGHT ON LEFT.experiment = RIGHT.experiment
+ WHERE left_staff < right_staff
+ )
+ SELECT LEFT.name AS person_1,
+ RIGHT.name AS person_2
+ FROM person AS
+LEFT JOIN person AS
+ RIGHT JOIN together ON LEFT.ident = left_staff
+ AND RIGHT.ident = right_staff;
+```
+
+[out/filter_pairs.out](https://gvwilson.github.io/sql-tutorial/out/filter_pairs.out)
+
+```
+| person_1 | person_2 |
+|-----------------|------------------|
+| Kartik Gupta | Vedika Rout |
+| Pranay Khanna | Vedika Rout |
+| Indrans Sridhar | Romil Kapoor |
+| Abram Chokshi | Ishaan Ramaswamy |
+| Pranay Khanna | Vedika Rout |
+| Kartik Gupta | Abram Chokshi |
+| Abram Chokshi | Romil Kapoor |
+| Kartik Gupta | Divit Dhaliwal |
+| Divit Dhaliwal | Abram Chokshi |
+| Pranay Khanna | Ishaan Ramaswamy |
+| Indrans Sridhar | Romil Kapoor |
+| Kartik Gupta | Ishaan Ramaswamy |
+| Kartik Gupta | Nitya Lal |
+| Kartik Gupta | Abram Chokshi |
+| Pranay Khanna | Romil Kapoor |
+```
+
+- во временной табличке `person` выбираем имена сотрудников из таблицы `staff`; используем `personal ' ' family AS name`, чтобы объединить значения из столбцов `personal` и `family`
+- временная табличка `together` использует оператор `LEFT JOIN` для объединения таблицы `performed` с собой на основе столбца `experiment`. Затем выбираются пары сотрудников, участвовавших в одном и том же эксперименте, исключая случаи, когда идентификатор левого сотрудника (`left_staff`) больше идентификатора правого сотрудника (`right_staff`)
+- затем выполняется основной `SELECT`, который использует `person` и `together` для объединения имен сотрудников на основе их идентификаторов. Он выполняет два `LEFT JOIN`, чтобы объединить `person` с самим собой и затем объединить результат с `together` на основе идентификаторов сотрудников.
+- затем выбираются имена сотрудников для отображения в итоговом результате.
+
+### EXISTS
+
+[src/correlated_subquery.sql](https://gvwilson.github.io/sql-tutorial/src/correlated_subquery.sql)
+
+```
+SELECT name,
+ building
+FROM department
+WHERE EXISTS
+ (SELECT 1
+ FROM staff
+ WHERE dept = department.ident )
+ORDER BY name;
+```
+
+[out/correlated_subquery.out](https://gvwilson.github.io/sql-tutorial/out/correlated_subquery.out)
+
+```
+| name | building |
+|-------------------|------------------|
+| Genetics | Chesson |
+| Histology | Fashet Extension |
+| Molecular Biology | Chesson |
+```
+
+- выбираем столбцы `name` и `building` из таблицы `department`
+- `WHERE EXISTS (SELECT 1 FROM staff WHERE dept = department.ident )` — используем подзапрос, который проверяет существование хотя бы одной записи в таблице `staff`, для которой значение столбца `dept` совпадает с значением столбца `ident` из таблицы `department`
+- `ORDER BY name` — устанавливаем порядок сортировки результатов по столбцу `name` в алфавитном порядке
+
+### NOT EXISTS в SQL
+
+[src/nonexistence.sql](https://gvwilson.github.io/sql-tutorial/src/nonexistence.sql)
+
+```
+SELECT name,
+ building
+FROM department
+WHERE NOT EXISTS
+ (SELECT 1
+ FROM staff
+ WHERE dept = department.ident )
+ORDER BY name;
+```
+
+[out/nonexistence.out](https://gvwilson.github.io/sql-tutorial/out/nonexistence.out)
+
+```
+| name | building |
+|---------------|----------|
+| Endocrinology | TGVH |
+```
+
+- выбираем столбцы `name` и `building` из таблицы `department`
+- `WHERE NOT EXISTS` — выбираем только те записи из `department`, для которых не существует записей в таблице `staff`
+- `SELECT 1 FROM staff WHERE dept = department.ident` — проверяем, существуют ли записи в таблице `staff`, связанные с отделом из таблицы `department`
+- `ORDER BY name` — сортируем результат по столбцу name
+
+**Избегание коррелированных подзапросов**
+
+[src/avoid_correlated_subqueries.sql](https://gvwilson.github.io/sql-tutorial/src/avoid_correlated_subqueries.sql)
+
+```
+SELECT DISTINCT department.name AS name,
+ department.building AS building
+FROM department
+JOIN staff ON department.ident = staff.dept
+ORDER BY name;
+```
+
+[out/avoid_correlated_subqueries.out](https://gvwilson.github.io/sql-tutorial/out/avoid_correlated_subqueries.out)
+
+```
+| name | building |
+|-------------------|------------------|
+| Genetics | Chesson |
+| Histology | Fashet Extension |
+| Molecular Biology | Chesson |
+```
+
+- `SELECT DISTINCT` — выбираем уникальные значения `name` и `building` из таблицы `department`
+- `JOIN staff ON department.ident = staff.dept` — объединяем таблицы `department` и `staff` на основе условия, что значение столбца `ident` из `department` равно значению `dept` из `staff`
+- `ORDER BY name` — результаты выборки сортируем в алфавитном порядке по столбцу `name`
+
+### Опережение и отставание
+
+[src/lead_lag.sql](https://gvwilson.github.io/sql-tutorial/src/lead_lag.sql)
+
+```
+ WITH ym_num AS (
+ SELECT strftime ('%Y-%m', started) AS ym,
+ COUNT(*) AS num
+ FROM experiment
+ GROUP BY ym
+ )
+ SELECT ym,
+ lag (num) OVER (
+ ORDER BY ym
+ ) AS prev_num,
+ num,
+ lead (num) OVER (
+ ORDER BY ym
+ ) AS next_num
+ FROM ym_num
+ ORDER BY ym;
+```
+
+- в этой таблице извлекается случайное число с помощью `random()`
+- конкатенируем значения `personal` и `family` под именем `name` с помощью `' '` для разделения
+- таким образом создаём временную таблицу, содержащую столбцы `rand` с случайными числами и `name` со значениями из столбцов `personal` и `family` таблицы `staff`
+- делаем выборку из временной таблицы `decorated`; в выборку включаем столбцы `rand`, `name`
+- `abs(rand) % 10` — это мы вычисляем остаток от деления абсолютного значения `rand` на 10
+- ну и в конце оставляем только строки, где `selector` меньше 5
+
+### Создание индексов
+
+[src/create_use_index.sql](https://gvwilson.github.io/sql-tutorial/src/create_use_index.sql)
+
+```
+EXPLAIN query PLAN
+SELECT filename
+FROM plate
+WHERE filename like '%07%';
+
+CREATE INDEX plate_file ON plate(filename);
+
+EXPLAIN query PLAN
+SELECT filename
+FROM plate
+WHERE filename like '%07%';
+```
+
+[out/create_use_index.out](https://gvwilson.github.io/sql-tutorial/out/create_use_index.out)
+
+```
+QUERY PLAN
+`--SCAN plate USING COVERING INDEX sqlite_autoindex_plate_1
+QUERY PLAN
+`--SCAN plate USING COVERING INDEX plate_file
+```
+
+- выбираем все значения столбца `filename` из таблицы `plate`, где значение столбца `filename` содержит подстроку `07`
+- создаём индекс с именем `plate_file` для столбца `filename` в таблице `plate`
+- запрашиваем план выполнения запроса (`EXPLAIN query PLAN`)
+
+### Генерация последовательности значений
+
+[src/generate_sequence.sql](https://gvwilson.github.io/sql-tutorial/src/generate_sequence.sql)
+
+```
+SELECT value
+FROM generate_series(1, 5);
+```
+
+[out/generate_sequence.out](https://gvwilson.github.io/sql-tutorial/out/generate_sequence.out)
+
+```
+| value |
+|-------|
+| 1 |
+| 2 |
+| 3 |
+| 4 |
+| 5 |
+```
+
+- `generate_series(1, 5)` — генерируем ряд чисел от 1 до 5
+- `SELECT value` — выбираем этот столбец `value` со сгенерированными числами от 1 до 5
+
+### Генерируем последовательность на основе данных
+
+[src/data_range_sequence.sql](https://gvwilson.github.io/sql-tutorial/src/data_range_sequence.sql)
+
+```
+CREATE TABLE temp (num integer NOT NULL);
+
+INSERT INTO temp
+VALUES (1),
+ (5);
+
+SELECT value
+FROM generate_series ((SELECT min(num)
+ FROM TEMP),
+ (SELECT max(num)
+ FROM TEMP));
+```
+
+[out/data_range_sequence.out](https://gvwilson.github.io/sql-tutorial/out/data_range_sequence.out)
+
+```
+| value |
+|-------|
+| 1 |
+| 2 |
+| 3 |
+| 4 |
+| 5 |
+```
+
+- создаём временную таблицу `temp`, которая содержит 1 столбец с именем `num` типа integer; этот столбец не может быть пустым
+- помещаем в `temp` значения 1 и 5 в столбец `num`
+- используем `generate_series` для создания последовательности чисел между минимальным и максимальным значениями из столбца `num` в таблице `temp`
+
+### Генерация последовательностей дат
+
+[src/date_sequence.sql](https://gvwilson.github.io/sql-tutorial/src/date_sequence.sql)
+
+```
+SELECT date(
+ (SELECT julianday(min(started))
+ FROM experiment) + value) AS some_day
+FROM
+ (SELECT value
+ FROM generate_series(
+ (SELECT 0),
+ (SELECT count(*) - 1
+ FROM experiment)))
+LIMIT 5;
+```
+
+[out/date_sequence.out](https://gvwilson.github.io/sql-tutorial/out/date_sequence.out)
+
+```
+| some_day |
+|------------|
+| 2023-01-29 |
+| 2023-01-30 |
+| 2023-01-31 |
+| 2023-02-01 |
+| 2023-02-02 |
+```
+
+- `SELECT julianday(min(started)) FROM experiment` — находим минимальную дату в столбце `started` из `experiment`, преобразуем её в `Julian` день (числовое представление даты) и возвращаем этот `Julian` день
+- внешним подзапросом вычисляем разницу между этим минимальным `Julian` днем и каждым `value` из `generate_series`
+- затем складываем эти разницы с минимальным `Julian` днем, и конвертируем обратно в дату с помощью `date()`
+- ну и выбираем только первые 5 результатов этого вычисления с помощью `LIMIT 5`
+
+### Подсчитываем количество значений за день, без пропусков
+
+[src/experiments_per_day.sql](https://gvwilson.github.io/sql-tutorial/src/experiments_per_day.sql)
+
+```
+ WITH -- complete sequence of days with 0 as placeholder for number of experiments
+ all_days AS (
+ SELECT DATE (
+ (
+ SELECT julianday (MIN(started))
+ FROM experiment
+ ) + VALUE
+ ) AS some_day,
+ 0 AS zeroes
+ FROM (
+ SELECT VALUE
+ FROM generate_series (
+ (
+ SELECT 0
+ ),
+ (
+ SELECT COUNT(*) - 1
+ FROM experiment
+ )
+ )
+ )
+ ), -- sequence of actual days with actual number of experiments started
+ actual_days AS (
+ SELECT started,
+ COUNT(started) AS num_exp
+ FROM experiment
+ GROUP BY started
+ ) -- combined by joining on day and taking actual number (if available) or zero
+ SELECT all_days.some_day AS DAY,
+ COALESCE(actual_days.num_exp, all_days.zeroes) AS num_exp
+ FROM all_days
+LEFT JOIN actual_days ON all_days.some_day = actual_days.started
+ LIMIT 5;
+```
+
+[out/experiments_per_day.out](https://gvwilson.github.io/sql-tutorial/out/experiments_per_day.out)
+
+```
+| day | num_exp |
+|------------|---------|
+| 2023-01-29 | 1 |
+| 2023-01-30 | 1 |
+| 2023-01-31 | 0 |
+| 2023-02-01 | 0 |
+| 2023-02-02 | 1 |
+```
+
+- создаём последовательность всех дней с нулевым значением в качестве заполнителя для количества экспериментов
+- создаём последовательность фактических дней с реальным числом экспериментов
+- объединяем эти последовательности, соединяя их по дням и беря реальное количество (если доступно) или ноль
+- выводит результат, показывая дни (`all_days.some_day`) и соответствующее количество экспериментов (`COALESCE(actual_days.num_exp, all_days.zeroes) AS num_exp`), при этом используется функция `COALESCE`, чтобы использовать фактическое количество экспериментов, если оно доступно, или ноль, если нет
+
+### JOIN таблички с собой же
+
+[src/self_join.sql](https://gvwilson.github.io/sql-tutorial/src/self_join.sql)
+
+```
+ WITH person AS (
+ SELECT ident,
+ personal || ' ' || family AS name
+ FROM staff
+ )
+ SELECT LEFT.name,
+ RIGHT.name
+ FROM person AS
+LEFT JOIN person AS RIGHT
+ LIMIT 10;
+```
+
+[out/self_join.out](https://gvwilson.github.io/sql-tutorial/out/self_join.out)
+
+```
+| name | name |
+|--------------|------------------|
+| Kartik Gupta | Kartik Gupta |
+| Kartik Gupta | Divit Dhaliwal |
+| Kartik Gupta | Indrans Sridhar |
+| Kartik Gupta | Pranay Khanna |
+| Kartik Gupta | Riaan Dua |
+| Kartik Gupta | Vedika Rout |
+| Kartik Gupta | Abram Chokshi |
+| Kartik Gupta | Romil Kapoor |
+| Kartik Gupta | Ishaan Ramaswamy |
+| Kartik Gupta | Nitya Lal |
+```
+
+- создаём временную общую таблицу `person` с помощью WITH
+- объединяем столбцы `personal` и `family` в один столбец `name`
+- при помощи `SELECT` выбираем из `person` значения столбца `name` через алиасы `left` и `right`
+- после этого происходит объединение `person` с собой с помощью оператора `LEFT JOIN`, при этом таблица алиасируется как `RIGHT`
+
+Этот SQL-код, однако, содержит ошибку, правильный синтаксис должен быть следующим:
+
+```
+ WITH person AS (
+ SELECT ident,
+ personal || ' ' || family AS name
+ FROM staff
+ )
+ SELECT LEFT.name,
+ RIGHT.name
+ FROM person AS LEFT
+LEFT JOIN person AS RIGHT ON < условие соединения >
+ LIMIT 10;
+```
+
+В исходном примере условие соединения (`ON`) не было указано
+
+### Генерируем уникальные пары значений
+
+[src/unique_pairs.sql](https://gvwilson.github.io/sql-tutorial/src/unique_pairs.sql)
+
+```
+ WITH person AS (
+ SELECT ident,
+ personal || ' ' || family AS name
+ FROM staff
+ )
+ SELECT LEFT.name,
+ RIGHT.name
+ FROM person AS
+LEFT JOIN person AS RIGHT ON LEFT.ident < RIGHT.ident
+ WHERE LEFT.ident <= 4
+ AND RIGHT.ident <= 4;
+```
+
+[out/unique_pairs.out](https://gvwilson.github.io/sql-tutorial/out/unique_pairs.out)
+
+```
+| name | name |
+|-----------------|-----------------|
+| Kartik Gupta | Divit Dhaliwal |
+| Kartik Gupta | Indrans Sridhar |
+| Kartik Gupta | Pranay Khanna |
+| Divit Dhaliwal | Indrans Sridhar |
+| Divit Dhaliwal | Pranay Khanna |
+| Indrans Sridhar | Pranay Khanna |
+```
+
+- создаём временную таблицу `person`, которая содержит результат выбора из таблицы `staff`
+- из `person` выбираем значения `left.name` и `right.name` с использованием операции слияния (`JOIN`). В этом случае происходит слияние `person` с собой, причем каждая копия `person` используется в качестве левой и правой таблиц соответственно. Слияние выполняется по условию, что идентификатор слева меньше идентификатора справа.
+- затем применяем дополнительное условие с помощью `WHERE`, которое фильтрует результаты `JOIN`-операции. Это условие проверяет, что идентификаторы слева и справа меньше или равны 4
+
+### Фильтрация пар
+
+[src/filter_pairs.sql](https://gvwilson.github.io/sql-tutorial/src/filter_pairs.sql)
+
+```
+ WITH person AS (
+ SELECT ident,
+ personal || ' ' || family AS name
+ FROM staff
+ ),
+ together AS (
+ SELECT LEFT.staff AS left_staff,
+ RIGHT.staff AS right_staff
+ FROM performed AS
+ LEFT JOIN performed AS RIGHT ON LEFT.experiment = RIGHT.experiment
+ WHERE left_staff < right_staff
+ )
+ SELECT LEFT.name AS person_1,
+ RIGHT.name AS person_2
+ FROM person AS
+LEFT JOIN person AS
+ RIGHT JOIN together ON LEFT.ident = left_staff
+ AND RIGHT.ident = right_staff;
+```
+
+[out/filter_pairs.out](https://gvwilson.github.io/sql-tutorial/out/filter_pairs.out)
+
+```
+| person_1 | person_2 |
+|-----------------|------------------|
+| Kartik Gupta | Vedika Rout |
+| Pranay Khanna | Vedika Rout |
+| Indrans Sridhar | Romil Kapoor |
+| Abram Chokshi | Ishaan Ramaswamy |
+| Pranay Khanna | Vedika Rout |
+| Kartik Gupta | Abram Chokshi |
+| Abram Chokshi | Romil Kapoor |
+| Kartik Gupta | Divit Dhaliwal |
+| Divit Dhaliwal | Abram Chokshi |
+| Pranay Khanna | Ishaan Ramaswamy |
+| Indrans Sridhar | Romil Kapoor |
+| Kartik Gupta | Ishaan Ramaswamy |
+| Kartik Gupta | Nitya Lal |
+| Kartik Gupta | Abram Chokshi |
+| Pranay Khanna | Romil Kapoor |
+```
+
+- во временной табличке `person` выбираем имена сотрудников из таблицы `staff`; используем `personal ' ' family AS name`, чтобы объединить значения из столбцов `personal` и `family`
+- временная табличка `together` использует оператор `LEFT JOIN` для объединения таблицы `performed` с собой на основе столбца `experiment`. Затем выбираются пары сотрудников, участвовавших в одном и том же эксперименте, исключая случаи, когда идентификатор левого сотрудника (`left_staff`) больше идентификатора правого сотрудника (`right_staff`)
+- затем выполняется основной `SELECT`, который использует `person` и `together` для объединения имен сотрудников на основе их идентификаторов. Он выполняет два `LEFT JOIN`, чтобы объединить `person` с самим собой и затем объединить результат с `together` на основе идентификаторов сотрудников.
+- затем выбираются имена сотрудников для отображения в итоговом результате.
+
+### EXISTS
+
+[src/correlated_subquery.sql](https://gvwilson.github.io/sql-tutorial/src/correlated_subquery.sql)
+
+```
+SELECT name,
+ building
+FROM department
+WHERE EXISTS
+ (SELECT 1
+ FROM staff
+ WHERE dept = department.ident )
+ORDER BY name;
+```
+
+[out/correlated_subquery.out](https://gvwilson.github.io/sql-tutorial/out/correlated_subquery.out)
+
+```
+| name | building |
+|-------------------|------------------|
+| Genetics | Chesson |
+| Histology | Fashet Extension |
+| Molecular Biology | Chesson |
+```
+
+- выбираем столбцы `name` и `building` из таблицы `department`
+- `WHERE EXISTS (SELECT 1 FROM staff WHERE dept = department.ident )` — используем подзапрос, который проверяет существование хотя бы одной записи в таблице `staff`, для которой значение столбца `dept` совпадает с значением столбца `ident` из таблицы `department`
+- `ORDER BY name` — устанавливаем порядок сортировки результатов по столбцу `name` в алфавитном порядке
+
+### NOT EXISTS в SQL
+
+[src/nonexistence.sql](https://gvwilson.github.io/sql-tutorial/src/nonexistence.sql)
+
+```
+SELECT name,
+ building
+FROM department
+WHERE NOT EXISTS
+ (SELECT 1
+ FROM staff
+ WHERE dept = department.ident )
+ORDER BY name;
+```
+
+[out/nonexistence.out](https://gvwilson.github.io/sql-tutorial/out/nonexistence.out)
+
+```
+| name | building |
+|---------------|----------|
+| Endocrinology | TGVH |
+```
+
+- выбираем столбцы `name` и `building` из таблицы `department`
+- `WHERE NOT EXISTS` — выбираем только те записи из `department`, для которых не существует записей в таблице `staff`
+- `SELECT 1 FROM staff WHERE dept = department.ident` — проверяем, существуют ли записи в таблице `staff`, связанные с отделом из таблицы `department`
+- `ORDER BY name` — сортируем результат по столбцу name
+
+**Избегание коррелированных подзапросов**
+
+[src/avoid_correlated_subqueries.sql](https://gvwilson.github.io/sql-tutorial/src/avoid_correlated_subqueries.sql)
+
+```
+SELECT DISTINCT department.name AS name,
+ department.building AS building
+FROM department
+JOIN staff ON department.ident = staff.dept
+ORDER BY name;
+```
+
+[out/avoid_correlated_subqueries.out](https://gvwilson.github.io/sql-tutorial/out/avoid_correlated_subqueries.out)
+
+```
+| name | building |
+|-------------------|------------------|
+| Genetics | Chesson |
+| Histology | Fashet Extension |
+| Molecular Biology | Chesson |
+```
+
+- `SELECT DISTINCT` — выбираем уникальные значения `name` и `building` из таблицы `department`
+- `JOIN staff ON department.ident = staff.dept` — объединяем таблицы `department` и `staff` на основе условия, что значение столбца `ident` из `department` равно значению `dept` из `staff`
+- `ORDER BY name` — результаты выборки сортируем в алфавитном порядке по столбцу `name`
+
+### Опережение и отставание
+
+[src/lead_lag.sql](https://gvwilson.github.io/sql-tutorial/src/lead_lag.sql)
+
+```
+ WITH ym_num AS (
+ SELECT strftime ('%Y-%m', started) AS ym,
+ COUNT(*) AS num
+ FROM experiment
+ GROUP BY ym
+ )
+ SELECT ym,
+ lag (num) OVER (
+ ORDER BY ym
+ ) AS prev_num,
+ num,
+ lead (num) OVER (
+ ORDER BY ym
+ ) AS next_num
+ FROM ym_num
+ ORDER BY ym;
+```
+
+[out/lead_lag.out](https://gvwilson.github.io/sql-tutorial/out/lead_lag.out)
+
+```
+| ym | prev_num | num | next_num |
+|---------|----------|-----|----------|
+| 2023-01 | | 2 | 5 |
+| 2023-02 | 2 | 5 | 5 |
+| 2023-03 | 5 | 5 | 1 |
+| 2023-04 | 5 | 1 | 6 |
+| 2023-05 | 1 | 6 | 5 |
+| 2023-06 | 6 | 5 | 3 |
+| 2023-07 | 5 | 3 | 2 |
+| 2023-08 | 3 | 2 | 4 |
+| 2023-09 | 2 | 4 | 6 |
+| 2023-10 | 4 | 6 | 4 |
+| 2023-12 | 6 | 4 | 5 |
+| 2024-01 | 4 | 5 | 2 |
+| 2024-02 | 5 | 2 | |
+```
+
+- создаём временную таблицу `ym_num` из 2 столбцов: `ym` (год-месяц ‘YYYY-MM’) и `num` (количество записей в каждом месяце)
+- используем SQLite `strftime` для извлечения года и месяца из `started`, агрегируем результаты с помощью `GROUP BY`
+- в основном запросе выбираем данные из `ym_num`, выполняем следующие операции и получаем год-месяц `ym`, количество записей в предыдущем месяце `(lag)num`, текущее количество записей `num` и количество записей в следующем месяце `(lead)num`
+- результаты упорядочиваем по столбцу `ym` (год-месяц)
+
+### Оконные функции
+
+[src/window_functions.sql](https://gvwilson.github.io/sql-tutorial/src/window_functions.sql)
+
+```
+ WITH ym_num AS (
+ SELECT strftime ('%Y-%m', started) AS ym,
+ COUNT(*) AS num
+ FROM experiment
+ GROUP BY ym
+ )
+ SELECT ym,
+ num,
+ SUM(num) OVER (
+ ORDER BY ym
+ ) AS num_done,
+ CUME_DIST() OVER (
+ ORDER BY ym
+ ) AS progress
+ FROM ym_num
+ ORDER BY ym;
+```
+
+[out/window_functions.out](https://gvwilson.github.io/sql-tutorial/out/window_functions.out)
+
+```
+| ym | num | num_done | progress |
+|---------|-----|----------|--------------------|
+| 2023-01 | 2 | 2 | 0.0769230769230769 |
+| 2023-02 | 5 | 7 | 0.153846153846154 |
+| 2023-03 | 5 | 12 | 0.230769230769231 |
+| 2023-04 | 1 | 13 | 0.307692307692308 |
+| 2023-05 | 6 | 19 | 0.384615384615385 |
+| 2023-06 | 5 | 24 | 0.461538461538462 |
+| 2023-07 | 3 | 27 | 0.538461538461538 |
+| 2023-08 | 2 | 29 | 0.615384615384615 |
+| 2023-09 | 4 | 33 | 0.692307692307692 |
+| 2023-10 | 6 | 39 | 0.769230769230769 |
+| 2023-12 | 4 | 43 | 0.846153846153846 |
+| 2024-01 | 5 | 48 | 0.923076923076923 |
+| 2024-02 | 2 | 50 | 1.0 |
+```
+
+- создаём временную таблицу `ym_num`, которая содержит: `ym` — год и месяц, извлеченные из `started` в `experiment` с помощью `strftime('%Y-%m')`; `num` — количество записей в `experiment` для каждого сочетания года и месяца
+- выбираем `ym` и `num` из таблицы `ym_num`, добавляем 2 дополнительных столбца: `num_done` — сумма количества экспериментов по всем предыдущим годам и месяцам (`sum(num) OVER (ORDER BY ym)`); `progress` — кумулятивное распределение количества экспериментов по всем предыдущим годам и месяцам (`cume_dist() OVER (ORDER BY ym)`)
+- упорядочиваем результаты по столбцу `ym` (год и месяц)
+
+**Внезапно небольшое задание: объясните, что делает запрос ниже**
+
+[src/explain_window_function.sql](https://gvwilson.github.io/sql-tutorial/src/explain_window_function.sql)
+
+```
+EXPLAIN query PLAN
+ WITH ym_num AS (
+ SELECT strftime ('%Y-%m', started) AS ym,
+ COUNT(*) AS num
+ FROM experiment
+ GROUP BY ym
+ )
+ SELECT ym,
+ num,
+ SUM(num) OVER (
+ ORDER BY ym
+ ) AS num_done,
+ CUME_DIST() OVER (
+ ORDER BY ym
+ ) AS progress
+ FROM ym_num
+ ORDER BY ym;
+```
+
+[out/explain_window_function.out](https://gvwilson.github.io/sql-tutorial/out/explain_window_function.out)
+
+```
+QUERY PLAN
+|--CO-ROUTINE (subquery-3)
+| |--CO-ROUTINE (subquery-4)
+| | |--CO-ROUTINE ym_num
+| | | |--SCAN experiment
+| | | `--USE TEMP B-TREE FOR GROUP BY
+| | |--SCAN ym_num
+| | `--USE TEMP B-TREE FOR ORDER BY
+| `--SCAN (subquery-4)
+`--SCAN (subquery-3)
+```
+
+- создаём временную табличку `ym_num` с результатами агрегирования по месяцам, где данные из `started`преобразуются в формат год-месяц (`strftime('%Y-%m', started) AS ym`) и подсчитываем количество событий (`count(*) AS num`)
+- группируем результаты по полю `ym`
+- выбираем поля `ym` и `num` из `ym_num` и добавляем 2 дополнительных поля: `num_done` и `progress`; `num_done` — общее количество событий/мес, сгруппированных в порядке увеличения месяца; поле `progress` — прогресс в процентном соотношении относительно общего числа записей (`cume_dist()`)
+- в итоге выводим данные в порядке увеличения значения `ym` (год-месяц)
+
+### Используем PARTITION BY в SQL
+
+[src/partition_window.sql](https://gvwilson.github.io/sql-tutorial/src/partition_window.sql)
+
+```
+WITH y_m_num AS
+ (SELECT strftime('%Y', started) AS YEAR,
+ strftime('%m', started) AS MONTH,
+ count(*) AS num
+ FROM experiment
+ GROUP BY YEAR,
+ MONTH)
+SELECT YEAR,
+ MONTH,
+ num,
+ sum(num) OVER (PARTITION BY YEAR
+ ORDER BY MONTH) AS num_done
+FROM y_m_num
+ORDER BY YEAR,
+ MONTH;
+```
+
+[out/partition_window.out](https://gvwilson.github.io/sql-tutorial/out/partition_window.out)
+
+```
+| year | month | num | num_done |
+|------|-------|-----|----------|
+| 2023 | 01 | 2 | 2 |
+| 2023 | 02 | 5 | 7 |
+| 2023 | 03 | 5 | 12 |
+| 2023 | 04 | 1 | 13 |
+| 2023 | 05 | 6 | 19 |
+| 2023 | 06 | 5 | 24 |
+| 2023 | 07 | 3 | 27 |
+| 2023 | 08 | 2 | 29 |
+| 2023 | 09 | 4 | 33 |
+| 2023 | 10 | 6 | 39 |
+| 2023 | 12 | 4 | 43 |
+| 2024 | 01 | 5 | 5 |
+| 2024 | 02 | 2 | 7 |
+```
+
+- создаём временную таблицу `y_m_num` с тремя столбцами: `YEAR,` `MONTH` и `num`.
+- временную табличку заполняем записями из `experiment`. Для каждой записи определяем год и месяц даты в столбце `started` (через `strftime`), считаем количество записей (`count(*)`) для каждого года и месяца, группируем результаты по году и месяцу
+- выбираем данные из `y_m_num`, добавляем столбец `num_done` — накопительное значение для `num` в пределах каждого года `sum(num) OVER (PARTITION BY YEAR ORDER BY MONTH)` — суммируем значение `num` для каждого месяца при сортировке по месяцам внутри каждого года
+- в итоге сортируем результаты по году и месяцу с помощью `ORDER BY YEAR, MONTH`
+
+### Данные типа blob
+
+[src/blob.sql](https://gvwilson.github.io/sql-tutorial/src/blob.sql)
+
+```
+CREATE TABLE images (name text NOT NULL,
+ content blob);
+
+INSERT INTO images(name, content)
+VALUES ("biohazard", readfile("img/biohazard.png")),
+ ("crush", readfile("img/crush.png")),
+ ("fire", readfile("img/fire.png")),
+ ("radioactive", readfile("img/radioactive.png")),
+ ("tripping", readfile("img/tripping.png"));
+
+SELECT name,
+ length(content)
+FROM images;
+```
+
+[out/blob.out](https://gvwilson.github.io/sql-tutorial/out/blob.out)
+
+```
+| name | length(content) |
+|-------------|-----------------|
+| biohazard | 19629 |
+| crush | 15967 |
+| fire | 18699 |
+| radioactive | 16661 |
+| tripping | 17208 |
+```
+
+- создаём таблицу `images` со столбцами: `name` — текстовый столбец, не может быть пустым; `content` — столбец двоичных данных (`blob`)
+- вставляем 5 пар `name`–`blob` в `images` с помощью `INSERT INTO`
+- `readfile` читает содержимое файла `name` и возвращает его как двоичные данные `blob`
+- выполняем выборку данных из `images` с помощью `SELECT`, получая значения `name` и вычисляя количество байт двоичных данных в `content`
+
+**Ещё одна БД**
+
+[src/lab_log_db.sh](https://gvwilson.github.io/sql-tutorial/src/lab_log_db.sh)
+
+```
+sqlite3 data/lab_log.db
+```
+
+[src/lab_log_schema.sql](https://gvwilson.github.io/sql-tutorial/src/lab_log_schema.sql)
+
+```
+.schema
+```
+
+[out/lab_log_schema.out](https://gvwilson.github.io/sql-tutorial/out/lab_log_schema.out)
+
+```
+CREATE TABLE sqlite_sequence(name,
+ seq);
+
+CREATE TABLE person(ident integer PRIMARY KEY autoincrement,
+ details text NOT NULL);
+
+CREATE TABLE machine(ident integer PRIMARY KEY autoincrement,
+ name text NOT NULL,
+ details text NOT NULL);
+
+CREATE TABLE usage(ident integer PRIMARY KEY autoincrement,
+ log text NOT NULL);
+```
+
+- создаём `sqlite_sequence` со столбцами `name` и `seq` (для значения счетчика, он используется в качестве `AUTOINCREMENT`)
+- создаём `person` со столбцами `ident` (целочисленный, является первичным ключом (`PRIMARY KEY`), автоматически инкрементируется) и `details` (текстовый столбец, не может иметь значение `NULL`)
+- создаём `machine` со столбцами `ident` (целочисленный, является первичным ключом (`PRIMARY KEY`), автоматически инкрементируется), `name` (текстовый, не может иметь значение `NULL`), `details`(текстовый, не может иметь значение `NULL`)
+- создаём `usage` со столбцами `ident` (целочисленный, является первичным ключом (`PRIMARY KEY`), автоматически инкрементируется) и `log` (текстовый столбец, не может иметь значение `NULL`)
+
+### Сохранение JSON
+
+[src/json_in_table.sql](https://gvwilson.github.io/sql-tutorial/src/json_in_table.sql)
+
+```
+SELECT *
+FROM machine;
+```
+
+[out/json_in_table.out](https://gvwilson.github.io/sql-tutorial/out/json_in_table.out)
+
+```
+| ident | name | details |
+|-------|----------------|---------------------------------------------------------|
+| 1 | WY401 | {"acquired": "2023-05-01"} |
+| 2 | Inphormex | {"acquired": "2021-07-15", "refurbished": "2023-10-22"} |
+| 3 | AutoPlate 9000 | {"note": "needs software update"} |
+```
+
+### Выбираем отдельные поля в JSON
+
+[src/json_field.sql](https://gvwilson.github.io/sql-tutorial/src/json_field.sql)
+
+```
+SELECT details->'$.acquired' AS single_arrow,
+ details->>'$.acquired' AS double_arrow
+FROM machine;
+```
+
+[out/json_field.out](https://gvwilson.github.io/sql-tutorial/out/json_field.out)
+
+```
+| single_arrow | double_arrow |
+|--------------|--------------|
+| "2023-05-01" | 2023-05-01 |
+| "2021-07-15" | 2021-07-15 |
+| | |
+```
+
+- `details->'$.acquired' AS single_arrow` — с помощью `->` извлекаем значение JSON поля `acquired` из столбца `details` для каждой строки из `machine`, обозначаем его как `single_arrow`
+- `details->>'$.acquired' AS double_arrow` — оператор `->>` также используется для извлечения JSON по указанному пути, но возвращает текст, в отличие от `->`, который возвращает JSON значение; здесь мы извлекаем значение JSON поля `acquired` из столбца `details` для каждой строки из `machine`, обозначаем его как `double_arrow`
+
+### Доступ к JSON-объекту
+
+[src/json_array.sql](https://gvwilson.github.io/sql-tutorial/src/json_array.sql)
+
+```
+SELECT ident,
+ json_array_length(log->'$') AS LENGTH,
+ log->'$[0]' AS FIRST
+FROM USAGE;
+```
+
+[out/json_array.out](https://gvwilson.github.io/sql-tutorial/out/json_array.out)
+
+```
+| ident | length | first |
+|-------|--------|--------------------------------------------------------------|
+| 1 | 4 | {"machine":"Inphormex","person":["Gabrielle","Dub\u00e9"]} |
+| 2 | 5 | {"machine":"Inphormex","person":["Marianne","Richer"]} |
+| 3 | 2 | {"machine":"sterilizer","person":["Josette","Villeneuve"]} |
+| 4 | 1 | {"machine":"sterilizer","person":["Maude","Goulet"]} |
+| 5 | 2 | {"machine":"AutoPlate 9000","person":["Brigitte","Michaud"]} |
+| 6 | 1 | {"machine":"sterilizer","person":["Marianne","Richer"]} |
+| 7 | 3 | {"machine":"WY401","person":["Maude","Goulet"]} |
+| 8 | 1 | {"machine":"AutoPlate 9000"} |
+```
+
+- `json_array_length(log->'$') AS LENGTH` — вычисляем длину массива, находящегося внутри JSON-объекта в столбце `log`; используем оператор `->`, чтобы получить массив из корневого уровня JSON-объекта, и `json_array_length` для подсчета количества элементов в этом массиве; результат помещаем в столбец с именем `LENGTH`
+- `log->'$[0]' AS FIRST` — извлекаем первый элемент из массива, указанного в корневом уровне JSON-объекта в столбце `log`; используем оператор `->`, чтобы получить доступ к массиву, и указываем индекс элемента в квадратных скобках; результат сохраняем в столбец `FIRST`
+
+### Распаковка JSON
+
+[src/json_unpack.sql](https://gvwilson.github.io/sql-tutorial/src/json_unpack.sql)
+
+```
+SELECT ident,
+ json_each.key AS KEY,
+ json_each.value AS value
+FROM USAGE,
+ json_each(usage.log)
+LIMIT 10;
+```
+
+[out/json_unpack.out](https://gvwilson.github.io/sql-tutorial/out/json_unpack.out)
+
+```
+| ident | key | value |
+|-------|-----|--------------------------------------------------------------|
+| 1 | 0 | {"machine":"Inphormex","person":["Gabrielle","Dub\u00e9"]} |
+| 1 | 1 | {"machine":"Inphormex","person":["Gabrielle","Dub\u00e9"]} |
+| 1 | 2 | {"machine":"WY401","person":["Gabrielle","Dub\u00e9"]} |
+| 1 | 3 | {"machine":"Inphormex","person":["Gabrielle","Dub\u00e9"]} |
+| 2 | 0 | {"machine":"Inphormex","person":["Marianne","Richer"]} |
+| 2 | 1 | {"machine":"AutoPlate 9000","person":["Marianne","Richer"]} |
+| 2 | 2 | {"machine":"sterilizer","person":["Marianne","Richer"]} |
+| 2 | 3 | {"machine":"AutoPlate 9000","person":["Monique","Marcotte"]} |
+| 2 | 4 | {"machine":"sterilizer","person":["Marianne","Richer"]} |
+| 3 | 0 | {"machine":"sterilizer","person":["Josette","Villeneuve"]} |
+```
+
+- `SELECT ident, json_each.key AS KEY, json_each.value AS value` — определяем, что нужно выбрать из таблицы `usage` и JSON-объектов, распарсенных с помощью функции `json_each`; из каждой строки выбираем идентификатор, а также ключ и его значение из каждого JSON-объекта в столбце `log`
+- `FROM usage, json_each(usage.log)` — указываем источник данных для выборки; `usage` указывается после ключевого слова FROM, а `json_each` вызывается перед `log`, чтобы разобрать JSON-объекты из этого столбца
+- `LIMIT 10` — выбираем только первые 10 строк
+
+### Последний элемент в массиве
+
+[src/json_array_last.sql](https://gvwilson.github.io/sql-tutorial/src/json_array_last.sql)
+
+```
+SELECT ident,
+ log->'$[#-1].machine' AS FINAL
+FROM USAGE
+LIMIT 5;
+```
+
+[out/json_array_last.out](https://gvwilson.github.io/sql-tutorial/out/json_array_last.out)
+
+```
+| ident | final |
+|-------|--------------|
+| 1 | "Inphormex" |
+| 2 | "sterilizer" |
+| 3 | "Inphormex" |
+| 4 | "sterilizer" |
+| 5 | "sterilizer" |
+```
+
+- `SELECT ident, log->'$[#-1].machine' AS FINAL` — выбираем 2 столбца из `machine`; `ident`возвращается как есть, а столбец `log` обрабатывается так:
+ - `log->'$[#-1].machine` — извлекаем данные из столбца `log` (`->` используется для доступа к JSON-полю в столбце `log`)
+ - `$[#-1]` — обращаемся к последнему элементу массива, который хранится в `log`
+ - `.machine` — хотим извлечь значение поля `machine` из объекта, находящегося в последнем элементе массива
+
+### Модифицируем JSON
+
+[src/json_modify.sql](https://gvwilson.github.io/sql-tutorial/src/json_modify.sql)
+
+```
+SELECT ident,
+ name,
+ json_set(details, '$.sold', json_quote('2024-01-25')) AS updated
+FROM machine;
+```
+
+[out/json_modify.out](https://gvwilson.github.io/sql-tutorial/out/json_modify.out)
+
+```
+| ident | name | updated |
+|-------|----------------|--------------------------------------------------------------|
+| 1 | WY401 | {"acquired":"2023-05-01","sold":"2024-01-25"} |
+| 2 | Inphormex | {"acquired":"2021-07-15","refurbished":"2023-10-22","sold":" |
+| | | 2024-01-25"} |
+| 3 | AutoPlate 9000 | {"note":"needs software update","sold":"2024-01-25"} |
+```
+
+- `SELECT ident, name, ... FROM machine;` — выбираем значения столбцов `ident` и `name` из таблицы `machine`
+- `json_set(details, '$.sold', json_quote('2024-01-25')) AS updated` — при помощи `json_set`обновляем JSON-объект в столбце `details`; функция добавляет/изменяет свойство `sold` в JSON-объекте в столбце `details`, присваивая ему новое значение, полученное с помощью функции `json_quote`; результат сохраняем как `updated`
+
+**Обновляем табличку penguins:**
+
+[src/count_penguins.sql](https://gvwilson.github.io/sql-tutorial/src/count_penguins.sql)
+
+```
+SELECT species,
+ count(*) AS num
+FROM penguins
+GROUP BY species;
+```
+
+[out/count_penguins.out](https://gvwilson.github.io/sql-tutorial/out/count_penguins.out)
+
+```
+| species | num |
+|-----------|-----|
+| Adelie | 152 |
+| Chinstrap | 68 |
+| Gentoo | 124 |
+```
+
+### Immediate If в SQL
+
+[src/make_active.sql](https://gvwilson.github.io/sql-tutorial/src/make_active.sql)
+
+```
+ALTER TABLE penguins ADD active integer NOT NULL DEFAULT 1;
+
+UPDATE penguins
+SET active = iif(species = 'Adelie', 0, 1);
+```
+
+- изменяем таблицу `penguins`, добавляя новый столбец `active` типа `integer`, который не может содержать значение `NULL`, и устанавливаем значение по умолчанию 1 для всех строк
+- обновляем значения в столбце `active` в `penguins`; значение столбца `active` устанавливается на 0, если значение в `species` равно `'Adelie'`, иначе устанавливается на 1
+- функция `IIF` (Immediate If) используется здесь для реализации условного выражения (1 аргумент – условие, 2 – результат, если условие истинно, и 3 – результат, если условие ложно)
+
+[src/active_penguins.sql](https://gvwilson.github.io/sql-tutorial/src/active_penguins.sql)
+
+```
+SELECT species,
+ count(*) AS num
+FROM penguins
+WHERE active
+GROUP BY species;
+```
+
+[out/active_penguins.out](https://gvwilson.github.io/sql-tutorial/out/active_penguins.out)
+
+```
+| species | num |
+|-----------|-----|
+| Chinstrap | 68 |
+| Gentoo | 124 |
+```
+
+### Представление VIEW в SQL
+
+[src/views.sql](https://gvwilson.github.io/sql-tutorial/src/views.sql)
+
+```
+CREATE VIEW IF NOT EXISTS active_penguins (species, island, bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g, sex) AS
+SELECT species,
+ island,
+ bill_length_mm,
+ bill_depth_mm,
+ flipper_length_mm,
+ body_mass_g,
+ sex
+FROM penguins
+WHERE active;
+
+SELECT species,
+ count(*) AS num
+FROM active_penguins
+GROUP BY species;
+```
+
+[out/views.out](https://gvwilson.github.io/sql-tutorial/out/views.out)
+
+```
+| species | num |
+|-----------|-----|
+| Chinstrap | 68 |
+| Gentoo | 124 |
+```
+
+- создаём представление (`VIEW`) с именем `active_penguins`, если его еще не существует
+- представление содержит столбцы `species`, `island`, `bill_length_mm`, `bill_depth_mm`, `flipper_length_mm`, `body_mass_g`, и `sex`; данные для представления берутся из `penguins`, при условии, что пингвины являются активными (`WHERE active`)
+- выполняем выборку из представления `active_penguins`: выбираем вид пингвина (`species`) и количество таких пингвинов (`num`), удовлетворяющих условиям, заданным в представлении `active_penguins`
+
+**Напоминание о часах работы:**
+
+[src/all_jobs.sql](https://gvwilson.github.io/sql-tutorial/src/all_jobs.sql)
+
+```
+CREATE TABLE job (name text NOT NULL,
+ billable real NOT NULL);
+
+INSERT INTO job
+VALUES ('calibrate', 1.5),
+ ('clean', 0.5);
+
+SELECT *
+FROM job;
+```
+
+[out/all_jobs.out](https://gvwilson.github.io/sql-tutorial/out/all_jobs.out)
+
+```
+| name | billable |
+|-----------|----------|
+| calibrate | 1.5 |
+| clean | 0.5 |
+```
+
+### Добавляем проверку CHECK
+
+[src/all_jobs_check.sql](https://gvwilson.github.io/sql-tutorial/src/all_jobs_check.sql)
+
+```
+CREATE TABLE job (name text NOT NULL,
+ billable real NOT NULL, CHECK (billable > 0.0));
+
+INSERT INTO job
+VALUES ('calibrate', 1.5);
+
+INSERT INTO job
+VALUES ('reset', -0.5);
+
+SELECT *
+FROM job;
+```
+
+[out/all_jobs_check.out](https://gvwilson.github.io/sql-tutorial/out/all_jobs_check.out)
+
+```
+Runtime error near line 9: CHECK constraint failed: billable > 0.0 (19)
+| name | billable |
+|-----------|----------|
+| calibrate | 1.5 |
+```
+
+- создаём таблицу `job` с 2 столбцами, которые не могут быть пустыми: `name` (текстовый тип данных) и `billable` (вещественные тип данных)
+- ограничение (`CHECK`) гарантирует, что значение столбца `billable` должно быть больше чем 0.0
+- добавляем новую запись в `job` с указанными значениями `'calibrate'` для столбца `name` и 1.5 для столбца `billable` — сейчас под условие `CHECK` это попадает
+- пытаемся добавить еще одну запись в таблицу `job` с указанными значениями `'reset'` для столбца `name`и -0.5 для столбца `billable`. Однако, так как -0.5 меньше либо равно 0.0, то это нарушает условие `CHECK`
+
+**ACID**
+
+ACID — это акроним, который описывает набор свойств транзакций баз данных, предназначенных для обеспечения целостности данных в случае ошибок, сбоев питания и других непредвиденных ситуаций:
+
+1. **Атомарность (Atomicity)**: Транзакция должна быть атомарной, что означает, что она должна быть выполнена целиком или не выполнена вообще. Если одна часть транзакции не может быть выполнена, то все изменения, сделанные в рамках этой транзакции, должны быть отменены.
+2. **Согласованность (Consistency)**: Транзакция должна приводить базу данных из одного согласованного состояния в другое согласованное состояние. Это означает, что все правила и ограничения, установленные на данные, должны быть соблюдены во время выполнения транзакции.
+3. **Изолированность (Isolation)**: Транзакции должны быть изолированы друг от друга, чтобы предотвратить взаимное влияние. Каждая транзакция должна быть выполнена так, как если бы она была единственной выполняемой транзакцией в базе данных. Это гарантирует, что результаты одной транзакции не будут видны другим транзакциям до их завершения.
+4. **Долговечность (Durability)**: Результаты выполненной транзакции должны быть постоянными и доступными даже в случае сбоя системы или перезагрузки. Это достигается путем записи изменений в постоянное хранилище, например SSD.
+
+### TRANSACTION в SQL
+
+[src/transaction.sql](https://gvwilson.github.io/sql-tutorial/src/transaction.sql)
+
+```
+CREATE TABLE job (name text NOT NULL,
+ billable real NOT NULL, CHECK (billable > 0.0));
+
+INSERT INTO job
+VALUES ('calibrate', 1.5);
+BEGIN TRANSACTION;
+
+INSERT INTO job
+VALUES ('clean', 0.5);
+ROLLBACK;
+
+SELECT *
+FROM job;
+```
+
+[out/transaction.out](https://gvwilson.github.io/sql-tutorial/out/transaction.out)
+
+```
+| name | billable |
+|-----------|----------|
+| calibrate | 1.5 |
+```
+
+- создаём таблицу `job` с 2 колонками, которые не могут быть пустыми:
+ - `name` текстового типа
+ - `billable` с типом данных `real` (вещественное число) и условием `CHECK (billable > 0.0)`, что гарантирует, что значение `billable` больше 0.0
+- добавляем в `job` запись: `('calibrate', 1.5)`
+- начинаем новую транзакцию.
+- добавляем другую запись в таблицу `job`: `('clean', 0.5)`
+- откатываем последнюю транзакцию, добавляя `'clean', 0.5`, поэтому данная строка не сохраняется
+
+### ROLLBACK в SQL
+
+[src/rollback_constraint.sql](https://gvwilson.github.io/sql-tutorial/src/rollback_constraint.sql)
+
+```
+ CREATE TABLE job (
+ name text NOT NULL,
+ billable real NOT NULL,
+ CHECK (billable > 0.0) ON CONFLICT ROLLBACK
+ );
+
+ INSERT INTO job
+ VALUES ('calibrate', 1.5);
+
+ INSERT INTO job
+ VALUES ('clean', 0.5),
+ ('reset', -0.5);
+
+ SELECT *
+ FROM job;
+```
+
+[out/rollback_constraint.out](https://gvwilson.github.io/sql-tutorial/out/rollback_constraint.out)
+
+```
+Runtime error near line 11: CHECK constraint failed: billable > 0.0 (19)
+| name | billable |
+|-----------|----------|
+| calibrate | 1.5 |
+```
+
+- создаём новую таблицу с именем `job` и 2 непустыми столбцами: текстовым `name` и вещественным `billable`
+- значение в `billable` должно быть больше 0 (`CHECK (billable > 0.0)`)
+- добавляем в `job` запись с именем `calibrate` со значением `billable` 1.5
+- вторая запись с именем `clean` имеет значение `billable` равное 0.5
+- третья запись с именем `reset` имеет значение `billable` равное -0.5 — тут возникает проблема с записью третьей строки, так как это нарушает ограничение `CHECK (billable > 0.0)`
+
+### Откат с помощью ROLLBACK
+
+[src/rollback_statement.sql](https://gvwilson.github.io/sql-tutorial/src/rollback_statement.sql)
+
+```
+CREATE TABLE job (name text NOT NULL,
+ billable real NOT NULL,
+ CHECK (billable > 0.0));
+INSERT OR ROLLBACK INTO job
+VALUES ('calibrate', 1.5);
+
+INSERT OR ROLLBACK INTO job
+VALUES ('clean', 0.5),
+ ('reset', -0.5);
+
+SELECT *
+FROM job;
+```
+
+[out/rollback_statement.out](https://gvwilson.github.io/sql-tutorial/out/rollback_statement.out)
+
+```
+Runtime error near line 11: CHECK constraint failed: billable > 0.0 (19)
+| name | billable |
+|-----------|----------|
+| calibrate | 1.5 |
+```
+
+- создаём таблицу `job` с 2 непустыми столбцами:
+ - текстовым столбцом `name`
+ - вещественнозначным `billable` с ограничением `CHECK (billable > 0.0)` — значение в этом столбце всегда будет больше нуля
+- вставляем данные в `job` с помощью оператора `INSERT OR ROLLBACK`, а именно одну запись с названием `calibrate` и значением `billable` равным 1.5
+- вставляем ещё 2 записи в таблицу `job` с помощью оператора `INSERT OR ROLLBACK`: `clean` со значением 0.5 для `billable`, `reset` со значением -0.5 `billable` (что не подходит по условию `CHECK`)
+
+### Вставка значений
+
+[src/upsert.sql](https://gvwilson.github.io/sql-tutorial/src/upsert.sql)
+
+```
+CREATE TABLE jobs_done (person text UNIQUE,
+ num integer DEFAULT 0);
+
+INSERT INTO jobs_done
+VALUES("zia", 1);
+
+.print "after first"
+SELECT *
+FROM jobs_done;
+
+.print
+INSERT INTO jobs_done
+VALUES("zia", 1);
+
+.print "after failed"
+SELECT *
+FROM jobs_done;
+
+INSERT INTO jobs_done
+VALUES("zia", 1) ON conflict(person) DO
+UPDATE
+SET num = num + 1;
+
+.print "\nafter upsert"
+SELECT *
+FROM jobs_done;
+```
+
+[out/upsert.out](https://gvwilson.github.io/sql-tutorial/out/upsert.out)
+
+```
+after first
+| person | num |
+|--------|-----|
+| zia | 1 |
+
+Runtime error near line 14: UNIQUE constraint failed: jobs_done.person (19)
+after failed
+| person | num |
+|--------|-----|
+| zia | 1 |
+
+after upsert
+| person | num |
+|--------|-----|
+| zia | 2 |
+```
+
+- создаём `jobs_done` со столбцами `person` (текстовый тип данных с уникальными значениями) и `num`(целочисленный тип, по умолчанию равен 0)
+- вставляем в `jobs_done` запись с именем `"zia"` и числом 1
+- пытаемся снова вставить строку с тем же именем `"zia"` и числом 1 и снова выводим результаты запроса `SELECT`
+- вставляем строку с тем же именем `"zia"` и числом 1 но уже указываем, чтобы в случае конфликта по столбцу `person`, обновить значение столбца `num`, увеличив его на 1
+
+### Создание триггера
+
+[src/trigger_setup.sql](https://gvwilson.github.io/sql-tutorial/src/trigger_setup.sql)
+
+```
+-- Track hours of lab work.
+CREATE TABLE job (person text NOT NULL,
+ reported real NOT NULL CHECK (reported >= 0.0));
+
+-- Explicitly store per-person total rather than using sum().
+CREATE TABLE total (person text UNIQUE NOT NULL,
+ hours real);
+
+-- Initialize totals.
+INSERT INTO total
+VALUES ("gene", 0.0),
+ ("august", 0.0);
+
+-- Define a trigger.
+CREATE TRIGGER total_trigger
+BEFORE
+INSERT ON job BEGIN -- Check that the person exists.
+
+SELECT CASE
+ WHEN NOT EXISTS
+ (SELECT 1
+ FROM total
+ WHERE person = new.person) THEN raise
+ (ROLLBACK, 'Unknown person ')
+ END; -- Update their total hours (or fail if non-negative constraint violated).
+
+UPDATE total
+SET hours = hours + new.reported
+WHERE total.person = new.person; END;
+```
+
+- создаём таблицу `job` со столбцами `person` и `reported`
+- создаём `total` со столбцами `person` и `hours`
+- устанавливаем значения `gene` и `august` в 0.0
+- создаём триггер `total_trigger`, который срабатывает перед вставкой новых записей в таблицу `job`. Этот триггер:
+ - проверяет, существует ли человек в таблице `total`, прежде чем разрешить вставку новых записей в таблицу `job`
+ - обновляет общее количество отработанных часов для соответствующего человека в таблице `total`путем добавления нового количества отработанных часов из таблицы `job`
+
+[src/trigger_successful.sql](https://gvwilson.github.io/sql-tutorial/src/trigger_successful.sql)
+
+```
+INSERT INTO job
+VALUES ('gene', 1.5),
+ ('august', 0.5),
+ ('gene', 1.0);
+```
+
+[out/trigger_successful.out](https://gvwilson.github.io/sql-tutorial/out/trigger_successful.out)
+
+```
+| person | reported |
+|--------|----------|
+| gene | 1.5 |
+| august | 0.5 |
+| gene | 1.0 |
+
+| person | hours |
+|--------|-------|
+| gene | 2.5 |
+| august | 0.5 |
+```
+
+**Срабатывание триггера**
+
+[src/trigger_firing.sql](https://gvwilson.github.io/sql-tutorial/src/trigger_firing.sql)
+
+```
+INSERT INTO job
+VALUES ('gene', 1.0),
+ ('august', -1.0) ;
+```
+
+[out/trigger_firing.out](https://gvwilson.github.io/sql-tutorial/out/trigger_firing.out)
+
+```
+Runtime error near line 6: CHECK constraint failed: reported >= 0.0 (19)
+
+| person | hours |
+|--------|-------|
+| gene | 0.0 |
+| august | 0.0 |
+```
+
+**Графическое представление**
+
+[src/lineage_setup.sql](https://gvwilson.github.io/sql-tutorial/src/lineage_setup.sql)
+
+```
+CREATE TABLE lineage (parent text NOT NULL,
+ child text NOT NULL);
+INSERT INTO lineage
+VALUES ('Arturo', 'Clemente'),
+ ('Darío', 'Clemente'),
+ ('Clemente', 'Homero'),
+ ('Clemente', 'Ivonne'),
+ ('Ivonne', 'Lourdes'),
+ ('Soledad', 'Lourdes'),
+ ('Lourdes', 'Santiago');
+```
+
+[src/represent_graph.sql](https://gvwilson.github.io/sql-tutorial/src/represent_graph.sql)
+
+```
+SELECT *
+FROM lineage;
+```
+
+[out/represent_graph.out](https://gvwilson.github.io/sql-tutorial/out/represent_graph.out)
+
+```
+| parent | child |
+|----------|----------|
+| Arturo | Clemente |
+| Darío | Clemente |
+| Clemente | Homero |
+| Clemente | Ivonne |
+| Ivonne | Lourdes |
+| Soledad | Lourdes |
+| Lourdes | Santiago |
+```
+
+![lineage diagram](https://habrastorage.org/getpro/habr/upload_files/237/8cf/ccc/2378cfccc943d4d0c31e071280f5d279.png "Огромная шпаргалка с готовыми запросами SQL (SQLite)")
+
+lineage diagram
+
+### Рекурсивный запрос
+
+[src/recursive_lineage.sql](https://gvwilson.github.io/sql-tutorial/src/recursive_lineage.sql)
+
+```
+ WITH RECURSIVE descendent AS (
+ SELECT 'Clemente' AS person,
+ 0 AS generations
+ UNION ALL
+ SELECT lineage.child AS person,
+ descendent.generations + 1 AS generations
+ FROM descendent
+ JOIN lineage ON descendent.person = lineage.parent
+ )
+ SELECT person,
+ generations
+ FROM descendent;
+```
+
+[out/recursive_lineage.out](https://gvwilson.github.io/sql-tutorial/out/recursive_lineage.out)
+
+```
+| person | generations |
+|----------|-------------|
+| Clemente | 0 |
+| Homero | 1 |
+| Ivonne | 1 |
+| Lourdes | 2 |
+| Santiago | 3 |
+```
+
+- определяем общий термин `descendent` (потомок) как рекурсивное общее выражение. Начинаем с одной записи, где `'Clemente'` – это начальное имя, а 0 – это количество поколений.
+- далее мы выполняем рекурсивное объединение с самим собой (с `descendent`) и таблицей `lineage`, чтобы найти всех потомков для каждого найденного человека. Выбираем потомка из таблицы `lineage`, увеличиваем количество поколений на 1 и продолжаем делать это для всех найденных потомков, пока они находятся
+- если новых потомков больше не найдено, используем `SELECT` для выбора столбцов `person` и `generations` из `descendent`
+
+**База данных отслеживания контактов**
+
+[src/contact_person.sql](https://gvwilson.github.io/sql-tutorial/src/contact_person.sql)
+
+```
+SELECT *
+FROM person;
+```
+
+[out/contact_person.out](https://gvwilson.github.io/sql-tutorial/out/contact_person.out)
+
+```
+| ident | name |
+|-------|-----------------------|
+| 1 | Juana Baeza |
+| 2 | Agustín Rodríquez |
+| 3 | Ariadna Caraballo |
+| 4 | Micaela Laboy |
+| 5 | Verónica Altamirano |
+| 6 | Reina Rivero |
+| 7 | Elias Merino |
+| 8 | Minerva Guerrero |
+| 9 | Mauro Balderas |
+| 10 | Pilar Alarcón |
+| 11 | Daniela Menéndez |
+| 12 | Marco Antonio Barrera |
+| 13 | Cristal Soliz |
+| 14 | Bernardo Narváez |
+| 15 | Óscar Barrios |
+```
+
+[src/contact_contacts.sql](https://gvwilson.github.io/sql-tutorial/src/contact_contacts.sql)
+
+```
+SELECT *
+FROM contact;
+```
+
+[out/contact_contacts.out](https://gvwilson.github.io/sql-tutorial/out/contact_contacts.out)
+
+```
+| left | right |
+|-------------------|-----------------------|
+| Agustín Rodríquez | Ariadna Caraballo |
+| Agustín Rodríquez | Verónica Altamirano |
+| Juana Baeza | Verónica Altamirano |
+| Juana Baeza | Micaela Laboy |
+| Pilar Alarcón | Reina Rivero |
+| Cristal Soliz | Marco Antonio Barrera |
+| Cristal Soliz | Daniela Menéndez |
+| Daniela Menéndez | Marco Antonio Barrera |
+```
+
+![contact diagram](https://habrastorage.org/getpro/habr/upload_files/9fd/0e1/86e/9fd0e186e42b4756c9242e4e349f7109.png "Огромная шпаргалка с готовыми запросами SQL (SQLite)")
+
+contact diagram
+
+### Продолжаем работать с bi_contact
+
+[src/bidirectional.sql](https://gvwilson.github.io/sql-tutorial/src/bidirectional.sql)
+
+```
+CREATE TEMPORARY TABLE bi_contact (LEFT text, RIGHT text);
+
+ INSERT INTO bi_contact
+ SELECT LEFT,
+ RIGHT
+ FROM contact
+UNION ALL
+ SELECT RIGHT,
+ LEFT
+ FROM contact;
+```
+
+[out/bidirectional.out](https://gvwilson.github.io/sql-tutorial/out/bidirectional.out)
+
+```
+| original_count |
+|----------------|
+| 8 |
+
+| num_contact |
+|-------------|
+| 16 |
+```
+
+- создаём временную табличку `bi_contact` с 2 столбцами: `LEFT` и `RIGHT`, оба текстовые
+- вставляем в `bi_contact` данные из другой таблицы при помощи `SELECT`
+- используем `UNION ALL` для объединения результатов 2 операций `SELECT` в один набор данных; данные из столбца `LEFT` и `RIGHT` таблицы `contact` вставляем в таблицу `bi_contact`. Первый набор данных берёт значения из столбцов `LEFT` и `RIGHT` таблицы `contact`, а второй набор данных берёт значения из столбцов `RIGHT` и `LEFT` таблицы `contact`
+- в общем, вставляем в `bi_contact` комбинацию значений из столбцов `LEFT` и `RIGHT` таблицы `contact`и их перевёрнутые комбинации
+
+### Обновляем идентификаторы групп
+
+[src/update_group_ids.sql](https://gvwilson.github.io/sql-tutorial/src/update_group_ids.sql)
+
+```
+ SELECT left.name AS left_name,
+ left.ident AS left_ident,
+ right.name AS right_name,
+ right.ident AS right_ident,
+ min(left.ident, right.ident) AS new_ident
+ FROM (
+ person AS
+LEFT JOIN bi_contact ON left.name = bi_contact.left
+ )
+ JOIN person AS RIGHT ON bi_contact.right = right.name;
+```
+
+[out/update_group_ids.out](https://gvwilson.github.io/sql-tutorial/out/update_group_ids.out)
+
+```
+| left_name | left_ident | right_name | right_ident | new_ident |
+|-----------------------|------------|-----------------------|-------------|-----------|
+| Juana Baeza | 1 | Micaela Laboy | 4 | 1 |
+| Juana Baeza | 1 | Verónica Altamirano | 5 | 1 |
+| Agustín Rodríquez | 2 | Ariadna Caraballo | 3 | 2 |
+| Agustín Rodríquez | 2 | Verónica Altamirano | 5 | 2 |
+| Ariadna Caraballo | 3 | Agustín Rodríquez | 2 | 2 |
+| Micaela Laboy | 4 | Juana Baeza | 1 | 1 |
+| Verónica Altamirano | 5 | Agustín Rodríquez | 2 | 2 |
+| Verónica Altamirano | 5 | Juana Baeza | 1 | 1 |
+| Reina Rivero | 6 | Pilar Alarcón | 10 | 6 |
+| Pilar Alarcón | 10 | Reina Rivero | 6 | 6 |
+| Daniela Menéndez | 11 | Cristal Soliz | 13 | 11 |
+| Daniela Menéndez | 11 | Marco Antonio Barrera | 12 | 11 |
+| Marco Antonio Barrera | 12 | Cristal Soliz | 13 | 12 |
+| Marco Antonio Barrera | 12 | Daniela Menéndez | 11 | 11 |
+| Cristal Soliz | 13 | Daniela Menéndez | 11 | 11 |
+| Cristal Soliz | 13 | Marco Antonio Barrera | 12 | 12 |
+```
+
+### Рекурсивно устанавливаем метки
+
+[src/recursive_labeling.sql](https://gvwilson.github.io/sql-tutorial/src/recursive_labeling.sql)
+
+```
+ WITH recursive labeled AS (
+ SELECT person.NAME AS NAME,
+ person.ident AS label
+ FROM person
+ UNION -- not 'union all'
+ SELECT person.NAME AS NAME,
+ labeled.label AS label
+ FROM (
+ person
+ JOIN bi_contact ON person.NAME = bi_contact.LEFT
+ )
+ JOIN labeled ON bi_contact.RIGHT = labeled.NAME
+ WHERE labeled.label < person.ident
+ )
+ SELECT NAME,
+ min(label) AS group_id
+ FROM labeled
+ GROUP BY NAME
+ ORDER BY label,
+ NAME;
+```
+
+[out/recursive_labeling.out](https://gvwilson.github.io/sql-tutorial/out/recursive_labeling.out)
+
+```
+| name | group_id |
+|-----------------------|----------|
+| Agustín Rodríquez | 1 |
+| Ariadna Caraballo | 1 |
+| Juana Baeza | 1 |
+| Micaela Laboy | 1 |
+| Verónica Altamirano | 1 |
+| Pilar Alarcón | 6 |
+| Reina Rivero | 6 |
+| Elias Merino | 7 |
+| Minerva Guerrero | 8 |
+| Mauro Balderas | 9 |
+| Cristal Soliz | 11 |
+| Daniela Menéndez | 11 |
+| Marco Antonio Barrera | 11 |
+| Bernardo Narváez | 14 |
+| Óscar Barrios | 15 |
+```
+
+- определяем рекурсивное выражение `labeled` — оно начинается с базового запроса, который выбирает имена и идентификаторы из таблицы person,
+ - затем используется `UNION` для объединения с другим запросом, который соединяет таблицы `person`и `bi_contact`, используя столбцы `name` и `left` в `bi_contact` и имена и метки из `person`
+ - затем объединяет результаты этого соединения с ранее помеченными записями из `labeled`
+ - `WHERE` устанавливает условие, что метка предыдущей записи должна быть меньше, чем идентификатор текущей записи `person`
+- выполняем основной запрос — выбираем имена из `labeled` и вычисляем минимальную метку для каждого имени как `group_id` с помощью функции `min()`
+- результат группируем по именам и сортируем сначала по метке, а затем по имени
+- attention: только не используйте тут `UNION ALL`, иначе возникнет бесконечная рекурсия)
+
+### Работа с SQL в Python при помощи sqlite3
+
+[src/basic_python_query.py](https://gvwilson.github.io/sql-tutorial/src/basic_python_query.py)
+
+```
+import sqlite3
+
+connection = sqlite3.connect("db/penguins.db")
+cursor = connection.execute("SELECT count(*) FROM penguins;")
+rows = cursor.fetchall()
+print(rows)
+```
+
+[out/basic_python_query.out](https://gvwilson.github.io/sql-tutorial/out/basic_python_query.out)
+
+```
+[(344,)]
+```
+
+- импортируем библиотечку `sqlite3` (к слову, она является одной из стандартных библиотек) для работы с SQLite
+- устанавливаем соединение с БД, расположенной в файле `"db/penguins.db"`, используя метод `sqlite3.connect()`. Если этого файл не существует, то он будет создан
+- создаём объект `cursor` для выполнения SQL-запросов
+- `select count(*) from penguins;` — подсчитываем количество всех записей в таблице `penguins`
+- `fetchall()` — получаем результат выполнения запроса, сохраняем его в переменную `rows`
+
+### Инкрементная выборка
+
+[src/incremental_fetch.py](https://gvwilson.github.io/sql-tutorial/src/incremental_fetch.py)
+
+```
+import sqlite3
+
+connection = sqlite3.connect("db/penguins.db")
+cursor = connection.cursor()
+cursor = cursor.execute("SELECT species, island FROM penguins LIMIT 5;")
+while row := cursor.fetchone():
+ print(row)
+```
+
+[out/incremental_fetch.out](https://gvwilson.github.io/sql-tutorial/out/incremental_fetch.out)
+
+```
+('Adelie', 'Torgersen')
+('Adelie', 'Torgersen')
+('Adelie', 'Torgersen')
+('Adelie', 'Torgersen')
+('Adelie', 'Torgersen')
+```
+
+- коннектимся к БД с помощью `sqlite3.connect("db/penguins.db")`
+- `connection.cursor()` — создаём объект `cursor`, это указатель на результат выполнения запросов
+- `select species, island from penguins limit 5;` — выбираем первые 5 записей из таблицы `penguins`, возвращая значения столбцов `species` и `island`
+- пока переменная `row` из `cursor.fetchone()` непустая, печатаем её (мы сразу создаём переменную `row` и тут же используем её при помощи `:=` )
+
+### Простые операции CREATE, INSERT, DELETE и другие с помощью sqlite3
+
+[src/insert_delete.py](https://gvwilson.github.io/sql-tutorial/src/insert_delete.py)
+
+```
+import sqlite3
+
+connection = sqlite3.connect(":memory:")
+cursor = connection.cursor()
+cursor.execute("CREATE TABLE example(num integer);")
+
+cursor.execute("INSERT INTO example VALUES (10),(20);")
+print("after insertion", cursor.execute("SELECT * FROM example;").fetchall())
+
+cursor.execute("DELETE FROM example WHERE num < 15;")
+print("after deletion", cursor.execute("SELECT * FROM example;").fetchall())
+```
+
+[out/insert_delete.out](https://gvwilson.github.io/sql-tutorial/out/insert_delete.out)
+
+```
+after insertion [(10,), (20,)]
+after deletion [(20,)]
+```
+
+- `connection = sqlite3.connect(":memory:")` — создаём подключение к БД SQLite, созданной в оперативной памяти
+- `cursor = connection.cursor()` — создаём объект курсора, который используется для выполнения операций в БД
+- `cursor.execute("CREATE TABLE example(num integer);")` — создаём новую таблицу с именем `example` и одним столбцом `num` для хранения целых чисел
+- `cursor.execute("INSERT INTO example VALUES (10),(20);")` — вставляем 2 строки в `example` с числами 10 и 20 в столбец `num`
+- `print("after insertion", cursor.execute("SELECT * FROM example;").fetchall())` — выводим содержимое таблицы `example` после вставки строк; выполняем операцию `SELECT`, чтобы выбрать все строки из таблицы, используя метод `.fetchall()` для извлечения результатов запроса
+- `cursor.execute("DELETE FROM example WHERE num < 15;")` — удаляем строки из таблицы `example`, в которых значение столбца `num` меньше 15
+- ну и в конце выводим содержимое таблицы `example` после удаления строк; также выполняем `SELECT`, чтобы выбрать все строки из таблицы, используя метод `.fetchall()` для извлечения результатов запроса
+
+### Интерполируем значения
+
+[src/interpolate.py](https://gvwilson.github.io/sql-tutorial/src/interpolate.py)
+
+```
+import sqlite3
+
+connection = sqlite3.connect(":memory:")
+cursor = connection.cursor()
+cursor.execute("CREATE TABLE example(num integer);")
+
+cursor.executemany("insert into example values (?);", [(10,), (20,)])
+print("after insertion", cursor.execute("SELECT * FROM example;").fetchall())
+```
+
+[out/interpolate.out](https://gvwilson.github.io/sql-tutorial/out/interpolate.out)
+
+```
+after insertion [(10,), (20,)]
+```
+
+![XKCD Exploits of a Mom](https://habrastorage.org/getpro/habr/upload_files/97d/5ba/947/97d5ba947d39291b99f978a29fa5638e.png "Огромная шпаргалка с готовыми запросами SQL (SQLite)")
+
+XKCD Exploits of a Mom
+
+- `connection = sqlite3.connect(":memory:")` — устанавливаем соединение с БД SQLite в оперативной памяти
+- `cursor = connection.cursor()` — создаём объект курсора, который используется для выполнения операций БД
+- `cursor.execute("create table example(num integer);")` — создаём таблицу `example` с одним столбцом `num` типа integer
+- `cursor.executemany("insert into example values (?);", [(10,), (20,)])` — вставляем значения 10 и 20 в столбец `num` таблицы `example` с использованием параметризованного запроса
+
+### Выполнение полноценных SQL-запросов в Python
+
+[src/script_execution.py](https://gvwilson.github.io/sql-tutorial/src/script_execution.py)
+
+```
+import sqlite3
+
+SETUP = """\
+DROP TABLE IF EXISTS example;
+CREATE TABLE example(num integer);
+INSERT INTO example
+VALUES (10),
+ (20);
+"""
+
+connection = sqlite3.connect(":memory:")
+cursor = connection.cursor()
+cursor.executescript(SETUP)
+print("after insertion", cursor.execute("SELECT * FROM example;").fetchall())
+```
+
+[out/script_execution.out](https://gvwilson.github.io/sql-tutorial/out/script_execution.out)
+
+```
+after insertion [(10,), (20,)]
+```
+
+- удаляем таблицу `example`, если она существует
+- создаём таблицу `example` с одним столбцом `num` типа integer
+- вставляем 2 записи в таблицу `example` с числами 10 и 20
+- выполняем `SETUP` с помощью метода курсора `executescript()`, который создает новую таблицу и вставляет данные
+- выводим `after insertion` для обозначения того, что последующий запрос к базе данных будет относиться к состоянию после вставки данных
+- выполняем запрос к БД для выбора всех записей из таблицы `example` с помощью метода `execute()` и `fetchall()` для извлечения результатов
+
+### Исключения SQLite в Python
+
+[src/exceptions.py](https://gvwilson.github.io/sql-tutorial/src/exceptions.py)
+
+```
+import sqlite3
+
+SETUP = """\
+CREATE TABLE example(num integer check(num > 0));
+
+INSERT INTO example
+VALUES (10);
+
+INSERT INTO example
+VALUES (-1);
+
+INSERT INTO example
+VALUES (20);
+"""
+
+connection = sqlite3.connect(":memory:")
+cursor = connection.cursor()
+try:
+ cursor.executescript(SETUP)
+except sqlite3.Error as exc:
+ print(f"SQLite exception: {exc}")
+print("after execution", cursor.execute("SELECT * FROM example;").fetchall())
+```
+
+[out/exceptions.out](https://gvwilson.github.io/sql-tutorial/out/exceptions.out)
+
+```
+SQLite exception: CHECK constraint failed: num > 0
+after execution [(10,)]
+```
+
+- устанавливаем соединение с БД в оперативной памяти с помощью `sqlite3.connect(":memory:")`
+- создаём курсор для выполнения операци
+- создаём таблицу `example` и вставляем в нее 3 значения с помощью `executescript()`
+- в блоке `try-except` обрабатывается исключение `sqlite3.Error`, если произойдет какая-либо ошибка при выполнении запросов
+- выводим содержимое таблицы `example` после выполнения запросов с помощью метода `fetchall()`
+
+### Python и SQLite, ещё некоторые возможности
+
+[src/embedded_python.py](https://gvwilson.github.io/sql-tutorial/src/embedded_python.py)
+
+```
+import sqlite3
+
+SETUP = """\
+CREATE TABLE example(num integer);
+INSERT INTO example
+VALUES (-10),
+ (10),
+ (20),
+ (30);
+"""
+
+def clip(value):
+ if value < 0:
+ return 0
+ if value > 20:
+ return 20
+ return value
+
+connection = sqlite3.connect(":memory:")
+connection.create_function("clip", 1, clip)
+cursor = connection.cursor()
+cursor.executescript(SETUP)
+for row in cursor.execute("SELECT num, clip(num) FROM example;").fetchall():
+ print(row)
+```
+
+[out/embedded_python.out](https://gvwilson.github.io/sql-tutorial/out/embedded_python.out)
+
+```
+(-10, 0)
+(10, 10)
+(20, 20)
+(30, 20)
+```
+
+- создаём БД SQLite в оперативной памяти, создаём табличку `example`, заполняем её таблицу значениями (-10, 10, 20, 30)
+- затем определяем функцию `clip`, которая принимает один аргумент и возвращает этот аргумент, если он находится между 0 и 20, или возвращает 0, если аргумент меньше 0, или возвращает 20, если аргумент больше 20
+- выбираем значения из столбца `num` таблицы `example` и применяет функцию `clip` к каждому значению
+
+### Работа с датой и временем
+
+[src/dates_times.py](https://gvwilson.github.io/sql-tutorial/src/dates_times.py)
+
+```
+from datetime import date
+import sqlite3
+
+# Convert date to ISO-formatted string when writing to database
+def _adapt_date_iso(val):
+ return val.isoformat()
+
+sqlite3.register_adapter(date, _adapt_date_iso)
+
+# Convert ISO-formatted string to date when reading from database
+def _convert_date(val):
+ return date.fromisoformat(val.decode())
+
+sqlite3.register_converter("date", _convert_date)
+
+SETUP = """\
+CREATE TABLE events(happened date NOT NULL,
+ description text NOT NULL);
+"""
+
+connection = sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES)
+cursor = connection.cursor()
+cursor.execute(SETUP)
+
+cursor.executemany(
+ "insert into events values (?, ?);",
+ [(date(2024, 1, 10), "started tutorial"), (date(2024, 1, 29), "finished tutorial")],
+)
+
+for row in cursor.execute("SELECT * FROM EVENTS;").fetchall():
+ print(row)
+```
+
+[out/dates_times.out](https://gvwilson.github.io/sql-tutorial/out/dates_times.out)
+
+```
+(datetime.date(2024, 1, 10), 'started tutorial')
+(datetime.date(2024, 1, 29), 'finished tutorial')
+```
+
+- определяем функцию `_adapt_date_iso(val)` — она принимает дату и возвращает ее строковое представление в формате `ISO`
+- определяем функцию `_convert_date(val)` — она принимает строку в формате `ISO` и возвращает объект типа `date`
+- затем эти функции регистрируются в SQLite, чтобы обеспечить корректное преобразование данного типа данных при записи и чтении из базы данных
+- после этого создается строка `SETUP`, которая содержит SQL-команду для создания таблицы `events` с двумя столбцами: `happened` типа `date` и `description` типа `text`
+- с помощью `cursor.executemany` в таблицу `events` вставляются 2 записи с использованием значений типа `date` и строк
+- с помощью `select * from events` и `cursor.execute` извлекаем значения всех строк из таблицы `events`
+
+### SQL в Jupyter Notebooks
+
+```
+pip install jupysql
+```
+
+```
+%load_ext sql
+```
+
+```
+%sql sqlite:///data/penguins.db
+```
+
+```
+Connecting to 'sqlite:///data/penguins.db'
+```
+
+- Подключение к БД:
+ - `sqlite://` — протокол с 2 слэшами в конце
+ - `/data/penguins.db` — 1 слэш спереди, это путь к локальной БД
+- 1 знак процента `%sql` — для выполнения однострочных SQL-запросов
+- 2 знака процента `%%sql` показывает, что вся ячейка будет восприниматься как один SQL-запрос
+
+```
+%%sql
+SELECT species,
+ count(*) AS num
+FROM penguins
+GROUP BY species;
+```
+
+```
+Running query in 'sqlite:///data/penguins.db'
+```
+
+|species|num|
+|---|---|
+|Adelie|152|
+|Chinstrap|68|
+|Gentoo|124|
+
+### Pandas и SQL
+
+[src/install_pandas.sh](https://gvwilson.github.io/sql-tutorial/src/install_pandas.sh)
+
+```
+pip install pandas
+```
+
+[src/select_pandas.py](https://gvwilson.github.io/sql-tutorial/src/select_pandas.py)
+
+```
+import pandas as pd
+import sqlite3
+
+connection = sqlite3.connect("db/penguins.db")
+query = "SELECT species, count(*) AS num FROM penguins GROUP BY species;"
+df = pd.read_sql(query, connection)
+print(df)
+```
+
+[out/select_pandas.out](https://gvwilson.github.io/sql-tutorial/out/select_pandas.out)
+
+```
+species num
+0 Adelie 152
+1 Chinstrap 68
+2 Gentoo 124
+```
+
+- `select species, count(*) as num from penguins group by species;` — извлекаем информацию о количестве пингвинов каждого вида из `penguins` и группируем результаты по видам
+- выполняем запрос к БД с использованием метода `read_sql` библиотеки `pandas`, который читает результаты запроса и преобразует их в объект `DataFrame` (df)
+
+### Polars и SQL
+
+[src/install_polars.sh](https://gvwilson.github.io/sql-tutorial/src/install_polars.sh)
+
+```
+pip install polars pyarrow adbc-driver-sqlite
+```
+
+[src/select_polars.py](https://gvwilson.github.io/sql-tutorial/src/select_polars.py)
+
+```
+import polars as pl
+
+query = "SELECT species, count(*) AS num FROM penguins GROUP BY species;"
+uri = "sqlite:///db/penguins.db"
+df = pl.read_database_uri(query, uri, engine="adbc")
+print(df)
+```
+
+[out/select_polars.out](https://gvwilson.github.io/sql-tutorial/out/select_polars.out)
+
+```
+shape: (3, 2)
+┌───────────┬─────┐
+│ species ┆ num │
+│ --- ┆ --- │
+│ str ┆ i64 │
+╞═══════════╪═════╡
+│ Adelie ┆ 152 │
+│ Chinstrap ┆ 68 │
+│ Gentoo ┆ 124 │
+└───────────┴─────┘
+```
+
+- импортирует библиотеку Polars – она похожа на pandas, но с фокусом на параллельную обработку данных
+- выбираем столбец `species` и вычисляем количество записей для каждого вида пингвинов из таблицы `penguins`; результат группируем по столбцу `species`
+- устанавливаем строку подключения к базе данных SQLite в переменной `uri`
+- используем `pl.read_database_uri` для выполнения SQL-запроса `query` к БД, указанной в `uri`, используя движок `adbc`
+- выводим результат выполнения запроса в виде таблицы данных
+
+### ORM
+
+[src/orm.py](https://gvwilson.github.io/sql-tutorial/src/orm.py)
+
+```
+from sqlmodel import Field, Session, SQLModel, create_engine, select
+
+class Department(SQLModel, table=True):
+ ident: str = Field(default=None, primary_key=True)
+ name: str
+ building: str
+
+engine = create_engine("sqlite:///db/assays.db")
+with Session(engine) as session:
+ statement = select(Department)
+ for result in session.exec(statement).all():
+ print(result)
+```
+
+[out/orm.out](https://gvwilson.github.io/sql-tutorial/out/orm.out)
+
+```
+building='Chesson' name='Genetics' ident='gen'
+building='Fashet Extension' name='Histology' ident='hist'
+building='Chesson' name='Molecular Biology' ident='mb'
+building='TGVH' name='Endocrinology' ident='end'
+```
+
+- создаём класс `Department`, который представляет модель данных для отделов; каждый атрибут класса соответствует столбцу в таблице БД
+- создаём объект `engine`, который представляет собой подключение к SQLite БД, где `assays.db` – это имя файла БД
+- создаём `Session` для взаимодействия с базой данных через созданный `engine`
+- формируем SQL-запрос с помощью `select(Department)`, который выбирает все данные из таблицы, представленной моделью `Department`
+- выполняем запрос к БД через `session.exec(statement).all()`, который возвращает все строки, удовлетворяющие условию запроса
+
+### Продолжаем работать с ORM
+
+[src/orm_relation.py](https://gvwilson.github.io/sql-tutorial/src/orm_relation.py)
+
+```
+class Staff(SQLModel, table=True):
+ ident: str = Field(default=None, primary_key=True)
+ personal: str
+ family: str
+ dept: Optional[str] = Field(default=None, foreign_key="department.ident")
+ age: int
+
+engine = create_engine("sqlite:///db/assays.db")
+SQLModel.metadata.create_all(engine)
+with Session(engine) as session:
+ statement = select(Department, Staff).where(Staff.dept == Department.ident)
+ for dept, staff in session.exec(statement):
+ print(f"{dept.name}: {staff.personal} {staff.family}")
+```
+
+[out/orm_relation.out](https://gvwilson.github.io/sql-tutorial/out/orm_relation.out)
+
+```
+Histology: Divit Dhaliwal
+Molecular Biology: Indrans Sridhar
+Molecular Biology: Pranay Khanna
+Histology: Vedika Rout
+Genetics: Abram Chokshi
+Histology: Romil Kapoor
+Molecular Biology: Ishaan Ramaswamy
+Genetics: Nitya Lal
+```
+
+- объявляем класс `Staff`; он использует SQLModel, что позволяет использовать этот класс как схему для создания таблицы в БД. Указание `table=True` в качестве аргумента класса говорит SQLModel о том, что данный класс должен отображаться в базу данных как таблица. У `Staff` есть несколько атрибутов :
+ - `ident` – строковое поле, которое будет использоваться в качестве первичного ключа в базе данных. Оно имеет значение по умолчанию `None` и задается как первичный ключ (`primary_key=True`)
+ - `personal` – строковое поле
+ - `family` – строковое поле
+ - `dept` – опциональное строковое поле; имеет значение по умолчанию `None` и устанавливается как внешний ключ (`foreign_key="department.ident"`)
+ - `age` – целочисленное поле
+- после определения `Staff`, создается экземпляр движка для работы с БД SQLite с помощью вызова функции `create_engine` из библиотеки SQLAlchemy
+- затем вызываем метод `create_all` у метаданных SQLModel, что приводит к созданию всех таблиц, определенных в виде классов с помощью SQLModel, на основе ранее созданного движка базы данных
+- далее устанавливаем сессия БД с использованием созданного ранее движка
+- формируется SQL-запрос, который выбирает данные из таблиц `Department` и `Staff`, объединяя их по условию, что поле `Staff.dept` равно полю `Department.ident`
+- выполняем этот запрос в сессии БД, и для каждой строки результата выводится название отдела и персональные данные сотрудника \ No newline at end of file