我整理 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 的逻辑执行顺序。虽然数据库引擎真正执行时会有优化和调整但逻辑上的顺序是固定的from找到数据源where过滤行group by分组聚合计算having过滤分组select挑选列distinct去重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在各主流数据库里差异很小。先建一张模拟订单表 orderscreate 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后者的结果是 3101、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 中完成因为总额是聚合结果。有两种写法。第一种用 havingselect user_id, sum(amount) as total_paid_amount from orders where status paid group by user_id having sum(amount) 100;这条 SQL 简洁直接having 在分组后对聚合结果过滤条件为真才保留该分组。第二种用子查询加 ASselect 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下游可引用 totalselect 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))。排查这类问题我会先跑一条侦察 SQLselect 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 搭配时的字段限制再分享一个容易踩的坑。看这条 SQLselect 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 全部串起来写一遍。写完之后你会发现统计查询的基础逻辑并没有那么玄它就是你把一个业务口径用数据库语言说清楚的过程。