Огромная шпаргалка с готовыми запросами SQL (SQLite)

Публикую шпаргалку по SQL, которая долгое время помогала мне, да и сейчас я периодически в неё заглядываю.
Все примеры изначально писались для СУБД SQLite, но почти всё из этого применимо также и к другим СУБД.
Вначале идут очень простые запросы, с них можно начать новичкам. Если хочется чего-то более интересного — листайте вниз. Здесь есть и примеры довольно сложных запросов с агрегирующими функциями, триггерами, длинными подзапросами, с оконными функциями. Помимо этого, часть примеров посвящена работе с SQL в Python, используя sqlite3, pandas, polars. Этот список запросов с комментариями можно использовать как наглядное пособие для изучения SQL.
Большинство советов я публиковал в своем канале по анализу данных, где вы найдете большое количество советов, инструментов и примеров с кодом. А здесь большая полезная папка, которую я собрал в которой куча полезного для работы с данными.
Кстати, все эти примеры SQL заботливо собраны в одном архиве, вы можете скачать его и экспериментировать локально. После скачивания и разархивирования, у вас будет 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
.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 можно показать так:

Подсчёт с пропущенными значениями
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, которые мы рассмотрели выше:

Агрегирование в 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_starCOUNT(sex)— считаем все значения из столбцаsexCOUNT(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
Выбор нужных столбцов для агрегирования
src/arbitrary_in_aggregation.sql
SELECT sex,
body_mass_g
FROM penguins
GROUP BY sex;
out/arbitrary_in_aggregation.out
| 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
Фильтрация входных данных
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
| sex | average_mass_g |
|--------|----------------|
| | 3362.5 |
| FEMALE | 3417.3 |
| MALE | 3752.5 |
- при помощи
FILTERмы находим среднее только тех значенийbody_mass_g, которые меньше 4000 - округляем до 1 знака после запятой, сохраняем в столбец
average_mass_g - группируем по
sex
Вот так выглядит связь основных понятий, которые мы только что обсуждали:

Кстати, вот так выглядит создание БД в оперативной памяти:
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 |
- создаём табличку
backupc текстовыми столбцамиpersonиjob - помещаем внутрь
backupзначения столбцовpersonиjobиз таблицыwork, где значения столбцаpersonравно'tae' - удаляем из
workвсе записи со значениемpersonравным'tae' - отображаем записи таблички
backup
Вот так выглядит связь основных понятий, которые мы только что обсуждали:

Объединение табличек при помощи 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— все возможные комбинации строк из этих таблиц (если вwork3 строки, а вjob4 строки, то результат будет иметь 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
Вот так выглядит связь основных понятий, которые мы только что обсуждали:

Объединение значений
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равнаNULLLEFT 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, включаем автоматическое инкрементирование значений - помещаем в таблицу
person3 пары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в таблицеworkJOIN 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
Сравнение отдельных значений с агрегированными
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
| 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 — табличные выражения
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
| 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-диаграмма показывает отношения между отдельными табличками и выглядит так:


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из таблицыstaffFROM 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
Создание индексов
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%';
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)
Генерация последовательности значений
SELECT value
FROM generate_series(1, 5);
| value |
|-------|
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
generate_series(1, 5)— генерируем ряд чисел от 1 до 5SELECT value— выбираем этот столбецvalueсо сгенерированными числами от 1 до 5
Генерируем последовательность на основе данных
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));
| value |
|-------|
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
- создаём временную таблицу
temp, которая содержит 1 столбец с именемnumтипа integer; этот столбец не может быть пустым - помещаем в
tempзначения 1 и 5 в столбецnum - используем
generate_seriesдля создания последовательности чисел между минимальным и максимальным значениями из столбцаnumв таблицеtemp
Генерация последовательностей дат
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;
| 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
Подсчитываем количество значений за день, без пропусков
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;
| 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 таблички с собой же
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;
| 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) не было указано
Генерируем уникальные пары значений
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;
| 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
Фильтрация пар
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;
| 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
SELECT name,
building
FROM department
WHERE EXISTS
(SELECT 1
FROM staff
WHERE dept = department.ident )
ORDER BY name;
| 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из таблицыdepartmentORDER BY name— устанавливаем порядок сортировки результатов по столбцуnameв алфавитном порядке
NOT EXISTS в SQL
SELECT name,
building
FROM department
WHERE NOT EXISTS
(SELECT 1
FROM staff
WHERE dept = department.ident )
ORDER BY name;
| name | building |
|---------------|----------|
| Endocrinology | TGVH |
- выбираем столбцы
nameиbuildingиз таблицыdepartment WHERE NOT EXISTS— выбираем только те записи изdepartment, для которых не существует записей в таблицеstaffSELECT 1 FROM staff WHERE dept = department.ident— проверяем, существуют ли записи в таблицеstaff, связанные с отделом из таблицыdepartmentORDER BY name— сортируем результат по столбцу name
Избегание коррелированных подзапросов
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
| name | building |
|-------------------|------------------|
| Genetics | Chesson |
| Histology | Fashet Extension |
| Molecular Biology | Chesson |
SELECT DISTINCT— выбираем уникальные значенияnameиbuildingиз таблицыdepartmentJOIN staff ON department.ident = staff.dept— объединяем таблицыdepartmentиstaffна основе условия, что значение столбцаidentизdepartmentравно значениюdeptизstaffORDER BY name— результаты выборки сортируем в алфавитном порядке по столбцуname
Опережение и отставание
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
Создание индексов
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%';
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)
Генерация последовательности значений
SELECT value
FROM generate_series(1, 5);
| value |
|-------|
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
generate_series(1, 5)— генерируем ряд чисел от 1 до 5SELECT value— выбираем этот столбецvalueсо сгенерированными числами от 1 до 5
Генерируем последовательность на основе данных
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));
| value |
|-------|
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
- создаём временную таблицу
temp, которая содержит 1 столбец с именемnumтипа integer; этот столбец не может быть пустым - помещаем в
tempзначения 1 и 5 в столбецnum - используем
generate_seriesдля создания последовательности чисел между минимальным и максимальным значениями из столбцаnumв таблицеtemp
Генерация последовательностей дат
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;
| 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
Подсчитываем количество значений за день, без пропусков
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;
| 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 таблички с собой же
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;
| 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) не было указано
Генерируем уникальные пары значений
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;
| 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
Фильтрация пар
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;
| 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
SELECT name,
building
FROM department
WHERE EXISTS
(SELECT 1
FROM staff
WHERE dept = department.ident )
ORDER BY name;
| 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из таблицыdepartmentORDER BY name— устанавливаем порядок сортировки результатов по столбцуnameв алфавитном порядке
NOT EXISTS в SQL
SELECT name,
building
FROM department
WHERE NOT EXISTS
(SELECT 1
FROM staff
WHERE dept = department.ident )
ORDER BY name;
| name | building |
|---------------|----------|
| Endocrinology | TGVH |
- выбираем столбцы
nameиbuildingиз таблицыdepartment WHERE NOT EXISTS— выбираем только те записи изdepartment, для которых не существует записей в таблицеstaffSELECT 1 FROM staff WHERE dept = department.ident— проверяем, существуют ли записи в таблицеstaff, связанные с отделом из таблицыdepartmentORDER BY name— сортируем результат по столбцу name
Избегание коррелированных подзапросов
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
| name | building |
|-------------------|------------------|
| Genetics | Chesson |
| Histology | Fashet Extension |
| Molecular Biology | Chesson |
SELECT DISTINCT— выбираем уникальные значенияnameиbuildingиз таблицыdepartmentJOIN staff ON department.ident = staff.dept— объединяем таблицыdepartmentиstaffна основе условия, что значение столбцаidentизdepartmentравно значениюdeptизstaffORDER BY name— результаты выборки сортируем в алфавитном порядке по столбцуname
Опережение и отставание
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;
| 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(год-месяц)
Оконные функции
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;
| 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
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
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
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;
| 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
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;
| 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
Ещё одна БД
sqlite3 data/lab_log.db
.schema
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
SELECT *
FROM machine;
| 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
SELECT details->'$.acquired' AS single_arrow,
details->>'$.acquired' AS double_arrow
FROM machine;
| 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_arrowdetails->>'$.acquired' AS double_arrow— оператор->>также используется для извлечения JSON по указанному пути, но возвращает текст, в отличие от->, который возвращает JSON значение; здесь мы извлекаем значение JSON поляacquiredиз столбцаdetailsдля каждой строки изmachine, обозначаем его какdouble_arrow
Доступ к JSON-объекту
SELECT ident,
json_array_length(log->'$') AS LENGTH,
log->'$[0]' AS FIRST
FROM USAGE;
| 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для подсчета количества элементов в этом массиве; результат помещаем в столбец с именемLENGTHlog->'$[0]' AS FIRST— извлекаем первый элемент из массива, указанного в корневом уровне JSON-объекта в столбцеlog; используем оператор->, чтобы получить доступ к массиву, и указываем индекс элемента в квадратных скобках; результат сохраняем в столбецFIRST
Распаковка JSON
SELECT ident,
json_each.key AS KEY,
json_each.value AS value
FROM USAGE,
json_each(usage.log)
LIMIT 10;
| 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-объекта в столбцеlogFROM usage, json_each(usage.log)— указываем источник данных для выборки;usageуказывается после ключевого слова FROM, аjson_eachвызывается передlog, чтобы разобрать JSON-объекты из этого столбцаLIMIT 10— выбираем только первые 10 строк
Последний элемент в массиве
SELECT ident,
log->'$[#-1].machine' AS FINAL
FROM USAGE
LIMIT 5;
| 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
SELECT ident,
name,
json_set(details, '$.sold', json_quote('2024-01-25')) AS updated
FROM machine;
| 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из таблицыmachinejson_set(details, '$.sold', json_quote('2024-01-25')) AS updated— при помощиjson_setобновляем JSON-объект в столбцеdetails; функция добавляет/изменяет свойствоsoldв JSON-объекте в столбцеdetails, присваивая ему новое значение, полученное с помощью функцииjson_quote; результат сохраняем какupdated
Обновляем табличку penguins:
SELECT species,
count(*) AS num
FROM penguins
GROUP BY species;
| species | num |
|-----------|-----|
| Adelie | 152 |
| Chinstrap | 68 |
| Gentoo | 124 |
Immediate If в 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 – результат, если условие ложно)
SELECT species,
count(*) AS num
FROM penguins
WHERE active
GROUP BY species;
| species | num |
|-----------|-----|
| Chinstrap | 68 |
| Gentoo | 124 |
Представление VIEW в 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;
| 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
Напоминание о часах работы:
CREATE TABLE job (name text NOT NULL,
billable real NOT NULL);
INSERT INTO job
VALUES ('calibrate', 1.5),
('clean', 0.5);
SELECT *
FROM job;
| name | billable |
|-----------|----------|
| calibrate | 1.5 |
| clean | 0.5 |
Добавляем проверку CHECK
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;
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 — это акроним, который описывает набор свойств транзакций баз данных, предназначенных для обеспечения целостности данных в случае ошибок, сбоев питания и других непредвиденных ситуаций:
- Атомарность (Atomicity): Транзакция должна быть атомарной, что означает, что она должна быть выполнена целиком или не выполнена вообще. Если одна часть транзакции не может быть выполнена, то все изменения, сделанные в рамках этой транзакции, должны быть отменены.
- Согласованность (Consistency): Транзакция должна приводить базу данных из одного согласованного состояния в другое согласованное состояние. Это означает, что все правила и ограничения, установленные на данные, должны быть соблюдены во время выполнения транзакции.
- Изолированность (Isolation): Транзакции должны быть изолированы друг от друга, чтобы предотвратить взаимное влияние. Каждая транзакция должна быть выполнена так, как если бы она была единственной выполняемой транзакцией в базе данных. Это гарантирует, что результаты одной транзакции не будут видны другим транзакциям до их завершения.
- Долговечность (Durability): Результаты выполненной транзакции должны быть постоянными и доступными даже в случае сбоя системы или перезагрузки. Это достигается путем записи изменений в постоянное хранилище, например SSD.
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;
| 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
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;
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со значениемbillable1.5 - вторая запись с именем
cleanимеет значениеbillableравное 0.5 - третья запись с именем
resetимеет значениеbillableравное -0.5 — тут возникает проблема с записью третьей строки, так как это нарушает ограничениеCHECK (billable > 0.0)
Откат с помощью ROLLBACK
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;
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.5billable(что не подходит по условиюCHECK)
Вставка значений
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;
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
Создание триггера
-- 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
- проверяет, существует ли человек в таблице
INSERT INTO job
VALUES ('gene', 1.5),
('august', 0.5),
('gene', 1.0);
| person | reported |
|--------|----------|
| gene | 1.5 |
| august | 0.5 |
| gene | 1.0 |
| person | hours |
|--------|-------|
| gene | 2.5 |
| august | 0.5 |
Срабатывание триггера
INSERT INTO job
VALUES ('gene', 1.0),
('august', -1.0) ;
Runtime error near line 6: CHECK constraint failed: reported >= 0.0 (19)
| person | hours |
|--------|-------|
| gene | 0.0 |
| august | 0.0 |
Графическое представление
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');
SELECT *
FROM lineage;
| parent | child |
|----------|----------|
| Arturo | Clemente |
| Darío | Clemente |
| Clemente | Homero |
| Clemente | Ivonne |
| Ivonne | Lourdes |
| Soledad | Lourdes |
| Lourdes | Santiago |

Рекурсивный запрос
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;
| person | generations |
|----------|-------------|
| Clemente | 0 |
| Homero | 1 |
| Ivonne | 1 |
| Lourdes | 2 |
| Santiago | 3 |
- определяем общий термин
descendent(потомок) как рекурсивное общее выражение. Начинаем с одной записи, где'Clemente'– это начальное имя, а 0 – это количество поколений. - далее мы выполняем рекурсивное объединение с самим собой (с
descendent) и таблицейlineage, чтобы найти всех потомков для каждого найденного человека. Выбираем потомка из таблицыlineage, увеличиваем количество поколений на 1 и продолжаем делать это для всех найденных потомков, пока они находятся - если новых потомков больше не найдено, используем
SELECTдля выбора столбцовpersonиgenerationsизdescendent
База данных отслеживания контактов
SELECT *
FROM person;
| 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 |
SELECT *
FROM contact;
| 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 |

Продолжаем работать с bi_contact
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;
| 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и их перевёрнутые комбинации
Обновляем идентификаторы групп
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;
| 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 |
Рекурсивно устанавливаем метки
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;
| 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
import sqlite3
connection = sqlite3.connect("db/penguins.db")
cursor = connection.execute("SELECT count(*) FROM penguins;")
rows = cursor.fetchall()
print(rows)
[(344,)]
- импортируем библиотечку
sqlite3(к слову, она является одной из стандартных библиотек) для работы с SQLite - устанавливаем соединение с БД, расположенной в файле
"db/penguins.db", используя методsqlite3.connect(). Если этого файл не существует, то он будет создан - создаём объект
cursorдля выполнения SQL-запросов select count(*) from penguins;— подсчитываем количество всех записей в таблицеpenguinsfetchall()— получаем результат выполнения запроса, сохраняем его в переменнуюrows
Инкрементная выборка
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)
('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
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())
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 в столбецnumprint("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()для извлечения результатов запроса
Интерполируем значения
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())
after insertion [(10,), (20,)]

connection = sqlite3.connect(":memory:")— устанавливаем соединение с БД SQLite в оперативной памятиcursor = connection.cursor()— создаём объект курсора, который используется для выполнения операций БДcursor.execute("create table example(num integer);")— создаём таблицуexampleс одним столбцомnumтипа integercursor.executemany("insert into example values (?);", [(10,), (20,)])— вставляем значения 10 и 20 в столбецnumтаблицыexampleс использованием параметризованного запроса
Выполнение полноценных SQL-запросов в Python
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())
after insertion [(10,), (20,)]
- удаляем таблицу
example, если она существует - создаём таблицу
exampleс одним столбцомnumтипа integer - вставляем 2 записи в таблицу
exampleс числами 10 и 20 - выполняем
SETUPс помощью метода курсораexecutescript(), который создает новую таблицу и вставляет данные - выводим
after insertionдля обозначения того, что последующий запрос к базе данных будет относиться к состоянию после вставки данных - выполняем запрос к БД для выбора всех записей из таблицы
exampleс помощью методаexecute()иfetchall()для извлечения результатов
Исключения SQLite в Python
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())
SQLite exception: CHECK constraint failed: num > 0
after execution [(10,)]
- устанавливаем соединение с БД в оперативной памяти с помощью
sqlite3.connect(":memory:") - создаём курсор для выполнения операци
- создаём таблицу
exampleи вставляем в нее 3 значения с помощьюexecutescript() - в блоке
try-exceptобрабатывается исключениеsqlite3.Error, если произойдет какая-либо ошибка при выполнении запросов - выводим содержимое таблицы
exampleпосле выполнения запросов с помощью методаfetchall()
Python и SQLite, ещё некоторые возможности
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)
(-10, 0)
(10, 10)
(20, 20)
(30, 20)
- создаём БД SQLite в оперативной памяти, создаём табличку
example, заполняем её таблицу значениями (-10, 10, 20, 30) - затем определяем функцию
clip, которая принимает один аргумент и возвращает этот аргумент, если он находится между 0 и 20, или возвращает 0, если аргумент меньше 0, или возвращает 20, если аргумент больше 20 - выбираем значения из столбца
numтаблицыexampleи применяет функциюclipк каждому значению
Работа с датой и временем
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)
(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
pip install pandas
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)
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
pip install polars pyarrow adbc-driver-sqlite
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)
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
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)
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
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}")
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 - выполняем этот запрос в сессии БД, и для каждой строки результата выводится название отдела и персональные данные сотрудника
The end
Что ж, пользуйтесь этими примерами SQL-запросов на здоровье; особенно эта подборка может быть полезной, если хочется кому-то объяснить что-то из SQL, и нужен подходящий пример
Всех с пятницей!
Сохраняйте себе шпаргалку, чтобы не потерять.



