☰
SQL统计查询核心:去重原理与AS更名运算的细节解析
2026/10/14 22:01:54 网站建设 项目流程

我整理 SQL 笔记时发现,统计查询中最容易被忽略、也最影响结果准确性的,就是去重原理和更名运算(AS 子句)。这两个语法单看都很简单,但一旦组合进统计查询,就藏着不少细节。这篇文章我想把“统计查询中的去重原理”和“更名运算(AS 子句)”讲透,从执行原理到实操场景,再到排查技巧,一次性说清楚。适合刚学 SQL 的初学者,也适合写了好几年查询但没仔细抠过语义的开发者。

1. 为什么统计查询里“去重”和“更名”总是绑在一起

1.1 先看一条典型的错误写法

我在给团队做内部培训时,经常用一个模拟订单表来演示。表结构大概是 user_id、user_name、order_id、product_name、amount、status、order_date 这些字段,数据量不大,但特意造了一些重复场景:同一个用户下了多单,同一个订单出现了重复行,还有少量空值。

假设业务需求是“统计平台有多少用户下过单”。很多人第一反应是写:

select count(user_id) from orders;

这条 SQL 能跑,但结果会虚高。因为同一个用户下了三单,他的 user_id 就出现在三行里,count(user_id) 会把他数三次。你需要的是“用户数”,而不是“订单行数”。

正确的写法是:

select count(distinct user_id) from orders;

这一步叫作“去重”。去完重之后,count 数的就是唯一的 user_id 集合,也就是真实的用户数。

但问题来了:这条 SQL 的结果列名是什么?在大多数数据库里,结果列名会直接显示成count(distinct user_id)。如果你只是自己临时看一眼,倒无所谓;但如果这个结果要交给下游报表,或者作为子查询被外层引用,这个列名就非常不友好。

这时候就需要“更名运算”,也就是 AS 子句登场:

select count(distinct user_id) as unique_user_count from orders;

输出列名就变成了unique_user_count,清晰明了。这就是为什么在实际工作中,去重和更名总是一起出现:去重负责“把数字算对”,更名负责“把结果说清楚”。

1.2 理解 SQL 的逻辑执行顺序,才能理解一切语法

很多人学 SQL 是先背语法,遇到复杂需求就懵。我建议换个思路:先理解 SQL 的逻辑执行顺序。虽然数据库引擎真正执行时会有优化和调整,但逻辑上的顺序是固定的:

  1. from:找到数据源
  2. where:过滤行
  3. group by:分组
  4. 聚合计算
  5. having:过滤分组
  6. select:挑选列
  7. distinct:去重
  8. order by:排序

这个顺序解释了很多规则。比如,为什么 where 里不能用聚合结果?因为 where 执行时数据还没有分组和聚合。为什么 distinct 在 select 之后作用?因为先确定要输出的列,再去重。为什么别名不能在 where 里引用?因为 select 阶段在 where 之后。

理解了执行顺序,你再去看 distinct 和 AS 的作用时机,一切就顺理成章了。

2. 核心细节解析与实操要点

2.1 去重原理:DISTINCT 到底在做什么

第一,DISTINCT 是对整行去重,不是对单个字段去重。

这是一个特别容易踩的坑。我以前做过一个访问日志统计,想查“哪些用户访问过”,于是写:

select distinct user_id from access_log;

这个没问题,因为 select 列表只有一个字段。但如果我写成:

select distinct user_id, page_url from access_log;

含义就完全变了。它不再只是用户粒度去重,而是“用户 + 访问页面”组合粒度的去重。同一个用户访问了三个不同页面,会产生三行。如果你期望只返回不重复的用户,那就写错了。

所以,当你写select distinct 列1, 列2时,一定要想清楚自己到底要对“什么粒度”去重。distinct 作用在 select 列表的所有列上,只有当这些列的组合完全相同时,才视为重复。

第二,DISTINCT 会把 NULL 合并成一个值。

在普通比较运算里,null 既不等于 null,也不不等于 null。但在 distinct 的语义中,null 值被视为相互“相等”,去重后所有 null 会合并成一个。

我曾在统计中对一个可空字段做 distinct,结果数量比我预期少,排查半天才发现是 null 被合并了。如果你要统计“非空且不同”的值,这没问题;但如果你希望 null 也按每行出现一次参与统计,那就需要用 coalesce 把 null 转成一个有意义的业务值,比如coalesce(remark, 'unknown'),再去做 distinct。

第三,DISTINCT 放在聚合函数内还是外,结果完全不同。

这是最反直觉的一点。看两个写法:

select count(distinct user_id) from orders;

和

select distinct count(user_id) from orders;

前者是“先对 user_id 去重,再计数”,结果是唯一用户数,这是我们要的。

后者是“先对 user_id 计数得到总行数(不去重),再对这个数字做 distinct”。因为 count(user_id) 的结果只有一个数值,distinct 之后还是一行,所以这条 SQL 结果基本没有意义,甚至不会报错,但语义完全错了。

你可能会觉得谁会写第二种,但我在实际代码 review 里真的见过。特别是从其他语言转来写 SQL 的开发,很容易把“distinct”理解为修饰字段的形容词,而不是作用于结果集的运算。

第四,DISTINCT 会触发排序吗?

在某些数据库里,distinct 的实现会伴随隐式排序,因为要去重就需要比较行。但这意味着数据量大时,distinct 的代价不低。MySQL 8.0 里 distinct 不一定排序,但整体性能开销依然需要评估。如果只是判断是否存在重复,用group by ... having count(*) > 1可能更灵活。

2.2 更名运算 AS 子句:不只是给列起别名

第一,AS 解决的是“引用问题”和“可读性问题”。

统计查询里,聚合函数生成的结果列名往往是一长串表达式,比如count(distinct user_id)、sum(case when status = 'paid' then amount else 0 end)。这种列名在结果集里非常难用。你写子查询时想where total > 100,但 total 这个名称根本不存在,除非你给表达式起了别名。

我用一个生活类比:你打电话给快递员说“我有一个包裹”,他没法找到你;但你说“我在 3 号楼 502 室”,他就能准确定位。AS 就是把count(distinct user_id)变成unique_user_count,让数据库、下游程序和人,都能通过这个明确的标识去引用它。

第二,AS 也能给表起别名。

在自连接或多表连接中,表别名几乎是必须的。比如订单表自连接查找“同一用户在两日内再次下单”的场景,你必须给同一张表起两个不同的别名,否则数据库分不清你引用的是哪一份。

子查询的临时表也需要别名。像这样:

select a.user_id, b.total_amount from users a inner join ( select user_id, sum(amount) as total_amount from orders group by user_id ) b on a.user_id = b.user_id;

这里 a、b 都是表别名,total_amount是列别名。没有这些别名,整条 SQL 会非常冗长,连接条件也不好写。

第三,AS 的“执行时机”很特殊。

SQL 的逻辑执行顺序里,select 阶段在 where 之后,所以你在 select 里定义的别名,不能在同一个 select 的 where 中使用。这是个新手高频问题。比如:

select user_id, sum(amount) as total from orders where total > 100 group by user_id;

在很多数据库里会直接报错,因为 where 执行时 total 还不存在。解决方式有两种:一是把过滤条件放在 having 里,二是包一层子查询。具体怎么写,后面实操章节会详细演示。

第四,别名尽量别用空格和保留字。

标准 SQL 允许别名包含空格,但需要用双引号括起来,比如as "user count"。但不同数据库对引号的处理不统一,很容易踩坑。我的建议是:别名只用小写字母、数字和下划线,不用空格、不用中文、不用保留字。这样能保证 SQL 在不同数据库之间迁移时不出问题。

2.3 统计查询中聚合函数与去重、更名的配合

统计查询里,最常见的组合就是count(distinct xxx)加as。但业务需求往往不止这么多,还有几个典型组合值得掌握。

组合一:group by + count(distinct 列) + as

比如统计每个地区的用户数:

select region, count(distinct user_id) as active_user_count from orders group by region;

这里 group by 按地区分组,count(distinct user_id) 保证同一用户在一个地区只计一次,as 把结果列命名清楚。三者各司其职,缺一不可。

组合二:sum + case when + as

统计某段时间内每用户的有效付费金额,只算已支付订单:

select user_id, sum(case when status = 'paid' then amount else 0 end) as paid_amount from orders group by user_id;

这个表达式本身是一个完整逻辑:case when 过滤出需要计入的订单,sum 求和,as 命名。所有人看到paid_amount都知道它的业务含义。

组合三:where 与 having 的选择

这里有一个关键点:where 在分组之前执行,having 在分组之后执行。所以“先过滤行再分组计算”的条件放 where,“基于聚合结果过滤分组”的条件放 having。

比如“统计每个用户的付费总额,但只统计状态为已支付的订单”,状态过滤针对原始行,放 where 里更高效,因为未支付订单根本不会进入分组计算。而“找出付费总额超过 100 的用户”,总额是聚合结果,必须放 having。

很多初学者分不清这两点,写出来的 SQL 要么结果不对,要么性能很差。记住执行顺序,就不会纠结了。

组合四:count(distinct 多字段)

有些数据库支持count(distinct 列1, 列2),比如 SQL Server 和 PostgreSQL,但 MySQL 不支持这种写法。如果你想统计“不重复的用户与商品组合数”,MySQL 里只能写成count(distinct concat(user_id, '-', product_name)),但这种方式有隐患,比如字段值里恰好包含分隔符,就会造成误合并。更稳妥的做法是先做子查询去重,再 count 外层的行。

3. 实操过程与核心环节实现

3.1 环境准备与测试数据构造

动手之前先准备一个简单的环境。我最推荐新手用 SQLite,它是单文件数据库,不需要安装服务,随手建一个 .db 文件就能跑,很适合验证 SQL 语义。如果你在公司用的是 MySQL 或 PostgreSQL,也没关系,后面涉及的语法都是标准 SQL,在各主流数据库里差异很小。

先建一张模拟订单表 orders:

create table orders ( order_id integer primary key, user_id integer, user_name text, product_name text, amount numeric, status text, order_date text );

插入一些测试数据,刻意制造重复和空值:

insert into orders values (1, 101, '张三', '键盘', 299, 'paid', '2025-03-01'), (2, 101, '张三', '鼠标', 129, 'paid', '2025-03-01'), (3, 101, '张三', '显示器', 899, 'pending', '2025-03-02'), (4, 102, '李四', '键盘', 299, 'paid', '2025-03-03'), (5, 102, '李四', '耳机', 199, 'paid', '2025-03-04'), (6, 103, '王五', '鼠标', 129, 'paid', '2025-03-05'), (7, 103, '王五', '键盘', 299, 'pending', '2025-03-06'), (8, null, null, '耳机', 199, 'paid', '2025-03-07');

这里有 8 行数据,但真实用户分别是 101、102、103,再加一个 user_id 为 null 的行。我们先跑两条查询做对照基准:

select count(*) from orders; select count(distinct user_id) from orders;

前者的结果是 8,后者的结果是 3(101、102、103)。这个差异就是 distinct 带来的语义差别。

3.2 场景一:统计唯一用户数,对比去重前后

第一种写法,统计 user_id 的非空行数:

select count(user_id) from orders;

结果是多少?user_id 为 null 的行不会计入,所以是 7。这不是用户数,而是“有 user_id 的订单行数”。张三有 3 个订单,被数了 3 次。

第二种写法,统计唯一用户数:

select count(distinct user_id) as unique_user_count from orders;

结果是 3。这就是业务上真正需要的“下过单的用户数”。

为了让结果更直观,可以在一条 SQL 里同时输出两个数:

select count(user_id) as raw_order_count, count(distinct user_id) as unique_user_count from orders;

这样两列并列,看着就非常清楚,字段名也说明了一切。这也是我常说的:统计查询的输出,列名就是口径说明的一部分。

3.3 场景二:按用户统计订单总额,并给结果列更名

第二个场景:按用户统计订单总额,只统计已支付订单,并把结果列命名为 total_paid_amount。

先写一个“能跑但不够好”的版本:

select user_id, sum(amount) from orders where status = 'paid' group by user_id;

这条 SQL 结果对,但输出列名是sum(amount),完全无法体现“已支付”这个业务口径。改进后:

select user_id, sum(amount) as total_paid_amount from orders where status = 'paid' group by user_id;

这里有几个关键点。第一,where status = 'paid' 在分组之前执行,未支付订单根本不会进入 sum 计算,这比先分组再过滤更高效。第二,group by user_id 让每个用户单独成组,sum 对组内 amount 求和。第三,as total_paid_amount 让结果列名变成有业务含义的名称。

如果把需求再扩展一下:还想看每个用户有几笔有效订单,涉及多少种不同商品。可以写成:

select user_id, count(*) as paid_order_count, count(distinct product_name) as distinct_product_count, sum(amount) as total_paid_amount from orders where status = 'paid' group by user_id;

这条 SQL 可以说是统计查询的典型模板:where 做行过滤,group by 做维度分组,聚合函数做计算,distinct 控制在聚合内部的去重粒度,as 给每个输出列赋予业务含义。如果你能把这四层逻辑讲清楚,统计查询基本就掌握了七成。

3.4 场景三:利用 AS 重命名子查询表,完成个性化筛选

第三个场景:找出付费总额大于 100 的用户。这个需求不能在同一层 where 中完成,因为总额是聚合结果。有两种写法。

第一种,用 having:

select user_id, sum(amount) as total_paid_amount from orders where status = 'paid' group by user_id having sum(amount) > 100;

这条 SQL 简洁直接,having 在分组后对聚合结果过滤,条件为真才保留该分组。

第二种,用子查询加 AS:

select user_id, total_paid_amount from ( select user_id, sum(amount) as total_paid_amount from orders where status = 'paid' group by user_id ) t where total_paid_amount > 100;

这里有几个细节值得说。

第一,内层子查询的聚合列用了as total_paid_amount,外层 where 才能按这个名称引用。如果你不写别名,外层根本没有办法引用这一个聚合结果。想写where sum(amount) > 100是语法错误的,因为外层看不到叫 sum(amount) 的列。

第二,我给子查询结果集起了表别名 t。这几乎是必须的,因为子查询本身没有名字,外层 from 引用它时需要一个标识。

第三,这条 SQL 展示了 AS 和 distinct 在统计查询中配合工作:内层负责去重和聚合,外层负责基于聚合结果做二次筛选。这是“先算中间结果,再加工”的经典模式。

哪种写得更推荐?如果只是一个简单的聚合后过滤,having 是最好的选择,简洁、高效。但如果你需要复用中间结果,或者要继续嵌套更多查询,子查询 + AS 的方式更灵活。两种写法都要掌握,因为它们解决的是不同层级的复杂性问题。

3.5 常见 SQL 写法对照:加不加 AS、加不加 DISTINCT 的结果差异

我把前面几个关键写法和它们对应的含义整理成一张表:

SQL 写法执行含义结果特征
select count(user_id) from orders统计 user_id 非空行数可能重复计算同一用户
select count(distinct user_id) from orders先对 user_id 去重再计数得到唯一用户数
select count(user_id) from orders group by user_id每用户一个分组,统计组内行数每个用户对应的订单数
select distinct user_id from orders列出唯一用户集合每行一个用户
select user_id, sum(amount) from orders group by user_id每用户订单总额列名是 sum(amount),不够友好
select user_id, sum(amount) as total from orders group by user_id每用户订单总额,列名是 total下游可引用 total
select distinct count(user_id) from orders先统计行数再去重结果基本只有一行,无意义
select count(distinct user_id) from orders先去重再统计结果是唯一用户数

每一条写法之间的差异并不是“文字游戏”,而是实实在在的语义差异。我在培训时经常让学员对着这张表,一条一条说出执行过程和输出结果,能说清楚,才算真的理解了。

4. 常见问题与排查技巧实录

4.1 DISTINCT 去重后数量比预期少,怀疑 NULL 被合并

有次一个同事跑数,用count(distinct remark)统计备注字段的唯一值数量,结果比手工数的还少。我让他先查一下数据里有没有 null,一查,确实有十几行 remark 是 null。

原因就是前面说的:distinct 会把所有 null 视为同一个值,因此不管你有多少 null,去重后只有一个 null 的“位置”,count 只把它计为 1。如果你的业务口径是“非空且不同的值”,那这个行为没问题;如果希望 null 也按某种方式统计,就需要先转换,比如count(distinct coalesce(remark, 'unknown'))。

排查这类问题,我会先跑一条侦察 SQL:

select remark, count(*) from orders group by remark order by count(*) desc;

查看空值分布后,再来决定统计口径。

4.2 在 WHERE 中引用 SELECT 别名,报错找不到列

这是最经典的问题。看这个写法:

select user_id, sum(amount) as total_paid_amount from orders where total_paid_amount > 100 group by user_id;

数据库直接报错“unknown column total_paid_amount”。原因就是 select 阶段的别名在 where 执行时还不存在。解决方案有三种:

第一种,把过滤条件放到 having 里:

select user_id, sum(amount) as total_paid_amount from orders group by user_id having sum(amount) > 100;

第二种,包一层子查询。这个前面讲过了。

第三种,在 where 中重复书写原始表达式。但如果你写where sum(amount) > 100,多数数据库会直接报错,因为 where 里不允许直接使用聚合函数。这个方法在标准 SQL 中不可行。

我推荐的做法很简单:如果是对聚合结果的过滤,优先用 having;如果还需要基于中间结果做更多加工,就包一层子查询。搞不清楚时,就把 SQL 按逻辑执行顺序拆开看一遍,一眼就能定位问题。

4.3 DISTINCT 与 ORDER BY 搭配时的字段限制

再分享一个容易踩的坑。看这条 SQL:

select distinct user_id from orders order by product_name;

在不少数据库里会直接报错,原因是:distinct 去重后,结果集中只有 user_id 这一列,而 order by 里的 product_name 不在结果集中,排序没有依据。

如果你确实需要去重后按其他字段排序,可以把逻辑拆成两层:

select user_id from ( select user_id, max(order_date) as last_order_date from orders group by user_id ) t order by last_order_date;

这样既保证了 user_id 的唯一性,又能通过聚合值排序。顺便一提,“取每个用户最近一次下单日期”本身就是统计查询里非常高频的需求,用子查询加 AS 是最直观的写法。

4.4 别名使用关键字或特殊字符导致的语法错误

我曾给一个列起名叫 description,结果在某些数据库里直接报语法错误,后来才发现 description 在个别方言里是保留字。更常见的坑是别名带空格,比如as user count,标准 SQL 要求写成as "user count",但一旦你用了单引号或者不加引号,数据库就会解析失败。

我的建议非常朴素:别名只用小写字母、数字和下划线,不要用空格、不用中文、不用保留字,不要以数字开头。这样能保证 SQL 在不同数据库之间迁移时不出问题。如果你非要给报表展示一个带空格的友好名称,那可以在查询结果导出的阶段再处理,而不是在 SQL 阶段硬撑。

4.5 排查统计结果异常的通用思路

最后分享一个通用的排查思路,遇到统计查询结果不对时,我通常按三步走。

第一步,把 SQL 按逻辑执行顺序拆开,检查每一步处理的数据粒度是否与业务口径一致。先看 from 的数据源,再看 where 过滤了哪些行,然后看 group by 的分组粒度是否合理,最后看 distinct 的作用范围。

第二步,检查 distinct 到底是作用在单个字段、多个字段组合,还是聚合函数内部。这个差异直接决定结果语义。

第三步,检查数据本身。重点看 null、重复行、多义字段。很多统计结果异常,本质上是数据问题,不是 SQL 语法问题。

这套排查方法,我实践下来效率很高。SQL 本身是一门非常讲究口径的语言,同样的数据、同样的需求,写法差一个 distinct,结果可能差出好几倍。把执行顺序和 distinct 语义吃透,排查起来会快很多。

5. 一些个人实操中的体会

这篇写到这里,核心内容差不多讲完了,最后分享几点我做统计查询的体会。

第一点是关于数据粒度。去重和更名看似只是两个小语法,但它们真正考验的是你对“数据粒度”的理解。你写的每一条统计 SQL,其实都在回答一个业务问题:我要在什么粒度上看数据,重复的记录怎么处理,结果的列名能否准确传达含义。粒度搞清楚了,distinct 和 as 的用法就水到渠成;粒度没搞清楚,哪怕语法写得再漂亮,结果也经不起推敲。

第二点是关于别名的习惯。建议给所有统计查询的结果列都显式写上别名,不管是简单的 count(*),还是复杂的 sum(case when ... end)。这个习惯在团队协作里特别重要,因为 SQL 经常会被交接、被下游报表系统引用,一个清晰别名能省掉大量沟通成本。

第三点是一个实用的小技巧。在验证一条统计 SQL 时,我会刻意构造包含“一个人多行”和“空值”的测试数据,用真实结果去反推 SQL 语义。比如 count(user_id) 和 count(distinct user_id) 结果差多少,就能直接看出 distinct 是否生效。这种调试方式比单纯读文档直观得多。

如果你正在学 SQL,或者写统计查询时偶尔觉得别扭,我建议先把“逻辑执行顺序、distinct 作用范围、AS 引用时机”这三件事反复消化,再构造一份有小样本数据,把 select、from、where、group by、having、distinct、as 全部串起来写一遍。写完之后你会发现,统计查询的基础逻辑并没有那么玄,它就是你把一个业务口径用数据库语言说清楚的过程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询