psql на каждый день
В Brew прилетает баг: «в приложении пропал колд брю из меню». Прежде чем лезть в код, хочется на тридцать секунд заглянуть в саму базу — есть ли там вообще этот напиток, какой у него stock, не сломалась ли структура таблицы. Можно открыть Adminer и кликать мышкой, но это медленно и не ложится в терминальный workflow. Рабочий инструмент для такого — psql: официальный консольный клиент Postgres, который ставится вместе с сервером (brew install libpq на macOS).
Цель юнита узкая: не выучить весь psql, а собрать «аптечку» из горстки команд, которой хватает на 90% случаев «надо быстро посмотреть в БД». Это escape-hatch-юнит — здесь нет Go и нет sqlc, потому что урок про сам клиент.
Мета-команды: то, что отличает psql от «просто SQL»
В psql два вида ввода. Обычный SQL (SELECT ...;) уходит на сервер и исполняется им. А команды, начинающиеся с обратного слэша (\dt, \d, \x) — это мета-команды: их обрабатывает сам psql на клиенте, до и вместо отправки на сервер. Они не часть SQL и не работают из драйвера в твоём приложении — это инструмент интерактивной работы руками.
ты набираешь… где это обрабатывается
─────────────────────────────────────────────────────────────────────
\dt \d \x \timing ──▶ psql (КЛИЕНТ) исполняет сам, на сервер не идёт
SELECT … ; INSERT … ; ──▶ psql пересылает ──▶ postgres (СЕРВЕР) исполняетИменно поэтому урок — escape-hatch: мета-команду нельзя «написать в query.sql и сгенерировать через sqlc». Зато за один символ они дают то, на что в чистом SQL ушёл бы громоздкий запрос к системным каталогам (information_schema, pg_catalog).
Подключиться к песочнице:
psql 'postgres://brew:brew@localhost:5432/brew?sslmode=disable'Внутри — приглашение brew=#. Дальше всё интерактивно.
Осмотреться: \l, \dt, \d
Три команды отвечают на вопрос «что вообще здесь есть» на трёх уровнях вложенности.
\l (list) — список баз на сервере. Покажет brew плюс системные postgres, template0, template1. Полезно, когда не уверен, к той ли базе подключился.
\dt (describe tables) — таблицы текущей базы. В нашей brew это 9 таблиц схемы Brew. Рядом живут \dv (вьюхи), \di (индексы), \ds (последовательности), \dn (схемы), \df (функции) — буква после \d сужает тип.
\d <имя> — структура одного объекта: колонки, типы, nullable, дефолты, индексы и — что особенно ценно — внешние ключи в обе стороны (на кого ссылается таблица и кто ссылается на неё). По \d drinks сразу видно, что base_price — это bigint (цена в центах, не float), а на drinks ссылаются order_items и inventory. Это карта связей без единого JOIN'а к pg_catalog.
Сделать вывод читаемым: \x и \timing
\x (expanded) переключает таблицу «в столбик»: вместо широкой строки, которая не влезает в терминал и переносится кашей, каждое поле печатается на своей строке имя | значение. Незаменимо для таблиц с десятком колонок или с длинным body/description. \x auto — умный режим: широкие результаты в столбик, узкие обычной таблицей.
\timing включает замер времени каждого запроса (Time: 2.255 ms). Это первый, грубый сигнал «быстро/медленно» — не профайлинг (для него есть EXPLAIN ANALYZE в модуле 06), но достаточно, чтобы заметить, что запрос внезапно стал занимать секунды. Вывод \timing зависит от машины и нагрузки, поэтому в демо ниже его нет — попробуй сам в make db-shell.
Не держать всё в голове: \i, \?, \h
\i <файл> исполняет SQL-файл — ровно так наш make db-reset накатывает schema/brew.sql и seed.sql. Удобно для повторяемых скриптов: не копировать запрос в приглашение, а держать в файле под версионным контролем.
\? — справка по всем мета-командам (их десятки). \h <команда> — справка по синтаксису SQL: \h INSERT напомнит грамматику INSERT со всеми опциями, не отрываясь от терминала. Две команды, которые делают остальные необязательными для запоминания.
Вся аптечка одной таблицей:
| Команда | Что делает | Где исполняется |
|---|---|---|
\l | список баз на сервере | psql (клиент) |
\dt | таблицы текущей базы (\dv/\di/\ds/\dn/\df — по типу объекта) | psql (клиент) |
\d <имя> | структура объекта: колонки, типы, PK, индексы, FK в обе стороны | psql (клиент) |
\x | вывод «в столбик» (\x auto — по ширине результата) | psql (клиент) |
\timing | замер времени каждого запроса (грубый сигнал, не профайлинг) | psql (клиент) |
\i <файл> | выполнить SQL из файла | psql (клиент) |
\? / \h | справка: по мета-командам / по синтаксису SQL | psql (клиент) |
SELECT … ; | обычный SQL — уходит на сервер и исполняется им | postgres (сервер) |
Последняя строка — для контраста: всё с \ остаётся на клиенте, обычный SQL уходит на сервер (та же граница, что на диаграмме выше).
Что показывает наш код
«Код» этого юнита — psql-скрипт demo.sql: маленькая экскурсия, которая прогоняет три ключевые мета-команды по схеме Brew, ничего не меняя в данных.
\dt -- какие таблицы есть в базе
\d drinks -- структура одной таблицы: колонки, типы, PK, FK
\x on -- расширенный вывод (строка столбиком)
SELECT id, sku, name, category, base_price, stock FROM drinks WHERE sku = 'CLD-01';
\x offmake run запускает его через psql -f demo.sql — то же самое, что набрать эти команды руками в make db-shell, только разом и воспроизводимо. Это и есть «аптечка»: осмотреться (\dt), разобрать одну таблицу (\d), прочитать строку удобно (\x).
Запуск
Подними песочницу (из корня репозитория) и накати схему Brew:
docker compose up -d
make lecture L=00-getting-connected/00-03-psql-survival-kit T=db-resetЗапусти экскурсию:
make lecture L=00-getting-connected/00-03-psql-survival-kit(T=run — значение по умолчанию. Изнутри каталога юнита это просто make db-reset и make run.)
Вывод:
== \dt — какие таблицы есть в базе brew (схема Brew: 9 таблиц) ==========
List of tables
Schema | Name | Type | Owner
--------+----------------------+-------+-------
public | articles | table | brew
public | customers | table | brew
public | drinks | table | brew
public | inventory | table | brew
public | order_items | table | brew
public | orders | table | brew
public | outbox | table | brew
public | processed_outbox_ids | table | brew
public | shops | table | brew
(9 rows)
== \d drinks — структура: колонки, типы, PK и кто на таблицу ссылается ==
Table "public.drinks"
Column | Type | Collation | Nullable | Default
-------------+--------------------------+-----------+----------+---------
id | bigint | | not null |
sku | text | | not null |
name | text | | not null |
description | text | | not null |
category | text | | not null |
base_price | bigint | | not null |
stock | integer | | not null | 0
created_at | timestamp with time zone | | not null | now()
updated_at | timestamp with time zone | | not null | now()
Indexes:
"drinks_pkey" PRIMARY KEY, btree (id)
Referenced by:
TABLE "inventory" CONSTRAINT "inventory_drink_id_fkey" FOREIGN KEY (drink_id) REFERENCES drinks(id) ON DELETE CASCADE
TABLE "order_items" CONSTRAINT "order_items_drink_id_fkey" FOREIGN KEY (drink_id) REFERENCES drinks(id)
== \x + SELECT — расширенный вывод: одна строка столбиком (для широких) ==
-[ RECORD 1 ]--------
id | 4
sku | CLD-01
name | Колд брю
category | cold
base_price | 520
stock | 40Колд брю на месте (stock = 40) — значит, баг не в данных, а в коде приложения. На это ушло три команды и десять секунд.
Заборчик
- psql — инструмент для разведки и отладки руками: посмотреть, прикинуть, проверить гипотезу. В продакшене так не работают с данными: приложение не запускает
psqlи не парсит текстовый вывод — оно ходит в базу через драйвер типизированным запросом (к этому конвейеру курс придёт в 00-04 и 00-05). - Мета-команды (
\dt,\d) вообще существуют только в psql: изpgxв Go их не вызвать, это не SQL. \d-вывод иpg_catalog— это удобная карта, но не контракт. Структуру, на которую опирается приложение, фиксируют миграции (модуль 02), а не «я посмотрел через\dи вроде совпадает».
Что забрать с собой
- В psql два вида ввода: SQL (исполняет сервер) и мета-команды с
\(обрабатывает клиент). Мета-команды — не SQL и из приложения недоступны. - Аптечка на каждый день:
\dt/\d— осмотреться и разобрать таблицу;\x— читаемый вывод;\timing— грубый замер;\i— выполнить файл;\?и\h— справка, чтобы не держать всё в голове. \d <таблица>показывает внешние ключи в обе стороны — это карта связей без запросов к системным каталогам.
Дальше — юнит 00-04 «подключение из Go»: уходим из интерактивного psql в код приложения. Откроем pgxpool, выполним первый запрос с параметром через $1 — и на анти-демо увидим, почему склейка SQL строками открывает дорогу инъекции, а биндинг параметров её закрывает.