SQLLab

Глава 27 из 28

Приложение A. Шпаргалка psql

psql — консольный клиент PostgreSQL, и часто он единственный под рукой: на сервере, в контейнере, в CI. Большинство набирает в нём только запросы. Эта шпаргалка собирает то, что экономит больше всего времени: команды \d для осмотра базы, режимы вывода, замер времени, переменные, загрузку и выгрузку через \copy, \watch, запуск скриптов и файл .psqlrc. Весь вывод в ней получен реальным запуском psql 16.14.

В браузерной песочнице этого сайта psql нет: там только окно с SQL. Поэтому команды psql ниже оформлены как обычный код и не запускаются. Запускаемые примеры показывают, как получить то же самое запросами к системным каталогам: именно так psql работает под капотом.

Подключение и запуск

Подключиться можно набором флагов или строкой:

bash
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:

sql
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 разворачивает значение по умолчанию:

sql
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 — удалённые столбцы, которые остаются в каталоге как призраки. Индексы и внешние ключи берутся готовыми определениями:

sql
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+ нет:

sql
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:

sql
SELECT regexp_replace(pg_get_functiondef('add_tax(numeric)'::regprocedure), 'FUNCTION \S+\.', 'FUNCTION ') AS def;
sql
SELECT pg_get_viewdef('big_orders'::regclass, true);

Права из \dp лежат в столбце relacl массивом. Функция aclexplode раскладывает его на строки, grantee = 0 значит PUBLIC:

sql
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 — только в диалоге
AUTOCOMMIToff открывает транзакцию неявно, фиксировать придётся COMMIT
ECHO, ECHO_HIDDENпечатать выполняемые запросы; печатать запросы за \d-командами
VERBOSITYverbose добавляет в ошибки 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:

bash
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.

sql
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 глав: внутреннее устройство, оптимизация, администрирование. Покупка разовая, доступ навсегда.