Глава 27 из 28
Приложение A. Шпаргалка psql
psql — консольный клиент PostgreSQL, и часто он единственный под рукой: на сервере, в контейнере, в CI. Большинство набирает в нём только запросы. Эта шпаргалка собирает то, что экономит больше всего времени: команды \d для осмотра базы, режимы вывода, замер времени, переменные, загрузку и выгрузку через \copy, \watch, запуск скриптов и файл .psqlrc. Весь вывод в ней получен реальным запуском psql 16.14.
В браузерной песочнице этого сайта psql нет: там только окно с SQL. Поэтому команды psql ниже оформлены как обычный код и не запускаются. Запускаемые примеры показывают, как получить то же самое запросами к системным каталогам: именно так psql работает под капотом.
Подключение и запуск
Подключиться можно набором флагов или строкой:
psql -h db.example.com -p 5432 -U app -d shop
psql "postgresql://app@db.example.com:5432/shop?sslmode=require"
Пароль не передавайте в командной строке: он останется в истории оболочки и виден в ps. Используйте файл ~/.pgpass (строки host:port:db:user:password, права 0600) или переменную PGPASSWORD для разового запуска. Остальные параметры тоже читаются из PGHOST, PGPORT, PGUSER, PGDATABASE.
| Флаг | Что делает |
|---|---|
-c "SQL" | выполнить команду и выйти; можно повторять |
-f файл.sql | выполнить файл |
-X | не читать .psqlrc (для воспроизводимых скриптов) |
-1 | весь файл в одной транзакции |
-v имя=значение | задать переменную |
-A -t, --csv | вывод без выравнивания и заголовков, вывод в CSV: для скриптов |
-E | показывать запросы, которые psql отправляет сам для \d-команд |
Куда вы подключены, показывает \conninfo:
shop=> \conninfo
You are connected to database "shop" as user "sandbox" via socket in "/var/run/postgresql" at port "5432".
В приглашении shop=> стрелка значит обычного пользователя, # — суперпользователя. Справка: \? — все метакоманды, \? variables — переменные, \h CREATE INDEX — синтаксис SQL-команды. Выход: \q или Ctrl+D.
Осмотр базы: семейство \d
Команды с обратной косой чертой обрабатывает сам psql, сервер их не видит. Он превращает их в запросы к каталогам и печатает результат таблицей. Примеры ниже получены в базе shop с объектами из раздела «Данные для примеров».
| Команда | Что показывает |
|---|---|
\l | базы данных |
\c база | переключиться на другую базу |
\dn | схемы |
\dt | таблицы схем из search_path |
\d имя | столбцы, индексы, ограничения и внешние ключи таблицы |
\di, \dv, \dm, \ds | индексы, представления, материализованные представления, последовательности |
\df, \dT, \dx | функции, пользовательские типы, расширения |
\du | роли |
\dp имя (или \z) | права на объект |
\dconfig | параметры конфигурации (с версии 15) |
\sf функция, \sv представление | исходный текст объекта |
Суффикс + добавляет подробности и размеры (\d+, \dt+, \df+), S добавляет системные объекты (\dtS). В конце можно указать шаблон с *: \dt public.*, \dt *.customers, \d cust*.
shop=> \dt
List of relations
Schema | Name | Type | Owner
--------+-----------+-------+---------
public | customers | table | sandbox
public | orders | table | sandbox
(2 rows)
\d имя — самая используемая команда: типы, NOT NULL, значения по умолчанию, индексы и внешние ключи видны сразу.
shop=> \d orders
Table "public.orders"
Column | Type | Collation | Nullable | Default
-------------+---------------+-----------+----------+------------------------------------
id | integer | | not null | nextval('orders_id_seq'::regclass)
customer_id | integer | | not null |
amount | numeric(10,2) | | not null |
created_at | date | | not null | CURRENT_DATE
Indexes:
"orders_pkey" PRIMARY KEY, btree (id)
"orders_customer_idx" btree (customer_id)
Foreign-key constraints:
"orders_customer_id_fkey" FOREIGN KEY (customer_id) REFERENCES customers(id)
У \d customers есть ещё раздел Referenced by: какие таблицы на неё ссылаются. Это быстрый способ узнать, что сломается при DROP или DELETE.
Размеры показывает \dt+:
shop=> \dt+
List of relations
Schema | Name | Type | Owner | Persistence | Access method | Size | Description
--------+-----------+-------+---------+-------------+---------------+------------+-------------
public | customers | table | sandbox | permanent | heap | 16 kB |
public | orders | table | sandbox | permanent | heap | 8192 bytes |
(2 rows)
Почему 16 kB у customers и 8192 байта у orders, хотя строк поровну: в customers есть столбцы text, для них PostgreSQL создаёт служебную TOAST-таблицу (хранилище длинных значений). Её индекс добавляет 8 kB, и \dt+ считает всё вместе. Индексы в этот размер не входят.
\sf и \sv выдают исходный текст как готовый CREATE OR REPLACE, который удобно править через \e:
shop=> \sf add_tax
CREATE OR REPLACE FUNCTION public.add_tax(a numeric)
RETURNS numeric
LANGUAGE sql
IMMUTABLE
AS $function$ SELECT a * 1.2 $function$
Права смотрите через \dp. Пустой столбец Access privileges значит «права по умолчанию»: всё есть только у владельца. После GRANT SELECT ON customers TO PUBLIC там появляется строка =r/sandbox. Формат роль=права/кто_выдал, пустое имя означает PUBLIC, буквы: r SELECT, a INSERT, w UPDATE, d DELETE. Подробнее о правах в главе про роли.
Как это устроено и чем заменить
Каждая из этих команд — обычный запрос к pg_catalog. Увидеть его можно, включив ECHO_HIDDEN (или запустив psql -E):
shop=> \set ECHO_HIDDEN on
shop=> \dn public
********* QUERY **********
SELECT n.nspname AS "Name",
pg_catalog.pg_get_userbyid(n.nspowner) AS "Owner"
FROM pg_catalog.pg_namespace n
WHERE n.nspname OPERATOR(pg_catalog.~) '^(public)$' COLLATE pg_catalog.default
ORDER BY 1;
**************************
Так вы узнаёте, в каких таблицах каталога лежит нужное, и получаете заготовку для собственного запроса. Он работает там, где psql нет: в веб-консоли, DBeaver, приложении, песочнице. Ниже запускаемые эквиваленты. Схема примеров в песочнице создаётся заново при каждом запуске, поэтому вместо её имени стоит current_schema().
Аналог \dt:
SELECT tablename AS "Name", 'table' AS "Type"
FROM pg_tables
WHERE schemaname = current_schema()
ORDER BY 2;
Name | Type
-----------+-------
customers | table
orders | table
Аналог \d orders, часть со столбцами. format_type собирает тип вместе с точностью, pg_get_expr разворачивает значение по умолчанию:
SELECT a.attname AS "Column",
format_type(a.atttypid, a.atttypmod) AS "Type",
CASE WHEN a.attnotnull THEN 'not null' END AS "Nullable",
pg_get_expr(d.adbin, d.adrelid) AS "Default"
FROM pg_attribute a
LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
WHERE a.attrelid = 'orders'::regclass AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum;
Column | Type | Nullable | Default
-------------+---------------+----------+------------------------------------
id | integer | not null | nextval('orders_id_seq'::regclass)
customer_id | integer | not null |
amount | numeric(10,2) | not null |
created_at | date | not null | CURRENT_DATE
Условие attnum > 0 отсекает системные столбцы (ctid, xmin), NOT attisdropped — удалённые столбцы, которые остаются в каталоге как призраки. Индексы и внешние ключи берутся готовыми определениями:
SELECT regexp_replace(indexdef, ' ON \S+\.', ' ON ') AS "Indexes" FROM pg_indexes
WHERE schemaname = current_schema() AND tablename = 'orders'
UNION ALL
SELECT conname || ': ' || pg_get_constraintdef(oid) FROM pg_constraint
WHERE conrelid = 'orders'::regclass AND contype = 'f';
Indexes
---------------------------------------------------------------------
CREATE UNIQUE INDEX orders_pkey ON orders USING btree (id)
CREATE INDEX orders_customer_idx ON orders USING btree (customer_id)
orders_customer_id_fkey: FOREIGN KEY (customer_id) REFERENCES customers(id)
regexp_replace убирает случайное имя схемы песочницы; в обычной базе он не нужен. Размеры, как в \dt+, плюс размер вместе с индексами, которого у \dt+ нет:
SELECT relname AS "Name",
pg_size_pretty(pg_table_size(oid)) AS "Size",
pg_size_pretty(pg_total_relation_size(oid)) AS "Total with indexes"
FROM pg_class
WHERE relnamespace = current_schema()::regnamespace AND relkind = 'r'
ORDER BY relname;
Name | Size | Total with indexes
-----------+------------+--------------------
customers | 16 kB | 32 kB
orders | 8192 bytes | 40 kB
Аналоги \sf и \sv — функции pg_get_functiondef и pg_get_viewdef:
SELECT regexp_replace(pg_get_functiondef('add_tax(numeric)'::regprocedure), 'FUNCTION \S+\.', 'FUNCTION ') AS def;
SELECT pg_get_viewdef('big_orders'::regclass, true);
Права из \dp лежат в столбце relacl массивом. Функция aclexplode раскладывает его на строки, grantee = 0 значит PUBLIC:
GRANT SELECT ON customers TO PUBLIC;
SELECT relname, a.privilege_type
FROM pg_class c, aclexplode(c.relacl) a
WHERE c.oid = 'customers'::regclass AND a.grantee = 0;
relname | privilege_type
-----------+----------------
customers | SELECT
Формат вывода
Широкая таблица в узком терминале превращается в кашу. Команда \x переключает расширенный режим: каждая строка выводится столбиком «имя | значение».
shop=> \x
Expanded display is on.
shop=> SELECT * FROM customers WHERE id = 1;
-[ RECORD 1 ]
id | 1
name | Анна
city | Москва
Режим \x auto включает столбики только когда строка не помещается в ширину терминала. Его стоит держать включённым всегда. Разовый вариант без смены режима: закончить запрос не ;, а \gx.
Остальное собрано в \pset:
| Команда | Эффект |
|---|---|
\pset null '∅' | как показывать NULL (по умолчанию пустая строка, неотличимая от '') |
\pset border 2 | рамка вокруг таблицы |
\pset linestyle unicode | линии символами Unicode |
\pset format csv | формат: aligned, unaligned, csv, html, wrapped и другие |
\pset pager off | не запускать постраничник (less) |
\t, \a | только данные без заголовков и итога; невыровненный вывод |
У Веры в customers нет города, и без настройки null это не видно:
shop=> \pset null '∅'
Null display is "∅".
shop=> SELECT * FROM customers;
id | name | city
----+-------+--------
1 | Анна | Москва
2 | Борис | Казань
3 | Вера | ∅
(3 rows)
Настройка null действует и в формате CSV: в файл попадёт ∅. Для выгрузки её лучше не менять (см. \copy ниже).
Время выполнения: \timing
\timing on печатает время после каждого запроса. Это время клиента: оно включает сеть и разбор ответа, поэтому больше, чем Execution Time из EXPLAIN ANALYZE (о нём в главе про EXPLAIN).
shop=> \timing on
Timing is on.
shop=> SELECT pg_sleep(1.2);
pg_sleep
----------
(1 row)
Time: 1200.941 ms (00:01.201)
Для запросов дольше секунды в скобках добавляется время в формате «минуты:секунды». Нужен план и разбор по узлам, берите EXPLAIN (ANALYZE, BUFFERS).
Переменные
Переменные psql — подстановка текста на стороне клиента. Задаются \set, читаются как :имя. Для строк используйте :'имя' (значение берётся в кавычки и экранируется), для идентификаторов :"имя".
shop=> \set min_amount 100
shop=> SELECT * FROM orders WHERE amount > :min_amount;
id | customer_id | amount | created_at
----+-------------+--------+------------
1 | 1 | 120.50 | 2026-01-10
3 | 2 | 300.00 | 2026-03-12
(2 rows)
shop=> \set city 'Москва'
shop=> SELECT name FROM customers WHERE city = :'city';
name
------
Анна
(1 row)
Значение можно передать при запуске: psql -v min_amount=100 -f report.sql. Это удобнее склейки SQL в оболочке, но не заменяет параметры драйвера: :'имя' экранирует значение, а не типизирует его.
\gset кладёт результат запроса в переменные: столбцы становятся именами, запрос должен вернуть ровно одну строку. С префиксом (\gset t_) переменные не затрут существующие.
shop=> SELECT count(*) AS n, max(amount) AS mx FROM orders \gset
shop=> \echo n=:n mx=:mx
n=3 mx=300.00
\gexec идёт дальше: каждую ячейку результата он выполняет как отдельный SQL. Так массовые операции пишутся запросом, а не руками:
shop=> SELECT format('ANALYZE %I', tablename) FROM pg_tables
shop-> WHERE schemaname = 'public' ORDER BY tablename \gexec
ANALYZE
ANALYZE
Сначала запустите запрос без \gexec и прочитайте, что он вернул: выполнится всё это от вашего имени. Для ветвления в скриптах есть \if, \elif, \else, \endif; условие обычно приходит из \gset.
Служебные переменные, которые стоит знать:
| Переменная | Назначение |
|---|---|
ON_ERROR_STOP | при ошибке остановить скрипт |
ON_ERROR_ROLLBACK | ошибка внутри транзакции откатывает только свой оператор; interactive — только в диалоге |
AUTOCOMMIT | off открывает транзакцию неявно, фиксировать придётся COMMIT |
ECHO, ECHO_HIDDEN | печатать выполняемые запросы; печатать запросы за \d-командами |
VERBOSITY | verbose добавляет в ошибки SQLSTATE и место в исходниках |
HISTSIZE, HISTCONTROL | размер истории, игнорирование повторов |
PROMPT1 | вид приглашения |
ERROR, SQLSTATE, LAST_ERROR_MESSAGE, ROW_COUNT | итог последнего запроса |
Команда \set без аргументов печатает все переменные. Итог последней ошибки доступен и после неё: после SELEC 1; переменная :ERROR равна true, а :SQLSTATE — 42601. Команда \errverbose печатает последнюю ошибку подробно, с местом в исходниках сервера.
Файлы: \copy, \o, \g, \i
SQL-команда COPY читает и пишет файлы на машине сервера и требует специальных ролей. Обычный пользователь получит отказ:
shop=> COPY customers TO '/tmp/x.csv';
ERROR: permission denied to COPY to a file
DETAIL: Only roles with privileges of the "pg_write_server_files" role may COPY to a file.
HINT: Anyone can COPY to stdout or from stdin. psql's \copy command also works for anyone.
Выход назван в подсказке. \copy — метакоманда клиента: файл читается и пишется на вашей машине, данные идут через соединение. Синтаксис повторяет COPY, прав суперпользователя не нужно. Выгрузка таблицы и результата запроса (запрос в скобках):
shop=> \copy customers TO '/tmp/customers.csv' WITH (FORMAT csv, HEADER)
COPY 3
shop=> \copy (SELECT customer_id, sum(amount) AS total FROM orders GROUP BY 1 ORDER BY 1) TO '/tmp/totals.csv' CSV HEADER
COPY 2
shop=> \! cat /tmp/customers.csv
id,name,city
1,Анна,Москва
2,Борис,Казань
3,Вера,
NULL записан пустым значением, как положено в CSV. Загрузка — тем же синтаксисом наоборот:
shop=> CREATE TABLE customers_copy (LIKE customers);
shop=> \copy customers_copy FROM '/tmp/customers.csv' WITH (FORMAT csv, HEADER)
COPY 3
Если файла нет, ошибку выдаёт клиент: /tmp/nofile.csv: No such file or directory. Для больших загрузок \copy быстрее серии INSERT. Можно обойтись без файла: \copy ... TO STDOUT пишет в стандартный вывод, FROM STDIN читает из стандартного ввода.
Куда направить результат запроса и как запустить внешнее:
| Команда | Что делает |
|---|---|
\o файл | весь последующий вывод запросов в файл; \o без аргумента возвращает вывод в терминал |
запрос \g файл | результат одного запроса в файл |
запрос \g | команда | результат одного запроса на вход программы (\g | wc -l); так же работает \o | команда |
\i файл | выполнить файл с SQL и метакомандами; \ir считает путь от каталога текущего скрипта |
\! команда | выполнить команду оболочки |
\e | открыть буфер запроса в редакторе ($PSQL_EDITOR, $EDITOR) |
\echo текст | напечатать строку |
shop=> SELECT count(*) FROM orders \g /tmp/cnt.txt
shop=> \! cat /tmp/cnt.txt
count
-------
3
(1 row)
В файл попадает вывод в том виде, как он на экране, с рамкой и итоговой строкой. Для машинной обработки переключитесь на \pset format csv или берите \copy.
Повтор запроса: \watch
\watch перезапускает последний запрос с заданным интервалом и печатает результат. Это самый простой мониторинг: очередь, размер таблицы, блокировки.
shop=> SELECT count(*) FROM orders \watch i=1 c=2
Sat Oct 17 15:35:23 2026 (every 1s)
count
-------
3
(1 row)
Sat Oct 17 15:35:24 2026 (every 1s)
count
-------
3
(1 row)
i=секунды задаёт интервал (по умолчанию 2), c=число — сколько повторов, и после них команда останавливается. Оба параметра есть в версии 16; в старых интервал задаётся числом: \watch 5. Без c повтор идёт до Ctrl+C. Хорошо сочетается с pg_stat_activity и pg_locks (главы про блокировки и мониторинг), но интервал ставьте разумный: опрос тоже нагрузка.
Скрипты и коды возврата
Запуск файла, как его стоит писать в CI и cron:
psql -X -v ON_ERROR_STOP=1 -f migrate.sql
Без ON_ERROR_STOP psql идёт дальше после ошибки и в конце возвращает 0, хотя часть команд упала. Файл из трёх запросов, второй делит на ноль:
$ psql -f e.sql
a
---
1
(1 row)
psql:/tmp/e.sql:2: ERROR: division by zero
b
---
2
(1 row)
$ echo $?
0
$ psql -v ON_ERROR_STOP=1 -f e.sql
a
---
1
(1 row)
psql:/tmp/e.sql:2: ERROR: division by zero
$ echo $?
3
Коды возврата: 0 — успех, 1 — фатальная ошибка самого psql, 2 — не удалось подключиться, 3 — ошибка в скрипте при ON_ERROR_STOP. Флаг -1 оборачивает файл в одну транзакцию, вместе с ON_ERROR_STOP получается «всё или ничего». Файл создаёт таблицу t1 и делит на ноль:
$ psql -1 -v ON_ERROR_STOP=1 -f s.sql
psql:/tmp/s.sql:2: ERROR: division by zero
$ psql -At -c "SELECT to_regclass('t1')"
Пустая строка вместо имени таблицы: CREATE TABLE откатился вместе с ошибкой. Команды, которые нельзя выполнять в транзакции (CREATE INDEX CONCURRENTLY, VACUUM), с -1 работать не будут.
.psqlrc: настройки по умолчанию
При запуске psql читает ~/.psqlrc (путь можно переопределить переменной PSQLRC) и выполняет как обычный скрипт. Бывают и версионные файлы вроде ~/.psqlrc-16. Хороший набор для начала:
\set QUIET 1
\pset null '∅'
\x auto
\timing on
\set HISTSIZE 5000
\set HISTCONTROL ignoredups
\set ON_ERROR_ROLLBACK interactive
\set PROMPT1 '%n@%/%R%# '
\unset QUIET
\set QUIET 1 глушит подтверждения вроде Timing is on. при каждом запуске. ON_ERROR_ROLLBACK interactive спасает в диалоге: опечатка внутри BEGIN больше не отравляет транзакцию (current transaction is aborted), потому что psql ставит перед каждой командой неявную точку сохранения. В скриптах этот режим не действует, поведение миграций не меняется. В приглашении %n — пользователь, %/ — база, %R — статус ввода, %# — # или >.
Проверка на файле с первыми тремя настройками и \timing (путь задан через PSQLRC):
$ PSQLRC=/tmp/psqlrc.test psql -c "SELECT * FROM customers WHERE id = 3"
id | name | city
----+------+------
3 | Вера | ∅
(1 row)
Time: 1.319 ms
$ PSQLRC=/tmp/psqlrc.test psql -X -c "SELECT * FROM customers WHERE id = 3"
id | name | city
----+------+------
3 | Вера |
(1 row)
С -X файл не читается: NULL снова пуст, времени нет. Поэтому скрипты для CI и cron запускайте с -X: результат не должен зависеть от чужого .psqlrc на машине.
Что запомнить
\d имя— первое, что вы набираете у незнакомой таблицы. РазделReferenced byпокажет, кто на неё ссылается, прежде чем вы её удалите.\x autoи\pset nullстоит прописать в.psqlrc. Без второго пустая строка иNULLвыглядят одинаково.\copy, а неCOPY, когда файл на вашей машине.COPYработает с файлами сервера и требует особых ролей.- В скриптах всегда
-X -v ON_ERROR_STOP=1, для миграций ещё и-1. Иначе ошибка посреди файла даёт код 0 и полупримененную миграцию. \timingизмеряет клиента,EXPLAIN ANALYZE— сервер. Для разбора запроса нужен второй.\gexecвыполняет всё, что вернул запрос. Сначала прочитайте результат без него.\watch i=2 c=10— мониторинг одной командой. Не ставьте дробные доли секунды на боевой базе.- Если непонятно, откуда
\dберёт данные, включитеpsql -E. Получите готовый запрос кpg_catalogи перенесите его туда, гдеpsqlнет.
Администрированию PostgreSQL посвящён курс «PostgreSQL DBA».
Данные для примеров
Две связанные таблицы, индекс, представление и функция: на них показаны результаты \d-команд и их аналоги в запросах к каталогу. В консольных примерах те же объекты создавались в базе shop.
CREATE TABLE customers (id int PRIMARY KEY, name text NOT NULL, city text);
CREATE TABLE orders (
id serial PRIMARY KEY,
customer_id int NOT NULL REFERENCES customers(id),
amount numeric(10,2) NOT NULL,
created_at date NOT NULL DEFAULT current_date
);
CREATE INDEX orders_customer_idx ON orders (customer_id);
CREATE VIEW big_orders AS SELECT * FROM orders WHERE amount > 100;
CREATE FUNCTION add_tax(a numeric) RETURNS numeric LANGUAGE sql IMMUTABLE AS $$ SELECT a * 1.2 $$;
INSERT INTO customers VALUES (1, 'Анна', 'Москва'), (2, 'Борис', 'Казань'), (3, 'Вера', NULL);
INSERT INTO orders (customer_id, amount, created_at)
VALUES (1, 120.50, '2026-01-10'), (1, 80, '2026-02-11'), (2, 300, '2026-03-12');
Откройте остальные главы
Ещё 20 глав: внутреннее устройство, оптимизация, администрирование. Покупка разовая, доступ навсегда.