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

PostgreSQL: установка в Docker, psql и первая таблица

Коротко

Локально PostgreSQL проще всего запустить в Docker: команда docker run с паролем суперпользователя, портом и именованным томом поднимает сервер, а консольный клиент psql уже есть внутри контейнера. Дальше создают отдельного владельца базы для приложения, описывают таблицы с подходящими типами (bigint, text, numeric для денег, timestamptz для моментов времени) и ограничениями NOT NULL, CHECK, UNIQUE, DEFAULT, а резервную копию делают через pg_dump и обязательно проверяют восстановлением. Актуальная стабильная версия — PostgreSQL 18; урок проверен на 17.11, и всё в нём так же работает в 18, кроме пути, куда монтируется том.

Пошаговый урокУровень: НачальныйPostgreSQLПрактика: ~45–90 мин

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

  • запустить PostgreSQL в Docker с именованным томом и убедиться, что данные переживают пересоздание контейнера
  • работать в psql: подключаться, смотреть базы, таблицы и роли командами \l, \dt, \d, \du
  • создать базу с отдельным владельцем и пользователем только для чтения и описать таблицы с типами и ограничениями NOT NULL, CHECK, UNIQUE, DEFAULT
  • сделать резервную копию через pg_dump, восстановить её через pg_restore и подключиться к базе из приложения по строке подключения

Проверено на

PostgreSQL 17.11 (postgres:17-alpine)PostgreSQL 18.6 — сверкаDockerpsql 17Node.js 24.18 + pg 8.23.1

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

Содержание
  1. 01Какую версию взять: PostgreSQL 17 или 18
  2. 02Запуск в Docker: пароль, порт и том для данных
  3. 03psql: подключение и мета-команды
  4. 04База и пользователь с минимальными правами
  5. 05Типы данных: что выбрать для каждого поля
  6. 06Первая таблица: ограничения и изменение данных
  7. 07Резервная копия и подключение из приложения
  8. 08Частые ошибки и упражнения

Какую версию взять: PostgreSQL 17 или 18

PostgreSQL — реляционная СУБД с открытым исходным кодом: сервер принимает SQL-запросы, хранит данные на диске и сам следит за правилами целостности, которые вы описали в схеме. В уроке «SQL с нуля» вы уже читали данные из готовой учебной базы магазина кофе. Здесь мы поднимем такой сервер сами, создадим ту же базу — таблицы clients, products, orders, order_items — с нуля и научимся её беречь.

По политике версий PostgreSQL новая основная (major) версия выходит примерно раз в год и поддерживается пять лет; минорные выпуски с исправлениями выходят минимум раз в квартал. Состояние на дату проверки урока:

18 (18.6)

Статус
Актуальная стабильная, вышла 25.09.2025, поддержка до 14.11.2030
Что важно для этого урока
В Docker-образе том монтируется в /var/lib/postgresql; есть функция uuidv7(); \dt печатает заголовок «List of tables»

17 (17.11)

Статус
Поддерживается до 08.11.2029
Что важно для этого урока
Том монтируется в /var/lib/postgresql/data; UUID генерирует gen_random_uuid() (версия 4)

19

Статус
Бета-версия (Beta 4 от 24.09.2026)
Что важно для этого урока
Только для пробы, не для работы
Данные — со страницы Versioning Policy на postgresql.org, 11 октября 2026

Урок написан на образе postgres:17-alpine: на нём идут все уроки трека SQL, и команды с выводом совпадают от урока к уроку. Мы прогнали каждый SQL-файл и каждую демонстрацию из урока и на PostgreSQL 18.6 — результаты те же, отличаются только путь тома и заголовок \dt. Для нового проекта берите 18: её поддерживают на год дольше. Дамп учебной базы, снятый в 17, восстановился в 18.6 без ошибок — так и выглядит переход между основными версиями.

Запуск в Docker: пароль, порт и том для данных

Официальный образ postgres содержит сервер и клиентские утилиты: psql, pg_dump, pg_restore. Единственная обязательная переменная — POSTGRES_PASSWORD, пароль суперпользователя postgres. Данные храним в именованном томе (volume) — области диска, которой управляет Docker и которая живёт отдельно от контейнера.

bash
docker volume create shop-pgdata

docker run -d --rm --name shop-db \
  -e POSTGRES_PASSWORD=change-me-locally \
  -p 127.0.0.1:5432:5432 \
  -v shop-pgdata:/var/lib/postgresql/data \
  postgres:17-alpine

docker logs shop-db
Запуск сервера. В PowerShell команды те же, перенос строки — обратная кавычка вместо обратной косой черты (проверено в PowerShell 7.6)
  • -d — запустить в фоне; --rm — удалить контейнер после остановки: он одноразовый, всё ценное лежит в томе
  • --name shop-db — имя, по которому мы будем обращаться к контейнеру
  • -p 127.0.0.1:5432:5432 — открыть порт только на вашем компьютере; без 127.0.0.1 база видна всей локальной сети
  • -v shop-pgdata:/var/lib/postgresql/data — подключить том туда, где PostgreSQL 17 хранит данные
  • postgres:17-alpine — тег с закреплённой основной версией: минорные обновления придут, а 18 не подменит 17 внезапно
terminal
$ docker logs shop-db 2>&1 | tail -1
2026-10-12 01:02:45.235 UTC [1] LOG:  database system is ready to accept connections
Последняя строка журнала: сервер готов
terminal
$ docker run -d --rm --name pg-wrong -e POSTGRES_PASSWORD=change-me-locally -v pg-wrong:/var/lib/postgresql postgres:17-alpine
$ docker exec pg-wrong psql -U postgres -c "CREATE DATABASE shop"
CREATE DATABASE
$ docker inspect pg-wrong --format '{{range .Mounts}}{{.Name}} -> {{.Destination}}{{println}}{{end}}'
pg-wrong -> /var/lib/postgresql
6ad9b1ca6430d46b5b43f74b3cc4757853465c4e675bff418b1636558222dc07 -> /var/lib/postgresql/data

$ docker stop pg-wrong
$ docker run -d --rm --name pg-wrong -e POSTGRES_PASSWORD=change-me-locally -v pg-wrong:/var/lib/postgresql postgres:17-alpine
$ docker exec pg-wrong psql -U postgres -c '\l shop'
                                          List of databases
 Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
------+-------+----------+-----------------+---------+-------+--------+-----------+-------------------
(0 rows)
Ошибка пути: Docker подключил второй, анонимный том к /var/lib/postgresql/data, и созданная база исчезла после перезапуска

psql: подключение и мета-команды

psql — консольный клиент PostgreSQL. Проще всего запустить его прямо в контейнере через docker exec -it. Внутри контейнера подключение идёт через Unix-сокет, и образ разрешает его без пароля; подключение снаружи, по TCP через опубликованный порт, требует пароль (метод scram-sha-256).

terminal
$ docker exec -it shop-db psql -U postgres
postgres=# \conninfo
You are connected to database "postgres" as user "postgres" via socket in "/var/run/postgresql" at port "5432".
postgres=# SELECT version();
                                         version
------------------------------------------------------------------------------------------
 PostgreSQL 17.11 on x86_64-pc-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
(1 row)

postgres=# \q
Первое подключение суперпользователем; приглашение postgres=# означает суперпользователя, обычный пользователь видит =>

Команды, которые начинаются с обратной косой черты, — мета-команды: их выполняет сам psql, а не сервер. SQL-команда заканчивается точкой с запятой; пока её нет, psql ждёт продолжения и меняет приглашение на shop->. Самые нужные мета-команды:

\l

Что делает
список баз данных с владельцами и кодировкой

\c shop

Что делает
переключиться на базу shop

\dt

Что делает
таблицы текущей базы

\d orders

Что делает
структура таблицы: столбцы, типы, значения по умолчанию, индексы, ограничения, внешние ключи

\du

Что делает
роли (пользователи) и их атрибуты

\dp products

Что делает
права доступа к таблице

\conninfo

Что делает
к какой базе и под каким пользователем вы подключены

\x

Что делает
вертикальный вывод — удобно для широких строк

\i file.sql

Что делает
выполнить файл с SQL

\? / \h CREATE TABLE

Что делает
справка по мета-командам / по синтаксису SQL

\q

Что делает
выйти

Без интерактивного режима psql тоже полезен: флаг -c выполняет одну команду, -f — файл, а -v ON_ERROR_STOP=1 останавливает скрипт на первой ошибке и возвращает ненулевой код выхода. По умолчанию psql продолжает выполнять скрипт после ошибки — для скриптов схемы это опасно.

База и пользователь с минимальными правами

Суперпользователь postgres обходит все проверки прав, поэтому приложению его не дают. Создадим две роли: shop_app — владелец базы shop, от его имени работает приложение и создаются таблицы; shop_readonly — только чтение, например для отчётов. Все файлы урока лежат в папке db/ рядом с вами.

db/roles.sql
-- Выполняет суперпользователь postgres один раз
CREATE ROLE shop_app LOGIN PASSWORD 'change-me-app';
CREATE ROLE shop_readonly LOGIN PASSWORD 'change-me-readonly';

-- Владелец базы — пользователь приложения, а не суперпользователь
CREATE DATABASE shop OWNER shop_app;
Роли и база; пароли здесь — учебные заглушки
db/grants.sql
-- Выполняет владелец shop_app в базе shop
GRANT SELECT ON ALL TABLES IN SCHEMA public TO shop_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO shop_readonly;
Чтение всех текущих таблиц и таблиц, которые shop_app создаст позже
bash
docker cp db shop-db:/db

docker exec shop-db psql -U postgres -v ON_ERROR_STOP=1 -f /db/roles.sql

docker exec shop-db psql -U shop_app -d shop -v ON_ERROR_STOP=1 \
  -f /db/schema.sql -f /db/seed.sql -f /db/grants.sql
Копируем папку db в контейнер и выполняем файлы по порядку: роли — суперпользователем, схему, данные и права — владельцем базы
terminal
$ docker exec shop-db psql -U postgres -v ON_ERROR_STOP=1 -f /db/roles.sql
CREATE ROLE
CREATE ROLE
CREATE DATABASE
$ docker exec shop-db psql -U shop_app -d shop -v ON_ERROR_STOP=1 -f /db/schema.sql -f /db/seed.sql -f /db/grants.sql
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 3
INSERT 0 3
INSERT 0 3
INSERT 0 4
GRANT
ALTER DEFAULT PRIVILEGES
$ docker exec shop-db psql -U postgres -c '\l shop' -c '\du'
                                                List of databases
 Name |  Owner   | Encoding | Locale Provider |  Collate   |   Ctype    | Locale | ICU Rules | Access privileges
------+----------+----------+-----------------+------------+------------+--------+-----------+-------------------
 shop | shop_app | UTF8     | libc            | en_US.utf8 | en_US.utf8 |        |           |
(1 row)

                               List of roles
   Role name   |                         Attributes
---------------+------------------------------------------------------------
 postgres      | Superuser, Create role, Create DB, Replication, Bypass RLS
 shop_app      |
 shop_readonly | 
Реальный вывод: база shop принадлежит shop_app, у обеих новых ролей нет особых атрибутов

Проверим, что shop_readonly действительно только читает. Начиная с PostgreSQL 15, схемой public владеет специальная роль pg_database_owner — то есть владелец базы, — а остальным по умолчанию доступно только использование схемы (U), без создания объектов (C).

terminal
$ docker exec shop-db psql -U shop_readonly -d shop -c "SELECT name, price FROM products ORDER BY id"
            name            |  price
----------------------------+----------
 Espresso Blend 1 kg        | 12900.00
 Ethiopia Yirgacheffe 250 g |  5600.00
 Paper filters V60          |  2500.00
(3 rows)

$ docker exec shop-db psql -U shop_readonly -d shop -c "DELETE FROM orders"
ERROR:  permission denied for table orders
$ docker exec shop-db psql -U shop_readonly -d shop -c "CREATE TABLE notes (id int)"
ERROR:  permission denied for schema public
LINE 1: CREATE TABLE notes (id int)
                     ^
$ docker exec shop-db psql -U shop_app -d shop -c '\dn+ public'
                                       List of schemas
  Name  |       Owner       |           Access privileges            |      Description
--------+-------------------+----------------------------------------+------------------------
 public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema
        |                   | =U/pg_database_owner                   |
(1 row)
SELECT разрешён, изменение данных и создание таблиц — нет

Типы данных: что выбрать для каждого поля

Тип столбца — первое ограничение: в integer не запишешь текст, в date — 31 февраля. Для типичного бизнес-приложения хватает короткого списка:

Идентификатор строки

Тип
bigint GENERATED ALWAYS AS IDENTITY
Почему
Номер выдаёт сама база; bigint не упрётся в предел integer (2 147 483 647)

Имя, email, город, комментарий

Тип
text
Почему
По документации нет разницы в скорости между text и varchar(n); длину при необходимости ограничивают через CHECK

Количество, вес в граммах

Тип
integer
Почему
Целые числа до 2 147 483 647, 4 байта

Цена, сумма заказа

Тип
numeric(10,2)
Почему
Точная арифметика: документация рекомендует numeric для денег. real и double precision — приближённые

Дата без времени

Тип
date
Почему
День заказа или рождения

Момент события

Тип
timestamptz
Почему
Хранится в UTC и показывается в часовом поясе сессии

Да/нет

Тип
boolean
Почему
true, false или NULL — «неизвестно»

Публичный номер заказа

Тип
uuid
Почему
Не угадывается перебором; в 17 — gen_random_uuid(), в 18 добавлена uuidv7()

Гибкие атрибуты товара

Тип
jsonb
Почему
Двоичный JSON: быстрее обрабатывается и индексируется; документация советует его вместо json

Разница видна на практике. Сложение 0.1 + 0.2 в double precision даёт неточный результат, а в numeric — ровно 0.3. Переполнение integer — ошибка, а не тихое искажение. Момент оплаты, записанный как 10:15+05, хранится в UTC и выводится в поясе сессии; в актуальной базе часовых поясов Asia/Almaty — это UTC+5.

terminal
shop=> SELECT 0.1::double precision + 0.2::double precision AS float_sum,
shop->        0.1::numeric + 0.2::numeric                   AS numeric_sum;
      float_sum      | numeric_sum
---------------------+-------------
 0.30000000000000004 |         0.3
(1 row)

shop=> SELECT 2147483647::integer + 1;
ERROR:  integer out of range
shop=> SELECT id, paid_at FROM orders WHERE id = 1;
 id |        paid_at
----+------------------------
  1 | 2026-09-02 05:15:00+00
(1 row)

shop=> SET TimeZone = 'Asia/Almaty';
SET
shop=> SELECT id, paid_at FROM orders WHERE id = 1;
 id |        paid_at
----+------------------------
  1 | 2026-09-02 10:15:00+05
(1 row)

shop=> SELECT name, attrs ->> 'roast' AS roast
shop-> FROM products
shop-> WHERE attrs ? 'roast';
            name            | roast
----------------------------+-------
 Espresso Blend 1 kg        | dark
 Ethiopia Yirgacheffe 250 g | light
(2 rows)
Реальные ответы PostgreSQL 17.11: деньги, переполнение, часовой пояс и поиск по jsonb

Первая таблица: ограничения и изменение данных

Схема ниже повторяет учебную базу из урока «SQL с нуля» и добавляет несколько столбцов, чтобы показать типы из таблицы выше: public_id (uuid), paid_at (timestamptz), is_active (boolean) и attrs (jsonb).

db/schema.sql
-- Учебная схема магазина кофе: клиенты, товары, заказы, позиции заказа
CREATE TABLE clients (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name       text NOT NULL CHECK (name <> ''),
  city       text,
  email      text UNIQUE,
  created_on date NOT NULL DEFAULT CURRENT_DATE
);

CREATE TABLE products (
  id        bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name      text          NOT NULL,
  category  text          NOT NULL,
  price     numeric(10,2) NOT NULL CHECK (price > 0),
  weight_g  integer       CHECK (weight_g > 0),
  is_active boolean       NOT NULL DEFAULT true,
  attrs     jsonb         NOT NULL DEFAULT '{}'
);

CREATE TABLE orders (
  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  public_id  uuid NOT NULL UNIQUE DEFAULT gen_random_uuid(),
  client_id  bigint NOT NULL REFERENCES clients (id),
  ordered_on date NOT NULL DEFAULT CURRENT_DATE,
  status     text NOT NULL DEFAULT 'new'
             CHECK (status IN ('new', 'paid', 'shipped', 'cancelled')),
  promo_code text,
  paid_at    timestamptz
);

CREATE TABLE order_items (
  order_id   bigint        NOT NULL REFERENCES orders (id) ON DELETE CASCADE,
  product_id bigint        NOT NULL REFERENCES products (id),
  quantity   integer       NOT NULL CHECK (quantity > 0),
  price      numeric(10,2) NOT NULL CHECK (price > 0),
  PRIMARY KEY (order_id, product_id)
);
Каждое правило бизнеса записано прямо в схему: база отклонит неверную строку, даже если в коде приложения ошибка
db/seed.sql
-- Вымышленные данные: никаких реальных клиентов и контактов
INSERT INTO clients (name, city, email, created_on) VALUES
  ('Aigerim Seitova',  'Almaty', 'aigerim@example.com', '2026-06-02'),
  ('Daniyar Akhmetov', 'Astana', NULL,                  '2026-06-15'),
  ('Madina Omarova',   'Almaty', 'madina@example.com',  '2026-07-01');

INSERT INTO products (name, category, price, weight_g, attrs) VALUES
  ('Espresso Blend 1 kg',        'coffee', 12900.00, 1000, '{"roast": "dark"}'),
  ('Ethiopia Yirgacheffe 250 g', 'coffee',  5600.00,  250, '{"roast": "light"}'),
  ('Paper filters V60',          'gear',    2500.00, NULL, '{"pack": 100}');

INSERT INTO orders (client_id, ordered_on, status, paid_at) VALUES
  (1, '2026-09-02', 'shipped', '2026-09-02 10:15:00+05'),
  (2, '2026-09-05', 'paid',    '2026-09-05 18:40:00+05'),
  (1, '2026-09-14', 'new',     NULL);

INSERT INTO order_items (order_id, product_id, quantity, price) VALUES
  (1, 1, 1, 12900.00),
  (1, 3, 2,  2500.00),
  (2, 2, 2,  5600.00),
  (3, 2, 1,  5600.00);
Учебные данные: три клиента, три товара, три заказа
  • NOT NULL — значение обязательно
  • CHECK (условие) — значение проходит проверку; условие, которое дало NULL, считается выполненным, поэтому weight_g можно оставить пустым
  • UNIQUE — повторы запрещены, но два NULL по умолчанию не считаются равными: несколько клиентов без email допустимы
  • DEFAULT — значение, если столбец не указан в INSERT: сегодняшняя дата, статус new, новый UUID
  • PRIMARY KEY — UNIQUE + NOT NULL и автоматически созданный индекс
  • REFERENCES — внешний ключ: заказ не может ссылаться на несуществующего клиента. Как выбирать связи и правила удаления, разбираем в уроке о проектировании БД
terminal
shop=> \dt
            List of relations
 Schema |    Name     | Type  |  Owner
--------+-------------+-------+----------
 public | clients     | table | shop_app
 public | order_items | table | shop_app
 public | orders      | table | shop_app
 public | products    | table | shop_app
(4 rows)

shop=> \d orders
                                    Table "public.orders"
   Column   |           Type           | Collation | Nullable |           Default
------------+--------------------------+-----------+----------+------------------------------
 id         | bigint                   |           | not null | generated always as identity
 public_id  | uuid                     |           | not null | gen_random_uuid()
 client_id  | bigint                   |           | not null |
 ordered_on | date                     |           | not null | CURRENT_DATE
 status     | text                     |           | not null | 'new'::text
 promo_code | text                     |           |          |
 paid_at    | timestamp with time zone |           |          |
Indexes:
    "orders_pkey" PRIMARY KEY, btree (id)
    "orders_public_id_key" UNIQUE CONSTRAINT, btree (public_id)
Check constraints:
    "orders_status_check" CHECK (status = ANY (ARRAY['new'::text, 'paid'::text, 'shipped'::text, 'cancelled'::text]))
Foreign-key constraints:
    "orders_client_id_fkey" FOREIGN KEY (client_id) REFERENCES clients(id)
Referenced by:
    TABLE "order_items" CONSTRAINT "order_items_order_id_fkey" FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
\dt и \d показывают, что база запомнила: типы, значения по умолчанию, индексы и все ограничения с именами

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

terminal
shop=> INSERT INTO clients (name) VALUES (NULL);
ERROR:  null value in column "name" of relation "clients" violates not-null constraint
DETAIL:  Failing row contains (4, null, null, null, 2026-10-12).
shop=> INSERT INTO clients (name) VALUES ('');
ERROR:  new row for relation "clients" violates check constraint "clients_name_check"
DETAIL:  Failing row contains (5, , null, null, 2026-10-12).
shop=> INSERT INTO clients (name, email) VALUES ('Duplicate', 'aigerim@example.com');
ERROR:  duplicate key value violates unique constraint "clients_email_key"
DETAIL:  Key (email)=(aigerim@example.com) already exists.
shop=> INSERT INTO products (name, category, price) VALUES ('Free sample', 'coffee', 0);
ERROR:  new row for relation "products" violates check constraint "products_price_check"
DETAIL:  Failing row contains (4, Free sample, coffee, 0.00, null, t, {}).
shop=> INSERT INTO orders (client_id, status) VALUES (1, 'done');
ERROR:  new row for relation "orders" violates check constraint "orders_status_check"
DETAIL:  Failing row contains (4, 782914a2-f487-46e0-a5e7-6655ab194e20, 1, 2026-10-12, done, null, null).
shop=> INSERT INTO orders (client_id) VALUES (999);
ERROR:  insert or update on table "orders" violates foreign key constraint "orders_client_id_fkey"
DETAIL:  Key (client_id)=(999) is not present in table "clients".
shop=> INSERT INTO clients (id, name) VALUES (100, 'Manual id');
ERROR:  cannot insert a non-DEFAULT value into column "id"
DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.
HINT:  Use OVERRIDING SYSTEM VALUE to override.
shop=> INSERT INTO clients (name) VALUES ('No email 1'), ('No email 2') RETURNING id, email;
 id | email
----+-------
  7 |
  8 |
(2 rows)

INSERT 0 2
Реальные ошибки PostgreSQL 17.11 на каждое ограничение

Обратите внимание на номера: отклонённые вставки израсходовали значения 4, 5 и 6, и следующий клиент получил id 7. Это нормально: документация прямо говорит, что последовательности не возвращают номера при откате, поэтому идентификаторы идут с пропусками. Если бизнесу нужна сплошная нумерация документов, её делают отдельным механизмом.

Изменения данных — INSERT, UPDATE, DELETE — умеют возвращать затронутые строки через RETURNING. Это избавляет от второго запроса: приложение сразу получает id новой записи или новые цены.

terminal
shop=> INSERT INTO clients (name, city)
shop-> VALUES ('Timur Bekov', 'Shymkent')
shop-> RETURNING id, created_on;
 id | created_on
----+------------
  9 | 2026-10-12
(1 row)

INSERT 0 1
shop=> UPDATE products
shop-> SET price = price * 1.10
shop-> WHERE category = 'coffee'
shop-> RETURNING id, name, price;
 id |            name            |  price
----+----------------------------+----------
  1 | Espresso Blend 1 kg        | 14190.00
  2 | Ethiopia Yirgacheffe 250 g |  6160.00
(2 rows)

UPDATE 2
shop=> DELETE FROM orders
shop-> WHERE status = 'new'
shop-> RETURNING id, public_id;
 id |              public_id
----+--------------------------------------
  3 | 6138bdcb-be64-4598-a3e1-5c2c2ef80458
(1 row)

DELETE 1
shop=> SELECT count(*) AS items_left FROM order_items;
 items_left
------------
          3
(1 row)
RETURNING после вставки, обновления и удаления; удаление заказа 3 забрало и его позицию благодаря ON DELETE CASCADE

Резервная копия и подключение из приложения

Том защищает данные от удаления контейнера. Проверим: остановленный контейнер с --rm исчезает, но новый контейнер на том же томе видит всю базу и пропускает инициализацию.

terminal
$ docker stop shop-db
shop-db
$ docker run -d --rm --name shop-db -e POSTGRES_PASSWORD=change-me-locally -p 127.0.0.1:5432:5432 -v shop-pgdata:/var/lib/postgresql/data postgres:17-alpine
$ docker logs shop-db 2>&1 | head -3

PostgreSQL Database directory appears to contain a database; Skipping initialization

$ docker exec shop-db psql -U shop_app -d shop -c "SELECT count(*) AS clients FROM clients"
 clients
---------
       6
(1 row)
Пересоздание контейнера: данные на месте

От ошибочного DELETE, сбоя диска или удалённого тома защищает только резервная копия. pg_dump делает согласованную копию одной базы, не блокируя читающих и пишущих пользователей. Простой формат — SQL-скрипт, его восстанавливают через psql. Формат custom (-Fc) сжат и восстанавливается через pg_restore, который умеет выбирать отдельные объекты.

bash
# 1. Дамп в формате custom (-Fc): сжатый, читается pg_restore
docker exec shop-db pg_dump -U shop_app -d shop -Fc -f /tmp/shop.dump
docker cp shop-db:/tmp/shop.dump ./shop.dump

# 2. Проверка: восстановить в отдельную пустую базу
docker exec shop-db psql -U postgres -c "CREATE DATABASE shop_restore OWNER shop_app"
docker exec shop-db pg_restore -U shop_app -d shop_restore /tmp/shop.dump
Дамп внутри контейнера и копия на ваш диск через docker cp — одинаково работает в bash и PowerShell
terminal
$ docker exec shop-db pg_restore -U shop_app -d shop_restore /tmp/shop.dump; echo "exit $?"
exit 0
$ docker exec shop-db psql -U shop_app -d shop -c "SELECT (SELECT count(*) FROM clients) AS clients, (SELECT count(*) FROM products) AS products, (SELECT count(*) FROM orders) AS orders, (SELECT count(*) FROM order_items) AS items"
 clients | products | orders | items
---------+----------+--------+-------
       6 |        3 |      2 |     3
(1 row)

$ docker exec shop-db psql -U shop_app -d shop_restore -c "SELECT (SELECT count(*) FROM clients) AS clients, (SELECT count(*) FROM products) AS products, (SELECT count(*) FROM orders) AS orders, (SELECT count(*) FROM order_items) AS items"
 clients | products | orders | items
---------+----------+--------+-------
       6 |        3 |      2 |     3
(1 row)
Восстановление без ошибок, количество строк совпадает

Текстовый дамп начинается со строки \restrict со случайным ключом. Её добавили в выпуске 17.6 по соображениям безопасности: при восстановлении psql блокирует мета-команды, которые могли бы оказаться в данных. Поэтому текстовые дампы восстанавливайте psql той же или более новой версии. Готовый дамп в существующую базу накатывается через pg_restore --clean --if-exists: он удаляет и создаёт объекты заново.

terminal
$ docker exec shop-db pg_dump -U shop_app -d shop -f /tmp/shop.sql
$ docker exec shop-db head -8 /tmp/shop.sql
--
-- PostgreSQL database dump
--

\restrict AuNO2dNJw5UDhjrML04TbDxP98WIJkamUGNY5xdp8sLuCz1X2zVIZqKR8umAdei

-- Dumped from database version 17.11
-- Dumped by pg_dump version 17.11
Начало текстового дампа PostgreSQL 17.11

Приложение подключается по TCP со строкой подключения формата postgresql://пользователь:пароль@хост:порт/база. Её хранят в переменной окружения, файл .env добавляют в .gitignore. Для проверки возьмём Node.js и драйвер pg.

.env
DATABASE_URL=postgresql://shop_app:change-me-app@localhost:5432/shop
Строка подключения: пароль — учебная заглушка, файл не коммитится
check-db.js
import pg from "pg";

const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
await client.connect();

const who = await client.query("SELECT current_user, current_database()");
console.log(who.rows[0]);

// Параметр $1 передаётся отдельно от текста запроса — без склейки строк
const cheap = await client.query(
  "SELECT name, price FROM products WHERE price < $1 ORDER BY price",
  [7000],
);
console.log(cheap.rows);

await client.end();
Проверка подключения: package.json с "type": "module", запуск node --env-file=.env check-db.js
terminal
$ npm install pg
$ node --env-file=.env check-db.js
{ current_user: 'shop_app', current_database: 'shop' }
[
  { name: 'Paper filters V60', price: '2500.00' },
  { name: 'Ethiopia Yirgacheffe 250 g', price: '6160.00' }
]
Цены пришли строками: node-postgres не превращает numeric в JavaScript-число, чтобы не потерять точность

Частые ошибки и упражнения

Conflict. The container name "/shop-db" is already in use

Причина
Ещё работает контейнер shop-db из урока «SQL с нуля» — он запущен без тома
Что сделать
docker stop shop-db: контейнер с --rm удалится, имя освободится

Bind for 0.0.0.0:5432 failed: port is already allocated

Причина
Порт 5432 уже занят другим PostgreSQL или контейнером
Что сделать
Опубликуйте другой порт: -p 127.0.0.1:5433:5432 — и укажите 5433 в строке подключения

После пересоздания контейнера база пустая

Причина
Том не подключён или подключён не к тому пути
Что сделать
Для 17 — /var/lib/postgresql/data, для 18+ — /var/lib/postgresql; проверьте docker inspect

password authentication failed for user "shop_app"

Причина
Неверный пароль в строке подключения или POSTGRES_PASSWORD изменили после первого запуска
Что сделать
Переменные образа действуют только при инициализации пустого тома; пароль меняют через \password или ALTER ROLE

Скрипты из /docker-entrypoint-initdb.d не выполнились

Причина
В томе уже есть база
Что сделать
Скрипты запускаются только на пустом каталоге данных

psql ничего не отвечает, приглашение shop->

Причина
Команда без точки с запятой
Что сделать
Допишите ; или сбросьте ввод командой \r

permission denied for schema public

Причина
Таблицу создаёт не владелец базы
Что сделать
Создавайте объекты владельцем или выдайте право явно: GRANT CREATE ON SCHEMA public TO …

psql: error: /db/…: No such file or directory

Причина
Контейнер пересоздан, файлы из docker cp пропали вместе с ним
Что сделать
Повторите docker cp или смонтируйте папку в контейнер
Как проверено7 · пройдено1 · частичносверено 11 октября 2026 г.
  • запущено, работает

    docker volume create + docker run (17-alpine, том, порт на 127.0.0.1), docker logs

    Git Bash (MSYS_NO_PATHCONV=1) и PowerShell 7.6.6 на Windows 11, Docker Desktop: имена kb-pg-start / kb-pg-start-data / kb-pg-start-ps, порты 55432/55493 вместо 5432 (5432 занят). В статье имена заменены на shop-db / shop-pgdata, порт — 5432; остальной текст команд и вывод — реальные

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

    db/roles.sql, db/schema.sql, db/seed.sql, db/grants.sql (docker cp + psql -f, ON_ERROR_STOP)

    postgres:17-alpine (PostgreSQL 17.11): полный прогон в порядке статьи скриптом run-article.sh, все выводы psql в статье — из run-article.log (UUID и время из этого прогона). Те же файлы и демонстрации на postgres:18-alpine (18.6, том в /var/lib/postgresql) — вывод идентичен, кроме заголовка \dt («List of tables») и порядка stdout/stderr

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

    ошибки ограничений, NULL в UNIQUE, RETURNING, типы (float/numeric, переполнение integer, timestamptz Asia/Almaty, jsonb), права shop_readonly

    реальный вывод psql 17.11; транскрипты с приглашениями shop=> / shop-> собраны из вывода psql -a (эхо ввода) и построчного запуска — ответы сервера не изменены

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

    ошибка пути тома в 17 (/var/lib/postgresql) и пересоздание контейнера с правильным томом

    docker inspect показал анонимный том на /var/lib/postgresql/data, база shop пропала после перезапуска; с правильным путём — «Skipping initialization», данные на месте

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

    pg_dump -Fc + docker cp + pg_restore в shop_restore; текстовый дамп с \restrict; pg_restore --clean --if-exists

    exit 0, количество строк совпало (6/3/2/3); --clean --if-exists восстановил удалённые строки; дамп из 17.11 восстановлен в 18.6 (pg_restore --no-owner --no-acl) без ошибок; docker cp дампа проверен и в PowerShell

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

    .env + check-db.js (Node.js, pg)

    Node.js 24.18.0, pg 8.23.1, node --env-file=.env check-db.js по TCP 127.0.0.1:55432 с паролем — вывод в статье реальный; неверный пароль → password authentication failed for user "shop_app"

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

    ошибки занятого порта и занятого имени контейнера

    docker run -p 127.0.0.1:5432:5432 при занятом 5432 → «Bind for 0.0.0.0:5432 failed: port is already allocated», контейнер не стартовал; второй docker run с тем же --name → «Conflict. The container name "/kb-pg-name" is already in use» (в статье имя shop-db — то же, что в уроке sql-select)

  • частично

    упражнения: ex2.sql (stock) и автоинициализация через /docker-entrypoint-initdb.d

    ex2 — ALTER/UPDATE/ошибка products_stock_check, вывод в решении реальный (VERIFIED). initdb.d — POSTGRES_DB=shop, 01-schema.sql и 02-seed.sql выполнены (лог entrypoint), order_items = 4; на Windows путь передан как $(pwd -W). Вариант "$PWD/db-init" из статьи — синтаксис bash для Linux/macOS, на Linux не запускался (в Git Bash на Windows он монтирует пустую папку — проверено)

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

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

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

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

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

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

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