不少朋友接触 PostgreSQL,第一反应都是装个 pgAdmin 或者 DataGrip,然后鼠标点点点。但真到了服务器上排查问题、写一次性脚本、或者处理几百万行数据导入导出的时候,你会发现所有图形界面都使不上劲——身边只剩一个黑乎乎的终端窗口。这时候,psql 就是你绕不开的看家工具。
作为 PostgreSQL 官方自带的命令行客户端,psql 不是那种“能跑就行”的附属品。它内置了一套完整的元命令体系、脚本变量机制和基于 Readline 的快捷键操作,用熟了之后,大部分日常开发、运维、数据核对工作都可以在终端里完成,效率反而比切窗口点鼠标高得多。这篇文章不打算讲那些晦涩的数据库原理,就纯粹聊聊 psql 的常用操作、元命令、快捷键,以及我在真实项目里用出来的习惯和踩过的坑。
1. 拿到终端第一步:连接数据库的正确姿势与常见翻车点
1.1 最基础的连接命令与参数拆解
psql 的连接语法说简单也简单,说复杂也复杂,核心就一行:
psql -h 192.168.1.100 -p 5432 -U postgres -d mydb这里每个参数对应一个意思:
-h:数据库主机地址,本地连可以省略或者填localhost。平时开发连远程库这里填服务器 IP 或域名。-p:端口,PostgreSQL 默认是 5432。如果你的实例改了端口,这里必须显式指定。-U:用户名,默认跟当前系统用户名一致。很多人第一次连不上就是卡在这——本机用户名是lisi,数据库里只有postgres用户,不指定-U必然报错。-d:数据库名,默认也会跟你用户名走。不指定的话连的是和用户同名的库,如果那个库不存在,你会看到psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: database "lisi" does not exist。
我建议把完整参数写成习惯,不要依赖默认值。尤其是-h不写时,psql 会优先走 Unix 套接字而不是 TCP,有些环境里套接字的认证方式跟 TCP 完全不同(最常见的就是 peer 认证),同样一套密码,走 TCP 能进,走套接字直接拒绝。
连接成功后,终端会变成这样:
psql (16.3) Type "help" for help. mydb=#提示符最末尾的符号有讲究:=表示当前不在事务块里,#表示当前是超级用户。如果你看到的是mydb->,说明你已经在一个未提交的事务里了,这时候执行任何语句都不会真正生效,直到 COMMIT 或 ROLLBACK。
1.2 连接串——用一个字符串搞定所有参数
除了散装参数,psql 还支持直接传连接串(URI 格式),这在写脚本或者配置 CI/CD 的时候特别好用:
psql "postgresql://postgres:yourpassword@192.168.1.100:5432/mydb?sslmode=require"连接串的好处是把主机、端口、用户、密码、SSL 模式全部打包在一个字符串里,环境变量DATABASE_URL可以直接喂给它。很多应用框架(比如 Django、Rails、Spring Boot)里的配置就是这个格式,你复制出来就能直接连,不用再手工拆字段。
1.3 密码处理:别再往命令行里裸写密码了
新手最喜欢干的事是psql -h xx -U postgres -p 密码,实际上psql压根没有-p密码参数——-p是端口。密码要么交互式输入,要么用环境变量PGPASSWORD,要么写到.pgpass文件里。
交互式输入最安全但麻烦,没法用在脚本里;环境变量是脚本里最常见的做法:
export PGPASSWORD='yourpassword' psql -h localhost -U postgres -d mydb但环境变量有两个坑:一是ps aux能看到当前进程的环境变量吗?看不到,但是/proc/<pid>/environ里能读出来,所以多用户服务器上还是要谨慎;二是如果密码里有特殊字符,必须用单引号包好,否则 shell 会做变量展开或分词。
我个人推荐用.pgpass文件,这是 PostgreSQL 官方的密码文件机制。在 Linux/macOS 下放在用户主目录,Windows 下放在%APPDATA%\postgresql\,内容格式是:
hostname:port:database:username:password比如:
192.168.1.100:5432:postgres:postgres:My!Passw0rd文件权限必须改成 600:
chmod 600 ~/.pgpass设置好之后连接时会自动读取,不再提示输密码。注意.pgpass里database也可以写*表示匹配所有库,字段也可以用*通配,但密码里的冒号没法转义,所以密码里最好别用冒号。
1.4 连接不上的排查思路
连不上数据库时,错误信息本身就是最好的线索。我列几个高频报错和常见原因:
| 报错关键字 | 真实原因 |
|---|---|
| Connection refused | 端口没监听、防火墙挡了、PostgreSQL 没启动 |
| Password authentication failed | 密码错误,或者 pg_hba.conf 里认证方式不是 md5/scram |
| Peer authentication failed | 本地套接字连接用了系统用户身份验证,你必须有同名系统用户 |
| No pg_hba.conf entry for host... | 这个 IP 网段没被 pg_hba.conf 放行 |
| database "xx" does not exist | 库名写错了,或者没指定 -d 用了默认值 |
远程连接遇到问题,八成是 pg_hba.conf 和监听地址的问题。确认一下listen_addresses是不是'*',再确认pg_hba.conf里有没有对应网段的host记录。本地套接字连不上,大部分是用户身份不匹配。
2. psql 里跑查询:格式化输出、执行计划与提示符配置
连接进去了,下面就是日常干活。psql 不只是执行 SQL,它自带了一整套展示层优化,用好了比客户端工具还顺手。
2.1 默认输出的那些坑:对齐、截断和 NULL
直接执行SELECT * FROM users;,如果列很宽,psql 默认输出会以终端宽度为界截断,中间显示...之类的省略内容。数据一长,看起来就像被狗啃了。
有两种办法解决:
一是切到扩展显示模式,用\x开关,再执行查询就会变成一行一个字段的纵向输出。字段多、行宽超屏的场景下这个模式非常舒服。
mydb=# \x Expanded display is on. mydb=# SELECT * FROM users WHERE id = 1; -[ RECORD 1 ]--- id | 1 username | alice email | alice@example.com created_at | 2025-06-01 10:23:45二是用\pset调整显示参数:
\pset format wrapped -- 自动换行而不是截断 \pset null '(NULL)' -- 让 NULL 值显示得更明显 \pset border 2 -- 给表格加完整边框线\pset null这个我特别推荐,默认 NULL 在 psql 里显示为空字符串,跟空字符串混在一起你根本分不清,生产环境核对数据时极容易误判。
2.2 行数、耗时和影响行数的实时反馈
想知道查询跑了多久,最直接的办法是打开计时器:
mydb=# \timing on Timing is on. mydb=# SELECT count(*) FROM orders; count ------- 152340 (1 row) Time: 412.345 ms注意Time包含的是从 psql 发送 SQL 到拿回结果的整个链路耗时,不只是数据库执行时间。做性能对比时,建议多跑几次取稳定值,或者直接在 SQL 层面用EXPLAIN ANALYZE看真实执行耗时。
如果是从脚本里跑批量 DML,想知道影响了多少行,psql 会在语句结束后打一行INSERT 0 100、UPDATE 5之类的结果标签,这就是客户端行数反馈,没法关掉,但对核对批量操作非常有用。
2.3 执行计划、watch 和反斜杠 g 的连招
EXPLAIN ANALYZE是优化慢查询的核心武器,在 psql 里配合\g命令可以反复执行查看:
EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'pending' ORDER BY created_at DESC LIMIT 100;写完这句之后,直接按\g回车就可以重复执行上一次的查询。对排查“为什么相同的 SQL 一会快一会慢”这类问题时,这个组合极其顺手。
还有一种实时监控场景:开店大促时盯着某个计数。psql 提供了一个\watch元命令,可以每隔几秒自动重跑上一条 SQL:
mydb=# SELECT count(*) FROM orders WHERE status = 'pending'; count ------- 152340 (1 row) mydb=# \watch 5每 5 秒自动刷新一次结果,直到按Ctrl+C打断。这个用途在监控导入进度、核对同步延迟时非常实用,不用自己写循环脚本。
2.4 自定义提示符:让每个连接都知道自己在哪
多环境切换(开发、测试、生产)时最怕的就是在错误的库上跑了 DELETE。除了肉眼盯着提示符看,你可以把环境名称直接烙在 psql 提示符里。
用\set设置PROMPT1:
mydb=# \set PROMPT1 '%n@%M:%>`%x%# ' alice@192.168.1.100:5432>这里%n是用户名,%M是主机名带端口,%>是数据库名,%x在事务内显示*,%#显示#表示超级用户。我实际使用时还会把生产环境的 PROMPT1 专门改成带红色或特殊前缀的样式,把误操作风险压到最低。
这个\set配置可以写进~/.psqlrc文件,每次启动 psql 自动加载。文件位置在 Linux 是~/.psqlrc,Windows 是%APPDATA%\postgresql\psqlrc.conf。
3. 元命令:反斜杠后面那半个世界
psql 里所有以反斜杠开头的命令都叫元命令(meta-command)。它们不是 SQL,而是客户端提供的便捷操作。这部分是 psql 比大多数命令行工具强大的核心原因。
3.1 最常用的一批元命令速查
我按使用频率排个序,把日常最高频的列出来:
| 元命令 | 作用 |
|---|---|
\l | 列出所有数据库 |
\c dbname | 切换到另一个数据库,相当于断开重连 |
\dt | 列出当前 schema 下的所有表 |
\dt+ | 列出表并显示大小、描述等额外信息 |
\d tablename | 查看表结构(字段、类型、约束、索引) |
\d+ tablename | 查看表结构并显示存储参数、表注释 |
\du | 列出所有角色/用户及其权限 |
\dn | 列出所有 schema |
\df | 列出函数 |
\dv | 列出视图 |
\di | 列出索引 |
\db | 列出表空间 |
\conninfo | 查看当前连接信息 |
\q | 退出 psql |
\? | 查看元命令帮助 |
\h | 查看 SQL 命令帮助,比如\h SELECT |
\timing | 开关执行计时 |
\x | 开关扩展显示 |
\o 文件名 | 把查询结果输出到文件 |
\i 文件名 | 执行 SQL 脚本文件 |
\d家族是整个元命令体系里信息量最大的一个。\d tablename出来的结构信息非常全,比如:
mydb=# \d users Table "public.users" Column | Type | Collation | Nullable | Default ------------+-----------------------------+-----------+----------+------------------------------------ id | integer | | not null | nextval('users_id_seq'::regclass) username | character varying(64) | | not null | email | character varying(255) | | | created_at | timestamp without time zone | | not null | now() Indexes: "users_pkey" PRIMARY KEY, btree (id) "users_username_key" UNIQUE CONSTRAINT, btree (username)字段、默认值、约束、索引一次全看清楚,不用再去翻 information_schema 写查询。
3.2 信息查询与权限诊断
排查权限问题是我工作中最常遇到的一个应用场景。比如用户反馈“连上了但查不了某张表”,用\dp tablename可以看这张表的权限授予情况:
mydb=# \dp orders Access privileges Schema | Name | Type | Access privileges | Column privileges | Policies --------+---------+-------+-----------------------------+-------------------+---------- public | orders | table | postgres=arwdDxt/postgres +| | | | | app_user=arwd/postgres +| |看到app_user=arwd表示app_user有 INSERT、SELECT、UPDATE、DELETE 权限,但d之后缺了D(TRUNCATE)和x(REFERENCES)等。权限符号含义不熟时,直接看\h GRANT帮助,或者用SELECT * FROM information_schema.table_privileges WHERE table_name='orders'查明细。
3.3 用 \g 和 \o 组合出报表
\o的输出重定向很好用。比如你想把查询结果导出成 CSV 用于 Excel 分析:
mydb=# \o /tmp/orders_report.csv mydb=# \pset format csv Output format is csv. mydb=# SELECT id, status, amount FROM orders WHERE created_at > '2025-01-01'; mydb=# \o最后那个不带参数的\o是把输出重定向回终端。这个过程中终端可能看不到任何查询结果,但orders_report.csv文件里已经落好了数据。
这里有个小坑:\pset format csv输出的 CSV 没有表头,如果你需要表头,得在 SQL 里用 UNION 手工拼一行字段名,或者用COPY ... TO STDOUT WITH CSV HEADER(前提是有对应权限)。数据量不大时我更喜欢后者:
COPY (SELECT id, status, amount FROM orders WHERE created_at > '2025-01-01') TO STDOUT WITH CSV HEADER;这条命令直接在终端输出带表头的 CSV,再用 shell 重定向写到文件即可。
3.4 \i 执行脚本与变量传递
把一段复杂 SQL 写成文件,然后在 psql 里用\i执行,是实操中管理长语句的常规做法:
-- fix_orders.sql BEGIN; UPDATE orders SET status = 'cancelled' WHERE created_at < '2020-01-01' AND status = 'pending'; DELETE FROM order_logs WHERE created_at < '2020-01-01'; COMMIT;执行:
mydb=# \i /path/to/fix_orders.sql脚本里也可以用 psql 变量机制传参:
mydb=# \set since '2020-01-01' mydb=# SELECT count(*) FROM orders WHERE created_at < :'since';注意冒号加引号的写法:'since',psql 会把它替换成带引号的字符串字面量,安全且规范。如果你直接写:since且变量值里有特殊字符,很可能会造成 SQL 注入或语法错误。脚本里传参这个能力,在对多环境执行同一套变更脚本时非常有用。
4. 命令行编辑与快捷键:手不离键盘的操作流
psql 底层用的是 GNU Readline 库,这也意味着它在终端里支持一整套类 Emacs 的快捷键。这部分用好了,你的操作节奏能明显上一个台阶,不用频繁地在鼠标和键盘之间切换。
4.1 Readline 快捷键全表
| 快捷键 | 作用 |
|---|---|
Ctrl+A/Ctrl+E | 光标移到行首 / 行尾 |
Ctrl+U | 删除光标到行首的所有字符 |
Ctrl+K | 删除光标到行尾的所有字符 |
Ctrl+W | 删除光标前的一个单词 |
Ctrl+Y | 粘贴被删除的内容(yank) |
Ctrl+L | 清屏 |
Ctrl+Z | 挂起 psql,回到 shell(用fg恢复) |
Tab | 自动补全表名、列名、函数名、元命令名 |
上/下方向键 | 浏览命令历史 |
Ctrl+R | 反向搜索历史命令 |
Ctrl+G | 退出搜索模式 |
Alt+B/Alt+F | 光标按单词前移/后移 |
Ctrl+R这个反向搜索是我最依赖的快捷键。当你需要找出十分钟前跑过的一条长 SQL 时,按下Ctrl+R输入几个关键字母,psql 会实时匹配历史命令,按Ctrl+R继续往更早的历史翻,找到后直接回车执行。这个操作比用方向键一条条翻高效得多。
还有个容易被忽略的技巧:方向键上下浏览历史时,如果当前行已经输入了部分内容,Readline 会做前缀匹配——只浏览以当前输入开头的历史命令。比如你输入了SELECT,按上键,只会翻到之前以 SELECT 开头的命令。
4.2 多行输入与 \e 编辑器模式
写复杂 SQL 时,在终端一行行续写很痛苦。psql 支持直接在提示符下多行输入,SQL 没写完时会变成次级提示符(默认是mydb->),直到分号结束才执行。
如果这段 SQL 实在太长,我一般直接用\e打开外部编辑器(默认是 vi,可以\setenv EDITOR vim改成自己的编辑器),编辑完保存退出后,psql 自动把内容发送给数据库执行。
mydb=# \e这会拉起$EDITOR,在里面写好 SQL(不要加分号也行,psql 会自动处理),保存退出后立刻执行。对于超过十行的复杂查询,这比在终端里一点点敲要舒服得多。
4.3 命令历史:在哪存、怎么复用
psql 的历史记录在 Linux/macOS 下存在~/.psql_history,Windows 下在%APPDATA%\postgresql\psql_history。这个文件是纯文本,可以grep,也可以直接编辑。
跨环境切换时,我习惯保留自己的历史文件而不同步到生产服务器。生产环境默认的历史文件可能会把线上库名、表名留痕,如果有安全要求,可以在~/.psqlrc里设置\set HISTFILE /dev/null或者干脆\set HISTSIZE 0关闭历史记录。
4.4 一个提升效率的小习惯:把同一条 SQL 变体保存在 shell 历史里
这个习惯可能有点反直觉:在 psql 里按Ctrl+C打断一条已经编辑但不想立即执行的 SQL,psql 会把当前输入留在历史里但不会执行。如果你的 SQL 还没写完却要去查另一条数据,先别急着删掉自己敲了一半的内容,按Ctrl+C回到干净提示符,查完数据后按上方向键,刚才敲了一半的语句会重新出现,继续补完即可。
5. 事务控制与脚本化:从交互到自动化的一步之遥
5.1 交互模式下的自动提交与手动事务
psql 默认对每条独立 SQL 是自动提交的——只要语句执行完没有报错,事务立刻生效。这意味着你执行一条DELETE FROM orders WHERE ...,如果没有先BEGIN,删了就是删了,不能反悔。
要养成事务习惯,有两种方式:
一是显式BEGIN:
mydb=# BEGIN; mydb=# DELETE FROM orders WHERE id = 12345; mydb=# SELECT count(*) FROM orders WHERE id = 12345; mydb=# ROLLBACK;在事务块里,提示符会从=变成>(比如mydb=*>,注意那个*),提醒你在事务中。执行完一句 DELETE 后,检查一下影响行数,确认无误再COMMIT,不对就ROLLBACK。这是生产环境操作任何数据的保命操作。
二是在启动 psql 时用-1参数强制把整个会话包成一个大事务:
psql -1 -f fix_orders.sql这样脚本里任何一步失败,整个事务全部回滚,不会出现执行了一半留下一堆脏数据的尴尬。
5.2 脚本化执行的几个关键开关
写自动化脚本用 psql 时,有几个选项非常重要,平时交互模式用不到,但脚本里少一个都可能出大事:
-v ON_ERROR_STOP=1:遇到第一条 SQL 错误就停止执行,并返回非零退出码。不带这个参数,脚本里的 SQL 即使报错也会继续往下跑,最后你拿到一个“看似成功”的退出码,实际上中间漏掉了若干步骤。--echo-all或者-a:把每一条执行的 SQL 一并打印出来,方便审计。--no-psqlrc:启动时不加载~/.psqlrc,避免个人配置干扰脚本行为。--single-transaction/-1:如上所述,整个脚本包成一个事务。
一个典型的脚本执行命令长这样:
psql "postgresql://postgres:pass@localhost:5432/mydb" -v ON_ERROR_STOP=1 -f migrate.sql在 CI/CD 流水线里,这种写法能保证迁移脚本的确定性。
5.3 用 \if 元命令做条件判断
psql 从 9.6 开始支持在脚本里用\if、\elif、\else做条件分支,这在处理“某列不存在才添加”这类幂等迁移时非常实用:
\if :{?column_exists} SELECT 1; \else ALTER TABLE users ADD COLUMN phone varchar(20); \endif需要先在前面定义变量:
\set column_exists (SELECT 1 FROM information_schema.columns WHERE table_name='users' AND column_name='phone')要用 psql 变量装一个子查询的结果,语法是\set var (SELECT ...)。这个能力让脚本既能重复执行,又不会因对象已存在而报错,配合迁移工具非常好用。
5.4 管道配合:psql 不只是查数据
psql 标准输出是普通文本,所以可以直接接到 shell 工具链里。比如把最大的一张表捞出来:
psql -h localhost -U postgres -d mydb -Atc "SELECT relname FROM pg_class WHERE relkind='r' ORDER BY reltuples DESC LIMIT 1"-A去掉对齐空格,-t只要裸数据,-c执行单条 SQL 后退出。这几个参数组合在生产脚本里出场率非常高。数据再经过管道交给排序、去重、统计等命令,psql 就变成了一个灵活的数据接口。
6. 日常实战经验与高频翻车现场总结
6.1 乱码问题:显示中文变问号
psql 里中文显示不正常,先分清楚两种情况:
- 存储没问题,只是终端显示乱码——检查客户端字符集:
\encoding看看当前编码,改成 UTF8:\encoding UTF8。 - 查询结果里直接就是乱码——说明数据入库时编码就错了,或者连接参数里没有指定客户端编码。
环境变量层面还有个PGCLIENTENCODING,必要时可以export PGCLIENTENCODING=UTF8强制客户端编码。
6.2 管道 SIGPIPE 导致的“神秘退出”
用 psql 配合管道命令时,如果管道对端提前退出(比如psql ... | head -5),psql 会收到 SIGPIPE 直接终止,报错可能是broken pipe或者直接无输出。这在非交互脚本里会造成困扰。解决办法是避免 psql 直接接 head 这类提前关闭管道的命令,改用 SQL 层面的LIMIT控制返回行数,或者把结果先落到临时文件再分段处理。
6.3 长查询跑到一半想取消
交互模式下执行了一条跑很久的查询,想取消:在 psql 里直接按Ctrl+C,psql 会向服务端发送取消当前查询的信号。注意这个操作不一定会让服务端立即停止,如果已经在做 CPU 密集的聚合或排序,可能得等当前计算结束才能安全取消。取消后事务不会自动回滚,还在当前事务块里,需要自己判断是 COMMIT 还是 ROLLBACK。
6.4 历史命令里误存了密码
早年我在~/.psql_history里敲过CREATE USER xx PASSWORD 'abc123'这类语句,或者不小心在命令行里把密码写进 SQL,然后整段都进了历史文件。如果你的终端环境不是私有的,一定注意检查:
grep -i "password" ~/.psql_history发现敏感信息,直接编辑这个文件删除对应行。更保险的做法是给.psql_history也设个 600 权限。
6.5 本地与远程库的 \d 输出差异
同一个\d orders,在本地和远程看到的输出可能不一样,最常见的原因是搜索路径(search_path)不同。psql 默认展示当前搜索路径里第一个 schema 下的对象,如果你的表在configschema 下,而搜索路径里没包含它,\dt根本看不到。排查思路是先跑SHOW search_path;,确认当前的 schema 搜索顺序。
6.6 元命令与 SQL 语句的混写边界
元命令不是 SQL,因此不能和 SQL 混在同一行(通过\g传递名字空间做嵌套的除外)。一个高频错误是写:
SELECT * FROM users \x;这不会生效,反而会报语法错误。正确做法是先把\x打开,再单独执行 SQL。记住一个原则:要么先元命令后 SQL,要么每条语句独立成行。
7. 把 psql 调到最顺手:一份个人配置文件参考
最后分享一个我自己用了很久的~/.psqlrc配置,可以直接抄走,根据自己的习惯改:
\set PROMPT1 '%n@%M:%>`%x%# ' \set PROMPT2 '%M %p > ' \timing on \x auto \pset null '(NULL)' \pset border 2 \pset pager always \encoding UTF8 \set HISTSIZE 5000 \set HISTCONTROL ignoredups逐个说明一下:
\x auto:当查询结果列数太多导致超宽时自动切扩展显示,不会打断宽表浏览。\pset pager always:结果超过一屏自动用 less 分页,避免刷屏。\set HISTCONTROL ignoredups:连续的重复命令只记一次,历史文件不会膨胀成垃圾场。\set HISTSIZE 5000:历史条数给足,免得想翻一个半月前的命令时找不到。
如果你经常在 psql 里写复杂 SQL,可以把默认编辑器改成 vim 或 nano:
\setenv EDITOR vim然后\e就直接进 vim 了,写长查询的体验会好很多。
我在实际使用中还有一个不太起眼但很重要的心得:凡是涉及生产数据的操作,不管多熟练,都先把\timing on打开,先跑一条SELECT count(*)确认连接和数据状态,再进入事务块做变更。psql 是个非常强大的工具,但它的强大建立在“你知道自己在干什么”的前提下。把上面这些命令和快捷键内化成肌肉记忆之后,你会发现命令行操作数据库不仅不 low,反而是效率最高、最接近数据库本质的工作方式。