SELECT 基础
SELECT 是 SQL 中最核心的语句,负责从表中读取数据。本节讲解 SELECT 的完整语法结构、各子句的执行顺序、选择列与列别名、DISTINCT 初步、表达式计算,以及 SELECT 无 FROM 与 FROM 子查询等基础用法。学完本节,你将能写出结构清晰、语义正确的查询骨架。
前置知识
阅读本节前,建议先了解:RETURNING 子句
基础概念
SELECT 语句由若干子句(Clause) 组成,每个子句负责一个阶段的处理。理解各子句的逻辑执行顺序是写好查询的前提——它不同于书写顺序,也是解释绝大多数报错的钥匙。
| 术语 | 英文 | 说明 |
|---|---|---|
| 子句 | Clause | SELECT 语句的组成部分,如 FROM、WHERE |
| 投影 | Projection | 决定输出哪些列,即 SELECT 列表 |
| 别名 | Alias | 用 AS 给列或表起的临时名字 |
| 表达式 | Expression | 由列、常量、运算符、函数组成的可求值式子 |
| 关系 | Relation | 查询的数据来源,表、视图、子查询都算 |
| 派生表 | Derived Table | 出现在 FROM 中的子查询 |
| 逻辑执行顺序 | Logical Order | 各子句在语义上的求值次序 |
各子句的逻辑执行顺序如下(注意与书写顺序不同):
FROM / JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT
→ ORDER BY → LIMIT / OFFSET这条顺序决定了两条最重要的规则:SELECT 中定义的列别名只在 ORDER BY 阶段之后可用,因此 WHERE 里不能引用别名;而 ORDER BY 在 SELECT 之后执行,所以可以使用别名。
| 子句 | 执行阶段 | 作用 |
|---|---|---|
FROM | 1 | 确定数据来源,生成行集合 |
WHERE | 2 | 按条件过滤行 |
GROUP BY | 3 | 把行划分成组 |
HAVING | 4 | 过滤分组结果 |
SELECT | 5 | 计算输出列与别名 |
DISTINCT | 6 | 去除重复行 |
ORDER BY | 7 | 排序 |
LIMIT / OFFSET | 8 | 截取行 |
语法与用法
SELECT 的完整骨架:
SELECT [ALL | DISTINCT [ON (表达式 [, ...])]]
[* | 表达式 [AS 别名] [, ...]]
[FROM 关系 [AS 别名] [, ...]]
[WHERE 条件表达式]
[GROUP BY 分组表达式 [, ...]]
[HAVING 组过滤条件]
[ORDER BY 排序表达式 [ASC | DESC] [NULLS FIRST | NULLS LAST] [, ...]]
[LIMIT {行数 | ALL}]
[OFFSET 偏移量 [ROW | ROWS]] ;准备本节贯穿使用的示例库与表:
-- 示例库统一为 demo
CREATE DATABASE demo;
-- 用户表:含可空字段,便于演示 NULL 与别名
CREATE TABLE users (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
email TEXT,
city TEXT,
age INTEGER CHECK (age >= 0),
balance NUMERIC(12, 2) NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO users (username, email, city, age, balance, is_active) VALUES
('alice', 'alice@example.com', '北京', 28, 1200.00, true),
('bob', 'bob@example.com', '上海', 35, 300.50, true),
('carol', NULL, '广州', 42, 0.00, false),
('dave', 'dave@example.com', '北京', 24, 850.00, true),
('eve', NULL, '深圳', 31, 420.75, true),
('frank', 'frank@example.com', NULL, 55, 9800.00, false);最简查询只写 SELECT 与 FROM:
-- 查询所有列(不推荐用于生产,见「注意事项」)
SELECT * FROM users;
-- 选择指定列
SELECT id, username, city FROM users;
-- 选择全部列并显式列出
SELECT id, username, email, city, age, balance, is_active, created_at FROM users;列别名用 AS(可省略,但不推荐省略):
-- 用 AS 起别名(推荐)
SELECT username AS 用户名, balance AS 余额 FROM users;
-- 省略 AS 的等价写法(不推荐,可读性差)
SELECT username 用户名, balance 余额 FROM users;
-- 别名含空格或大小写时用双引号
SELECT username AS "user name", city AS "所在城市" FROM users;详细说明
选择列与列别名
SELECT 列表中的每一项都是一个输出表达式,可以是列引用、常量、函数调用或它们的组合。别名只在输出结果中生效,不影响原表。
-- 输出列可以是任意表达式
SELECT
username,
balance,
balance * 1.06 AS balance_with_tax, -- 算术表达式
upper(username) AS username_upper, -- 函数调用
'active' AS status_tag -- 常量列
FROM users; username | balance | balance_with_tax | username_upper | status_tag
----------+----------+------------------+----------------+------------
alice | 1200.00 | 1272.00 | ALICE | active
bob | 300.50 | 318.53 | BOB | active
carol | 0.00 | 0.00 | CAROL | active
(3 rows)别名在 WHERE 中不可用、在 ORDER BY 中可用,这是逻辑执行顺序的直接结果:
-- 正确:ORDER BY 在 SELECT 之后执行,可以使用别名
SELECT username, balance * 2 AS double_balance
FROM users
ORDER BY double_balance DESC;
-- 错误!WHERE 在 SELECT 之前执行,此时别名尚不存在
SELECT username, balance * 2 AS double_balance
FROM users
WHERE double_balance > 1000;
-- 报错:column "double_balance" does not exist
-- 正确:重复表达式(优化器会识别为同一表达式)
SELECT username, balance * 2 AS double_balance
FROM users
WHERE balance * 2 > 1000;
-- 正确:用子查询让别名在上一层可被引用
SELECT username, double_balance
FROM (SELECT username, balance * 2 AS double_balance FROM users) t
WHERE double_balance > 1000;ORDER BY 中别名的解析优先级有细微规则:若别名与输入列同名,PostgreSQL 优先按输入列解析,行为可能不符合直觉:
-- 危险:别名 double_balance 不与任何列重名,此处无歧义
SELECT username, balance AS amount FROM users ORDER BY amount DESC;
-- 危险:别名与输入列同名时,ORDER BY 优先按输入列解析
SELECT username, balance * 2 AS balance FROM users ORDER BY balance DESC;
-- 实际按 users.balance 排序,而非 balance * 2DISTINCT 初步
DISTINCT 作用于整个输出行,去除完全重复的行,在逻辑执行顺序上位于 SELECT 之后:
-- 去重单列:列出所有出现过的城市
SELECT DISTINCT city FROM users;
-- 多列去重:组合唯一
SELECT DISTINCT city, is_active FROM users ORDER BY city, is_active;
-- DISTINCT 作用于整行,不是第一列
SELECT DISTINCT city, age FROM users; -- (北京, 28) 与 (北京, 24) 都保留 city
--------
北京
上海
广州
深圳
(null)
(5 rows)注意 DISTINCT 会把所有 NULL 视为同一组,因此 NULL 只出现一次。DISTINCT 内部需要通过排序或哈希去重,大表上代价不低,详见去重(DISTINCT)。
算术与字符串表达式
SELECT 列表中可以自由使用运算符与函数,无需 FROM 也能计算:
-- 算术运算:+ - * / %
SELECT 10 + 3 AS add_r, 10 - 3 AS sub_r, 10 * 3 AS mul_r, 10 / 3 AS int_div, 10 % 3 AS mod_r;
-- 整数除法会截断;要得到小数需至少一侧为小数类型
SELECT 10 / 3 AS trunc_div, 10 / 3.0 AS real_div, round(10::numeric / 3, 4) AS exact_div;
-- 字符串拼接用 ||,任一操作数为 NULL 则结果整体为 NULL
SELECT 'Post' || 'greSQL' AS concat_result;
-- concat() 函数会忽略 NULL,更安全
SELECT username, concat(city, '-', username) AS label FROM users LIMIT 3; add_r | sub_r | mul_r | int_div | mod_r
-------+-------+-------+---------+-------
13 | 7 | 30 | 3 | 1
(1 row)
trunc_div | real_div | exact_div
-----------+----------+-----------
3 | 3.333333 | 3.3333
(1 row)SELECT 无 FROM
SELECT 可以不带 FROM,此时它是一个只返回一行的"计算器",常用于验证表达式、函数行为或获取系统信息:
-- 计算表达式
SELECT 1 + 1 AS result;
-- 查看当前时间、当前用户、版本
SELECT now() AS now_time, current_user AS who, version() AS pg_version;
-- 生成序列:从 1 到 5
SELECT generate_series(1, 5) AS n;
-- 组合多个表达式做快速验证
SELECT
true AND NULL AS a, -- NULL
false OR NULL AS b, -- NULL
1 = NULL AS c, -- NULL
NULL IS NULL AS d; -- true result
--------
2
(1 row)FROM 子查询与别名
FROM 中的子查询称为派生表(Derived Table),必须带别名(PostgreSQL 允许省略 AS 关键字,但别名本身不能省):
-- 派生表:先算人均余额,再筛选
SELECT city, avg_balance
FROM (
SELECT city, round(avg(balance), 2) AS avg_balance
FROM users
WHERE city IS NOT NULL
GROUP BY city
) AS city_stats
WHERE avg_balance > 500
ORDER BY avg_balance DESC; city | avg_balance
-------+-------------
北京 | 1025.00
深圳 | 420.75
上海 | 300.50
(3 rows)-- 错误!PostgreSQL 中派生表必须有别名
SELECT * FROM (SELECT 1 AS a);
-- 正确
SELECT * FROM (SELECT 1 AS a) AS t;
-- 子查询里也可以设置列别名
SELECT * FROM (SELECT 1, 2) AS t(x, y);FROM 也可以列出多个关系,产生笛卡尔积(交叉连接):
-- 笛卡尔积:2 行 × 3 行 = 6 行,通常不是想要的结果
SELECT u.username, c.name
FROM (VALUES ('alice'), ('bob')) AS u(username)
CROSS JOIN (VALUES ('A'), ('B'), ('C')) AS c(name);SELECT * 的弊端值得单独强调:
| 问题 | 说明 |
|---|---|
| 网络与内存开销 | 传输全部列,宽表上浪费大量带宽 |
| 破坏"覆盖索引" | 无法走 Index Only Scan,必须回表取其他列 |
| 破坏程序稳定性 | 表结构变更后,按位置取值的代码会错位 |
| 语义不清晰 | 读者无法从语句看出到底用到了哪些列 |
| 列顺序不确定 | 依赖建表顺序,ALTER TABLE 后可能变化 |
-- 不推荐:宽表上取全部列
SELECT * FROM users;
-- 推荐:显式列出所需列
SELECT id, username, city FROM users;实战示例
下面构建一个"用户分层与余额报表"场景,串联 SELECT 的各类用法。
-- 步骤 1:基础投影 + 别名 + 表达式
SELECT
u.id,
u.username,
upper(u.username) AS username_upper,
u.city,
u.balance,
u.balance * 1.06 AS balance_with_tax,
u.balance >= 1000 AS is_vip
FROM users u
ORDER BY u.balance DESC; id | username | username_upper | city | balance | balance_with_tax | is_vip
----+----------+----------------+------+----------+------------------+--------
6 | frank | FRANK | | 9800.00 | 10388.00 | t
1 | alice | ALICE | 北京 | 1200.00 | 1272.00 | t
4 | dave | DAVE | 北京 | 850.00 | 901.00 | f
5 | eve | EVE | 深圳 | 420.75 | 445.99 | f
2 | bob | BOB | 上海 | 300.50 | 318.53 | f
3 | carol | CAROL | 广州 | 0.00 | 0.00 | f
(6 rows)-- 步骤 2:城市分布去重统计
SELECT DISTINCT city FROM users ORDER BY city NULLS LAST; city
--------
上海
北京
广州
深圳
(null)
(5 rows)-- 步骤 3:派生表 + 外层过滤,做"城市余额汇总"报表
SELECT
city,
user_cnt,
total_balance,
round(total_balance / user_cnt, 2) AS avg_balance
FROM (
SELECT
coalesce(city, '未知') AS city,
count(*) AS user_cnt,
sum(balance) AS total_balance
FROM users
GROUP BY coalesce(city, '未知')
) AS city_stats
WHERE total_balance > 0
ORDER BY total_balance DESC; city | user_cnt | total_balance | avg_balance
-------+----------+---------------+-------------
未知 | 1 | 9800.00 | 9800.00
北京 | 2 | 2050.00 | 1025.00
深圳 | 1 | 420.75 | 420.75
上海 | 1 | 300.50 | 300.50
(4 rows)-- 步骤 4:SELECT 无 FROM 做环境自检
SELECT
current_database() AS db,
current_user AS usr,
current_setting('server_version') AS pg_version,
now() AS server_time; db | usr | pg_version | server_time
--------+--------+------------+-------------------------------
demo | ch | 18.0 | 2026-09-24 10:12:33.401+08
(1 row)-- 步骤 5:用 SELECT 检查表达式行为(无需建表,快速验证)
SELECT
10 / 3 AS int_div,
10::numeric / 3 AS num_div,
'a' || NULL AS concat_null,
concat('a', NULL) AS concat_safe,
NULL = NULL AS eq_null,
NULL IS NULL AS is_null; int_div | num_div | concat_null | concat_safe | eq_null | is_null
---------+--------------------+-------------+-------------+---------+---------
3 | 3.3333333333333333 | | a | | t
(1 row)注意事项
SELECT *在生产代码中应避免。字段增删会直接影响应用层按位置取值的结果,且会破坏覆盖索引优化。WHERE中不能使用SELECT别名,但ORDER BY、GROUP BY、HAVING可以(GROUP BY中别名与列名冲突时优先按列名解析)。
-- 错误!别名在 WHERE 阶段尚未计算
SELECT balance * 2 AS dbl FROM users WHERE dbl > 1000;
-- 正确
SELECT balance * 2 AS dbl FROM users WHERE balance * 2 > 1000;- 无
AS的别名写法容易产生歧义。SELECT a b到底是"列a别名为b"还是"漏写逗号"。始终显式写AS:
-- 不推荐(省略 AS)
SELECT username city FROM users; -- 实际是 username AS city
-- 推荐
SELECT username AS city FROM users;DISTINCT作用于整行,不是仅第一列;SELECT DISTINCT city, age与SELECT DISTINCT city结果完全不同。DISTINCT与ORDER BY组合时,排序表达式必须出现在SELECT列表中(DISTINCT先于ORDER BY执行):
-- 错误!ORDER BY 的表达式不在 SELECT 列表中
SELECT DISTINCT city FROM users ORDER BY id;
-- 报错:for SELECT DISTINCT, ORDER BY expressions must appear in select list
-- 正确
SELECT DISTINCT city FROM users ORDER BY city;- 整数除法会截断小数。
10 / 3得到3,需要小数结果时必须显式转换类型或使用numeric常量。
-- 错误认知:以为会得到 3.33
SELECT 10 / 3;
-- 正确:任一侧为小数类型
SELECT 10 / 3.0; -- 3.3333333333333333
SELECT round(10::numeric / 3, 2); -- 3.33||拼接遇NULL结果整体为NULL。需要容忍NULL时使用concat()或coalesce()。- 派生表必须有别名(PostgreSQL 与部分数据库不同,后者可能允许省略):
-- 错误!
SELECT * FROM (SELECT 1 AS a);- 列别名不会自动转小写之外的规范化。未加双引号时,PostgreSQL 会把别名折叠为小写;需要保留大小写或空格必须用双引号。
-- 别名被折叠为小写
SELECT 1 AS MyColumn; -- 输出列名是 mycolumn
-- 保留原样
SELECT 1 AS "MyColumn"; -- 输出列名是 MyColumn最佳实践
- 始终显式列出所需列,不用
SELECT *。除了可读性,这直接决定了查询能否走仅索引扫描(Index Only Scan)。
-- 建覆盖索引后,显式列查询可走仅索引扫描
CREATE INDEX idx_users_city_username ON users (city) INCLUDE (username);
SELECT city, username FROM users WHERE city = '北京'; -- Index Only Scan- 所有别名一律显式写
AS,避免SELECT a b这类歧义,也便于代码审查。 - 别名语义化,让结果集自带业务含义:
balance * 1.06 AS balance_with_tax优于balance * 1.06 AS col1。 - 复杂逻辑分层书写。派生表或 CTE 让每一层语义独立可验证,比一层嵌套到底的表达式更易维护。
-- 分层:原始行 → 用户级指标 → 城市级报表
WITH user_metrics AS (
SELECT
coalesce(city, '未知') AS city,
username,
balance,
balance * 1.06 AS balance_with_tax
FROM users
WHERE is_active
),
city_report AS (
SELECT city, count(*) AS user_cnt, sum(balance_with_tax) AS total_taxed
FROM user_metrics
GROUP BY city
)
SELECT * FROM city_report ORDER BY total_taxed DESC;- 用
ORDER BY别名做报表排序,语义比重复表达式更清晰,也避免表达式写错。
-- 推荐:ORDER BY 用别名
SELECT city, sum(balance) AS total_balance
FROM users
GROUP BY city
ORDER BY total_balance DESC;- 表达式计算可先用
SELECT无FROM验证。NULL传播、类型转换、除零行为等都可零成本试出,再写入正式语句。 - 时区与时间函数优先返回带时区类型(
timestamptz),避免跨时区报表口径不一致。
下一节
继续学习:WHERE 条件筛选