第一个 SQL 查询
本节带你从零开始执行第一条 SQL 语句:连接数据库、运行查询、读懂结果。学完本节,你将能用 SELECT 检索数据、理解结果集的构成,并掌握 psql 中最常用的几种操作。
前置知识
阅读本节前,建议先了解:PostgreSQL 是什么
基础概念
在敲下第一个字符之前,先把几个绕不开的概念理清。
数据库(Database):一个独立的数据容器,通常一个应用对应一个数据库。PostgreSQL 实例(Instance)下可以建多个数据库,数据库之间默认互相隔离、不能跨库查询。
表(Table):数据库里存放数据的二维结构,由列(Column)和行(Row)组成。习惯上也把行称为记录(Record),在 PostgreSQL 内部则称为元组(Tuple)。
模式(Schema):数据库内部的命名空间。一个数据库可以有多个模式,表实际的全名是 模式名.表名,如 public.users。默认创建的表都在 public 模式下。
SQL(Structured Query Language):结构化查询语言,是与关系数据库交互的通用语言。按用途分为几类:
| 类别 | 全称 | 作用 | 代表语句 |
|---|---|---|---|
| DDL | Data Definition Language | 定义结构 | CREATE、ALTER、DROP、TRUNCATE |
| DML | Data Manipulation Language | 操作数据 | INSERT、UPDATE、DELETE |
| DQL | Data Query Language | 查询数据 | SELECT |
| DCL | Data Control Language | 权限控制 | GRANT、REVOKE |
| TCL | Transaction Control Language | 事务控制 | BEGIN、COMMIT、ROLLBACK |
结果集(Result Set):查询返回的行集合。SELECT 语句永远返回一个结果集,哪怕是 0 行。
psql:PostgreSQL 官方自带的命令行客户端(Interactive Terminal)。它是学习阶段最合适的工具,因为不隐藏任何细节。
NULL:表示"未知值",与空字符串、数字 0 都不同。这是 SQL 里最容易出错的概念,后文会单独说明。
语法与用法
SELECT 是所有查询的基础,最小骨架如下:
SELECT 列名1, 列名2 FROM 表名;| 子句 | 是否必需 | 作用 |
|---|---|---|
SELECT | 必需 | 指定要返回的列或表达式 |
FROM | 可选 | 指定数据来源表;省略时对常量求值 |
WHERE | 可选 | 过滤行 |
ORDER BY | 可选 | 排序 |
LIMIT | 可选 | 限制返回行数 |
最简形式甚至可以不带 FROM,直接对一个表达式求值:
-- 对常量求值:不需要任何表
SELECT 1 + 1; ?column?
----------
2
(1 row)表达式可以用 AS 起别名(AS 可省略,但建议保留):
SELECT
1 + 1 AS sum_result, -- 数值运算
'Hello' || ' PG' AS greeting, -- 字符串拼接
now() AS server_time, -- 函数调用
2 * 3.14 AS circle_area; sum_result | greeting | server_time | circle_area
------------+------------+------------------------------+-------------
2 | Hello PG | 2026-09-21 10:05:12.334+08 | 6.28
(1 row)psql 提供了大量以反斜杠开头的元命令(Meta-Command),用于控制客户端行为,与 SQL 语句是两个体系:
| 元命令 | 作用 |
|---|---|
\l | 列出所有数据库 |
\c 数据库名 | 连接到指定数据库 |
\dt | 列出当前数据库的表 |
\d 表名 | 查看表结构 |
\dn | 列出所有模式 |
\du | 列出所有角色 |
\x | 切换纵向展开显示(宽表友好) |
\timing | 开关 SQL 执行耗时统计 |
\i 文件路径 | 执行 SQL 文件 |
\? | 查看所有元命令帮助 |
\q | 退出 psql |
详细说明
连接数据库
先用 psql 建立连接。最简形式如下(连接参数由命令行或环境变量提供):
# 连接到本机 5432 端口,以当前系统用户身份登录
psql -d demo
# 明确指定主机、端口、用户、数据库
psql -h 127.0.0.1 -p 5432 -U postgres -d demopsql (18.6)
Type "help" for help.
demo=#提示符 demo=# 的含义是:已连到 demo 数据库、当前以超级用户身份操作(普通用户的提示符末尾是 =>)。
也可以先连到默认库,再切换:
psql -U postgrespostgres=# \c demo
You are now connected to database "demo" as user "postgres".
demo=#用元命令确认当前所处的环境:
demo=# \conninfo
You are connected to database "demo" as user "postgres" via socket in "/var/run/postgresql" at port "5432".
demo=# \dt
Did not find any relations.
demo=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | ICU Locale | Access privileges
-----------+----------+----------+-------------+-------------+------------+-------------------
demo | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | |
postgres | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | |
template0 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | |
template1 | postgres | UTF8 | en_US.UTF-8 | en_US.UTF-8 | |
(4 rows)语句的书写规则
理解下面三条规则,能避免 90% 的入门报错:
- 语句以分号结尾。psql 只有在遇到分号时才把缓冲区内容发给服务端。少写分号时提示符会变成
demo-#,表示语句尚未结束。 - SQL 关键字大小写不敏感。
select、SELECT、Select完全等价;但字符串字面量区分大小写,'Alice'与'alice'不同。本教程统一使用大写关键字便于阅读。 - 未加引号的标识符会被折叠为小写。
SELECT Name FROM Users实际查询的是name列和users表。如果需要区分大小写,必须用双引号:SELECT "Name" FROM "Users"。
demo=# SELECT 1
demo-# + 1;
?column?
----------
2
(1 row)-- 单引号是字符串字面量,双引号是标识符引用,二者不可混用
SELECT 'hello' AS a_text_literal; a_text_literal
----------------
hello
(1 row)结果集的解读
SELECT 的输出包含三部分:列头、数据行、行数统计。
SELECT
'PostgreSQL' AS product,
18 AS major_version,
true AS is_open_source; product | major_version | is_open_source
------------+---------------+----------------
PostgreSQL | 18 | t
(1 row)三点需要留意:
- 列名默认取表达式文本。
SELECT 1 + 1的列名是?column?,因为表达式没有天然名字;这也是建议写AS别名的主要原因。 - 布尔值显示为
t/f,而不是true/false。这是 psql 的默认显示风格,值本身仍是布尔类型。 (1 row)是客户端统计信息,不是服务端返回的数据。
NULL 的初步认识
NULL 表示未知,任何与 NULL 的比较结果都不是真或假,而是 NULL(未知)。
-- NULL 参与运算,结果仍是 NULL
SELECT
NULL + 1 AS null_plus_one,
NULL = NULL AS null_eq_null,
NULL IS NULL AS null_is_null; null_plus_one | null_eq_null | null_is_null
---------------+--------------+--------------
| | t
(1 row)为什么 NULL = NULL 不是 t?因为"未知值等于未知值"本身也是未知的。要判断是否为 NULL,必须用 IS NULL / IS NOT NULL,不能用 =。此外,字符串拼接时 NULL 会"传染":'a' || NULL 结果是 NULL 而不是 'a'。
生成测试数据
入门阶段手边可能没有数据表,generate_series() 可以在不建表的情况下生成序列,非常适合练习:
-- 生成 1 到 5 的序列
SELECT generate_series(1, 5) AS n; n
---
1
2
3
4
5
(5 rows)-- 带步长与行数统计
SELECT
generate_series(1, 10, 3) AS step_3,
generate_series(1, 10) AS natural; step_3 | natural
--------+---------
1 | 1
4 | 2
7 | 3
10 | 4
| 5
| 6
| 7
| 8
| 9
| 10
(10 rows)注意上面这个例子的现象:两个集合函数长度不同时,短的一方自动用 NULL 补齐,总行数取较长者。这属于容易踩的坑,实际写查询时应避免在同一层 SELECT 里放多个长度未知的集合函数。
实战示例
场景:一个电商系统需要核算某天的订单金额。为便于直接运行,这里先用临时表构造数据,再执行第一条"真正的"查询。
第 1 步:准备一张临时表
-- 临时表只在当前会话可见,会话结束自动删除,适合做练习
CREATE TEMP TABLE order_demo (
order_no TEXT NOT NULL,
sku TEXT NOT NULL,
qty INTEGER NOT NULL CHECK (qty > 0),
unit_price NUMERIC(10, 2) NOT NULL CHECK (unit_price >= 0),
is_paid BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO order_demo (order_no, sku, qty, unit_price, is_paid) VALUES
('SO-20260921-001', 'SKU-1001', 2, 299.00, true),
('SO-20260921-001', 'SKU-1004', 1, 899.00, true),
('SO-20260921-002', 'SKU-1002', 3, 59.90, false),
('SO-20260921-003', 'SKU-1003', 1, 129.00, true);CREATE TABLE
INSERT 0 4第 2 步:确认表结构
demo=# \d order_demo
Table "pg_temp_3.order_demo"
Column | Type | Collation | Nullable | Default
------------+--------------------------+-----------+----------+---------
order_no | text | | not null |
sku | text | | not null |
qty | integer | | not null |
unit_price | numeric(10,2) | | not null |
is_paid | boolean | | not null | false
created_at | timestamp with time zone | | not null | now()
Check constraints:
"order_demo_qty_check" CHECK (qty > 0)
"order_demo_unit_price_check" CHECK (unit_price >= 0)第 3 步:运行第一条查询
SELECT order_no, sku, qty, unit_price
FROM order_demo
ORDER BY order_no, sku; order_no | sku | qty | unit_price
-------------------+----------+-----+------------
SO-20260921-001 | SKU-1001 | 2 | 299.00
SO-20260921-001 | SKU-1004 | 1 | 899.00
SO-20260921-002 | SKU-1002 | 3 | 59.90
SO-20260921-003 | SKU-1003 | 1 | 129.00
(4 rows)第 4 步:在查询里做计算——每个明细行的小计
SELECT
order_no,
sku,
qty,
unit_price,
qty * unit_price AS line_total -- 表达式列,不存储在表中
FROM order_demo
ORDER BY line_total DESC; order_no | sku | qty | unit_price | line_total
-------------------+----------+-----+------------+------------
SO-20260921-001 | SKU-1004 | 1 | 899.00 | 899.00
SO-20260921-001 | SKU-1001 | 2 | 299.00 | 598.00
SO-20260921-002 | SKU-1002 | 3 | 59.90 | 179.70
SO-20260921-003 | SKU-1003 | 1 | 129.00 | 129.00
(4 rows)第 5 步:只取前 2 行(Top-N)
SELECT order_no, sku, qty * unit_price AS line_total
FROM order_demo
ORDER BY line_total DESC
LIMIT 2; order_no | sku | line_total
-------------------+----------+------------
SO-20260921-001 | SKU-1004 | 899.00
SO-20260921-001 | SKU-1001 | 598.00
(2 rows)第 6 步:用单个 SELECT 把多条常量组织成"虚拟表"
-- VALUES 可以当作临时数据源使用,适合做快速验证
SELECT t.sku, t.price
FROM (VALUES
('SKU-1001', 299.00),
('SKU-1002', 59.90),
('SKU-1003', 129.00)
) AS t (sku, price)
ORDER BY t.price DESC; sku | price
----------+--------
SKU-1001 | 299.00
SKU-1003 | 129.00
SKU-1002 | 59.90
(3 rows)第 7 步:查看执行耗时,建立性能感知
demo=# \timing
Timing is on.
demo=# SELECT count(*) FROM order_demo;
count
-------
4
(1 row)
Time: 0.412 ms注意事项
- 忘记分号不会报错,但语句不会执行。提示符从
demo=#变成demo-#就是信号,补上分号即可;如果写错了想放弃,输入\r清空缓冲区。 - 不要用双引号写字符串。这是从 MySQL 迁移过来最常犯的错误。
-- 错误!PostgreSQL 中双引号是标识符引用,这里会报 column "hello" does not exist
-- SELECT "hello";
-- 正确:字符串用单引号
SELECT 'hello' AS greeting;- 不要把
NULL当成 0 或空字符串。三者语义完全不同,混用会导致统计结果错误。
-- 错误!count(*) 会把 NULL 行也算进去
SELECT count(*) FROM order_demo WHERE is_paid = NULL; -- 恒返回 0 行
-- 正确:判断 NULL 必须用 IS NULL / IS NOT NULL
SELECT count(*) FROM order_demo WHERE is_paid IS NOT NULL;- 表达式结果列一定显式起别名。否则列名是
?column?,程序取值时无法按名访问,只能按下标,非常脆弱。 - 不要依赖
SELECT *。字段顺序变化、新增列都会影响调用方,生产代码应显式列出所需列。 generate_series等多集合函数不要混用。像上文例子里长度不同会自动补齐NULL,容易产生错误数据。需要多序列时应分别查询或用JOIN关联。- 布尔值显示为
t/f是客户端行为。在应用代码里通过驱动拿到的是真正的布尔值,不要按字符串t/f解析。 - 查询不写
ORDER BY时顺序未定义。PostgreSQL 不会保证"按物理存储顺序"返回,需要稳定顺序就必须显式排序。
最佳实践
- 学习阶段坚持使用 psql。它能显示真实的类型、真实的执行时间与完整的错误信息,而不是被 GUI 包装过的结果。
- 每条语句都显式列出列名,不用
SELECT *,尤其是会被程序调用的 SQL。 - 表达式与计算列一律加
AS别名,让结果集自解释。 - 开启
\timing观察耗时,从第一天就建立对查询成本的直觉。 - 把常用元命令练熟(
\d、\dt、\x、\c、\q),它们能显著减少调试时间。 - 练习脚本写成
.sql文件并用\i执行,便于反复重跑与版本管理,而不是手工粘贴。 - 遇到
NULL相关结果异常时,先用IS NULL单独验证,再决定业务逻辑,不要凭直觉假设。 - 用
CREATE TEMP TABLE做实验。它不会污染正式库,会话结束自动清理,是安全的练手方式。
下一节
继续学习:数据库与模式的基本操作