Работа с БД
Всё, что программа держит в переменных и объектах, исчезает вместе с процессом: Registrar из
CLI в статье про декораторы каждый запуск начинал с чистого листа
База данных переживает завершение процесса и даёт нескольким программам общие данные
В этой статье - SQLite (база в одном файле, модуль sqlite3 уже входит в Python) и коротко
PostgreSQL через psycopg
Теория
Реляционная модель в двух словах
Реляционная база хранит данные в таблицах: колонки задают имя и тип поля, строки - отдельные
записи
Строку однозначно определяет первичный ключ (PRIMARY KEY) - колонка или набор колонок,
значения которых не повторяются
Связь между таблицами задаёт внешний ключ (FOREIGN KEY) - колонка, значения которой обязаны
существовать в первичном ключе другой таблицы
Для сквозной модели курсов и студентов нужны три таблицы:
students- студенты:student_id(первичный ключ) иnamelessons- занятия:lesson_id(первичный ключ),kind(лекция или семинар) иcapacity(число мест)enrollments- записи на занятия: параstudent_idиlesson_idкак составной первичный ключ, каждая половина пары - внешний ключ на свою таблицу
Таблица enrollments нужна из-за связи «многие ко многим» (студент ходит на много занятий, на
занятие ходит много студентов): такую связь нельзя положить ни в одну из двух таблиц, её хранят
отдельной таблицей-связкой
SQLite и модуль sqlite3
SQLite - база данных в одном файле, которая работает внутри самого процесса: сервер не нужен,
ничего не нужно устанавливать, а модуль sqlite3 входит в стандартную библиотеку Python
Поэтому SQLite удобна для скриптов, тестов и небольших приложений, а для сервисов с большим
числом одновременно пишущих клиентов берут серверную базу вроде PostgreSQL
import sqlite3
conn = sqlite3.connect(":memory:") # база в памяти, пропадёт вместе с процессом
conn.execute(
"""
CREATE TABLE students (
student_id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL
)
"""
)
conn.execute("INSERT INTO students (student_id, name) VALUES ('S1', 'Алиса')")
conn.commit()
cursor = conn.execute("SELECT student_id, name FROM students")
print(cursor.fetchall()) # [('S1', 'Алиса')]
conn.close()
sqlite3.connect(path) открывает файл базы или создаёт его, а ":memory:" вместо пути даёт базу
в памяти - удобно для экспериментов и тестов
conn.execute(sql) выполняет запрос и возвращает курсор: fetchone() отдаёт одну строку (или
None, если строк не осталось), а fetchall() - список всех оставшихся строк
По умолчанию строка - это кортеж
Часть возможностей зависит от версии самой библиотеки SQLite, её показывает
sqlite3.sqlite_version
Что значит каждая строчка CREATE TABLE и остальных запросов, разобрано в разделе «Основные
запросы SQL» ниже
Типы данных
SQLite хранит значения пяти видов, и при чтении sqlite3 превращает их в обычные типы Python:
INTEGER- целое число, в Python этоintREAL- число с плавающей точкой, в Python этоfloatTEXT- строка, в Python этоstrBLOB- произвольные байты, в Python этоbytesNULL- «значения нет», в Python этоNone
Тип колонки в CREATE TABLE - скорее подсказка, чем жёсткое правило: в колонку INTEGER можно
вставить строку 'abc', и SQLite молча сохранит её как текст
Если нужна строгая проверка, таблицу создают с ключевым словом STRICT (SQLite 3.37 и новее):
тогда та же вставка упадёт с sqlite3.IntegrityError
CREATE TABLE ages (age INTEGER) STRICT;
Основные за просы SQL
SQL - язык, на котором программа общается с реляционной базой: запрос описывает, какие данные
нужны, а как их достать, решает сама база
Запросы делятся на два вида: описание структуры (CREATE TABLE) и работа с данными (INSERT,
SELECT, UPDATE, DELETE)
Несколько общих правил:
- ключевые слова не зависят от регистра, но принято писать их заглавными
- строки пишутся в одинарных кавычках:
'Алиса' - запрос заканчивается точкой с запятой, а при вызове
execute()из Python её обычно опускают - комментарий начинается с
--и идёт до конца строки
В примерах ниже используются таблицы students, lessons и enrollments, а результаты
запросов записаны комментариями под ними
CREATE TABLE - описать таблицу
CREATE TABLE IF NOT EXISTS lessons (
lesson_id TEXT NOT NULL PRIMARY KEY,
kind TEXT NOT NULL CHECK (kind IN ('lecture', 'seminar')),
capacity INTEGER NOT NULL CHECK (capacity > 0)
);
После имени таблицы в скобках идёт список колонок через запятую: у каждой есть имя, тип и
огран ичения (constraints) - правила, которые база проверяет при каждой записи
IF NOT EXISTS значит «создать, только если такой таблицы ещё нет»: без него повторный запуск
скрипта упадёт с sqlite3.OperationalError (таблица уже существует)
Ограничения, которые встречаются чаще всего:
PRIMARY KEY- первичный ключ: значения не повторяются (NOT NULLрядом нужен из-за особенности SQLite: у текстовых ключей без него допустимNULL, а PostgreSQL такого не разрешает)NOT NULL- колонка обязана иметь значение,NULLвставить нельзяUNIQUE- значения в колонке не повторяются, как у первичного ключа, но таких колонок в таблице может быть несколько (например,email TEXT UNIQUE)DEFAULT- значение по умолчанию, которое подставится, если приINSERTколонку не указали (например,status TEXT NOT NULL DEFAULT 'active')CHECK (условие)- значение должно проходить условие:capacity > 0не даст создать занятие без местREFERENCES таблица (колонка)- внешний ключ: значение обязано существовать в указанной колонке другой таблицы
Внешние ключи и составной первичный ключ - в таблице записей:
CREATE TABLE IF NOT EXISTS enrollments (
student_id TEXT NOT NULL REFERENCES students (student_id),
lesson_id TEXT NOT NULL REFERENCES lessons (lesson_id),
PRIMARY KEY (student_id, lesson_id)
);
Строка PRIMARY KEY (student_id, lesson_id) в конце - составной первичный ключ: не повторяться
должна пара значений целиком
Поэтому студент может ходить на много занятий, а на одно и то же занятие записаться дважды не
может
Числовой первичный ключ обычно не придумывают самим, а поручают базе
В SQLite колонка, объявленная ровно как INTEGER PRIMARY KEY, нумеруется автоматически: если при
INSERT её не указать, база подставит число на единицу больше самого большого из существующих
(в пустой таблице - 1):
CREATE TABLE IF NOT EXISTS notes (
note_id INTEGER PRIMARY KEY,
text TEXT NOT NULL
);
INSERT INTO notes (text) VALUES ('первая'), ('вторая');
SELECT note_id, text FROM notes;
-- 1 | первая
-- 2 | вторая
Тип должен называться именно INTEGER: у колонки INT PRIMARY KEY автонумерации нет, и номер
остаётся NULL
Если удалить последнюю строку, её номер получит следующая вставка, а чтобы номера не
использовались повторно, после PRIMARY KEY пишут AUTOINCREMENT
В PostgreSQL то же самое записывают иначе:
note_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY
INSERT - добавить строки
INSERT INTO students (student_id, name) VALUES ('S1', 'Алиса');
В скобках после имени таблицы перечислены колонки, а в VALUES - значения в том же порядке
Колонки лучше называть явно: запрос не сломается, если в таблицу позже добавят ещё одну
За один запрос можно вставить несколько строк, перечислив наборы значений через запятую:
INSERT INTO students (student_id, name) VALUES ('S2', 'Боря'), ('S3', 'Вера');
INSERT INTO lessons (lesson_id, kind, capacity)
VALUES ('L1', 'lecture', 30), ('L2', 'seminar', 15), ('L3', 'seminar', 10);
INSERT INTO enrollments (student_id, lesson_id)
VALUES ('S1', 'L1'), ('S1', 'L2'), ('S2', 'L1');
При включённых внешних ключах порядок важен: сначала студенты и занятия, потом записи, которые на них ссылаются
Если вставить строку с уже занятым ключом, запрос упадёт с IntegrityError
ON CONFLICT DO NOTHING в конце запроса меняет это поведение на тихий пропуск строки - именно
так устроены add_student и add_lesson в примере ниже:
INSERT INTO students (student_id, name) VALUES ('S1', 'Алиса')
ON CONFLICT DO NOTHING;
SELECT - прочитать строки
SELECT lesson_id, capacity
FROM lessons
WHERE kind = 'seminar'
ORDER BY capacity DESC
LIMIT 1;
-- L2 | 15
Части запроса пишутся именно в таком порядке:
SELECT- какие колонки вернуть: список через запятую или*, если нужны все колонкиFROM- из какой таблицы брать строкиWHERE- какие строки оставитьORDER BY- в каком порядке вернуть: по возрастанию по умолчанию (ASC), по убыванию -DESCLIMIT- сколько строк вернуть самое большее
Без ORDER BY порядок строк в результате не гарантирован - если он важен, сортировку нужно
просить явно
Условие в WHERE собирается из таких кусочков:
=- равно (знак один, не два),<>или!=- не равно<,>,<=,>=- сравнение по величинеAND,OR,NOT- соединить или отрицать условия, порядок задают скобкиIN ('S2', 'S3')- значение входит в списокLIKE 'Б%'- значение подходит под шаблон:%заменяет любое число символов,_- один символIS NULLиIS NOT NULL- проверка, есть ли значение
SELECT * FROM lessons WHERE capacity >= 15 AND kind = 'seminar';
-- L2 | seminar | 15
SELECT name FROM students WHERE name LIKE 'Б%' OR student_id IN ('S3') ORDER BY name;
-- Боря
-- Вера
NULL значит «значения нет» - это не пустая строка и не ноль
Сравнение с NULL через = никогда не бывает истинным, поэтому условие = NULL не вернёт ни
одной строки, а отсутствие значения проверяют через IS NULL (пример - в разделе про LEFT JOIN
ниже)
Агрегаты и GROUP BY
Агрегатные функции сворачивают много строк в одно значение: COUNT считает строки, SUM
суммирует, а MIN, MAX и AVG находят минимум, максимум и среднее
SELECT COUNT(*), SUM(capacity), MAX(capacity) FROM lessons;
-- 3 | 55 | 30
COUNT(*) считает все строки, а COUNT(колонка) - только те, где в колонке не NULL
GROUP BY разбивает строки на группы с одинаковым значением колонки, и агрегаты считаются
отдельно в каждой группе:
SELECT kind, COUNT(*) AS lessons_count, SUM(capacity) AS seats
FROM lessons
GROUP BY kind
ORDER BY kind;
-- lecture | 1 | 30
-- seminar | 2 | 25
AS задаёт псевдоним - короткое имя для колонки результата или для таблицы
Колонки в SELECT, которые не обёрнуты в агрегат, нужно перечислять и в GROUP BY:
PostgreSQL иначе откажется выполнять запрос, а SQLite выполнит и вернёт значение из какой-то
строки группы
WHERE отбирает строки до группировки и не может использовать агрегаты, а HAVING отбирает
готовые группы после неё:
SELECT kind, COUNT(*) AS lessons_count
FROM lessons
GROUP BY kind
HAVING COUNT(*) > 1;
-- seminar | 2