0%

SQL必会知识点

引言

写成一句SQL很容易,但完全手打却还是有些困难。这个文章作为拾遗,把一些常用的语法和常识都放在一起,以供翻阅。参考的书籍为SQL CookBook

正文

字段别名作用域

为字段取别名将在WHERE语句执行后才生效,但如果是子查询,则外部查询可以直接使用子查询别名;

正确示例:

1
2
3
4
5
SELECT empolyee_salary
FROM (SELECT id, SUM(salary) AS empolyee_salary
FROM employee_salary
GROUP BY id)
WHERE empolyee_salary > 1000

错误示例:

1
2
3
SELECT book_name AS book
FROM books
WHERE book = '1'

字符串拼接

拼接字符串,SQL Server使用 + 即可,MySQL使用CONCAT函数;

SQL Server

1
2
SELECT book + '|' + author
FROM books

MySQL

1
2
SELECT CONCAT(book, '|', author)
FROM books

CASE WHEN用法

SELECT中使用条件逻辑CASE返回对应的固定值,类似于Java中的Switch-Case语法;

通用

1
2
3
4
5
6
7
SELECT student_id,
CASE WHEN grade > 60 THEN '及格'
CASE WHEN grade > 80 THEN '良好'
ELSE WHEN grade > 90 THEN '优秀'
ELSE '不及格'
END AS 'final_grade'
FROM grades

当然,CASE也可以用于ORDER BY排序,例如,经理按照月薪降序排列,其他人则升序排列

通用

1
2
3
SELECT employee_id
FROM grades
ORDER BY CASE WHEN employee_type = 'manager'

生成随机数

SQL Server使用NEWID()RAND(),MySQL使用RAND();

SQL Server

1
2
3
4
5
6
7
8
9
10
SELECT student_name,
-- 一次查询出来的结果随机的数字是相同的
-- FLOOR为地板函数 向下取整
FLOOR(RAND() * 10),
-- 不同列则随机数可能不同
-- CEILING为天花板函数 向上取整
CEILING(RAND() * 10),
-- NEWID()类似UUID 保证唯一
NEWID()
FROM students

MySQL

1
2
3
4
SELECT student_name,
-- 会生成0-1间的小数
RAND()
FROM students

判断字段是否为空

SQL Server可以用ISNULL(字段, 默认值)COALESCE(字段, 字段, 字段, 默认值),MySQL可以使用IFNULL(字段, 默认值)COALESCE(字段, 字段, 字段, 默认值);

SQL Server

1
2
3
4
5
6
SELECT student_name,
-- COALESCE的好处在于可以用于判断多个字段为空时的操作
COALESCE(student_gender, student_info, 'empyt'),
-- ISNULL则只能对单个进行判断
ISNULL(student_gender, 'empty')
FROM students

MySQL

1
2
3
4
5
6
SELECT student_name,
-- COALESCE的好处在于可以用于判断多个字段为空时的操作
COALESCE(student_gender, student_info, 'empyt'),
-- ISNULL则只能对单个进行判断
IFNULL(student_gender, 'empty')
FROM students

计算时间差

SQL Server使用DATEDIFF(时间维度(年、天、时、分、秒), 开始日期,结束日期),MySQL
使用TIMESTAMPDIFF(时间维度(年、天、时、分、秒), 开始日期,结束日期);

SQL Server

1
2
-- 结果为1分钟
SELECT DATEDIFF(mi, '2019-11-26 21:54:40', '2019-11-26 21:55:00')

MySQL

1
2
-- 结果为0分钟
SELECT TIMESTAMPDIFF(mi, '2019-11-26 21:54:40', '2019-11-26 21:55:00')

使用UNION ALL代替UNION

尽量使用UNION ALL,而不使用UNION,因为UNION会对数据进行排序并进行相当于DISTINC的操作耗时;

FULL OUTER JOIN使用以及JOIN的关系

FULL OUTER JOIN用的很少,但是也很有用——可以筛选某个表中不存在的值,通俗理解就是筛选的两个表的补集;

其他JOIN的关系可以通过这个图理解:

Join图示

Join图示

通用

1
2
3
4
5
SELECT b.isbn
FROM book_infos AS b
full OUTER JOIN outer_book AS o ON o.isbn = b.isbn
-- 筛选在outer_book表中不存在记录的isbn号
WHERE o.isbn IS NULL

OVER()函数用法

SUM/MIN/MAX...() OVER (PARTITION BY 字段)可以用来进行累计统计分析和GROUP BY类似但不完全相同,在结果前加DISTINCT可以有相同的作用;

通用

1
2
3
4
5
6
7
8
-- 找出每种类型书籍的最低价格
SELECT DISTINCT MIN(price) OVER (PARTITION BY book_type), book_type
FROM book

-- 等价于GROUP BY
SELECT MIN(price), book_type
FROM book
GROUP BY book_type

因为原来没有接触过,现在可以确定的一个使用场景是:对每个不同分类下的最好/最坏…进行筛选并展示。

字符串切片

这是常用操作,讲某个字段值从前往后切割n位,SQL Server使用SUBSTRING(字段,起始位,终止位),MySQL使用SUBSTR(字段,起始位,终止位),注意这里的位置是从1开始计算的;

通用

1
2
3
-- Tom -> To
SELECT SUBSTRING(student_name, 1, 2)
FROM students

INEXIST的使用

找到一篇博文,博主讲的用法值得尝试,先记录,以观后效;

简言之,外表数据量>>子查询表数据量,使用IN合适;
反之,外表数据量<<子查询表数据量,使用EXIST合适;

使用MERGE进行表数据的同步

快速对表中的数据进行更新并且插入新数据——使用MERGE关键字,在对同数据库表进行表间数据更新的时候,使用MERGE可以说是最佳选择,速度很快,但是跨数据库服务器,则不能使用;

关于Merge操作数据的解释如图所示

Merge操作解释

Merge操作解释

通用

1
2
3
4
5
6
7
8
MERGE target_table USING source_table
ON merge_condition
WHEN MATCHED
THEN update_statement
WHEN NOT MATCHED
THEN insert_statement
WHEN NOT MATCHED BY SOURCE
THEN DELETE;

列出某个表的所有列名

通用

1
2
3
4
SELECT column_name, data_type
FROM information_schema
WHERE table_schema = 'SMEAGOL'
AND table_name = '表名'

创建分隔符列表

为表创建一个以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的处理

在进行聚合运算,例如:SUMAVG等函数时,函数本身不会对值为NULL的行进行计数和计算,所以,要得到一个客观的聚合值,则需要使用COALESCE等填充函数;

时间加减

当前日期向前/向后n天/月/年(但不能加减时/分/秒),MySQL可以直接使用INTERVAL ± 天数 DAY/MONTH/YEAR来操作,而SQL Server使用函数DATEADD()来操作;

SQL Sever

1
2
3
# GETDATE() = 2019/12/10 23:05:33
# 输出 2019/12/05 23:05:33
SELECT DATEADD(DAY, -5, GETDATE())

MySQL

1
2
3
# NOW() = 2019/12/10 23:05:33
# 输出 2019/12/05 23:05:33
SELECT NOW() - INTERVAL 5 DAY

获取当前系统时间

两个数据库各不相同,代码如下:

1
2
# 输出 2019/12/10 23:05:33
SELECT GETDATE()

MySQL

1
2
# 输出 2019/12/10 23:05:33
SELECT NOW()

计算两个日期之间的天数差

均使用DATEDIFF函数,只是MySQL只需要两个参数(终止时间, 起始时间),SQL Server需要三个参数(天/月/年, 起始时间, 终止时间);

SQL Sever

1
2
# 输出 3
SELECT DATEDIFF(DAY, '2019-10-07', '2019-10-10')

MySQL

1
2
# 输出 3
SELECT DATEDIFF('2019-10-10', '2019-10-07')

提取日期中的时分秒

MySQL使用DATE_FORMAT,SQL Server使用DATEPART;

SQL Sever

1
2
3
4
5
6
7
SELECT DATEPART(HOUR, GETDATE()) AS HOUR,
DATEPART(MINUTE, GETDATE()) AS MINUTE,
DATEPART(SECOND, GETDATE()) AS SECOND,
DATEPART(YEAR, GETDATE()) AS YEAR

HOUR MINUTE SECOND YEAR
22 14 30 2019

MySQL

1
2
3
4
5
6
7
8
SELECT DATE_FORMAT(NOW(),'%k') AS HOUR,
DATE_FORMAT(NOW(),'%i') AS MINUTE,
DATE_FORMAT(NOW(),'%s') AS SECOND,
DATE_FORMAT(NOW(),'%d') AS MONTH,
DATE_FORMAT(NOW(),'%Y') AS YEAR

HOUR MINUTE SECOND YEAR
22 14 30 2019

使用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
2
3
4
5
6
7
SELECT student_id, student_name, sudent_subsidy
FROM (SELECT student_id,
student_name,
sudent_subsidy,
DENSE_RANK() OVER(ORDER BY sudent_subsidy DESC) AS subsidy_rank
FROM students_info) AS infos
WHERE subsidy_rank <= 5

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
2
3
4
5
SELECT ROW_NUMBER() OVER(ORDER BY sudent_subsidy) AS row_num,
student_id,
sudent_name,
sudent_subsidy
FROM sudent_subsidy
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
2
3
4
5
SELECT RANK() OVER(ORDER BY sudent_subsidy) AS rank,
student_id,
sudent_name,
sudent_subsidy
FROM sudent_subsidy
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
2
3
4
5
SELECT DENSE_RANK() OVER(ORDER BY sudent_subsidy) AS rank,
student_id,
sudent_name,
sudent_subsidy
FROM sudent_subsidy
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
SELECT MAX(CASE WHEN job='CLERK'
THEN ename
ELSE NULL
END) AS 'CLERK',
MAX(CASE WHEN job='ANALYZER'
THEN ename
ELSE NULL
END) AS 'ANALYZER',
MAX(CASE WHEN job='MGRS'
THEN ename
ELSE NULL
END) AS 'MGRS',
MAX(CASE WHEN job='PREZ'
THEN ename
ELSE NULL
END) AS 'PREZ',
MAX(CASE WHEN job='SALES'
THEN ename
ELSE NULL
END) AS 'SALES'
FROM (SELECT ename,
job,
ROW_NUMBER() OVER(PARTITION BY job ORDER BY ename) AS 'rn'
FROM emp) AS e
GROUP BY rn

结果如下:

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
2
3
SELECT COALESCE(class, 'total'), SUM(sudent_subsidy)
FROM student_infos
GROUP BY ROLLUP(class)

MySQL

1
2
3
SELECT COALESCE(class, 'total'), SUM(sudent_subsidy)
FROM student_infos
GROUP BY class WITH ROLLUP

使用了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
2
3
SELECT class, class_name, GROUPING(class), GROUPING(class_name), SUM(sudent_subsidy) AS 'money'
FROM student_infos
GROUP BY ROLLUP(class, class_name)

MySQL

1
2
3
SELECT class, class_name, GROUPING(class), GROUPING(class_name), SUM(sudent_subsidy) AS 'money'
FROM student_infos
GROUP BY class, class_name WITH ROLLUP

输出的结果为

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
2
3
4
5
6
7
8
9
10
WITH result AS (
SELECT COALESC(class, '合计'), COALESC(class_name, '合计'),
CAST(GROUPING(class) AS CHAR(1)) + CAST(GROUPING(class_name) AS CHAR(1)) AS 'combine',
SUM(sudent_subsidy) AS 'total_money'
FROM student_infos
GROUP BY class, class_name WITH ROLLUP
)
SELECT class, class_name, total_money
FROM result
WHERE combine = '00'

MySQL

1
2
3
4
5
6
7
8
9
10
11
WITH result AS (
SELECT COALESC(class, '合计'), COALESC(class_name, '合计'),
CAST(GROUPING(class) AS CHAR(1)) + CAST(GROUPING(class_name) AS CHAR(1)) AS 'combine',
SUM(sudent_subsidy) AS 'total_money'
FROM student_infos
GROUP BY class, class_name WITH ROLLUP
)
SELECT class, class_name, total_money
FROM result
WHERE combine = '2'
OR combine = '0'

结果为:

class class_name money
A 尖子班 1000
B 快速班 1100
C 平行班 800
合计 合计 2900

GROUP BY 1/ORDER BY 1是什么意思

GROUP BY 1ORDER BY 1和使用列名的效果相同,12代替了SELECT语句中的列别名。

通用

1
2
3
4
5
6
SELECT account_id, open_emp_id
^^^^ ^^^^
1 2

FROM account
GROUP BY 1;

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
2
-- 输出为BCD
SELECT REPLACE('ABCD', 'A', '')

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
2
3
-- 输出为 123.6
-- DECIMAL(18, 1)中 18为整个数字的总位数 可以根据实际的情况进行调整 1为截断的位数
SELECT CAST(ROUND(123.55, 1) AS DECIMAL(18, 1))

MySQL

1
2
-- 输出为 123.5
SELECT ROUND(123.54, 1)

向上/向下取整

向上取整

SQL Server

1
2
3
4
5
-- 输出为 124
SELECT CEILING(123.54)

-- 输出为 123
SELECT CEILING(123)

MySQ

1
2
3
4
5
-- 输出为 124
SELECT CEILING/CEIL(123.54)

-- 输出为 123
SELECT CEILING/CEIL(123)

在MySQL中,CEILCEILING是同义的。

向下取整

通用

1
2
3
4
5
-- 输出为 123
SELECT FLOOR(123.23)

-- 输出为 123
SELECT FLOOR(123)

区分二叉树的节点类型

此题来源于:Binary Tree Nodes

若存在一颗二叉树,其存储方式如下:

node parent
1 2
3 2
6 8
9 8
2 5
8 5
5 NULL

我们需要将其划分为根节点叶子节点内部节点三种属性。
那么我们可以使用自连接来判断每个节点所在位置,而后进行输出。

通用

1
2
3
4
5
6
7
8
SELECT DISTINCT B1.N AS NODE,
CASE WHEN B1.P IS NULL THEN 'Root'
WHEN B2.N IS NULL AND B2.P IS NULL THEN 'Leaf'
ELSE 'Inner'
END AS ATTRIBUTE
FROM BST AS B1
LEFT JOIN BST AS B2 ON B1.N = B2.P
ORDER BY 1

结果为:

node attribute
1 Leaf
2 Inner
3 Leaf
5 Root
6 Leaf
8 Inner
9 Leaf

数字开方/绝对值/幂次方

开平方使用SQRT即可。

通用

1
2
3
-- 输出为2

SELECT SQRT(2, 3)

绝对值使用ABS即可。

通用

1
2
3
-- 输出为2

SELECT ABS(-2)

幂次方使用POWER即可。

通用

1
2
3
-- 输出为4

SELECT POWER(2, 2)

使用WITH进行复杂查询

如果一个查询过于复杂,无法在一个查询中完成所有操作,可以考虑使用WITH关键词进行分步操作。WITH可以用于分布操作,同样也可以用于递归,将上一个操作结果集中的数据继续进行相同的处理。

通用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
WITH STEP1 AS (
SELECT 'STEP 1 OPERATION'
FROM TEMP
), STEP 2 AS (
SELECT 'STEP 2 OPERATION CONTINUE WITH STEP 1'
FROM STEP1
), STEP3 AS(
...
), ...
, RESULT AS (
SELECT *
FROM STEPN
)
SELECT 'RESULT'
FROM RESULT

通过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
2
3
SELECT student, grade
FROM Students
INNER JOIN Grades ON score BETWEEN min_score AND max_score

结果

student grade
A 优秀
B 良好
C 较差

查看字符串是否包含特定字符

类似于Java的contains()函数,SQL Server和MySQL可以使用如下方式来得到结果,包含匹配的字符串,返回字符串的所在的第一个下标,不包含返回0

PS:匹配的字符串不仅可以是独立的字符,同样也可以是正则表达式。

SQL Server

1
2
3
4
5
6
7
-- 匹配的字符串存在的情况
SELECT PATINDEX('%O%', 'Oltremare')
-- 输出 1

-- 匹配的字符串不存在的情况
SELECT PATINDEX('%X%', 'Oltremare')
-- 输出 0

MySQL

1
2
3
4
5
6
7
-- 匹配的字符串存在的情况
SELECT INSTR('Oltremare', '%O%')
-- 输出 1

-- 匹配的字符串不存在的情况
SELECT INSTR('Oltremare', '%X%')
--输出 0

%用于模糊匹配,%X表示匹配以X结尾的X下标,反之,X%表示匹配以X开头的X下标,必须要使用至少一个%来进行匹配查询

待续…

-------- 本文结束 感谢阅读 --------