Таблицы, строки, ключи
База данных — это склад, где информация лежит в таблицах. Таблица похожа на лист в электронной таблице: сверху названия столбцов, ниже строки с данными. Разница в трёх вещах, и каждая из них важна.
Столбец — одна характеристика: имя, город, возраст. У столбца есть название и тип: name хранит текст, age — целые числа, price — числа с копейками. Тип не украшение: он решает, что с колонкой можно делать. Сложить два текста нельзя, а вычесть одну дату из другой — можно.
Строка (её называют ещё записью) — один объект целиком: конкретный покупатель со всеми его данными. Строк в таблице бывает и десять, и десять миллионов, и это почти не меняет скорость поиска — если база устроена правильно.
Ключ — столбец id, номер строки, уникальный внутри таблицы. Его называют первичным ключом. По нему на строку ссылаются другие таблицы, и он же гарантирует, что двух одинаковых строк не появится.
Наконец, главное отличие от электронной таблицы: таблиц много, и они связаны. Покупатели лежат в одной таблице, заказы в другой, товары в третьей. Заказ не хранит имя покупателя — он хранит его номер.
Зачем так? Чтобы имя хранилось один раз. Если покупатель сменил фамилию, её правят в одном месте, и во всех заказах она становится новой сама. В электронной таблице, где имя вписано в каждую строку, пришлось бы искать и править сотню строк — и одну обязательно пропустили бы.
Столбец, который хранит номер строки из другой таблицы, называют внешним ключом. В нашей базе это orders.customer_id, order_items.order_id, order_items.product_id. Собрать данные обратно в одну таблицу — работа запроса, и этому посвящён целый урок про соединения.
И последнее про таблицу: клетка может быть пустой. Не ноль и не пустая строка, а именно ничего — это состояние называется NULL. В нашей базе у одной покупательницы не указан возраст, а у одного отзыва нет текста: так бывает, когда данные не собрали, не успели или они вообще не нужны.
Пустота ведёт себя не как значение, и это источник самых тихих ошибок в отчётах: строка с незаполненным возрастом не попадёт ни в «моложе тридцати», ни в «старше тридцати», и человек просто исчезнет из обоих списков. Разбору этого посвящён отдельный урок; пока достаточно запомнить, что пустая клетка — третье состояние, а не ноль.
Зачем SQL, если есть выгрузка
Теперь про сам SQL. Это язык, на котором базе задают вопросы, и устроен он не так, как языки программирования.
В обычном языке вы описываете как получить результат: возьми список, пройди по нему, для каждого проверь условие, подходящее сложи в другой список. В SQL вы описываете какой результат нужен: «покажи имена покупателей из Москвы». Как именно их найти — быстро, по указателю, или медленно, перебором всех строк — решает база.
SELECT name FROM customers WHERE city = 'Москва'
Три строки, и они читаются почти как английская фраза: выбери имя из покупателей, где город — Москва. На нашей базе этот запрос вернёт четыре строки: столько покупателей из Москвы.
Заметьте, чего в этих трёх строках нет: ни цикла, ни условия в привычном смысле, ни указания, с какой строки начинать и когда остановиться. Всё это база решает сама. Поэтому SQL учится быстрее, чем язык программирования, — и поэтому же им пользуются люди, которые программировать и не собирались.
Второй вопрос, который задают чаще всего: зачем запрос, если можно выгрузить в таблицу и посчитать там. Ответ короткий: выгрузка отвечает на один вопрос, а запрос — на любой.
Выгрузили продажи за месяц, посчитали сумму. Спросили «а по городам?» — выгружаете заново. «А только по тем, кто зарегистрировался в этом году?» — заново. «А за прошлый месяц для сравнения?» — снова. Каждый раз это пять минут и возможность ошибиться в формуле.
Запрос вместо этого правится в одну строку. И у него есть три свойства, которых у выгрузки нет.
Он повторяем. Один и тот же запрос завтра даст то же самое на тех же данных. Таблица, которую правили руками, — нет.
Он проверяем. Запрос видно целиком: понятно, что он считает и чего не считает. В таблице с формулами в трёхстах ячейках это понятно только автору, и то неделю.
Он не ограничен объёмом. Миллион строк выгружать некуда, а считать по ним — обычная работа.
И ещё одно, менее очевидное: SQL почти не меняется. Он старше большинства языков программирования, и выученный сегодня работает через десять лет, в любой базе и на любой работе. Из инструментов, которые стоит выучить один раз, это самый долгоживущий.
Тут же снимается и следующий вопрос: баз много, а язык один? Почти. PostgreSQL, MySQL, SQLite, Oracle, MS SQL — у каждой свои добавки и мелкие расхождения в том, как пишутся даты и функции над текстом. Но основа — SELECT, WHERE, GROUP BY, соединения — везде одна, и человек, умеющий писать запросы в одной базе, пишет их в любой другой в тот же день.
Курс работает на SQLite: эта база помещается в одну вкладку браузера и не требует установки. Всё, что здесь написано, работает и в остальных базах без правок, а где мелочь отличается — курс скажет об этом прямо в том месте, где отличие важно.
Части запроса и порядок
Запрос состоит из частей, и каждая отвечает на свой вопрос.
| часть | на какой вопрос отвечает |
|---|---|
| SELECT | Какие колонки показать |
| FROM | Из какой таблицы брать строки |
| WHERE | Какие строки оставить |
| GROUP BY | Как свернуть их в итоги |
| HAVING | Какие итоги оставить |
| ORDER BY | В каком порядке показать |
| LIMIT | Сколько строк вернуть |
Порядок в тексте жёсткий: части пишутся именно так и не переставляются. Забыли порядок — база скажет об этом при разборе, и это хорошая ошибка: она находится сразу.
А вот выполняются части в другом порядке, и это не мелочь.
Сначала база берёт строки из таблицы (FROM), потом отсеивает лишние (WHERE), потом сворачивает в группы (GROUP BY), отбирает группы (HAVING), и только потом вычисляет то, что вы написали в SELECT. В самом конце сортирует и отрезает нужное число строк.
Отсюда следует правило, о которое спотыкаются все новички: на псевдоним из SELECT нельзя сослаться в WHERE. Когда WHERE выполняется, псевдонима ещё не существует.
-- так нельзя: имени «полный_возраст» ещё нет SELECT age + 1 AS полный_возраст FROM customers WHERE полный_возраст > 30 -- так можно: выражение повторяется целиком SELECT age + 1 AS полный_возраст FROM customers WHERE age + 1 > 30
Ещё две мелочи, которые стоит принять сразу.
Регистр не важен. SELECT, select и Select — одно и то же. Ключевые слова принято писать заглавными: так в длинном запросе сразу видно скелет. Названия таблиц и колонок пишут как есть.
Комментарии начинаются с двух дефисов и идут до конца строки: -- это пояснение. Многострочный комментарий заключают в /* … */. В рабочих запросах комментарий обычно объясняет не что делает строка, а почему она такая: «берём только оплаченные — неоплаченные отменяются через сутки».
И про то, как запрос выглядит на экране. Базе всё равно, написан он в одну строку или в семь: переносы и отступы она не различает. Человеку — не всё равно. Принято начинать каждую часть с новой строки, как во всех примерах курса: тогда запрос читается сверху вниз, а лишнее или недостающее видно, не вчитываясь.
Запросы разделяются точкой с запятой. Когда запрос один, её обычно не пишут — большинство инструментов и так поймёт, где конец. Когда их несколько подряд, она обязательна, иначе база прочитает два запроса как один и пожалуется на непонятное слово в середине.
Учебная база и как проверяются задания
Курс работает на настоящей базе — маленьком интернет-магазине «Полка». Шесть таблиц, все запросы из теории на ней выполняются, и любой из них можно скопировать и проверить.
| таблица | что в ней | строк |
|---|---|---|
| customers | покупатели: имя, город, возраст, дата регистрации | 12 |
| products | товары: название, категория, цена, остаток | 15 |
| orders | заказы: покупатель, дата, статус | 20 |
| order_items | строки заказов: товар, количество, цена | 29 |
| reviews | отзывы: товар, покупатель, оценка, текст | 13 |
| employees | сотрудники: должность, зарплата, город, руководитель | 8 |
Маленький размер — это не упрощение, а инструмент. Проверить результат запроса на двенадцати строках можно пересчётом вручную, и вы точно узнаете, ошиблись или нет. На миллионе строк такой возможности нет, и ошибка живёт годами.
Вот как выглядят первые строки таблицы покупателей:
| id | name | city | age | signup_date |
|---|---|---|---|---|
| 1 | Анна Смирнова | Москва | 28 | 2023-01-15 |
| 2 | Борис Кузнецов | Санкт-Петербург | 35 | 2023-02-03 |
| 3 | Вера Ильина | Казань | 22 | 2023-02-20 |
| 4 | Глеб Орлов | Москва | 41 | 2023-03-11 |
Теперь про задания. Часть из них обычные — выбрать ответ, соотнести, посчитать. Но главные — те, где вы пишете запрос, и его по-настоящему выполняют.
Устроено это так: вы пишете запрос, сервер создаёт чистую копию учебной базы, выполняет на ней ваш запрос и эталонный и сравнивает результаты. Совпали — задание зачтено, каким бы способом вы этого ни добились.
Из этого следуют две приятные вещи. Первая: верных решений много, и своё не обязано совпадать с задуманным автором. Вторая: испортить базу нельзя — она создаётся заново перед каждой проверкой, и даже DELETE без условия никому не навредит.
Иногда в задании важен не только результат, но и способ: «сделайте это одним соединением, а не подзапросом». Тогда об этом сказано прямо в условии, и проверка смотрит ещё и на то, чем вы воспользовались. Таких заданий немного, и все они там, где способ и есть предмет урока.
Что делать, когда запрос не засчитан. Первым делом посмотреть на результат, а не на текст: сервер показывает строки, которые вернул ваш запрос. Обычно уже по ним видно, что не так, — строк слишком много (забыт отбор), слишком мало (условие отсекло лишнее) или не те колонки.
Пустой ответ — тоже ответ, и он не означает ошибку. Запрос, который ничего не нашёл, выполнился правильно; просто подходящих строк нет. Различать «запрос сломан» и «строк не нашлось» придётся всё время, и начинать эту привычку лучше здесь, на двенадцати покупателях.