SQL для начинающих: создание и управление базой данных

Курс для тех, кто хочет с нуля освоить SQL и научиться создавать базы данных для своих проектов. Материал изложен простым языком с понятными аналогиями из повседневной жизни. Вы научитесь создавать таблицы, работать с данными и настраивать связи между ними.

1. Что такое база данных и как она устроена

Что такое база данных и как она устроена

Представь, что у тебя есть маленький магазин на районе. Ты продаёшь товары, ведёшь список клиентов, записываешь заказы в тетрадку. Проходит месяц — тетрадка заканчивается. Берёшь вторую. Потом третью. А потом клиент звонит и спрашивает: «А что я у вас покупал в марте?» — и ты часами листаешь три тетрадки, пытаясь найти нужную запись. Знакомо? Именно эту проблему решают базы данных — системы, которые хранят информацию так, чтобы её можно было быстро найти, обновить и не потерять.

База данных — это умная тетрадка

База данных — это организованное хранилище информации, к которому можно обращаться по определённым правилам. Не просто файл на диске, а структурированная система, которая знает, как данные связаны между собой, как их быстро найти и как защитить от ошибок.

Проведём аналогию. Представь огромную библиотеку. Книги не валяются в куче — они расставлены по полкам по темам, у каждой книги есть каталожная карточка с автором, названием и годом издания. Библиотекарь (то есть программа) знает, где лежит каждая книга, и может за секунды найти нужную. База данных работает точно так же: данные распределены по «полкам» (таблицам), у каждой записи есть «карточка» (идентификатор), а система управления базой данных — это тот самый библиотекарь.

Система управления базой данных (СУБД)

Отдельно база данных — это просто набор информации. Чтобы с ней работать, нужна система управления базой данных (СУБД). Это программа, которая умеет создавать базы, добавлять и удалять данные, искать записи по заданным условиям и следить за тем, чтобы информация не повредилась.

Существует множество СУБД: MySQL, PostgreSQL, SQLite, Microsoft SQL Server, Oracle. Все они понимают один и тот же язык — SQL (Structured Query Language, структурированный язык запросов). Это как английский язык для общения между людьми: неважно, в какой стране ты живёшь, — английский понимают везде. Так и SQL: неважно, какую СУБД ты выберешь — синтаксис запросов будет почти одинаковым.

| СУБД | Особенности | Когда использовать | |------|------------|-------------------| | SQLite | Не требует сервера, данные хранятся в одном файле | Личные проекты, мобильные приложения, прототипы | | MySQL | Быстрая, популярная, бесплатная | Веб-сайты, небольшие и средние проекты | | PostgreSQL | Мощная, поддерживает сложные запросы | Крупные проекты, аналитика, геоданные |

Для личного проекта новичку идеально подойдёт SQLite — не нужно ничего устанавливать отдельно, база данных живёт в одном файле, и ты можешь переносить её на флешке как обычный документ.

Как устроена база данных внутри

Самая распространённая модель хранения данных — реляционная. Слово «реляционная» происходит от английского relation (связь, отношение). Суть в том, что данные хранятся в таблицах, а таблицы связаны между собой через общие поля.

Представь таблицу в Excel: есть строки и столбцы. В базе данных всё устроено похоже, но с жёсткими правилами.

  • Таблица (table) — это контейнер для данных одной категории. Например, таблица «Клиенты», таблица «Заказы», таблица «Товары».
  • Строка (row или record) — это одна конкретная запись. Например, один клиент: Иванов Иван, телефон +7-999-123-45-67.
  • Столбец (column или field) — это одно свойство записи. Например, «Имя», «Телефон», «Дата регистрации».
  • Ячейка — это пересечение строки и столбца, конкретное значение. Например, в строке про Иванова в столбце «Телефон» стоит значение +7-999-123-45-67.
  • Вот как может выглядеть таблица «Клиенты»:

    | id | Имя | Телефон | Дата регистрации | |----|-----|---------|-----------------| | 1 | Иванов Иван | +7-999-123-45-67 | 2024-01-15 | | 2 | Петрова Мария | +7-999-765-43-21 | 2024-02-20 | | 3 | Сидоров Алексей | +7-999-555-12-34 | 2024-03-10 |

    Обрати внимание на столбец id — это первичный ключ (primary key). Он уникально идентифицирует каждую запись. Как паспортный номер у человека: неважно, сколько в базе Ивановых Иванов — по id ты всегда найдёшь конкретного. Первичный ключ не может повторяться и не может быть пустым.

    Реляционная модель: почему таблицы, а не одна большая куча

    Можно было бы хранить всё в одной гигантской таблице. Но представь: у тебя есть клиенты, заказы и товары. Если всё в одной таблице, то для каждого заказа придётся заново писать имя клиента, его телефон, адрес. Клиент сделал 10 заказов — и его данные повторились 10 раз. Изменился телефон — нужно искать и исправлять все 10 записей. А если пропустил одну — данные стали противоречивыми.

    Реляционная модель решает это через нормализацию — разбиение данных на отдельные таблицы и связывание их через общие поля. Клиент описан один раз в таблице «Клиенты», а в таблице «Заказы» просто стоит его id. Телефон изменился — исправил одну запись, и все заказы автоматически «узнали» новый номер.

    > Нормализация — это принцип, при котором каждая единица информации хранится в базе данных ровно в одном месте. Это устраняет дублирование и снижает риск противоречий в данных.

    SQL — язык общения с базой

    Чтобы создать таблицу, добавить запись или найти нужные данные, используется SQL. Это декларативный язык: ты описываешь, что ты хочешь получить, а не как это сделать. СУБД сама решает, какой путь поиска будет самым быстрым.

    Вот самые базовые операции, которые ты будешь выполнять постоянно:

  • Создать таблицу — CREATE TABLE
  • Добавить запись — INSERT INTO
  • Получить данные — SELECT
  • Изменить запись — UPDATE
  • Удалить запись — DELETE
  • Каждая из этих операций — это отдельная тема, которую мы разберём в следующих статьях. Пока что важно понять общий принцип: SQL — это набор команд, которые ты отправляешь СУБД, а она выполняет их и возвращает результат.

    Зачем это нужно именно тебе

    Если ты ведёшь личный проект — блог, интернет-магазин, каталог книг, трекер привычек — рано или поздно данные перестанут помещаться в текстовый файл или таблицу. База данных даст тебе три вещи:

  • Скорость — поиск по миллиону записей займёт доли секунды вместо часов.
  • Надёжность — данные не потеряются при сбое, потому что СУБД ведёт журнал изменений.
  • Гибкость — ты сможешь задавать сложные вопросы: «Покажи всех клиентов из Москвы, которые купили больше трёх товаров за последний месяц».
  • С этого момента мы переходим от теории к практике. В следующей статье ты узнаешь, как создать свою первую таблицу и правильно выбрать типы данных для каждого столбца.

    2. Создание таблиц и выбор типов данных

    Создание таблиц и выбор типов данных

    Когда архитектор проектирует дом, он сначала чертит фундамент, стены и перекрытия — и только потом красит стены и расставляет мебель. С базой данных та же логика: прежде чем заполнять её данными, нужно грамотно спроектировать таблицы. Ошибки на этом этапе потом обходятся дорого — как перекладывать фундамент в уже построенном доме.

    CREATE TABLE — строим первую таблицу

    Команда CREATE TABLE создаёт новую таблицу. Вот её базовая структура:

    Разберём по частям:

  • CREATE TABLE clients — создай таблицу с именем clients.
  • В скобках перечислены столбцы: каждый столбец имеет имя и тип данных.
  • id INTEGER PRIMARY KEY — столбец id хранит целые числа и является первичным ключом.
  • name TEXT NOT NULL — столбец name хранит текст, и его значение обязательно (нельзя оставить пустым).
  • phone TEXT — телефон, текст, но может быть пустым (если клиент не оставил номер).
  • registered_at TEXT — дата регистрации, хранится как текст.
  • > Важно: SQL-запросы нечувствительны к регистру в ключевых словах — CREATE TABLE и create table работают одинаково. Но имена таблиц и столбцов в некоторых СУБД чувствительны к регистру. Лучшая практика — писать ключевые слова большими буквами, а имена таблиц и столбцов в нижнем регистре с подчёркиванием.

    Типы данных: что и как хранить

    Тип данных определяет, какую информацию может содержать столбец и как СУБД будет с ней работать. Это как контейнеры на кухне: для сыпающихся продуктов — банка, для жидкости — бутылка, для заморозки — пакет. Положишь муку в бутылку — неудобно, но можно. А вот дату в числовой столбец — уже серьёзная проблема.

    Вот основные типы данных в SQLite, которые покроют 95% потребностей личного проекта:

    | Тип данных | Что хранит | Пример | |-----------|-----------|--------| | INTEGER | Целые числа | 1, 42, -7, 0 | | REAL | Дробные числа | 3.14, 99.90, -0.5 | | TEXT | Текст любой длины | «Иванов», «Привет мир!» | | BLOB | Двоичные данные (файлы) | Изображения, PDF-документы |

    В других СУБД список шире: в PostgreSQL есть BOOLEAN для логических значений, DATE и TIMESTAMP для дат, VARCHAR(n) для текста с ограничением длины. Но SQLite использует динамическую типизацию — это значит, что технически ты можешь записать текст в столбец с типом INTEGER, и СУБД не выдаст ошибку. Однако это плохая практика: запросы могут работать некорректно, а сортировка чисел как текста даст неправильный порядок («9» окажется больше «100», потому что сравниваются первые символы).

    Как выбрать тип данных для столбца

    Выбор типа — это не формальность, а проектировочное решение. Вот простой алгоритм:

    1. Спроси себя: что именно будет в этом столбце?

    Если это количество товара на складе — INTEGER. Если цена товара — REAL. Если название товара — TEXT.

    2. Будут ли здесь математические операции?

    Если да — нужен числовой тип (INTEGER или REAL). Сложить два текстовых значения «10» и «20» SQL попытается, но результат может быть неожиданным. А вот 10 + 20 в числовом столбце всегда даст 30.

    3. Нужно ли ограничить длину?

    В SQLite тип TEXT не имеет ограничения по длине. Но если ты работаешь с PostgreSQL или MySQL, можно указать VARCHAR(100) — текст максимум из 100 символов. Это полезно для полей вроде «Номер телефона» или «Почтовый индекс», где длина заранее известна.

    Ограничения столбцов: правила для данных

    Помимо типа данных, у столбца могут быть ограничения (constraints) — правила, которые СУБД будет строго соблюдать.

  • PRIMARY KEY — первичный ключ. Значение уникально и не может быть пустым. Обычно это числовой идентификатор, который присваивается автоматически.
  • NOT NULL — значение обязательно. Если попытаешься добавить запись без этого поля, СУБД вернёт ошибку.
  • UNIQUE — значение должно быть уникальным. Например, email пользователя: два клиента не могут иметь одинаковый email.
  • DEFAULT значение — значение по умолчанию. Если при добавлении записи ты не укажешь это поле, подставится значение по умолчанию.
  • CHECK (условие) — проверка условия. Например, цена не может быть отрицательной.
  • Посмотри, как это работает вместе:

    Здесь sku (артикул товара) должен быть уникальным — два разных товара не могут иметь одинаковый артикул. Цена не может быть отрицательной благодаря CHECK. Если при добавлении товара не указать количество, оно автоматически станет равным нулю.

    Проектирование таблицы: практический пример

    Допустим, ты создаёшь базу данных для личной библиотеки — хочешь учитывать прочитанные книги. Сначала подумай, какие данные тебе нужны:

  • Название книги
  • Автор
  • Год издания
  • Жанр
  • Оценка (от 1 до 10)
  • Дата прочтения
  • Краткое описание
  • Теперь переведи каждое поле в тип данных и ограничения:

    Обрати внимание: year — это INTEGER, потому что год — целое число, и по нему можно фильтровать («покажи книги после 2020 года»). rating ограничен диапазоном от 1 до 10. read_date и notes могут быть пустыми — не обязательно заполнять их при добавлении книги.

    Именование таблиц и столбцов

    Кажется мелочью, но плохие имена потом превращают работу в ад. Вот правила, которые стоит усвоить сразу:

  • Имена — на английском языке. Это стандарт де-факто, и SQL-запросы читаются естественнее: SELECT name FROM books вместо SELECT название FROM книги.
  • Используй единственное число для таблиц: book, а не books. Таблица описывает одну сущность. (Хотя в индустрии есть и сторонники множественного числа — главное, будь последователен в рамках своего проекта.)
  • Используй snake_case (слова через подчёркивание): read_date, а не readDate или ReadDate.
  • Не используй зарезервированные слова SQL в качестве имён: order, group, select, table. Если очень хочется — заключи в кавычки, но лучше просто придумай другое имя.
  • Что делать, если таблица уже создана, но нужно что-то изменить

    Бывает, что после создания таблицы понимаешь: забыл столбец или выбрал неправильный тип. SQL позволяет изменять структуру таблицы командой ALTER TABLE:

    В SQLite возможности ALTER TABLE ограничены — можно добавить столбец или переименовать таблицу, но нельзя удалить столбец или изменить его тип. В этом случае придётся создать новую таблицу с правильной структурой, перенести данные и удалить старую. Именно поэтому так важно тщательно проектировать таблицы заранее.

    Теперь у тебя есть пустая таблица с правильной структурой. В следующей статье ты узнаешь, как заполнить её данными и как изменять уже существующие записи.

    3. Добавление и изменение информации

    Добавление и изменение информации

    Таблица создана, структура продумана — но она пуста, как книжная полка в новой квартире. Пора заполнять её данными. В этой статье ты научишься трем операциям, которые составляют основу повседневной работы с базой данных: добавлять новые записи, изменять существующие и удалять ненужные. Эти три операции настолько фундаментальны, что их объединяют в аббревиатуру CRUDCreate, Read, Update, Delete. Read (чтение данных) — это отдельная большая тема, ей посвящена следующая статья. Сейчас разберём Create, Update и Delete.

    INSERT INTO — добавляем записи

    Команда INSERT INTO добавляет новую строку в таблицу. Вот самый простой вариант:

    Разберём, что произошло. После INSERT INTO указано имя таблицы books. В скобках перечислены столбцы, в которые мы записываем данные. После VALUES — сами значения в том же порядке. Столбец id мы не указали, потому что он имеет тип INTEGER PRIMARY KEYSQLite автоматически присвоит ему следующий номер.

    > Порядок значений в VALUES должен точно соответствовать порядку столбцов в скобках. Если перепутаешь — год окажется в поле жанра, а жанр в поле рейтинга. СУБД не проверяет смысл данных, только их тип.

    Добавление нескольких записей за раз

    Если нужно добавить сразу несколько книг, не обязательно писать отдельный INSERT для каждой. Можно перечислить несколько наборов значений через запятую:

    Это не только короче, но и быстрее — СУБД обрабатывает пакетную вставку эффективнее, чем множество одиночных запросов.

    Что будет, если нарушить ограничения

    Попробуй добавить запись, которая нарушает правила таблицы:

    СУБД вернёт ошибку: CHECK constraint failed: rating >= 1 AND rating <= 10. Запись не добавится. Это и есть защита, которую ты настроил через ограничения столбцов. База данных не позволит тебе сохранить некорректные данные — в отличие от Excel, где можно вписать что угодно в любую ячейку.

    Аналогично, если попытаться добавить запись без значения в столбец title (а у него стоит NOT NULL), СУБД откажется выполнять запрос. Попытка вставить дублирующийся sku в таблицу товаров с ограничением UNIQUE тоже вызовет ошибку.

    UPDATE — изменяем существующие записи

    Данные со временем меняются: клиент сменил телефон, цена товара выросла, ты поставил книге другую оценку после повторного прочтения. Для этого используется команда UPDATE:

    Этот запрос говорит: «В таблице books найди запись, где название — "Война и мир", и измени рейтинг на 9».

    Ключевая роль WHERE

    Клаузула WHERE — это фильтр, который определяет, какие именно строки изменить. Без неё UPDATE применится ко всем строкам таблицы. Это одна из самых опасных ошибок новичка:

    Представь, что ты часами заполнял базу оценками, а потом случайно выполнил этот запрос — и все оценки стали пятёрками. Без резервной копии данные не восстановить. Поэтому правило железное: никогда не пиши UPDATE без WHERE, если не уверен, что хочешь изменить все строки.

    Изменение нескольких столбцов одновременно

    Можно обновить сразу несколько полей через запятую:

    Обновление по первичному ключу

    Самый надёжный способ изменить конкретную запись — обратиться к ней по id:

    Почему это надёжнее, чем поиск по названию? Потому что id гарантированно уникален, а названия книг могут повторяться (например, «Анна Каренина» есть и в разделе «Роман», и в разделе «Классика»). Поиск по первичному ключу изменит ровно одну строку — ту, которую ты имел в виду.

    DELETE — удаляем ненужные записи

    Иногда данные нужно удалить: книга оказалась в базе по ошибке, клиент удалил аккаунт, товар снят с продажи. Команда DELETE удаляет строки из таблицы:

    Удалит запись с id = 4. Как и в случае с UPDATE, клаузула WHERE здесь критически важна:

    Таблица останется, структура сохранится, но все данные исчезнут. Это как выкинуть всё содержимое шкафа, но оставить сам шкаф.

    Мягкое удаление: альтернатива полному удалению

    На практике полное удаление записи часто нежелательно. Представь: клиент сделал заказ, оплатил его, а потом удалил аккаунт. Если ты удалишь запись о клиенте, заказ останется «сиротой» — будет ссылаться на несуществующего клиента.

    Распространённый приём — мягкое удаление (soft delete). В таблицу добавляют столбец is_deleted или deleted_at, и вместо удаления записи ставят в этом столбце метку:

    При выборке данных ты просто фильтруешь: «покажи все книги, где is_deleted = 0». Данные остаются в базе, но для пользователя они «удалены». Если ошибся — можно отменить, просто вернув is_deleted в 0.

    Транзакции: безопасность операций

    Что если во время выполнения UPDATE электричество выключится и компьютер перезагрузится? СУБД может записать часть изменений, а часть — нет. Данные окажутся в непоследовательном состоянии.

    Транзакция — это группа операций, которые выполняются как единое целое: либо все успешно, либо ни одной. Это принцип атомарности.

    BEGIN TRANSACTION открывает транзакцию, COMMIT подтверждает все изменения. Если между ними что-то пошло не так, можно выполнить ROLLBACK — и все изменения откатятся к состоянию на момент BEGIN.

    Для личного проекта с простыми операциями транзакции могут казаться излишними. Но когда ты начнёшь выполнять несколько связанных изменений одновременно (например, списать товар со склада и создать запись о заказе), транзакции станут страховкой от неконсистентных данных.

    Практический сценарий: заполняем базу библиотеки

    Вернёмся к таблице books из предыдущей статьи. Допустим, ты прочитал пять книг за месяц и хочешь добавить их все, а потом исправить оценку одной из них:

    Теперь у тебя есть заполненная база с пятью книгами, одна оценка исправлена, одна ошибочная запись удалена. В следующей статье ты узнаешь, как извлекать из этой базы именно те данные, которые тебе нужны — фильтровать, сортировать и группировать.

    4. Как находить и выбирать нужные данные

    Как находить и выбирать нужные данные

    Ты научился создавать таблицы и заполнять их данными. Но база данных — это не просто хранилище, а инструмент для ответов на вопросы. «Сколько книг я прочитал в этом году?» «Какие товары дороже 1000 рублей?» «Кто из клиентов делал заказы чаще всех?» Все эти вопросы задаются через команду SELECT — и именно она превращает кучу данных в полезную информацию.

    SELECT — базовый запрос на выборку

    Команда SELECT извлекает данные из таблицы. Самый простой вариант — получить все записи:

    Символ * означает «все столбцы». Запрос вернёт каждую строку таблицы books со всеми полями. Но чаще тебе нужны не все данные, а конкретные столбцы:

    Этот запрос вернёт только название, автора и оценку каждой книги — без года, жанра и прочих полей. Зачем это ограничивать? Во-первых, ответ становится компактнее и читабельнее. Во-вторых, СУБД обрабатывает меньше данных, а значит, запрос выполняется быстрее. Когда в таблице миллионы строк и десятки столбцов, эта разница ощутима.

    WHERE — фильтрация данных

    WHERE — это фильтр, который оставляет только строки, удовлетворяющие условию. Ты уже встречал его в UPDATE и DELETE, но в SELECT он используется чаще всего:

    Условия в WHERE могут быть любыми: сравнения, проверки на равенство, комбинации условий.

    Операторы сравнения

    | Оператор | Значение | Пример | |---------|---------|--------| | = | Равно | WHERE genre = 'Роман' | | != или <> | Не равно | WHERE genre != 'Фантастика' | | > | Больше | WHERE year > 1900 | | < | Меньше | WHERE rating < 8 | | >= | Больше или равно | WHERE rating >= 9 | | <= | Меньше или равно | WHERE year <= 1950 |

    Комбинирование условий

    Несколько условий соединяются через AND (и) и OR (или):

    AND сужает выборку — оба условия должны выполняться. OR расширяет — достаточно выполнения хотя бы одного.

    Проверка на NULL

    Столбцы без значения содержат NULL — это не пустая строка и не ноль, а именно «отсутствие значения». Обычные операторы сравнения с NULL не работают. Для проверки используются специальные конструкции:

    Ошибка новичка — писать WHERE notes = NULL. Такой запрос не вернёт ни одной строки, потому что NULL не равен ничему, даже самому себе.

    Поиск по части строки: LIKE

    Иногда нужно найти книгу, не помня точное название. Оператор LIKE ищет по шаблону:

    Символ % заменяет любое количество любых символов. _ (подчёркивание) заменяет ровно один символ. Например, LIKE '___на' найдёт четырёхбуквенные слова, заканчивающиеся на «на».

    ORDER BY — сортировка результатов

    По умолчанию SELECT возвращает строки в порядке их добавления. Но часто нужно отсортировать результат — по алфавиту, по рейтингу, по дате:

    DESC — по убыванию (от большего к меньшему), ASC — по возрастанию (от меньшего к большему). Если не указать направление, по умолчанию используется ASC.

    Можно сортировать по нескольким столбцам: сначала по жанру, а внутри жанра — по рейтингу:

    LIMIT — ограничение количества строк

    Если в таблице тысячи записей, а тебе нужны только первые пять, используй LIMIT:

    LIMIT часто сочетается с ORDER BY: сначала сортируешь, потом берёшь нужное количество. Можно указать смещение: LIMIT 5 OFFSET 10 пропустит первые 10 строк и вернёт следующие 5. Это полезно для пагинации — разбиения результатов на страницы.

    Агрегатные функции: считаем и обобщаем

    До сих пор мы работали с отдельными строками. Но иногда нужно получить одно число из множества строк: количество, сумму, среднее. Для этого существуют агрегатные функции:

    | Функция | Что делает | Пример | |--------|-----------|--------| | COUNT(*) | Считает количество строк | Сколько книг в базе | | SUM(столбец) | Суммирует значения | Общая сумма заказов | | AVG(столбец) | Считает среднее | Средний рейтинг книг | | MIN(столбец) | Находит минимум | Самая низкая оценка | | MAX(столбец) | Находит максимум | Самая высокая оценка |

    Агрегатные функции можно комбинировать с WHERE:

    GROUP BY — группировка данных

    А что если хочешь узнать средний рейтинг по каждому жанру, а не по всей базе? Для этого нужна группировка:

    Этот запрос сгруппирует книги по жанру и для каждой группы посчитает средний рейтинг и количество книг. AS avg_rating — это псевдоним (alias), который даёт понятное имя вычисляемому столбцу в результате.

    Результат может выглядеть так:

    | genre | avg_rating | total | |-------|-----------|-------| | Роман | 9.0 | 3 | | Антиутопия | 9.0 | 1 | | Фантастика | 9.0 | 1 |

    Фильтрация групп: HAVING

    WHERE фильтрует отдельные строки до группировки. А если нужно отфильтровать уже сгруппированные результаты — например, показать только жанры, в которых больше одной книги, — используется HAVING:

    Разница между WHERE и HAVING проста: WHERE — это фильтр для строк, HAVING — фильтр для групп. WHERE работает до GROUP BY, HAVING — после.

    Комбинирование всех элементов

    Вот порядок, в котором SQL-запрос собирается из клаузул:

    Не все клаузулы обязательны — можно использовать только нужные. Но когда они присутствуют все, порядок должен быть именно таким. Попробуем собрать сложный запрос:

    Этот запрос берёт книги после 1900 года, группирует по жанру, отбрасывает жанры со средним рейтингом 8 и ниже, а оставшиеся сортирует от лучшего к худшему. Каждый элемент работает на своём месте, и вместе они дают точный ответ на конкретный вопрос.

    Теперь ты умеешь не только хранить данные, но и извлекать из них знания. В следующей статье мы перейдём к самому мощному инструменту SQL — связям между таблицами, которые позволяют объединять данные из разных источников в один осмысленный результат.

    5. Связи между таблицами в личном проекте

    Связи между таблицами в личном проекте

    Представь, что ты ведёшь базу данных для интернет-магазина. Есть таблица клиентов, таблица товаров и таблица заказов. Каждый заказ — это конкретный клиент, который купил конкретный товар. Если хранить имя клиента и название товара прямо в таблице заказов, получишь ту самую проблему дублирования, о которой мы говорили в первой статье: один и тот же клиент покупает десятки товаров, и его данные копируются снова и снова. Решение — связи между таблицами, которые позволяют хранить данные в одном месте и ссылаться на них отовсюду.

    Внешний ключ: мост между таблицами

    В предыдущих статьях мы использовали первичный ключ (PRIMARY KEY) — уникальный идентификатор записи в таблице. Теперь появляется его «зеркальное отражение» — внешний ключ (foreign key). Это столбец в одной таблице, который хранит значение первичного ключа из другой таблицы. Именно через внешние ключи таблицы «знают» друг о друге.

    Построим простую систему для учёта заказов. Сначала — две базовые таблицы:

    Теперь — таблица заказов, которая связывает клиента с товаром:

    Столбец customer_id — это внешний ключ. Он ссылается на id в таблице customers. Аналогично product_id ссылается на id в таблице products. Конструкция FOREIGN KEY ... REFERENCES сообщает СУБД: «Значение в этом столбце должно существовать в указанной таблице».

    Заполним таблицы данными:

    Обрати внимание: в таблице orders мы не пишем «Иванов Иван» и «Ноутбук» — мы пишем их id. Иванов имеет id = 1, ноутбук — id = 1. Если Иванов сменит email, достаточно обновить одну запись в customers, и все его заказы автоматически будут ссылаться на актуальные данные.

    Типы связей между таблицами

    Связи между таблицами бывают трёх видов, и понимание каждого критически важно для проектирования.

    Один к одному (One-to-One)

    Каждой записи в таблице А соответствует ровно одна запись в таблице Б. Например, у каждого пользователя есть ровно один профиль с расширенными данными:

    Здесь user_id в таблице profiles — одновременно и первичный ключ, и внешний ключ. Это гарантирует, что профиль может быть только один. На практике связь «один к одному» встречается реже остальных — часто такие данные просто объединяют в одну таблицу.

    Один ко многим (One-to-Many)

    Одна запись в таблице А связана с множеством записей в таблице Б. Это самый распространённый тип связи. Один клиент — много заказов. Один автор — много книг. Один отдел — много сотрудников.

    Таблица orders из нашего примера — как раз такой случай. Один клиент может сделать много заказов, но каждый заказ принадлежит ровно одному клиенту. Внешний ключ customer_id стоит в таблице «многих» (заказов), а не в таблице «одного» (клиентов).

    Многие ко многим (Many-to-Many)

    Запись в таблице А может быть связана с множеством записей в таблице Б, и наоборот. Книга может принадлежать нескольким жанрам, и жанр может содержать много книг. Студент записан на несколько курсов, и на курс записано много студентов.

    Связь «многие ко многим» нельзя выразить одним внешним ключом. Для неё нужна промежуточная таблица (junction table или associative table):

    Таблица book_genres содержит только внешние ключи. Составной первичный ключ (book_id, genre_id) гарантирует, что одна и та же книга не будет дважды привязана к одному жанру. Заполним:

    Теперь «Мастер и Маргарита» связана сразу с двумя жанрами, и каждый жанр содержит несколько книг. Связь «многие ко многим» реализована.

    JOIN — объединяем данные из разных таблиц

    Внешние ключи хранят только id. Чтобы получить человекочитаемые данные (имя клиента вместо customer_id = 1), нужно объединить таблицы. Для этого используется JOIN:

    Результат:

    | order_id | customer_name | product_name | quantity | order_date | |---------|--------------|-------------|---------|-----------| | 1 | Иванов Иван | Ноутбук | 1 | 2024-10-01 | | 2 | Иванов Иван | Наушники | 2 | 2024-10-01 | | 3 | Петрова Мария | Клавиатура | 1 | 2024-10-05 |

    JOIN работает так: СУБД берёт строку из таблицы orders, смотрит значение customer_id, находит соответствующую строку в customers и «склеивает» их в одну строку результата. Затем делает то же самое с products.

    Типы JOIN

    Не всегда нужно объединять только совпадающие строки. Бывают ситуации, когда нужно увидеть и те записи, для которых нет пары.

    | Тип JOIN | Что возвращает | |---------|---------------| | INNER JOIN (или просто JOIN) | Только строки, где есть совпадение в обеих таблицах | | LEFT JOIN | Все строки из левой таблицы + совпадения из правой (или NULL) | | RIGHT JOIN | Все строки из правой таблицы + совпадения из левой (или NULL) |

    Представь: ты хочешь увидеть всех клиентов и их заказы, включая тех, кто ничего не заказывал:

    LEFT JOIN вернёт всех клиентов. Если у клиента нет заказов, столбец order_id будет NULL. Это позволяет найти, например, неактивных клиентов — тех, кто зарегистрировался, но так ничего и не купил.

    > В SQLite нет RIGHT JOIN, но его можно заменить LEFT JOIN, просто поменяв таблицы местами. На практике LEFT JOIN используется в подавляющем большинстве случаев.

    Целостность данных: что происходит при удалении

    Что произойдёт, если удалить клиента, у которого есть заказы? Ссылки в таблице orders станут «битыми» — customer_id будет указывать на несуществующую запись. Чтобы этого не допустить, внешние ключи обеспечивают ссылочную целостность (referential integrity).

    По умолчанию СУБД запретит удаление клиента, на которого ссылаются заказы. Но можно настроить поведение через ON DELETE:

  • ON DELETE CASCADE — при удалении клиента автоматически удалятся все его заказы. Опасно, но иногда полезно (например, при удалении пользователя удаляются его настройки).
  • ON DELETE RESTRICT — запретить удаление товара, если на него ссылаются заказы. Самый безопасный вариант.
  • ON DELETE SET NULL — при удалении связанной записи внешний ключ станет NULL. Подходит, когда связь необязательна.
  • Практический пример: запросы с JOIN

    Вернёмся к нашей базе и соберём несколько полезных запросов.

    Сколько заказов сделал каждый клиент:

    Общая сумма покупок по каждому клиенту:

    Книги и их жанры (через промежуточную таблицу):

    Здесь GROUP_CONCAT — функция SQLite, которая объединяет значения из нескольких строк в одну строку через разделитель. Результат: «Мастер и Маргарита — Роман, Классика».

    Проектирование связей: советы для личного проекта

    Когда проектируешь базу данных для своего проекта, следуй этим принципам:

  • Каждая сущность — отдельная таблица. Клиенты, товары, заказы, отзывы — всё это отдельные таблицы.
  • Связывай через внешние ключи, а не через дублирование данных. Никогда не пиши имя клиента в таблице заказов — пиши его id.
  • Выбирай правильный тип связи. Один клиент — много заказов? Внешний ключ в таблице заказов. Книга — много жанров? Промежуточная таблица.
  • Продумывай поведение при удалении. Что случится, если удалить клиента? Товар? Решай заранее, а не когда данные уже в продакшене.
  • Начинай с простого. Не нужно проектировать 20 таблиц для личного проекта. Начни с 3–4 ключевых сущностей, свяжи их и постепенно расширяй по мере необходимости.
  • Связи между таблицами — это то, что отличает базу данных от набора разрозненных файлов. Именно они позволяют задавать сложные вопросы и получать осмысленные ответы, объединяя информацию из разных источников в единый результат.