引言
写成一句SQL很容易,但完全手打却还是有些困难。这个文章作为拾遗,把一些常用的语法和常识都放在一起,以供翻阅。参考的书籍为SQL CookBook。
正文
字段别名作用域
为字段取别名将在WHERE语句执行后才生效,但如果是子查询,则外部查询可以直接使用子查询别名;
正确示例:
1 | SELECT empolyee_salary |
错误示例:
1 | SELECT book_name AS book |
字符串拼接
拼接字符串,SQL Server使用 +
即可,MySQL使用CONCAT函数;
SQL Server
1 | SELECT book + '|' + author |
MySQL
1 | SELECT CONCAT(book, '|', author) |
CASE WHEN用法
SELECT中使用条件逻辑CASE返回对应的固定值,类似于Java中的Switch-Case语法;
通用
1 | SELECT student_id, |
当然,CASE也可以用于ORDER BY
排序,例如,经理按照月薪降序排列,其他人则升序排列
通用
1 | SELECT employee_id |
生成随机数
SQL Server使用NEWID()
或RAND()
,MySQL使用RAND()
;
SQL Server
1 | SELECT student_name, |
MySQL
1 | SELECT student_name, |
判断字段是否为空
SQL Server可以用ISNULL(字段, 默认值)
或COALESCE(字段, 字段, 字段, 默认值)
,MySQL可以使用IFNULL(字段, 默认值)
或COALESCE(字段, 字段, 字段, 默认值)
;
SQL Server
1 | SELECT student_name, |
MySQL
1 | SELECT student_name, |
计算时间差
SQL Server使用DATEDIFF(时间维度(年、天、时、分、秒), 开始日期,结束日期)
,MySQL
使用TIMESTAMPDIFF(时间维度(年、天、时、分、秒), 开始日期,结束日期)
;
SQL Server
1 | -- 结果为1分钟 |
MySQL
1 | -- 结果为0分钟 |
使用UNION ALL
代替UNION
尽量使用UNION ALL
,而不使用UNION
,因为UNION
会对数据进行排序并进行相当于DISTINC
的操作耗时;
FULL OUTER JOIN
使用以及JOIN的关系
FULL OUTER JOIN
用的很少,但是也很有用——可以筛选某个表中不存在的值,通俗理解就是筛选的两个表的补集;
其他JOIN的关系可以通过这个图理解:
通用
1 | SELECT b.isbn |
OVER()
函数用法
SUM/MIN/MAX...() OVER (PARTITION BY 字段)
可以用来进行累计统计分析和GROUP BY
类似但不完全相同,在结果前加DISTINCT
可以有相同的作用;
通用
1 | -- 找出每种类型书籍的最低价格 |
因为原来没有接触过,现在可以确定的一个使用场景是:对每个不同分类下的最好/最坏…进行筛选并展示。
字符串切片
这是常用操作,讲某个字段值从前往后切割n位,SQL Server使用SUBSTRING(字段,起始位,终止位)
,MySQL使用SUBSTR(字段,起始位,终止位)
,注意这里的位置是从1开始计算的;
通用
1 | -- Tom -> To |
IN
与EXIST
的使用
找到一篇博文,博主讲的用法值得尝试,先记录,以观后效;
简言之,外表数据量>>子查询表数据量,使用IN
合适;
反之,外表数据量<<子查询表数据量,使用EXIST
合适;
使用MERGE
进行表数据的同步
快速对表中的数据进行更新并且插入新数据——使用MERGE
关键字,在对同数据库表进行表间数据更新的时候,使用MERGE
可以说是最佳选择,速度很快,但是跨数据库服务器,则不能使用;
关于Merge操作数据的解释如图所示:
通用
1 | MERGE target_table USING source_table |
列出某个表的所有列名
通用
1 | SELECT column_name, data_type |
创建分隔符列表
为表创建一个以X字符为分隔符的列表,例如:
class_id | sudent_name |
---|---|
A | Student_1 |
A | Student_2 |
A | Student_3 |
B | Student_4 |
B | Student_5 |
B | Student_6 |
变为
class_id | sudent_name |
---|---|
A | Student_1,Student_2,Student_3 |
B | Student_4,Student_5,Student_6 |
在SQL Server中使用WITH
关键字来创建,MySQL中使用GROUP_CONCAT
函数来创建;
统计行数
MySQL和SQL Server均使用COUNT()
,但需要注意的是如果COUNT()
中的参数为列名,则统计时会忽略NULL
,而如果是传*
或者常量,则会包含NULL
;
聚合函数对于NULL
的处理
在进行聚合运算,例如:SUM
、AVG
等函数时,函数本身不会对值为NULL
的行进行计数和计算,所以,要得到一个客观的聚合值,则需要使用COALESCE
等填充函数;
时间加减
当前日期向前/向后n天/月/年(但不能加减时/分/秒),MySQL可以直接使用INTERVAL ± 天数 DAY/MONTH/YEAR
来操作,而SQL Server使用函数DATEADD()
来操作;
SQL Sever
1 | # GETDATE() = 2019/12/10 23:05:33 |
MySQL
1 | # NOW() = 2019/12/10 23:05:33 |
获取当前系统时间
两个数据库各不相同,代码如下:
1 | # 输出 2019/12/10 23:05:33 |
MySQL
1 | # 输出 2019/12/10 23:05:33 |
计算两个日期之间的天数差
均使用DATEDIFF
函数,只是MySQL只需要两个参数(终止时间, 起始时间),SQL Server需要三个参数(天/月/年, 起始时间, 终止时间);
SQL Sever
1 | # 输出 3 |
MySQL
1 | # 输出 3 |
提取日期中的时分秒
MySQL使用DATE_FORMAT
,SQL Server使用DATEPART
;
SQL Sever
1 | SELECT DATEPART(HOUR, GETDATE()) AS HOUR, |
MySQL
1 | SELECT DATE_FORMAT(NOW(),'%k') AS HOUR, |
使用DENSE_RANK
对满足前N类或后N类数值档次的数据进行检索
例如学生的补贴分为5档,现在要筛选出前3档补贴的学生名单,那么可以使用DENSE_RANK
来实现;
student_id | sudent_name | sudent_subsidy |
---|---|---|
A | Student_1 | 400 |
B | Student_2 | 400 |
C | Student_3 | 500 |
D | Student_4 | 600 |
E | Student_5 | 700 |
F | Student_6 | 300 |
需要检索前3档补贴的同学名单
student_id | sudent_name | sudent_subsidy |
---|---|---|
E | Student_5 | 700 |
D | Student_4 | 600 |
A | Student_1 | 400 |
B | Student_2 | 400 |
通用
1 | SELECT student_id, student_name, sudent_subsidy |
ROW_NUMBER()
,RANK()
,DENSE_RANK()
的区别
结论:
ROW_NUMBER()
无论数据行是否重复,均会给出独立的行号;
RANK()
会跳跃排序,如果遇到排序的列有重复的值时,则会跳过,例如有两个并列第3,那么排名第4的值行号将被记为5
DENSE_RANK()
会连续排序,和RANK()相反,如果遇到排序的列有重复的值时,不会跳过,例如有两个并列第3,那么排名第4的值行号将被记为4
为方便理解,使用以下表数据为例
student_id | sudent_name | sudent_subsidy |
---|---|---|
A | Student_1 | 400 |
B | Student_2 | 400 |
C | Student_3 | 500 |
D | Student_4 | 600 |
E | Student_5 | 700 |
F | Student_6 | 300 |
ROW_NUMBER()
在任何时候都不会发生重复,计算表中有n条相同数据,ROW_NUMBER()
后的行号也是不同的
通用
1 | SELECT ROW_NUMBER() OVER(ORDER BY sudent_subsidy) AS row_num, |
row_num | student_id | sudent_name | sudent_subsidy |
---|---|---|---|
1 | F | Student_6 | 300 |
2 | A | Student_1 | 400 |
3 | B | Student_2 | 400 |
4 | C | Student_3 | 500 |
5 | D | Student_4 | 600 |
6 | E | Student_5 | 700 |
从返回的结果中,可以看出每一列数据均被赋予了新的一个行号,并不会因为某一列数据重复而重复。
RANK()
会跳跃排序,如果有并列,则会将跳过相同的行,直接加上向同行的偏移继续向后统计行号。
通用
1 | SELECT RANK() OVER(ORDER BY sudent_subsidy) AS rank, |
rank | student_id | sudent_name | sudent_subsidy |
---|---|---|---|
1 | F | Student_6 | 300 |
2 | A | Student_1 | 400 |
2 | B | Student_2 | 400 |
4 (rank 3不见了,直接从4开始) | C | Student_3 | 500 |
5 | D | Student_4 | 600 |
6 | E | Student_5 | 700 |
可以看到,行号从2直接跳到了4,重复行占据了本该3出现的行,则3就被抛弃了。
DENSE_RANK()
和RANK()
相反,它保留了重复行之后的行号。
通用
1 | SELECT DENSE_RANK() OVER(ORDER BY sudent_subsidy) AS rank, |
rank | student_id | sudent_name | sudent_subsidy |
---|---|---|---|
1 | F | Student_6 | 300 |
2 | A | Student_1 | 400 |
2 | B | Student_2 | 400 |
3 (接着重复行的rank值继续向下排列) | C | Student_3 | 500 |
4 | D | Student_4 | 600 |
5 | E | Student_5 | 700 |
结果集转置为多行
原表数据为:
emp_no | ename | job |
---|---|---|
1 | DENY | CLERK |
2 | GINY | ANALYZER |
3 | YINE | CLERK |
4 | MILLER | CLERK |
5 | JAMES | CLERK |
6 | CLRAKS | MGRS |
7 | BLAKES | MGRS |
8 | JONES | MGRS |
9 | KING | PREZ |
10 | TURNE | SALES |
11 | MARTIN | SALES |
如果一个查询的结果集如下所示:
ename | job |
---|---|
DENY | CLERK |
GINY | ANALYZER |
YINE | CLERK |
MILLER | CLERK |
JAMES | CLERK |
CLRAKS | MGRS |
BLAKES | MGRS |
JONES | MGRS |
KING | PREZ |
TURNE | SALES |
MARTIN | SALES |
我们需要将其转置为职位-姓名,则我们可以使用ROW_NUMBER() OVER(PARTITION BY job ORDER BY ename)
得到分组的行号,并结合MAX()
聚合函数将其按照行号进行分组,得到最终的结果。
通用
1 | SELECT MAX(CASE WHEN job='CLERK' |
结果如下:
CLERK | ANALYZER | MGRS | PREZ | SALES |
---|---|---|---|---|
DENY | GINY | BLAKES | KING | TURNE |
YINE | JONES | MARTIN | ||
JAMES | ||||
MILLER |
使用WITH ROLLUP
来对报表做简单小计
例如可以将报表中所有学生补贴总和进行总计,如果GROUP BY
的字段变多,那么小计的行也会随之增加;
原始数据如下:
student_id | sudent_name | sudent_subsidy | class |
---|---|---|---|
A | Student_1 | 400 | 1 |
B | Student_2 | 400 | 2 |
C | Student_3 | 500 | 3 |
D | Student_4 | 600 | 1 |
E | Student_5 | 700 | 2 |
F | Student_6 | 300 | 3 |
SQL Sever
1 | SELECT COALESCE(class, 'total'), SUM(sudent_subsidy) |
MySQL
1 | SELECT COALESCE(class, 'total'), SUM(sudent_subsidy) |
使用了ROLLUP
后的结果集为
CLASS | sudent_name |
---|---|
1 | 1000 |
2 | 1100 |
3 | 800 |
total | 2900 |
多列共同进行协同总计和展示
在WITH ROLLUP
的基础上进行扩展;
在23中,如果我们需要进行统计列不仅仅只是student_name
,那么,我们仅仅使用WITH ROLLUP
则会得到很多冗余行,例如,要按班级号和班级名称来共同计算每个班级的补贴和,并且展示班级号、班级名和补贴和,那么,我们需要引入GROUPING()
来对每列的数据进行分组标记,在此处我们可以通过如下代码来查看GROUPING()
在查询时会输出什么:
原始数据:
student_id | sudent_name | sudent_subsidy | class | class_name |
---|---|---|---|---|
A | Student_1 | 400 | 1 | 尖子班 |
B | Student_2 | 400 | 2 | 快速班 |
C | Student_3 | 500 | 3 | 平行班 |
D | Student_4 | 600 | 1 | 尖子班 |
E | Student_5 | 700 | 2 | 快速班 |
F | Student_6 | 300 | 3 | 平行班 |
SQL Sever
1 | SELECT class, class_name, GROUPING(class), GROUPING(class_name), SUM(sudent_subsidy) AS 'money' |
MySQL
1 | SELECT class, class_name, GROUPING(class), GROUPING(class_name), SUM(sudent_subsidy) AS 'money' |
输出的结果为
class | class_name | GROUPING(class) | GROUPING(class_name) | money |
---|---|---|---|---|
A | 尖子班 | 0 | 0 | 1000 |
A | NULL | 0 | 1 | 1000 |
B | 快速班 | 0 | 0 | 1100 |
B | NULL | 0 | 1 | 1100 |
C | 平行班 | 0 | 0 | 800 |
C | NULL | 0 | 1 | 800 |
NULL | NULL | 0 | 0 | 2900 |
我们可以看到class_name
列,若class_name
列在ROLLUP
后为空,则GROUPING(class_name)
的值为0,否则为1。这样我们就能够通过GROUPING
函数区分出我们所需要的列的组合以及对应的统计数据。现在,我们只需要两个GROUPING列均为0的来作为最终结果,所以,我们把两个GROUPING列使用字符串进行拼接,而后即可取出值为00
(SQL Server)/0
(MySQL)和2
(MySQL)的行来作为需要返回的列的标识:
SQL Server
1 | WITH result AS ( |
MySQL
1 | WITH result AS ( |
结果为:
class | class_name | money |
---|---|---|
A | 尖子班 | 1000 |
B | 快速班 | 1100 |
C | 平行班 | 800 |
合计 | 合计 | 2900 |
GROUP BY 1/ORDER BY 1是什么意思
GROUP BY 1
和 ORDER BY 1
和使用列名的效果相同,1
、2
代替了SELECT
语句中的列别名。
通用
1 | SELECT account_id, open_emp_id |
INT转String/String转INT
INT -> String
SQL Server
1 | SELECT CONVERT(CHAR, 123) |
MySQL
1 | SELECT CONVERT(123, CHAR) |
String -> INT
SQL Server
1 | SELECT CONVERT(INT, 123) |
MySQL
1 | SELECT CONVERT(123, UNSIGNED) |
字符串REPLACE
通用
1 | -- 输出为BCD |
MySQL GROUP BY也可指定排序顺序
如果需要对分组后的数据按照分组信息进行降序排序,可以使用GROUP BY 列名 DESC
实现,而不用再使用ORDER BY
。注意:MySQL中如果要使用这样的语法特性,则需要特定的版本(MySQL 8.0以前可以使用)!
保留n位小数点
SQL Server稍有不同,在SQL Server中ROUND
仅仅只起到从第几位其四舍五入的作用,如果小数点有6位,ROUND(3.333333, 4)
的结果为3.333300,位数仍然是6位,只是从第4位起就四舍五入了而已,如果要截断成4位则使用DECIMAL
即可。
SQL Server
1 | -- 输出为 123.6 |
MySQL
1 | -- 输出为 123.5 |
向上/向下取整
向上取整
SQL Server
1 | -- 输出为 124 |
MySQ
1 | -- 输出为 124 |
在MySQL中,CEIL
和CEILING
是同义的。
向下取整
通用
1 | -- 输出为 123 |
区分二叉树的节点类型
此题来源于:Binary Tree Nodes
若存在一颗二叉树,其存储方式如下:
node | parent |
---|---|
1 | 2 |
3 | 2 |
6 | 8 |
9 | 8 |
2 | 5 |
8 | 5 |
5 | NULL |
我们需要将其划分为根节点
、叶子节点
、内部节点
三种属性。
那么我们可以使用自连接来判断每个节点所在位置,而后进行输出。
通用
1 | SELECT DISTINCT B1.N AS NODE, |
结果为:
node | attribute |
---|---|
1 | Leaf |
2 | Inner |
3 | Leaf |
5 | Root |
6 | Leaf |
8 | Inner |
9 | Leaf |
数字开方/绝对值/幂次方
开平方使用SQRT
即可。
通用
1 | -- 输出为2 |
绝对值使用ABS
即可。
通用
1 | -- 输出为2 |
幂次方使用POWER
即可。
通用
1 | -- 输出为4 |
使用WITH进行复杂查询
如果一个查询过于复杂,无法在一个查询中完成所有操作,可以考虑使用WITH
关键词进行分步操作。WITH
可以用于分布操作,同样也可以用于递归,将上一个操作结果集中的数据继续进行相同的处理。
通用
1 | WITH STEP1 AS ( |
通过JOIN进行区间到具体值的映射
此题来源于:The Report
如果提供了一个学生表和一个成绩对应的分级表(表如下),但两个表之间并没有可以一一对应的主键外键关系,我们需要对每个学生的成绩所在的级别进行显示,我们可以使用如下的方式进行。
学生表Students
student | score |
---|---|
A | 82 |
B | 60 |
C | 25 |
成绩对应表Grades
grade | min_score | max_score |
---|---|---|
优秀 | 75 | 100 |
良好 | 50 | 74 |
中等 | 25 | 49 |
较差 | 0 | 24 |
那么我们可以这样使用JOIN
,来将每个学生的成绩级别进行映射,因为ON
会先于JOIN
对符合条件的数据进行匹配,所以每一条数据只会对应一个级别,我们就可以完成分数段到级别的匹配。
通用
1 | SELECT student, grade |
结果
student | grade |
---|---|
A | 优秀 |
B | 良好 |
C | 较差 |
查看字符串是否包含特定字符
类似于Java的contains()
函数,SQL Server和MySQL可以使用如下方式来得到结果,包含匹配的字符串,返回字符串的所在的第一个下标,不包含返回0
。
PS:匹配的字符串不仅可以是独立的字符,同样也可以是正则表达式。
SQL Server
1 | -- 匹配的字符串存在的情况 |
MySQL
1 | -- 匹配的字符串存在的情况 |
%
用于模糊匹配,%X
表示匹配以X
结尾的X
下标,反之,X%
表示匹配以X
开头的X
下标,必须要使用至少一个%来进行匹配查询。
待续…