Skip to content

SELECT 基础 ​

SELECT 是 SQL 中最核心的语句,负责从表中读取数据。本节讲解 SELECT 的完整语法结构、各子句的执行顺序、选择列与列别名、DISTINCT 初步、表达式计算,以及 SELECT 无 FROM 与 FROM 子查询等基础用法。学完本节,你将能写出结构清晰、语义正确的查询骨架。

前置知识

阅读本节前,建议先了解:RETURNING 子句

基础概念 ​

SELECT 语句由若干子句(Clause) 组成,每个子句负责一个阶段的处理。理解各子句的逻辑执行顺序是写好查询的前提——它不同于书写顺序,也是解释绝大多数报错的钥匙。

术语英文说明
子句ClauseSELECT 语句的组成部分,如 FROM、WHERE
投影Projection决定输出哪些列,即 SELECT 列表
别名Alias用 AS 给列或表起的临时名字
表达式Expression由列、常量、运算符、函数组成的可求值式子
关系Relation查询的数据来源,表、视图、子查询都算
派生表Derived Table出现在 FROM 中的子查询
逻辑执行顺序Logical Order各子句在语义上的求值次序

各子句的逻辑执行顺序如下(注意与书写顺序不同):

text
FROM / JOIN   →   WHERE   →   GROUP BY   →   HAVING   →   SELECT   →   DISTINCT
              →   ORDER BY   →   LIMIT / OFFSET

这条顺序决定了两条最重要的规则:SELECT 中定义的列别名只在 ORDER BY 阶段之后可用,因此 WHERE 里不能引用别名;而 ORDER BY 在 SELECT 之后执行,所以可以使用别名。

子句执行阶段作用
FROM1确定数据来源,生成行集合
WHERE2按条件过滤行
GROUP BY3把行划分成组
HAVING4过滤分组结果
SELECT5计算输出列与别名
DISTINCT6去除重复行
ORDER BY7排序
LIMIT / OFFSET8截取行

语法与用法 ​

SELECT 的完整骨架:

text
SELECT [ALL | DISTINCT [ON (表达式 [, ...])]]
       [* | 表达式 [AS 别名] [, ...]]
[FROM 关系 [AS 别名] [, ...]]
[WHERE 条件表达式]
[GROUP BY 分组表达式 [, ...]]
[HAVING 组过滤条件]
[ORDER BY 排序表达式 [ASC | DESC] [NULLS FIRST | NULLS LAST] [, ...]]
[LIMIT {行数 | ALL}]
[OFFSET 偏移量 [ROW | ROWS]] ;

准备本节贯穿使用的示例库与表:

sql
-- 示例库统一为 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:

sql
-- 查询所有列(不推荐用于生产,见「注意事项」)
SELECT * FROM users;

-- 选择指定列
SELECT id, username, city FROM users;

-- 选择全部列并显式列出
SELECT id, username, email, city, age, balance, is_active, created_at FROM users;

列别名用 AS(可省略,但不推荐省略):

sql
-- 用 AS 起别名(推荐)
SELECT username AS 用户名, balance AS 余额 FROM users;

-- 省略 AS 的等价写法(不推荐,可读性差)
SELECT username 用户名, balance 余额 FROM users;

-- 别名含空格或大小写时用双引号
SELECT username AS "user name", city AS "所在城市" FROM users;

详细说明 ​

选择列与列别名 ​

SELECT 列表中的每一项都是一个输出表达式,可以是列引用、常量、函数调用或它们的组合。别名只在输出结果中生效,不影响原表。

sql
-- 输出列可以是任意表达式
SELECT
  username,
  balance,
  balance * 1.06            AS balance_with_tax,   -- 算术表达式
  upper(username)           AS username_upper,     -- 函数调用
  'active'                  AS status_tag          -- 常量列
FROM users;
text
 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 中可用,这是逻辑执行顺序的直接结果:

sql
-- 正确: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 优先按输入列解析,行为可能不符合直觉:

sql
-- 危险:别名 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 * 2

DISTINCT 初步 ​

DISTINCT 作用于整个输出行,去除完全重复的行,在逻辑执行顺序上位于 SELECT 之后:

sql
-- 去重单列:列出所有出现过的城市
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) 都保留
text
  city
--------
 北京
 上海
 广州
 深圳
 (null)
(5 rows)

注意 DISTINCT 会把所有 NULL 视为同一组,因此 NULL 只出现一次。DISTINCT 内部需要通过排序或哈希去重,大表上代价不低,详见去重(DISTINCT)。

算术与字符串表达式 ​

SELECT 列表中可以自由使用运算符与函数,无需 FROM 也能计算:

sql
-- 算术运算:+ - * / %
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;
text
 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,此时它是一个只返回一行的"计算器",常用于验证表达式、函数行为或获取系统信息:

sql
-- 计算表达式
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
text
 result
--------
      2
(1 row)

FROM 子查询与别名 ​

FROM 中的子查询称为派生表(Derived Table),必须带别名(PostgreSQL 允许省略 AS 关键字,但别名本身不能省):

sql
-- 派生表:先算人均余额,再筛选
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;
text
  city | avg_balance
-------+-------------
 北京  |      1025.00
 深圳  |       420.75
 上海  |       300.50
(3 rows)
sql
-- 错误!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 也可以列出多个关系,产生笛卡尔积(交叉连接):

sql
-- 笛卡尔积: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 后可能变化
sql
-- 不推荐:宽表上取全部列
SELECT * FROM users;

-- 推荐:显式列出所需列
SELECT id, username, city FROM users;

实战示例 ​

下面构建一个"用户分层与余额报表"场景,串联 SELECT 的各类用法。

sql
-- 步骤 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;
text
 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)
sql
-- 步骤 2:城市分布去重统计
SELECT DISTINCT city FROM users ORDER BY city NULLS LAST;
text
  city
--------
 上海
 北京
 广州
 深圳
 (null)
(5 rows)
sql
-- 步骤 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;
text
  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)
sql
-- 步骤 4:SELECT 无 FROM 做环境自检
SELECT
  current_database() AS db,
  current_user       AS usr,
  current_setting('server_version') AS pg_version,
  now()              AS server_time;
text
    db  |  usr   | pg_version |         server_time
--------+--------+------------+-------------------------------
 demo   | ch     | 18.0       | 2026-09-24 10:12:33.401+08
(1 row)
sql
-- 步骤 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;
text
 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 中别名与列名冲突时优先按列名解析)。
sql
-- 错误!别名在 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:
sql
-- 不推荐(省略 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 执行):
sql
-- 错误!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 常量。
sql
-- 错误认知:以为会得到 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 与部分数据库不同,后者可能允许省略):
sql
-- 错误!
SELECT * FROM (SELECT 1 AS a);
  • 列别名不会自动转小写之外的规范化。未加双引号时,PostgreSQL 会把别名折叠为小写;需要保留大小写或空格必须用双引号。
sql
-- 别名被折叠为小写
SELECT 1 AS MyColumn;      -- 输出列名是 mycolumn

-- 保留原样
SELECT 1 AS "MyColumn";    -- 输出列名是 MyColumn

最佳实践 ​

  1. 始终显式列出所需列,不用 SELECT *。除了可读性,这直接决定了查询能否走仅索引扫描(Index Only Scan)。
sql
-- 建覆盖索引后,显式列查询可走仅索引扫描
CREATE INDEX idx_users_city_username ON users (city) INCLUDE (username);

SELECT city, username FROM users WHERE city = '北京';   -- Index Only Scan
  1. 所有别名一律显式写 AS,避免 SELECT a b 这类歧义,也便于代码审查。
  2. 别名语义化,让结果集自带业务含义:balance * 1.06 AS balance_with_tax 优于 balance * 1.06 AS col1。
  3. 复杂逻辑分层书写。派生表或 CTE 让每一层语义独立可验证,比一层嵌套到底的表达式更易维护。
sql
-- 分层:原始行 → 用户级指标 → 城市级报表
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;
  1. 用 ORDER BY 别名做报表排序,语义比重复表达式更清晰,也避免表达式写错。
sql
-- 推荐:ORDER BY 用别名
SELECT city, sum(balance) AS total_balance
FROM users
GROUP BY city
ORDER BY total_balance DESC;
  1. 表达式计算可先用 SELECT 无 FROM 验证。NULL 传播、类型转换、除零行为等都可零成本试出,再写入正式语句。
  2. 时区与时间函数优先返回带时区类型(timestamptz),避免跨时区报表口径不一致。

下一节

继续学习:WHERE 条件筛选

参考链接 ​