SQl полный гайд с запросами и примерами 2024
Публикую шпаргалку по SQL, которая долгое время помогала мне, да и сейчас я периодически в неё заглядываю.
Все примеры изначально писались для СУБД SQLite, но почти всё из этого применимо также и к другим СУБД.
Вначале идут очень простые запросы, с них можно начать новичкам. Если хочется чего-то более интересного — листайте вниз. Здесь есть и примеры довольно сложных запросов с агрегирующими функциями, триггерами, длинными подзапросами, с оконными функциями. Помимо этого, часть примеров посвящена работе с SQL в Python, используя sqlite3, pandas, polars. Этот список запросов с комментариями можно использовать как наглядное пособие для изучения SQL.
Большинство советов я публиковал в своем канале по анализу данных, где вы найдете большое количество советов, инструментов и примеров с кодом. А здесь большая полезная папка, которую я собрал в которой куча полезного для работы с данными.
Кстати, все эти примеры SQL заботливо собраны в одном архиве, вы можете скачать его и экспериментировать локально. После скачивания и разархивирования, у вас будет 3 группы файлов.
Выбираем все значения из таблички
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
.headers on .mode markdown SELECT * FROM little_penguins;
| 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
Выбираем нужные столбцы
SELECT species, island, sex FROM little_penguins;
| 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
Сортировка
SELECT species, sex, island FROM little_penguins ORDER BY island ASC, sex DESC;
| 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
SELECT species, sex, island FROM penguins ORDER BY species, sex, island LIMIT 10;
| 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 строк
Ещё некоторые параметры вывода
SELECT species, sex, island FROM penguins ORDER BY species, sex, island LIMIT 10 OFFSET 3;
| 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 первых строки
Удаляем дубликаты
SELECT DISTINCT species, sex, island FROM penguins;
| 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
Фильтруем результаты
SELECT DISTINCT species, sex, island FROM penguins WHERE island = 'Biscoe';
| species | sex | island | |---------|--------|--------| | Adelie | FEMALE | Biscoe | | Adelie | MALE | Biscoe | | Gentoo | FEMALE | Biscoe | | Gentoo | MALE | Biscoe | | Gentoo | | Biscoe |
- выбираем уникальные комбинации значений species, sex, island из penguins, где значения поля island равно Biscoe
Более сложные условия фильтрации
SELECT DISTINCT species, sex, island FROM penguins WHERE island = 'Biscoe' AND sex != 'MALE';
| species | sex | island | |---------|--------|--------| | Adelie | FEMALE | Biscoe | | Gentoo | FEMALE | Biscoe |
- выбираем уникальные комбинации значений species, sex, island из penguins, где значения поля island равно Biscoe, а значения поля sex не равно MALE
Некоторые математические действия
SELECT flipper_length_mm / 10.0, body_mass_g / 1000.0 FROM penguins LIMIT 3;
| 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
Переименовываем столбцы
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;
| 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
Подсчёт с пропущенными значениями
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;
| 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
SELECT DISTINCT species, sex, island FROM penguins WHERE island = 'Biscoe';
| species | sex | island | |---------|--------|--------| | Adelie | FEMALE | Biscoe | | Adelie | MALE | Biscoe | | Gentoo | FEMALE | Biscoe | | Gentoo | MALE | Biscoe | | Gentoo | | Biscoe |
- выбираем столбцы species, sex, island
- выводим все записи из penguins, где значение island равно 'Biscoe'
SELECT DISTINCT species, sex, island FROM penguins WHERE island = 'Biscoe' AND sex = 'FEMALE';
| species | sex | island | |---------|--------|--------| | Adelie | FEMALE | Biscoe | | Gentoo | FEMALE | Biscoe |
- выводим все записи из penguins, где значение island равно 'Biscoe' и значение sex равно 'FEMALE'
Условие с отрицанием
- условие с оператором отрицания != тоже без проблем работает
SELECT DISTINCT species, sex, island FROM penguins WHERE island = 'Biscoe' AND sex != 'FEMALE';
| species | sex | island | |---------|------|--------| | Adelie | MALE | Biscoe | | Gentoo | MALE | Biscoe |
Выбираем NULL значения
SELECT species, sex, island FROM penguins WHERE sex IS NULL;
| 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
Агрегирование в SQL
SELECT sum(body_mass_g) AS total_mass FROM penguins;
| total_mass | |------------| | 1437000 |
- суммируем все значения колонки body_mass_g, сохраняем в новый столбец total_mass
Распространённые агрегирующие функции в 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;
| 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
SELECT COUNT(*) AS count_star, COUNT(sex) AS count_specific, COUNT(DISTINCT sex) AS count_distinct FROM penguins;
| 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 соответственно
Группировка
SELECT AVG(body_mass_g) AS average_mass_g FROM penguins GROUP BY sex;
| average_mass_g | |------------------| | 4005.55555555556 | | 3862.27272727273 | | 4545.68452380952 |
- из таблички penguins находим среднее всех значений body_mass_g, сохраняем как average_mass_g
- группируем по значениям sex (группы FEMALE, MALE, NULL)
Как себя ведут неагрегированные столбцы
SELECT sex, AVG(body_mass_g) AS average_mass_g FROM penguins GROUP BY sex;
| sex | average_mass_g | |--------|------------------| | | 4005.55555555556 | | FEMALE | 3862.27272727273 | | MALE | 4545.68452380952 |
- для того, чтобы было видно названия отдельных групп, выбираем не только среднее AVG(body_mass_g), но и sex
- видим 3 группы: NULL, FEMALE, MALE
Выбор нужных столбцов для агрегирования
SELECT sex, body_mass_g FROM penguins GROUP BY sex;
| sex | body_mass_g | |--------|-------------| | | | | FEMALE | 3800 | | MALE | 3750 |
- здесь у нас популярная ошибка, мы просто выбираем body_mass_g, а не находим среднее, поэтому SQL выбирает любые значения из body_mass_g. Аккуратнее)
Фильтрация агрегированных значений
SELECT sex, AVG(body_mass_g) AS average_mass_g FROM penguins GROUP BY sex HAVING average_mass_g > 4000.0;
| sex | average_mass_g | |------|------------------| | | 4005.55555555556 | | MALE | 4545.68452380952 |
- здесь мы используем HAVING вместо WHERE (эффект тот же самый), оставляем только те значения из average_mass_g, которые больше 4000
Читабельный вывод
SELECT sex, ROUND(AVG(body_mass_g), 1) AS average_mass_g FROM penguins GROUP BY sex HAVING average_mass_g > 4000.0;
| sex | average_mass_g | |------|----------------| | | 4005.6 | | MALE | 4545.7 |
- округляем среднее AVG(body_mass_g до 1 знака после запятой, используя ROUND
Фильтрация входных данных
SELECT sex, ROUND( AVG(body_mass_g) FILTER (WHERE body_mass_g < 4000.0), 1) AS average_mass_g FROM penguins GROUP BY sex;
| 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
Кстати, вот так выглядит создание БД в оперативной памяти:
sqlite3 :memory:
- запускаем интерактивную оболочку SQLite, создаём новую базу данных в оперативной памяти для более быстрой работы
Создание табличек
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 — текстовый, не может быть пустым
Вставляем данные
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');
| 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
Обновляем строки
UPDATE work SET person = "tae" WHERE person = "tay";
| person | job | |--------|-----------| | mik | calibrate | | mik | clean | | mik | complain | | po | clean | | po | complain | | tae | complain |
- меняем все записи "tay" на "tae"
Удаляем строки
DELETE FROM work WHERE person = "tae"; SELECT * FROM work;
| person | job | |--------|-----------| | mik | calibrate | | mik | clean | | mik | complain | | po | clean | | po | complain |
- удаляем все строки, где значение person равно "tae"
Резервное копирование
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;
| person | job | |--------|----------| | tae | complain |
- создаём табличку backup c текстовыми столбцами person и job
- помещаем внутрь backup значения столбцов person и job из таблицы work, где значения столбца person равно 'tae'
- удаляем из work все записи со значением person равным 'tae'
- отображаем записи таблички backup
Вот так выглядит связь основных понятий, которые мы только что обсуждали:
concept map: data definition and modification
Объединение табличек при помощи JOIN
SELECT * FROM work CROSS JOIN job;
| 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
SELECT * FROM work INNER JOIN job ON work.job = job.name;
| 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 записей
SELECT work.person, SUM(job.billable) AS pay FROM work INNER JOIN job ON work.job = job.name GROUP BY work.person;
| person | pay | |--------|-----| | mik | 2.0 | | po | 0.5 |
- объединяем те строки таблиц work и job, где значение job в таблице work соответствует значению name в job
- суммируем значения billable из таблицы job для каждого значения person из таблицы work
- группируем результаты по значениям person из work
LEFT JOIN
SELECT * FROM work LEFT JOIN job ON work.job = job.name;
| 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
SELECT work.person, sum(job.billable) AS pay FROM work LEFT JOIN job ON work.job = job.name GROUP BY work.person;
| person | pay | |--------|-----| | mik | 2.0 | | po | 0.5 | | tay | |
- вычисляем сумму значений столбца billable из job, сохраняем как pay
- используем LEFT JOIN, чтобы гарантированно включить все строки из work в job
- группируем по столбцу person из work
Вот так выглядит связь основных понятий, которые мы только что обсуждали:
concept map: join
Объединение значений
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;
| 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
SELECT DISTINCT person FROM work WHERE job != 'calibrate';
| person | |--------| | mik | | po | | tay |
- выбираем уникальные значения из столбца person, где поле job не равно calibrate
Использование набора в условии WHERE при помощи IN
SELECT * FROM work WHERE person NOT IN ('mik', 'tay');
| person | job | |--------|----------| | po | clean | | po | complain |
- выбираем все строки из work, где person не равно 'mik' и не равно 'tay'
Подзапросы
SELECT DISTINCT person FROM work WHERE person not in (SELECT DISTINCT person FROM work WHERE job = 'calibrate');
| person | |--------| | po | | tay |
- внутренний подзапрос выбирает уникальные значения столбца person из work, где в поле job стоит 'calibrate'
- внешний, главный запрос выбирает те уникальные значения person, где person не равно значениям из внутренного подзапроса
Автоикремент и PRIMARY KEY
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");
| 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
Внутренняя табличка:
SELECT * FROM sqlite_sequence;
| name | seq | |--------|-----| | person | 3 |
- выводим все текущие значения автоинкрементных счетчиков для таблиц в БД SQLite
Изменение таблички при помощи ALTER
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;
| name | billable | ident | |-----------|----------|-------| | calibrate | 1.5 | 1 | | clean | 0.5 | 2 |
- добавляем новый столбец ident в табличку job; столбец заполняется целыми числами, не может быть пустым; ставим значение по умолчанию -1 для этого столбца
- делаем значение столбца ident равным 1 там, где name равен 'calibrate'
- устанавливаем значение ident равным 2 для строки, где name равен clean
Создание новой таблички на базе старой
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;
| 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
Удаление таблички
DROP TABLE work; ALTER TABLE new_work RENAME TO work;
- удаляем work из БД
- изменяем имя таблички new_work на work
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
Сравнение отдельных значений с агрегированными
SELECT body_mass_g FROM penguins WHERE body_mass_g > (SELECT AVG(body_mass_g) FROM penguins) LIMIT 5;
| body_mass_g | |-------------| | 4675 | | 4250 | | 4400 | | 4500 | | 4650 |
- выбираем только те строки, где значение в столбце body_mass_g больше, чем среднее значение body_mass_g по всем строкам в таблице penguins
- ну и выводим только первые 5 строк
Сравнение отдельных значений с агрегированными внутри групп
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;
| 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 — табличные выражения
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;
| 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
EXPLAIN query PLAN SELECT species, AVG(body_mass_g) FROM penguins GROUP BY species;
QUERY PLAN |--SCAN penguins `--USE TEMP B-TREE FOR GROUP BY
- EXPLAIN query PLAN — получаем план выполнения запроса, как будет выполнен запрос в базе данных
- выбираем столбец species, вычисляем среднее значение столбца body_mass_g для каждого вида из penguins
- GROUP BY species — группируем результаты по столбцу species
Нумеруем строки
- каждая таблица имеет специальный столбец rowid с уникальными числовыми идентификаторами
SELECT rowid, species, island FROM penguins LIMIT 5;
| rowid | species | island | |-------|---------|-----------| | 1 | Adelie | Torgersen | | 2 | Adelie | Torgersen | | 3 | Adelie | Torgersen | | 4 | Adelie | Torgersen | | 5 | Adelie | Torgersen |
Условия if-else
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;
| 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, но он быстро становится нечитаемым
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;
| 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
Работаем с диапазоном значений
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;
| 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 diagramassay ER diagram
SELECT * FROM staff;
| 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
SELECT personal, family FROM staff WHERE personal LIKE '%ya%' OR family GLOB '*De*';
| personal | family | |----------|--------| | Nitya | Lal |
- SELECT personal, family — хотим выбрать столбцы personal и family из таблицы staff
- FROM staff — ну понятно, запрос будет выполнен в таблице staff
- '%ya%' — хотим выбрать строки, в которых значение столбца personal содержит подстроку ya (с помощью LIKE) или значение столбца family содержит De (с помощью GLOB)
Выбираем первую и последнюю строки
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 ;
| 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
Пересечение отдельных табличек
SELECT personal, family, dept, age FROM staff WHERE dept = 'mb' INTERSECT SELECT personal, family, dept, age FROM staff WHERE age < 50 ;
| 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
Исключение
SELECT personal, family, dept, age FROM staff WHERE dept = 'mb' EXCEPT SELECT personal, family, dept, age FROM staff WHERE age < 50 ;
| 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
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;
| 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