Как делать11 октября 2026 г.8 мин чтения
SQL и PostgreSQLШаг 1 из 9

Базы данных: таблицы, ключи и зачем они бизнесу

Коротко

Реляционная база данных хранит информацию в таблицах: каждая строка — одна запись (клиент, заказ), каждый столбец — одно свойство с заданным типом. Первичный ключ однозначно идентифицирует строку, а внешний ключ связывает таблицы: заказ ссылается на клиента, и база не даст создать заказ несуществующему клиенту. СУБД, например PostgreSQL, проверяет эти правила сама, выдерживает одновременную работу многих пользователей и отвечает на вопросы на языке SQL — этого не умеет файл Excel.

ОсновыУровень: НачальныйОсновы баз данных

После материала вы сможете

  • описать таблицу, строку и столбец на примере клиентов и заказов интернет-магазина
  • объяснить, зачем нужен первичный ключ и почему телефон или имя для него не подходят
  • показать на схеме, как внешний ключ связывает заказ с клиентом и что база сделает при попытке нарушить связь
  • перечислить, что СУБД даёт по сравнению с файлом Excel: ограничения, одновременный доступ, транзакции и запросы

Проверено на

PostgreSQL 17 (17.11, образ postgres:17-alpine)

Сверено с документацией: 11 октября 2026 г.

Содержание
  1. 01Данные как таблицы: строки и столбцы
  2. 02Первичный ключ: как отличить одну запись от другой
  3. 03Внешний ключ: связь клиента и заказа
  4. 04Типы данных и ограничения
  5. 05СУБД против Excel: что база даёт поверх файла
  6. 06Реляционные и нереляционные базы — кратко
  7. 07Как SQL «спрашивает» базу

Данные как таблицы: строки и столбцы

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

Строка — одна запись: один клиент или один заказ. Столбец — одно свойство всех записей: имя, город, e-mail, дата регистрации. В таблице клиентов ниже восемь строк и пять столбцов. Значение NULL означает «значение неизвестно или отсутствует»: у клиента не указан e-mail или город. Это не пустая строка и не ноль — к этому различию мы ещё вернёмся в уроках по SQL.

1

name
Aigerim Seitova
city
Almaty
email
aigerim@example.com
created_on
2026-06-02

2

name
Daniyar Akhmetov
city
Astana
email
NULL
created_on
2026-06-15

3

name
Madina Omarova
city
Almaty
email
madina@example.com
created_on
2026-07-01

4

name
Timur Kim
city
Shymkent
email
timur@example.com
created_on
2026-07-20

5

name
Aliya Bekova
city
NULL
email
aliya@example.com
created_on
2026-08-05

6

name
Ruslan Ivanov
city
Almaty
email
NULL
created_on
2026-08-28

7

name
Zarina Kaliyeva
city
Karaganda
email
zarina@example.org
created_on
2026-09-10

8

name
Sergey Pak
city
Astana
email
sergey@example.com
created_on
2026-09-30
Таблица clients учебной базы: реальный вывод PostgreSQL 17 на запрос SELECT * FROM clients ORDER BY id (NULL — значение не задано).

Таблицы объединяются в базу данных, а базой управляет СУБД — система управления базами данных. PostgreSQL работает как сервер: приложение (сайт, CRM, мобильный бэкенд) подключается к нему по сети, отправляет запросы и получает ответы. Сам сервер может обслуживать много подключений одновременно.

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

Первичный ключ (primary key) — столбец или набор столбцов, по которому строку можно однозначно найти. По документации PostgreSQL первичный ключ требует двух вещей одновременно: значения уникальны и не бывают NULL. Ключ у таблицы может быть только один, и база автоматически строит для него индекс, чтобы поиск по ключу был быстрым.

Почему бы не взять в качестве ключа имя или телефон? Имена повторяются: в магазине легко окажутся две клиентки по имени Айгерим. Телефон и e-mail меняются, а ещё их может не быть вовсе. Если на изменчивое значение ссылаются заказы, при смене номера придётся переписывать все связанные записи. Поэтому ключ обычно делают отдельным числовым столбцом id, который ничего не значит для людей и никогда не меняется.

shop.sql
CREATE TABLE clients (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name       text NOT NULL,
    city       text,
    email      text UNIQUE,
    created_on date NOT NULL DEFAULT current_date
);

CREATE TABLE orders (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    client_id  bigint NOT NULL REFERENCES clients (id),
    ordered_on date NOT NULL,
    status     text NOT NULL
               CHECK (status IN ('new', 'paid', 'shipped', 'cancelled')),
    promo_code text
);
Фрагмент учебной базы (полный скрипт — в уроке «SQL с нуля»). GENERATED ALWAYS AS IDENTITY — номер выдаёт сама база: 1, 2, 3…
  • id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY — ключ; номера выдаёт база, вручную вписать его нельзя
  • name text NOT NULL — имя обязательно, строка без имени не сохранится
  • email text UNIQUE — два клиента с одним e-mail невозможны, но e-mail можно не указывать
  • created_on date NOT NULL DEFAULT current_date — если дату не передали, база подставит сегодняшнюю

Ключ может состоять и из нескольких столбцов. В таблице позиций заказа order_items ключ — пара (order_id, product_id): один и тот же товар не может дважды попасть в один заказ отдельной строкой, вместо этого растёт количество.

Внешний ключ: связь клиента и заказа

В заказе не хранят имя и город клиента — только его номер в столбце client_id. Ограничение REFERENCES clients (id) превращает этот столбец во внешний ключ (foreign key): значение обязано совпадать с первичным ключом какой-то строки в таблице клиентов. Документация называет это ссылочной целостностью — в базе не появятся «осиротевшие» заказы.

  1. 01clients
  2. 02orders
  3. 03order_items
  4. 04products
Связи учебной базы: заказ ссылается на клиента, позиция — на заказ и на товар.

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

SQL
INSERT INTO orders (client_id, ordered_on, status)
VALUES (99, '2026-10-10', 'new');

-- ERROR:  insert or update on table "orders" violates foreign key constraint "orders_client_id_fkey"
-- DETAIL:  Key (client_id)=(99) is not present in table "clients".
Реальный ответ PostgreSQL 17 — в комментариях: заказ клиенту 99 не создан.
SQL
DELETE FROM clients WHERE id = 1;

-- ERROR:  update or delete on table "clients" violates foreign key constraint "orders_client_id_fkey" on table "orders"
-- DETAIL:  Key (id)=(1) is still referenced from table "orders".
Удалить клиента с заказами нельзя: по умолчанию действует NO ACTION.

Что делать с зависимыми строками при удалении, решает проектировщик базы. По умолчанию удаление запрещено, пока на строку кто-то ссылается. Вариант ON DELETE CASCADE удаляет зависимые строки вместе с главной — он подходит, когда зависимая запись не существует сама по себе. В учебной базе так устроены позиции заказа: удалили заказ — удалились и его строки. А вот товары и заказы — независимые сущности, поэтому удалить товар, который кто-то уже купил, база не даст.

Обратите внимание на таблицу order_items. Один заказ содержит много товаров, а один товар встречается во многих заказах — это связь «многие ко многим». Её всегда раскладывают через промежуточную таблицу с двумя внешними ключами; ровно такую схему (products, orders, order_items) документация PostgreSQL разбирает в разделе об ограничениях. Цена в order_items хранится отдельно от каталога: каталог может подорожать, а сумма старого заказа меняться не должна.

1

product_id
1
quantity
1
price
12900.00

1

product_id
6
quantity
2
price
2500.00
Позиции заказа № 1 (реальный вывод psql): две строки ссылаются на один заказ и на два разных товара.

Типы данных и ограничения

Тип столбца — первое правило, которое база проверяет при записи: в столбец даты не попадёт строка «завтра», в числовой — «двенадцать». Для учебной базы хватает нескольких типов:

bigint, integer

Что хранит
Целые числа (8 и 4 байта)
Пример в учебной базе
id, количество товара, вес в граммах

numeric(10, 2)

Что хранит
Точное десятичное число: до 10 цифр, из них 2 после запятой
Пример в учебной базе
Цена 12900.00

text

Что хранит
Строка любой длины
Пример в учебной базе
Имя, город, статус заказа

date

Что хранит
Календарная дата
Пример в учебной базе
Дата заказа 2026-09-02

boolean

Что хранит
Истина или ложь
Пример в учебной базе
Флаг «подписан на рассылку» (в учебной базе нет)

Типа часто недостаточно: цена — число, но отрицательной быть не должна. Для таких правил служат ограничения (constraints). Если запись их нарушает, PostgreSQL отклоняет её целиком и сообщает, какое правило сработало:

NOT NULL

Правило
Значение обязательно
В учебной базе
Имя клиента, дата и статус заказа

UNIQUE

Правило
Значение не повторяется в таблице
В учебной базе
E-mail клиента

CHECK (…)

Правило
Логическое условие для значения
В учебной базе
price > 0, статус только из списка

PRIMARY KEY

Правило
Уникально и не NULL; один на таблицу
В учебной базе
id в каждой таблице

REFERENCES

Правило
Значение есть в другой таблице
В учебной базе
orders.client_id → clients.id
SQL
UPDATE orders SET status = 'delivered' WHERE id = 1;

-- ERROR:  new row for relation "orders" violates check constraint "orders_status_check"
-- DETAIL:  Failing row contains (1, 1, 2026-09-02, delivered, null).
Статус «delivered» не входит в список CHECK — строка не изменилась.
SQL
INSERT INTO clients (name, email)
VALUES ('Aigerim S.', 'aigerim@example.com');

-- ERROR:  duplicate key value violates unique constraint "clients_email_key"
-- DETAIL:  Key (email)=(aigerim@example.com) already exists.
Второй клиент с тем же e-mail отклонён ограничением UNIQUE.

Одна тонкость: ограничение CHECK считается выполненным, если условие истинно или равно NULL. Поэтому weight_g integer CHECK (weight_g > 0) разрешает не указывать вес у кружки, но не даст записать отрицательный. Если значение обязательно, к CHECK добавляют NOT NULL. Документация советует: в большинстве схем большая часть столбцов должна быть NOT NULL.

СУБД против Excel: что база даёт поверх файла

Таблица в Excel или Google Sheets тоже состоит из строк и столбцов, и для старта этого часто хватает. Разница проявляется, когда данных и людей становится больше. Подробно о признаках этого момента — в статье «Когда пора уходить от Excel»; здесь — техническая сторона.

Файл-таблица

  • Любая ячейка принимает что угодно: текст в столбце цены, дату в произвольном формате
  • Связь «заказ — клиент» держится на том, что имя написано одинаково
  • Два человека правят файл одновременно — правки перезаписывают друг друга или расходятся по копиям
  • Сбой посередине операции оставляет половину изменений
  • Отчёт — ручные фильтры, сводные таблицы и формулы

СУБД (PostgreSQL)

  • Типы и ограничения проверяются при каждой записи, ошибочная строка не сохранится
  • Внешний ключ гарантирует, что заказ ссылается на существующего клиента
  • Сервер обслуживает много подключений одновременно: сайт, CRM и бухгалтерия работают с одними данными
  • Транзакция выполняет несколько шагов по принципу «всё или ничего»
  • Отчёт — запрос на SQL, который повторяется одной командой

Транзакции стоит выделить отдельно. Документация PostgreSQL описывает их так: несколько шагов объединяются в одну операцию «всё или ничего», промежуточное состояние не видно другим пользователям, а при сбое ни один шаг не применяется. Для магазина это значит, что заказ не окажется оплаченным без списания товара со склада. Транзакциям посвящён отдельный урок траектории.

Реляционные и нереляционные базы — кратко

Всё, что описано выше, относится к реляционным базам: данные лежат в таблицах, связи задаются ключами, вопросы задаются на SQL. Самые распространённые реляционные СУБД в веб-разработке — PostgreSQL и MySQL. Как отмечает глоссарий MDN, большинство баз в веб-разработке устроены именно так, а базы с другой моделью данных называют NoSQL.

Реляционная

Как хранит
Таблицы со строгой схемой и связями
Пример СУБД
PostgreSQL, MySQL
Типичная задача
Клиенты, заказы, оплаты, учёт

Ключ — значение

Как хранит
Значение по ключу, часто в памяти
Пример СУБД
Redis
Типичная задача
Кеш, сессии, счётчики, очереди

Документная

Как хранит
Документы JSON-подобной структуры
Пример СУБД
MongoDB
Типичная задача
Записи с меняющимся набором полей

Это не соревнование: в одном проекте часто работают обе модели — PostgreSQL хранит заказы, Redis кеширует частые ответы. Для учёта, где важны связи и точность, начинают с реляционной базы. Когда и зачем добавлять NoSQL, разбирается в отдельном сравнении направления «Базы данных».

Как SQL «спрашивает» базу

SQL — язык запросов к реляционным базам. Его главная особенность: вы описываете, какой результат нужен, а не как его получить. Не нужно писать «открой таблицу заказов, переберите строки, для каждой найди клиента» — достаточно назвать таблицы, связь и условие. Как именно читать данные, решает сама СУБД.

SQL
SELECT o.id, o.ordered_on, c.name, o.status
FROM orders AS o
JOIN clients AS c ON c.id = o.client_id
WHERE c.city = 'Almaty'
ORDER BY o.ordered_on;
Все заказы клиентов из Алматы с именем клиента: две таблицы соединены по внешнему ключу.

1

ordered_on
2026-09-02
name
Aigerim Seitova
status
shipped

3

ordered_on
2026-09-14
name
Aigerim Seitova
status
paid

4

ordered_on
2026-09-20
name
Madina Omarova
status
cancelled

7

ordered_on
2026-10-03
name
Madina Omarova
status
shipped

8

ordered_on
2026-10-05
name
Ruslan Ivanov
status
new

9

ordered_on
2026-10-08
name
Aigerim Seitova
status
paid
Реальный вывод psql на PostgreSQL 17.

Запрос читается почти как предложение: выбрать номер, дату, имя и статус из заказов, присоединив клиентов по client_id, где город — Almaty, отсортировав по дате. Каждую часть — SELECT, WHERE, ORDER BY, JOIN — мы разберём по шагам в следующих уроках на этой же учебной базе. Её можно поднять одной командой в Docker и повторить все примеры у себя.

Как проверено4 · пройдено1 · не запускалосьсверено 11 октября 2026 г.
  • запущено, работает

    Схема clients/orders (фрагмент shop.sql)

    shop.sql выполнен в docker postgres:17-alpine (PostgreSQL 17.11) через docker exec -i kb-pg-db0103 psql -U postgres < shop.sql: CREATE TABLE ×4, INSERT 8/9/10/15; фрагмент вставлен в статью скриптом из того же файла

  • запущено, работает

    Ошибки ограничений: FK (client_id 99), удаление клиента с заказами, CHECK статуса, UNIQUE e-mail

    каждый запрос выполнен через psql -X -P null=NULL; текст ошибок вставлен в статью из файлов вывода без правок

  • запущено, работает

    Таблицы-результаты (clients, order_items заказа 1, запрос с JOIN по Алматы)

    таблицы в статье собраны парсером из реального вывода psql (выравнивание по строке с дефисами), без ручного набора

  • запущено, работает

    ON DELETE CASCADE для order_items

    BEGIN; DELETE FROM orders WHERE id = 1; count(order_items) = 0; ROLLBACK — утверждение в тексте подтверждено, код в статью не включён

  • не запускалось

    Упражнение «Схема салона на бумаге»

    упражнение без кода (схема на бумаге), решение — текстовое описание таблиц

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.