Системой управления базами данных (СУБД) называют программное средство, позволяющее хранить информацию и извлекать хранимую информацию. В каком-то смысле, даже обычный массив в языке Си вместе с операциями индексации и индексированного присваивания можно считать примером очень примитивной СУБД. Тем не менее, обычно к СУБД подобные элементарные вещи не относят.
Обычно СУБД поддерживают возможность изолировать подмножества хранимых данных друг от друга. Единицы изоляции называют базами данных (БД). Например, если в качестве СУБД рассматривать только вышеупомянутые операции индексации и индексированного присваивания языка Си, то отдельными БД для такой СУБД будут массивы.
По факту понятия СУБД и БД часто используют как синонимы: ни к какой особой путанице это не приводит.
В этой главе мы обсудим т.н. реляционную модель СУБД/БД. Реляционная БД (именно БД как единица изоляции) представляет собой набор именованных таблиц. Каждая таблица имеет некоторое фиксированное (на момент создания таблицы) множество именованных колонок.
Единица хранения (т.н. запись) – строка таблицы. В таблицу в любой момент можно добавлять новые строки или удалять из неё уже имеющиеся там. Обычно разрешаются «неполные» строки: некоторые колонки в такой строке могут быть пустыми.
Основа реляционной модели – язык SQL, на котором строятся запросы к реляционной БД. Изначально (в 70х годах) этот язык не отождествлялся с реляционной моделью. В настоящее время, говоря о реляционных БД/СУБД, подразумевают именно ту или иную реализацию языка SQL.
В качестве реализации языка SQL мы будем использовать СУБД SQLite. Для Windows-систем можно скачать с официального сайта архив sqlite-tools, содержащий интерпретатор SQL-команд sqlite3.
Работа с SQLite-базами при помощи интерпретатора команд – далеко не основной режим работы. Тем не менее, с точки зрения процесса обучения, он является наиболее удобными.
Если запустить sqlite3 без аргументов, он создаёт БД в памяти. Создать БД в файле можно одним из двух способов:
.open название_файлаКаждая команда языка SQL, вбитая в интерпретатор, должна завершаться точкой-с-запятой (.open не является командой языка SQL).
Чтобы создать таблицу, используется команда
CREATE TABLE название_таблицы(колонка1,колонка2, ...);Язык SQL является регистронезависимым (в разумных пределах: нелатинские буквы в названиях таблиц и колонок обычно допускаются, но поведение таких названий не регламентировано). Тем не менее, обычно части SQL-команд пишут заглавными буквами, а названия таблиц и колонок – маленькими. Если название таблицы или колонки состоит из нескольких слов или использует служебные символы, его экранируют двойными кавычками.
Для того, чтобы удалить таблицу foobar, можно воспользоваться командой
DROP TABLE foobar;Чтобы посмотреть названия таблиц и их колонок, можно воспользоваться командой .schema (естественно, это команда интерпретатора sqlite3, а не языка SQL; в других СУБД это делается по-другому).
Чтобы добавить строчку в таблицу foobar, пишут
INSERT INTO foobar VALUES(значение1, значение2, ...);Чтобы оставить ячейку пустой, можно воспользоваться специальным словом NULL. Числовые данные записываются цифрами (и десятичной точкой, если нужно). Текстовые данные заключаются в одинарные кавычки.
Довольно распространённая практика – включать в таблицу уникальный идентификатор записи. Обычно это делают так:
CREATE TABLE foobar(id INTEGER PRIMARY KEY, остальные колонки);В данном случае INTEGER PRIMARY KEY – указание типа данных колонки с названием id. Такой тип данных обеспечивает:
На последнем пункте остановимся отдельно: если подать NULL в качестве значения колонки с типом INTEGER PRIMARY KEY, значение ячейки будет сгенерировано автоматически. Это очень удобно! (Подобное поведение специфично для SQLite. В других СУБД автогенерация идентификатора реализуется по-другому.)
Основной режим работы с БД – извлечение нужной информации. Для этого используется команда SELECT. Наиболее простая её разновидность устроена так:
SELECT формат_вывода FROM таблица WHERE условия_на_запись;Такая команда достаёт из указанной таблицы все записи, удовлетворяющие заданному условию, и выдаёт их в заданном формате.
Например, рассмотрим БД со схемой:
CREATE TABLE people(id INTEGER PRIMARY KEY, name, age);Чтобы получить имена всех людей, которым больше 30 лет, можно сделать такой запрос:
SELECT name FROM people WHERE age > 30;В формате вывода можно перечислить несколько выражений через запятую, например (в предположении наличия таблицы numbers с колонками x и y):
SELECT x, y, x+y FROM numbers;В том месте команды SELECT, где указывается таблица, разрешаются более сложные выражения, чем одиночное название таблицы. Например, можно использовать запятую как операцию декартова произведения таблиц (декартово произведение двух таблиц – множество всех пар записей этих таблиц).
Например, в предположении следующей схемы БД
CREATE TABLE people(id INTEGER PRIMARY KEY, name);
CREATE TABLE ages (id INTEGER PRIMARY KEY, people_id, age);для получения имён всех людей, которым больше 30 лет, можно использовать запрос
SELECT name FROM people,ages WHERE people.id = people_id AND age > 30;Обратите внимание на то, что нам пришлось явно указать people.id, поскольку есть ещё колонка ages.id. В том случае, когда в запросе есть две таблицы с одинаковым названием, их можно на время запроса переименовать:
SELECT a.name, b.name FROM people AS a, people AS b; Ещё бывают полезны агрегатные запросы. Например, чтобы посчитать сумму построчных произведений чисел таблицы numbers с колонками x, y и z, можно воспользоваться запросом
SELECT sum(x*y*z) FROM numbers;Для sum(1) есть почти аббревиатура count(*) (на самом деле, функцию count можно использовать и с названием колонки в качестве аргумента; в этом случае вычисляется количество записей, в которых соответствующая ячейка непуста). Единственная существенная разница между sum(1) и count(*) состоит в том, что sum на пустом наборе записей даёт значение NULL, а count – ноль.
Удалить запись из таблицы foobar можно командой
DELETE FROM foobar WHERE условие;Например, наличие уникальных идентификаторов в таблице очень сильно упрощает удаление конкретных записей.
Как внутри FROM, так и внутри WHERE допускаются подзапросы.
Внутри FROM такой подзапрос трактуется естественным образом – как таблица, состоящая из результатов такого запроса. Внутри WHERE чаще всего подзапросы встречаются в виде
foo = (SELECT sum(...) FROM ...)
foo IN (SELECT bar FROM ...)В случае бинарных операторов «равно», «больше», «меньше», IS и т.п. подзапрос должен возвращать одну запись. Для оператора IN подзапрос должен возвращать одну колонку. В этом случае IN проверяет принадлежность значения результату подзапроса. (Также IN допускает правым аргументом список значений наподобие foo IN (1,2,3), а также — имена таблиц.)
Проверить подзапрос на пустоту можно одним из как минимум двух способов:
(SELECT count(*) FROM ...) > 0
-- или же --
EXISTS (SELECT foo FROM ...)Первый из них более гибок, но одновременно и более громоздок.
Теперь полезные приёмы, не касающиеся подзапросов.
Если требуется проверить значение на пустоту, нужно пользоваться выражением IS NULL, а не = NULL, как может показаться на первый взгляд.
Если требуется упорядочить результаты, можно воспользоваться модификаторами ORDER BY foo или ORDER BY foo DESC.
Если требуется ограничить количество результатов n записями, можно воспользоваться модификатором LIMIT n.
Если требуется для каждого достижимого значения некоторого выражения выдать ровно один результат с таким значением, можно воспользоваться модификатором GROUP BY выражение. Например,
SELECT foo,bar FROM baz GROUP BY bar;для таблицы
1|2
3|2
2|3
выдаст (в зависимости от фазы луны) одно из двух: либо (1,2) и (2,3), либо (3,2) и (2,3).
Ещё модификатор GROUP BY выражение можно применять для агрегатных запросов. Для каждого достижимого значения выражения выдаётся ровно один результат, в котором агрегатные функции считаются только по записям с одним и тем же значением выражения.
Все упражнения следует выполнять в предположении следующей схемы БД:
CREATE TABLE people (id INTEGER PRIMARY KEY, name);
CREATE TABLE ages (id INTEGER PRIMARY KEY, people_id, age);
CREATE TABLE parents(id INTEGER PRIMARY KEY, parent_id, child_id);Составьте запрос, который выдаёт имена всех людей, у которых есть хотя бы два ребёнка.
Составьте запрос, который выдаёт имена детей всех людей, у которых есть хотя бы два ребёнка. Один и тот же человек не должен находиться в выдаче более одного раза!
Составьте запрос, который выдаёт имена людей вместе с количеством внуков у каждого.
Составьте запрос, который выдаёт всех людей, являющихся внуками тех, у кого более двух детей.
@ 2016 arbrk1, all rights reversed