Практически во всех задачах, которые встречались нам ранее, данные поступали из стандартного ввода или из файла вроде input.txt.
Однако в реальных приложениях очень часто данные нужно хранить между запусками и периодически модифицировать.
В принципе данные можно хранить в простых текстовых файлах и обновлять их там же.
Или использовать библиотеки вроде pickle.
Но это не очень удобно и не слишком надёжно, если речь идёт о больших приложениях.
Понятие база данных — крайне общее.
Это — совокупность данных, организованная так, чтобы они могли быть найдены и обработаны при помощи ЭВМ.
Сейчас практически всегда, когда употребляется словосочетание «база данных», имеется в виду какой-нибудь конкретный подход для хранения и обработки данных,
или даже конкретное ПО (конкретная «СУБД», система управления базами данных), которое всё это реализует.
В этой теме мы будем работать с реляционными базами данных.
«Реляционные» — это от слова relations — отношения.
Если забить на формализм, то суть подхода в следующем.
Существует ровно две сущности: элемент (value) и таблица (table).
Элемент — это какой-то кусочек данных, которые мы храним: число, строка, время и т.п.
Таблица — ну, это прямоугольная таблица, в которой есть строки и столбцы.
Множество столбцов (их имён и типов) называется заголовком таблицы.
При помощи таблиц можно хранить разнообразные отношения (relations), в том числе чертовски сложные.
Например, вот так можно хранить граф:
Таблица «вершины»
node
A
B
C
D
E
F
G
Таблица «рёбра»
from
to
A
B
B
C
B
D
B
E
C
E
D
E
E
F
G
D
На множестве всех таблиц определены операции, которые все вместе называются «реляционной алгеброй».
Вот список почти всех таких операций (тоже по модулю некоторого занудства):
переименование таблицы — любую таблицу можно переименовать и получить новую таблицу с новым именем;
переименование столбцов — если поменять в таблице имена столцбов, то тоже получится таблица;
объединение таблиц — если у двух таблиц одинаковый заголовок (множество столбцов), то их можно объединить: в результате будет таблица с тем же
заголовком, и множеством строк, в которой каждая либо лежит в первой, либо во второй таблице, либо в обеих;
пересечение таблиц — если у двух таблиц одинаковый заголовок, то их можно пересечь: в результате будет таблица с тем же заголовком, и
множеством строк, в которой каждая лежит и в первой, и во второй таблице;
произведение таблиц — у получившийся таблицы будет суммарное количество столбцов, а строки — все возможные наборы, которые начинаются на строчку
первой таблицы и заканчиваются на строку второй;
выборка из таблицы — у любой таблицы можно взять подмножество строк. Получится таблица с тем же заголовком и частью строк;
проекция таблицы — у любой таблицы можно взять подмножество столбцов, после чего удалить (или не удалять) дубликаты.
Получится таблица, в котором меньше столбов, и столько же, либо меньше строк (из-за удаления дублей);
добавление строк — в таблицу можно добавить новую строку с данными, и получить таблицу с тем же заголовком, но одной «бонусной» строкой;
добавление столбцов — в таблицу можно добавить столбец с данным именем, значения которого в каждой строчке определяется функцией от значений
во всех остальных столбцах данной строки.
соединение таблиц — если у двух таблиц есть общие столбцы, то соединение таблиц — это новая таблица со всеми уникальными столбцами,
строки которой «склеены» по совпадающим значениям общих столбцов.
Эта операция — это композиция произведения, выборки и проекции: в произведении таблиц берём только те пары, где значения общих столбцов одинаковы. После
чего удаляем общие столбцы;
Примеры всех этих операций будут дальше.
Примечательно в них только то, что все они берут на вход одну или две таблицы и, может быть, какие-нибудь дополнительные данные, а на выходе всегда получают
новую таблицу.
Именно из-за замкнутости это всё называют «алгеброй».
Для того, чтобы формально описывать эти реляционные операции над таблицами, придуман специальный язык: SQL (Structured Query Language, язык
структурированных запросов).
У языка SQL есть некий стандарт (последняя версия ISO/IEC 9075:2016), однако конкретные «фразы» и
богатство возможностей языка сильно зависят от конкретной реализации.
Кроме реляционных операций язык позволяет создавать новые пустые таблицы с заданными столбцами, добавлять в таблицы новые записи,
изменять и удалять строчки, изменять структуру таблиц, объявлять взаимосвязи между таблицами и т.п.
Вот примеры некоторых операций для типичной реализации SQL:
Операция
Пример запроса
Создание таблицы
CREATETABLE students (name TEXT, age NUMERIC);
Добавление строчки
INSERTINTO students VALUES ('Вася', 13), ('Маша', 14);
Запрос всех данных
SELECT*FROM students;
Объединение таблиц
SELECT*FROM students UNIONSELECT*FROM students;
Пересечение таблиц
SELECT*FROM students INTERSECTSELECT*FROM students;
Произведение таблиц
SELECT*FROM students CROSSJOIN teachers;
Выборка из таблицы
SELECT*FROM students WHERE age <14;
Проекция таблицы
SELECT age FROM students;
Добавление столбцов
SELECT students.*, age*365as days FROM students;
Cоединение таблиц
SELECT*FROM students JOIN names USING (name);
Запрос данных с переимнованием
select stud.name as surname, stud.age asoldfrom students as stud;
SQLite — это, пожалуй, самая распространённая СУБД — система управления базами данных.
Коротко говоря, SQLite — это одна из программ, которая позволяет работать с базой данных и делать всё, описанное выше.
Она очень компактна, быстра, и встроена почти всюду: в браузеры, телефоны, языки программирования и т.п.
Её код передан в общественное достояние, она — одно из наиболее протестированного и надёжного ПО в мире.
Для экспериментов со своими данными удобнее всего установить sqlitebrowser.
В питоне работа с SQLite находится в модуле sqlite3.
Документация о всех командах, которые «понимает» SQLite на sqlite.org.
Во всех задачах на подготовку запросов даётся база данных в db-файле (который при желании можно скачать).
В каждой задаче нужно подготовить SQL-запрос, который решает поставленную задачу.
И текст самого запроса необходимо сдать в проверяющую систему.
Если запросов несколько, то они должны быть отделены точкой с запятой.
Готовить запросы удобно во встроенной в эту страницу систему.
(Только не пытайте использовать Internet Explorer, в нём работать не будет).
Синтакс языка SQL удобно изображать при помощи диаграмм такого вида (они называются railroad diagrams):
Чтобы получить валидный запрос, нужно пройти от начала до конца любым способом.
Более того, можно сделать такую схему, что только те запросы, которые получаются таким образом, будут валидными.
Их можно найти на sqlite.org/syntaxdiagrams.html.
Схема выше — упрощённая схема select-запроса (запрос данных).
Упражнение.
Для каждого из запросов ниже пройдите по схеме и разберитесь, как это работает.
select name
from students
where surname ='Шашков'
values ('Вася', 1), ('Коля', 2)
selectdistinct class,
casewhen supervisor ='Шашков'thentrueelsefalseendas godmode
from students
orderby class
select class, max(height)
from students
groupby class havingcount(*) >10orderby2
limit 10
Выведите отсортированный список всех классных руководителей.
Для отбора чего-то уникального используется ключевое слово DISTINCT.
Для упорядочивания — ORDER BY.
При этом можно указывать либо выражения, по которым нужно сортировать, либо номера столбцов итоговой таблицы.
Первая строчка ответа —
Выведите имя, фамилию, класс каждого школьника с именем Никита, Антон или Алевтина в порядке убывания среднего балла.
Для проверки принадлежности списку пригодится конструкция IN (..,..,..), она работает почти так же, как и в питоне, только .
Для обращения порядка сортировки — ключевое слово DESC. Первая строчка ответа —
sqlite любит даты в формате YYYY-MM-DD, время в формате HH:MM:SS и timestamp'ы в формате YYYY-MM-DD HH:MM:SS.
(Ну, это если упростить. Вообще смотрите документацию)
Впрочем, сам sqlite хранит даты и времена как строки и не валидирует, что туда сохраняют.
Чтобы дату, время или timestamp создать из строки в правильном формате, нужно применить функцию date, time или timestamp
соответственно.
Кстати, текущее время — это datetime('now')
Чтобы извлечь из даты или времени какую-то часть, удобнее всего использовать функцию strftime,
которую мы уже видели в листке про даты.
Вот примеры её вызова (учтите, что она возвращает именно строку)):
Выведите все столбцы по тем школьникам, у которых последнее изменение данных было в мае 2019 года.
Упорядочьте по возрастанию даты-времени последнего изменения.
Если нужно проверить строку на равенстно, то мы используем... равенство.
Если нужно проверить строку на принадлежность списку, то мы так и пишем var in ('str1', 'str2', 'etc').
Но иногда нужно проверить, что строка удовлетворяет какому-нибудь шаблону.
Для этого используется ключевое слово LIKE:
select*from t where col LIKE'some%likes __ hot'
_ — это один любой символ, % — любая последовательность символов.
Если вдруг вам нужно использовать процент или подчёркивание как сами символы, то нужно объявить экранирующий символ и их экранировать:
select*from t where col LIKE'% \% _ \_'ESCAPE'\'
Склейка строк и приведение типов
В отличие от питона в sql'е строки склеиваются при помощи оператора ||.
Любители использовать этот оператор для «или» страдают.
Чтобы сделать из чего-угодно строку, нужно писать CAST(strange AS TEXT).
Кстати, преобразования типов и других сортов устроены точно так же.
Полезные строковые функции
Кроме этого есть ещё пачка функций для работы со строками:
instr,
length,
lower,
ltrim,
replace,
rtrim,
substr,
trim,
upper.
Из неочевидного по имени: функция instr находит индекс первого вхождения второй строки-параметра внутри первой, если считать индексы с 1.
И 0, если не нашлось. А ещё функции upper и lower работают только с латиницей.
Нет времени объяснять.
Нужны фамилии и возраста тех, у кого в фамилии ровно 10 букв.
Результаты склеить в json'ы и отсортировать.
Первая строчка ниже, добудь мне их все!
Часто бывает такое, что вместо самих данных нужны какие-нибудь их агрегаты.
Например, количество, сумма, минимум, максимум или среднее значение.
Это позволяют сделать функции
avg,
count,
group_concat,
max,
min,
sum.
Вот документация по всем агрегирующим функциям.
Использовать их очень просто:
selectcount(*), sum(age), min(avg_score), count(distinct classroom),
group_concat(distinct name) -- Прикиньте, все уникальные имена склеит!from students
Группировка
А что если такую штуку нужно выполнить, скажем, для каждого класса?
30 запросов выполнять?
Вот чтобы ответ на этот вопрос был «нет» существует ещё и группировка.
Если забить на занудство и порядок столбцов, то синтаксис такой:
select field1, field2, sum(field3), avg(field4) fromtablewhere field5 between1and5groupby field1, field2 -- В sqlite можно писать и короче: group by 1, 2
В результате этого запроса суммирование будет производиться только по записям с данными значениями первых двух полей.
Обратите внимание: сначала идёт отбор, затем группировка.
Последующий отбор
Отбор при помощи WHERE производится до группировки.
Если нужно выбросить лишние записи после группировки, то это делается при помощи ключевого слова HAVING.
Получается что-то в духе
select country, sum(money) from cities
groupby country
havingcount(*) >10andsum(money) >1000000000000
Что может быть разумнее, чем посчитать среднее значение средних баллов?
Для каждого класса выведите округлённое до 2 знаков после запятой среднее значение среднего балла школьников этого класса.
И упорядочьте по убыванию этого среднего.
Каким-то образом там 10Е всех рвёт с результатом 4.32:
class
avgavg
10Е
4.32
Да-да, round в sqlite работает примерно также, как и в питоне.
Для каждого класса нужно посчитать число девочек.
Нужны только те классы, где их хотя бы 8.
Упорядочить по классу, но чур «нормально»: сначала 7 классы, в конце — 11:
class
count(*)
7Б
8
Подсказка
А-а-а! Как это сделать?
Здесь пригодятся count(*), group by, having, cast, substr, length,
numeric.
И да, для сортировки в питоне бы мы написали sort(key=lambda cls: (int(cls[:-1]), cls)).
Но в SQL нет срезов, индексы начинаются с 1, и -1 в качестве индекса не сработает, придётся считать самостоятельно.
Иногда нужно отобрать какие-нибудь особенно интересные записи.
Но чтобы оценить интересность, нужно выполнить ещё один запрос.
Так вот, это можно делать:
select*fromtable-- поле равно таблице с 1 строкой и 1 столбцомwhere fld = (selectmax(fld) fromtable)
-- поле лежит в таблице с 1 столбцомor fld in (select'foo'|| id fromtablewheresome=other)
-- несколько полей равны таблице с 1 строкой и соответствующим числом столбцовor (fld1, fld2) = (selectmin(fld1), max(fld2) fromtable limit 1)
-- строка из нескольких полей лежит в таблице с соответствующим числом столбцовor (fld1, fld2) in (select fld1 * fld1, fld1 + fld2 fromtable)
В поздапросе можно использовать имена столбцов из основного запроса.
Но если имена столбцов совпадают, то нужно указывать, о какой таблице идёт речь в формате таблица.поле:
SELECT name, resultFROM students
WHEREresult= (SELECTMax(result) FROM results
WHERE students.name = results.name) -- вот здесь разные поля с одним именем
Если имена таблиц при этом одинаковы, то эти имена нужно поменять:
select name, resultfrom students as main -- Можно использовать любое имя или не добавлять вовсеwhereresult= (selectmax(result) from students as sub
where sub.name = main.name) -- <-- Вот здесь одинаковые имена стали разными
Таблица как поле
А ещё если результат подзапроса — таблица 1 × 1, то её можно использовать в любом месте на равне с именами полей и прочими выражениями:
select col, (select foo from boo limit 1) fromtablewhere (select zoo from moo where doo = col) = mol
orderby (select goo from boo limit 1)
Каскады
Ещё один частый сценарий использования подзапросов — это последовательная обработка и агрегирование:
SELECT supervisor, Avg(cnt)
FROM (SELECT class, supervisor, Count(*) AS cnt
FROM students
GROUPBY class, supervisor)
GROUPBY supervisor