select count(*), * from my_table;
他的本意很简单:既想知道表里总共有多少条数据,又想顺便看看表里的内容长啥样。想法挺美好,但这句 SQL 在不同数据库里的表现,简直能把人逼疯。
今天顺手记录一下这个问题,估计不少从 MySQL 刚转 PostgreSQL 的同学都踩过这个坑。
1. 跑起来到底会咋样?
如果你用的是 PostgreSQL(或者 SQL Server),数据库会直接给你甩个报错:
ERROR: column "my_table.id" must appear in the GROUP BY clause or be used in an aggregate function
翻译过来就是:你用了聚合函数,其他列要么也聚合,要么就给我放到 GROUP BY 里去。
但如果你用的是 MySQL(特别是没开严格模式的老版本),奇葩的事情发生了——它居然不报错,还能查出结果:
| count(*) | id | name | age |
|---|---|---|---|
| 100 | 1 | 张三 | 20 |
乍一看好像没问题?总数是 100。但你仔细看后面的 id, name, age,它只返回了第一行(或者是随便哪一行)的数据。剩下的 99 行去哪了?被狗吃了? 这种悄悄丢数据的查询,要是真放到业务代码里,排查起来能让人怀疑人生。
2. 凭啥报错?底层逻辑是啥
其实这根本不是什么玄学,就是 SQL 最基础的维度冲突。
你想啊,count(*) 是个聚合函数,它的作用是把一堆数据“捏”成一个数字,也就是单行结果。 而后面的 * 是明细数据,是多行结果。
你把这俩放一块儿,就相当于对数据库说:“帮我把 100 行数据压缩成 1 个数字,然后再把这个数字跟原来的 100 行拼在同一行里显示。” 数据库直接就懵了:大哥,1 行怎么跟 100 行对齐?我该把总数填在哪一行旁边?
为了解决这种逻辑上的死胡同,SQL 标准定了一条死规矩:只要 SELECT 里有聚合函数,其他没聚合的列必须全部塞进 GROUP BY 里。 PG 是老实孩子,严格遵守标准,所以直接报错。而早期的 MySQL 比较“随性”,它心想既然你没说怎么分组,那我就闭着眼睛随便抓一行数据跟总数拼一起返回给你吧。这就导致了前面那种诡异的查询结果。(好在现在 MySQL 5.7 以后默认也把这个漏洞堵上了)。
3. 那我就是想既要总数又要明细,咋办?
实际开发中,这种需求还挺常见的。一般有这么几种搞法:
搞法一:用窗口函数(最爽) 如果你用的是 PG、MySQL 8.0 或者 Oracle,直接用 OVER(),一行代码搞定。
SELECT count(*) OVER(), * FROM my_table;
加了 OVER() 之后,意思是告诉数据库:“算个全局总数,但别给我压缩行数,把这个总数当成新的一列,给每一行都贴上一个。” 出来的结果就是每一行后面都跟着个 100,非常舒服。
搞法二:套个子查询(老派做法) 如果数据库版本太老不支持窗口函数,就只能笨办法了,把总数当成一个子查询硬塞进去:
SELECT t.*, (SELECT count(*) FROM my_table) as total_count
FROM my_table t;
搞法三:老老实实查两次(日常最常用) 说句实在话,在平时写业务代码(比如做分页列表)的时候,我们极少会把总数和明细放在一个 SQL 里查。 因为分页通常要加 LIMIT,混在一起写不仅 SQL 复杂,性能还容易出问题。大家基本都是让 ORM 框架(比如 MyBatis-Plus 的分页插件)在底层自动拆成两条 SQL 去跑: 一条 select count(*) 拿总数渲染分页条,另一条 select * limit x offset y 拿数据渲染列表。简单粗暴,不易出错。
总结
说白了,select count(*), * 就是把“统计结果”和“流水明细”这两种不同维度的东西强行凑在一起的产物。 以后写 SQL 的时候稍微留个心眼,遇到既要聚合又要明细的场景,条件反射地想到 OVER() 窗口函数,基本就不会踩坑了。
行了,今天就水到这里。大家平时写 SQL 还遇到过啥反直觉的坑?可以在评论区吐吐槽。
作者:idanyang
出处:http://www.cnblogs.com/idanyang/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
浙公网安备 33010602011771号