PostgreSQL: команды, лайфхаки и администрирование
Основные команды psql, SQL и администрирование, резервное копирование, роли, производительность и полезные приёмы на одной странице
Подключение
| Команда | Вариант | Что делает |
| psql | psql -h localhost -p 5432 -U app -d appdb | Подключение с хостом, портом, пользователем и базой. |
| psql URI | psql "postgresql://app@db.example.com:5432/appdb?sslmode=require" | Строка подключения целиком, с SSL. |
| psql -c | psql -U postgres -c "SELECT version();" | Выполнить одну команду и выйти. |
| psql -f | psql -d appdb -f migrate.sql | Выполнить SQL-файл. |
| psql -v | psql -d appdb -v schema=prod -f report.sql | Передать переменную, в скрипте использовать :'schema'. |
| psql -At | psql -At -c "SELECT count(*) FROM users;" | Чистый вывод без заголовков и рамок, удобно для скриптов. |
| psql -X | psql -X -d appdb -f script.sql | Не читать ~/.psqlrc (поведение скриптов предсказуемо). |
| psql -1 | psql -1 -d appdb -f migrate.sql | Весь файл в одной транзакции: при ошибке откатится целиком. |
| psql -q | psql -q -d appdb -f x.sql | Тихий режим, без служебных сообщений. |
Метакоманды psql
Команды, начинающиеся с обратной косой черты, работают только в psql, а не в SQL.
| Команда | Вариант | Что делает |
| \l | \l+ | Список баз данных (с размером и описанием при +). |
| \c | \c appdb app | Переключиться на базу и пользователя. |
| \dt | \dt public.* | Таблицы в схеме. |
| \dt+ | \dt+ orders | Таблицы с размером и описанием. |
| \d | \d orders | Структура таблицы: колонки, индексы, ограничения. |
| \d+ | \d+ orders | То же с размером, хранилищем и комментариями. |
| \di | \di | Индексы. |
| \dv / \dm | \dv ; \dm | Представления и материализованные представления. |
| \df | \df+ my_func | Функции и их определение. |
| \dn | \dn | Схемы. |
| \du | \du | Роли и их атрибуты. |
| \dp | \dp orders | Права доступа к таблице. |
| \dx | \dx | Установленные расширения. |
| \dT | \dT+ | Пользовательские типы. |
| \conninfo | \conninfo | Текущее подключение: база, пользователь, хост, порт, SSL. |
| \x | \x auto | Расширенный вывод: широкие строки показываются столбиком. |
| \timing | \timing on | Показывать время выполнения каждого запроса. |
| \i | \i schema.sql | Выполнить файл из текущей сессии. |
| \o | \o result.txt | Направить вывод в файл (\o без аргумента вернуть в терминал). |
| \copy | \copy orders TO 'orders.csv' CSV HEADER | Копирование с клиента (не требует прав суперпользователя на сервере). |
| \watch | SELECT count(*) FROM jobs; \watch 5 | Повторять запрос каждые 5 секунд. |
| \e | \e | Редактировать последний запрос во внешнем редакторе. |
| \s | \s history.txt | Сохранить историю psql в файл. |
| \set | \set ON_ERROR_STOP on | Остановить скрипт при первой ошибке (обязательно в миграциях). |
| \gset | SELECT count(*) AS n FROM users \gset | Записать результат запроса в переменную psql. |
| \q | \q | Выход. |
| \? | \? | Список всех метакоманд. |
Базы данных и схема
| Команда | Вариант | Что делает |
| CREATE DATABASE | CREATE DATABASE appdb OWNER app ENCODING 'UTF8'; | Создать базу с владельцем. |
| DROP DATABASE | DROP DATABASE IF EXISTS appdb; | Удалить базу (к ней не должно быть подключений). |
| CREATE SCHEMA | CREATE SCHEMA reporting AUTHORIZATION app; | Создать схему. |
| CREATE TABLE | CREATE TABLE orders (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, user_id bigint NOT NULL, total numeric(12,2) NOT NULL DEFAULT 0, created_at timestamptz NOT NULL DEFAULT now()); | Таблица с идентити-ключом, ограничениями и значениями по умолчанию. |
| ALTER TABLE | ALTER TABLE orders ADD COLUMN status text NOT NULL DEFAULT 'new'; | Добавить колонку (с DEFAULT в PG 11+ не переписывает таблицу). |
| ALTER TABLE ... ADD CONSTRAINT | ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id); | Внешний ключ. |
| DROP TABLE | DROP TABLE IF EXISTS tmp_import CASCADE; | Удалить таблицу и зависимые объекты (CASCADE, осторожно). |
| TRUNCATE | TRUNCATE TABLE logs RESTART IDENTITY; | Очистить таблицу быстро, сбросив счётчик идентити. |
| COMMENT ON | COMMENT ON COLUMN orders.total IS 'Сумма в рублях'; | Комментарий к объекту (виден в \d+). |
Индексы
| Команда | Вариант | Что делает |
| CREATE INDEX | CREATE INDEX idx_orders_user ON orders (user_id); | Обычный B-tree индекс. Блокирует запись на время создания. |
| CREATE INDEX CONCURRENTLY | CREATE INDEX CONCURRENTLY idx_orders_user ON orders (user_id); | Создание без блокировки записи. Нельзя внутри транзакции. |
| Частичный индекс | CREATE INDEX idx_open ON orders (created_at) WHERE status = 'open'; | Индекс только по нужной части строк, меньше и быстрее. |
| Индекс по выражению | CREATE INDEX idx_lower_email ON users (lower(email)); | Для поиска без учёта регистра. |
| GIN для jsonb | CREATE INDEX idx_payload ON events USING gin (payload jsonb_path_ops); | Быстрый поиск внутри JSON. |
| DROP INDEX CONCURRENTLY | DROP INDEX CONCURRENTLY idx_old; | Удалить индекс без блокировки. |
| REINDEX CONCURRENTLY | REINDEX INDEX CONCURRENTLY idx_orders_user; | Перестроить раздутый индекс без блокировки (PG 12+). |
Данные: выборки, изменения, транзакции
| Команда | Вариант | Что делает |
| INSERT ... RETURNING | INSERT INTO orders (user_id, total) VALUES (1, 990) RETURNING id; | Вернуть созданную строку, например её id. |
| ON CONFLICT | INSERT INTO users (email) VALUES ('a@x.ru') ON CONFLICT (email) DO UPDATE SET email = EXCLUDED.email; | Upsert: вставить или обновить при конфликте. |
| ON CONFLICT DO NOTHING | INSERT INTO tags (name) VALUES ('sql') ON CONFLICT DO NOTHING; | Пропустить дубликат без ошибки. |
| UPDATE ... FROM | UPDATE orders o SET status = 'paid' FROM payments p WHERE p.order_id = o.id AND p.ok; | Обновление по данным из другой таблицы. |
| DELETE ... USING | DELETE FROM cart c USING users u WHERE c.user_id = u.id AND u.blocked; | Удаление с join. |
| FOR UPDATE SKIP LOCKED | SELECT * FROM jobs WHERE state = 'new' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED; | Очередь задач: несколько воркеров не берут одну задачу. |
| BEGIN / COMMIT / ROLLBACK | BEGIN; UPDATE ...; ROLLBACK; | Транзакция. Сначала проверяйте изменения, затем фиксируйте. |
| SAVEPOINT | SAVEPOINT s1; ...; ROLLBACK TO SAVEPOINT s1; | Откат части транзакции. |
| COPY | COPY orders FROM '/tmp/orders.csv' CSV HEADER; | Массовая загрузка на сервере (нужны права; с клиента используйте \copy). |
| generate_series | SELECT generate_series(1, 5); | Генерация последовательностей, даты через interval. |
| EXPLAIN | EXPLAIN SELECT * FROM orders WHERE user_id = 1; | План запроса без выполнения. |
| EXPLAIN ANALYZE | EXPLAIN (ANALYZE, BUFFERS) SELECT ...; | План с реальным выполнением и чтением страниц. Для UPDATE и DELETE оборачивайте в транзакцию с откатом. |
Роли и права
| Команда | Вариант | Что делает |
| CREATE ROLE | CREATE ROLE app LOGIN PASSWORD 'change-me'; | Пользователь для приложения (LOGIN даёт право входить). |
| CREATE ROLE группа | CREATE ROLE readonly NOLOGIN; | Роль-группа без входа, права выдаются ей. |
| GRANT роль | GRANT readonly TO analyst; | Добавить пользователя в роль. |
| ALTER ROLE | ALTER ROLE app PASSWORD 'new'; | Сменить пароль или атрибуты. |
| ALTER ROLE VALID UNTIL | ALTER ROLE analyst VALID UNTIL '2027-01-01'; | Срок действия учётной записи. |
| GRANT CONNECT | GRANT CONNECT ON DATABASE appdb TO readonly; | Право подключаться к базе. |
| GRANT USAGE | GRANT USAGE ON SCHEMA public TO readonly; | Доступ к схеме (без него таблицы не видны). |
| GRANT SELECT ON ALL TABLES | GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly; | Чтение всех существующих таблиц схемы. |
| ALTER DEFAULT PRIVILEGES | ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly; | Права на таблицы, которые появятся в будущем. |
| REVOKE | REVOKE ALL ON orders FROM readonly; | Забрать права. |
| DROP ROLE | DROP OWNED BY old_user; DROP ROLE old_user; | Удалить роль: сначала передать или удалить её объекты. |
| \du | \du+ app | Проверка атрибутов роли в psql. |
Резервное копирование и восстановление
| Команда | Вариант | Что делает |
| pg_dump -Fc | pg_dump -Fc -f appdb.dump appdb | Бэкап в собственном сжатом формате (рекомендуемый). |
| pg_dump -Fd -j | pg_dump -Fd -j 4 -f appdb_dir appdb | Каталог-формат, параллельный дамп на 4 процесса. |
| pg_dump -Fp | pg_dump -Fp appdb > appdb.sql | Обычный SQL-скрипт (читаемый, но большой). |
| pg_dump -t | pg_dump -Fc -t orders -f orders.dump appdb | Одна таблица. |
| pg_dump -n | pg_dump -Fc -n reporting -f reporting.dump appdb | Одна схема. |
| pg_dump --schema-only | pg_dump --schema-only -f schema.sql appdb | Только структура без данных. |
| pg_dump --data-only | pg_dump --data-only -Fc -f data.dump appdb | Только данные. |
| pg_dumpall --globals-only | pg_dumpall --globals-only -f roles.sql | Роли и табличные пространства, которых нет в обычном дампе базы. |
| pg_restore | pg_restore -d appdb_new -j 4 appdb.dump | Восстановление из формата -Fc или -Fd в существующую базу. |
| pg_restore --clean | pg_restore --clean --if-exists -d appdb appdb.dump | Сначала удалить объекты, потом создать заново. |
| pg_restore -l | pg_restore -l appdb.dump | Оглавление дампа без восстановления. |
| psql -f (SQL-дамп) | psql -d appdb_new -f appdb.sql | Восстановление обычного SQL-дампа. |
| CREATE DATABASE TEMPLATE | CREATE DATABASE test_copy TEMPLATE appdb; | Быстрая копия базы (к исходной не должно быть подключений). |
Проверка резервной копии
# Восстановить в тестовую базу и проверить количество строк
createdb appdb_check
pg_restore -d appdb_check -j 4 appdb.dump
psql -d appdb_check -At -c "SELECT count(*) FROM orders;"
dropdb appdb_check
Производительность и диагностика
| Задача | SQL |
| Активные запросы и их длительность | SELECT pid, usename, state, now() - query_start AS duration, left(query, 80) FROM pg_stat_activity WHERE state <> 'idle' ORDER BY duration DESC; |
| Завершить запрос мягко | SELECT pg_cancel_backend(12345); |
| Разорвать сессию | SELECT pg_terminate_backend(12345); |
| Кто кого блокирует | SELECT pid, pg_blocking_pids(pid) AS blocked_by, left(query, 80) FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid)) > 0; |
| Размер базы | SELECT pg_size_pretty(pg_database_size('appdb')); |
| Самые большие таблицы | SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10; |
| Неиспользуемые индексы | SELECT relname, indexrelname, idx_scan FROM pg_stat_user_indexes WHERE idx_scan = 0 ORDER BY relname; |
| Обновить статистику | ANALYZE orders; |
| Очистка мёртвых строк | VACUUM (VERBOSE, ANALYZE) orders; |
| Кеш-попадания | SELECT sum(blks_hit)::float / nullif(sum(blks_hit + blks_read), 0) FROM pg_stat_database; |
| Самые медленные запросы | SELECT calls, mean_exec_time, left(query, 80) FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10; |
Для последнего запроса нужно расширение pg_stat_statements: добавьте его в shared_preload_libraries, перезапустите сервер и выполните CREATE EXTENSION pg_stat_statements;.
Настройки сервера
| Команда | Вариант | Что делает |
| SHOW | SHOW work_mem; | Текущее значение параметра. |
| SHOW config_file | SHOW config_file; | Путь к postgresql.conf. |
| SHOW ALL | SHOW ALL; | Все параметры и их источники. |
| pg_settings | SELECT name, setting, source FROM pg_settings WHERE source <> 'default'; | Что переопределено вручную. |
| ALTER SYSTEM | ALTER SYSTEM SET work_mem = '64MB'; | Записать в postgresql.auto.conf. |
| pg_reload_conf | SELECT pg_reload_conf(); | Применить параметры, которые не требуют перезапуска. |
| SET (сессия) | SET statement_timeout = '30s'; | Параметр только для текущей сессии. |
| pg_hba.conf | host appdb app 10.0.0.0/24 scram-sha-256 | Кто, откуда и каким способом может подключиться. |
Полезные параметры
| Параметр | Для чего |
| statement_timeout | Убивать запрос, который идёт дольше заданного времени. |
| lock_timeout | Не ждать блокировку дольше заданного времени (полезно перед миграциями). |
| idle_in_transaction_session_timeout | Разрывать сессии, которые держат открытую транзакцию без работы. |
| work_mem | Память на операции сортировки и хеширования в одном узле запроса. |
| maintenance_work_mem | Память для VACUUM и CREATE INDEX. |
| log_min_duration_statement | Логировать запросы дольше заданного времени (например, 500 мс). |
| shared_buffers | Общий кеш страниц сервера (обычно около четверти RAM). |
Лайфхаки
- Пароль без истории. Вместо
PGPASSWORD в команде используйте файл ~/.pgpass в формате host:port:db:user:password с правами chmod 600.
- Проверить миграцию в транзакции. Оберните скрипт в
BEGIN; и в конце выполните ROLLBACK;, пока не убедитесь, что всё работает. Или используйте psql -1.
- Остановиться при ошибке. В начале скрипта:
\set ON_ERROR_STOP on. Иначе psql продолжит выполнять после сбоя.
- Удобная запись в .psqlrc. Строка
\x auto и \timing on в ~/.psqlrc сразу делают вывод читаемее и показывают время запросов.
- Выгрузить CSV с клиента.
\copy (SELECT ...) TO 'out.csv' CSV HEADER. Запрос в скобках, а не просто имя таблицы.
- Быстрая проверка количества без блокировок. Для больших таблиц точный
count(*) медленный. Оценка: SELECT reltuples::bigint FROM pg_class WHERE relname = 'orders';.
- Идентификаторы без лишних расширений. Начиная с PostgreSQL 13,
gen_random_uuid() встроен в ядро.
- Пакетная вставка. Одна инструкция
INSERT ... VALUES (...), (...), (...) на тысячи строк быстрее тысяч отдельных INSERT.
- Очередь без дублей.
FOR UPDATE SKIP LOCKED позволяет нескольким воркерам брать задачи параллельно.
- Порог для сложных запросов. Если
EXPLAIN ANALYZE показывает Seq Scan по большой таблице с редким условием, проверьте индекс и актуальность статистики через ANALYZE.
- Сохранить вывод команды в переменную shell.
COUNT=$(psql -At -d appdb -c "SELECT count(*) FROM orders;").
- Список подключений по приложениям.
SELECT application_name, count(*) FROM pg_stat_activity GROUP BY 1;. Задайте имя приложения в строке подключения, чтобы его было видно.
- Не используйте OFFSET для глубокой пагинации. При больших смещениях запрос читает и отбрасывает строки. Используйте курсорную пагинацию по индексированному ключу:
WHERE id > :last_id ORDER BY id LIMIT 50.
- Округление времени.
date_trunc('day', created_at) группирует по суткам, date_trunc('hour', ...) по часам.
Работа с JSON
-- Значение по ключу как текст
SELECT payload->>'email' FROM events;
-- Вложенный путь
SELECT payload#>>'{address,city}' FROM events;
-- Фильтр по содержимому (с GIN-индексом)
SELECT * FROM events WHERE payload @> '{"type": "signup"}';
-- Развернуть массив в строки
SELECT jsonb_array_elements_text(tags) FROM posts;
Типичные задачи одной командой
# Создать пользователя и базу для приложения
psql -U postgres -c "CREATE ROLE app LOGIN PASSWORD 'secret';"
psql -U postgres -c "CREATE DATABASE appdb OWNER app;"
# Ежедневный бэкап с датой и проверкой
pg_dump -Fc -f /backup/appdb-$(date +%F).dump appdb && echo "backup ok"
# Сколько активных подключений и лимит
psql -At -U postgres -c "SELECT count(*) FROM pg_stat_activity;" \
-c "SHOW max_connections;"
# Выполнить SQL из файла с выводом времени
psql -d appdb -v ON_ERROR_STOP=1 -c "\timing on" -f report.sql
DROP DATABASE, DROP TABLE ... CASCADE, TRUNCATE и DELETE без WHERE необратимы. Перед опасными операциями сделайте бэкап, выполните в транзакции и проверьте результат. Команды pg_terminate_backend прерывают чужие сессии: сначала пробуйте pg_cancel_backend.
Миграции схемы лучше выполнять с SET lock_timeout = '5s';. Тогда если таблица занята долгой транзакцией, миграция упадёт с ошибкой, а не встанет в очередь и не заблокирует все запросы приложения.