mysql中的复杂查询
group by...having 和 where 的区别
mysql 先对数据按照 where 子句过滤,然后在对返回的结果分组,最后对分组后的数据按照 having 子句过滤。
where 是对行过滤,而 having 是对分组过滤,所以可以在 having 子句后面使用聚集函数,而在 where 子句后面使用聚集函数是没有意义的。
join...on 和 where 的区别
在联结查询中,join...on...子句和 where 子句是可以一起出现的,要想弄清楚它们的区别,首先要清楚联结查询的过程。联结查询时要先根据联结条件对两张表做笛卡尔积,生成一个大表,然后再对这个大表做过滤,最终得到查询结果。
join...on 和 where 的区别在于,join...on...子句是在联结时生效,而 where 子句是在联结后生效。
因此,当联结是外部联结(联结子句是 left join 或 right join)时,由于外部联结会将左表或右表中没有建立联结的行都返回,所以 on 后面的条件不会去掉那些不符合条件的行,而 where 子句是严格的,会将不等值的行都去掉。
而当联结是内部联结(等值联结)的时候,联结子句(join...on...)只会返回等值的行,这时写在 on 后面的条件和 where 子句中的条件是等价的,因此,使用内部联结的时候,完全可以用 where 子句代替 join...on...
窗口函数
参考mysql-8.0-documentation-window-functions
介绍
窗口函数是 mysql 8.0 加入的功能,目的是为了满足分组统计场景的查询需求。
我们使用 SQL 语句查数据时通常会有一些统计分析的需求(例如聚合和排序),一般有以下三类统计场景:
- 简单统计
按条件查出数据后,对返回数据整体做统计操作,这种统计使用select...from...where...order by配合聚合函数即可完成。 - 分组聚合
按条件查出数据后,对数据分组,然后对每个分组做聚合操作,每个分组返回一条数据即可,这种统计使用group by即可完成。 - 分组统计
很多时候我们既想分组,又想对分组聚合,又想把分组内部的数据全部返回,而不是每个分组只返回一个数据,这时光用group by是不能完成任务的,为了解决问题,我们经常要写一堆复杂的自联结,不仅 SQL 难以读懂,而且查询效率也很低下。
窗口函数的引入就是为了方便解决上述第三种查询场景,我们可以使用窗口函数对分组内部的数据做统计,然后把统计后的数据全部返回,并且可以按排序为每行计算出一个序号,方便我们使用序号对数据做二次过滤
对于窗口函数来说,“窗口”的概念非常重要。窗口函数会对组内的每条记录执行统计,这需要给窗口函数规定一个统计范围(统计范围不一定是整个分组,也可能只对分组的一部分做统计),我们把这个统计范围称为窗口,有的窗口函数的窗口是固定的,有的是滑动的,需要用户指定
用法
以下是窗口函数的用法
-- 简单写法(把窗口函数返回的行序号作为新的一列new_col)
select
window_function() over(partition by xxx order by xxx) as new_col from xxx
-- 上面也可以把分组条件拆成单独的window语句
select
window_function() over w as new_col
from xxx
window w as (partition by xxx order by xxx)
窗口函数的完整语法如下
window_function_name(expression)
OVER (
[partition_defintion]
[order_definition]
[frame_definition]
)
window_function_name(expression)是窗口函数,over()为窗口函数指定了分组规则,如果里面什么都不写就是在整个表上运行窗口函数
partition子句定义了按哪些列分组,order by子句定义了按哪些列排序,frame子句定义了一个滑动窗口,分为按行定义和按范围定义
窗口函数只能用在 select 列表和 order by 子句里。mysql 是对 where、group by、having 子句返回的结果上进行窗口函数的处理,处理顺序在 order by、limit、select distinct 之前
frame语法
-- frame_units是窗口类型,frame_extent是窗口范围
frame_units frame_extent
-- frame_units有两种,ROWS和RANGE,分别是按行的位置指定范围(行的位置就是指“第几行”)和按行的值指定范围(对RANGE模式不太理解,大概是值相同的行在统计上算作同一行)
frame_units:
{ROWS | RANGE}
-- frame_extent也有两种,只指定开始行(结尾行默认是当前行),和同时指定开始和结束行
frame_extent:
{frame_start | frame_between}
frame_between:
BETWEEN frame_start AND frame_end
frame_start, frame_end: {
CURRENT ROW -- 当前行
| UNBOUNDED PRECEDING -- 分组第一行
| UNBOUNDED FOLLOWING -- 分组最后一行
| expr PRECEDING -- 当前行之前的expr行,例如10 PRECEDING的意思是当前行之前的10行
| expr FOLLOWING -- 当前行之后的expr行
}
常用窗口函数
可以当作窗口函数的聚合函数:sum、min、max、avg、count
以下是专用窗口函数
| 函数 | 功能 |
|---|---|
| row_number() | 返回分组内排序后的行号 |
| rank() | 返回分组内排序后的序号,如果有并列的行,它们行号相同,行号是并列组前面的总记录数+1 |
| dense_rank() | 返回分组内排序后的序号,如果有并列的行,它们行号相同,行号是上一个并列组的序号+1 |
| first_value(expr)/last_value(expr) | 返回窗口中的第一个行或最后一个行,并对其做表达式计算 |
| lag(expr,N,default) | 返回分组内位于当前行之前N行(这里是指上方)的行,default是指如果没有之前N行返回的值,N和default的默认值分别为1和null |
| lead(expr,N,default) | 返回分组内位于当前行之后N行(下方)的行。(这里的之前之后是指遍历顺序) |
| nth_value(expr,N) | 返回窗口内的第n行 |
| ntile(n) | 将分组内行平均分成n个桶,返回每个行所属的桶号 |
以下是row_number、rank和dense_rank的区别
+------+------------+------+------------+
| val | row_number | rank | dense_rank |
+------+------------+------+------------+
| 1 | 1 | 1 | 1 |
| 1 | 2 | 1 | 1 |
| 2 | 3 | 3 | 2 |
| 3 | 4 | 4 | 3 |
| 3 | 5 | 4 | 3 |
| 3 | 6 | 4 | 3 |
| 4 | 7 | 7 | 4 |
| 4 | 8 | 7 | 4 |
| 5 | 9 | 9 | 5 |
+------+------------+------+------------+
其他
if函数
if函数可以用在select子句中
用法:if(expr1,v1,v2),当expr1的值为真时,返回v1,否则返回v2
case 函数
case 函数可以用在 select 子句里,并将其结果命名为一个字段,还可以按这个字段分组,比如以下 sql
select case
when age>=25 then "25岁及以上"
else "25岁以下"
end as age_cut,count(id) as number
from user_profile
group by age_cut;
这个 sql 返回的结果如下所示
age_cut,number
25岁以下,4
25岁及以上,3
case 函数有两种写法,第一种,
CASE <表达式>
WHEN <值1> THEN <操作>
WHEN <值2> THEN <操作>
...
ELSE <操作>
END CASE;
第二种(有点类似于 golang 的 switch 语句),
CASE
WHEN <条件1> THEN <操作>
WHEN <条件2> THEN <操作>
...
ELSE <操作>
END CASE;