mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2026-05-10 19:54:28 +08:00
del
This commit is contained in:
11
极客时间专栏/geek/MySQL 必知必会/特别放送/期末测试 | 一套习题,测出你的掌握程度.md
Normal file
11
极客时间专栏/geek/MySQL 必知必会/特别放送/期末测试 | 一套习题,测出你的掌握程度.md
Normal file
@@ -0,0 +1,11 @@
|
||||
<audio id="audio" title="期末测试 | 一套习题,测出你的掌握程度" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fb/9f/fbc8c67187c5553c52dbfeefee55cf9f.mp3"></audio>
|
||||
|
||||
你好,我是朱晓峰。
|
||||
|
||||
咱们课程的核心内容都已经更新完了,在临近告别前,我还给你准备了一份期末测试题,这套测试题共有12道单选题和8道多选题,满分100,核心考点都出自前面讲到的所有重要知识,希望可以帮助你进行一场自测。
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/28/a4/28d1be62669b4f3cc01c36466bf811a4.png" alt="">](http://time.geekbang.org/quiz/intro?act_id=444&exam_id=1513)
|
||||
|
||||
另外,我还为你准备了一份[结课问卷](https://jinshuju.net/f/H556x1),希望听一听你对这门课的反馈。只要填写,就有机会获得一个手绘护腕垫或者是价值99元的课程阅码。欢迎你花1分钟时间填写一下,期待你的畅所欲言。
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/2c/34/2c9d5fc92a31e86a25f68bab00dc4734.jpg" alt="">](https://jinshuju.net/f/H556x1)
|
||||
252
极客时间专栏/geek/MySQL 必知必会/特别放送/特别发送(一) | 经典面试题讲解第一弹.md
Normal file
252
极客时间专栏/geek/MySQL 必知必会/特别放送/特别发送(一) | 经典面试题讲解第一弹.md
Normal file
@@ -0,0 +1,252 @@
|
||||
<audio id="audio" title="特别发送(一) | 经典面试题讲解第一弹" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/df/83/df7d7cff7a07c33c5bbf098c40ff1283.mp3"></audio>
|
||||
|
||||
你好,我是朱晓峰。
|
||||
|
||||
到这里,“实践篇”的内容咱们就学完了。今天,我们来学点儿不一样的——5道经典面试题。这些都是在实际面试中的原题,当然,我没有完全照搬,而是结合咱们课程的具体情况,有针对性地进行了调整。我不仅会给你提供答案,还会和你一起分析,让你能够灵活地吃透这些题目,并能举一反三。
|
||||
|
||||
话不多说,我们现在开始。我先带你从一道简单的关于“索引”的面试题入手,索引在面试题里经常出现,来看看这一道你能不能做对。
|
||||
|
||||
## 第一题
|
||||
|
||||
下面关于索引的描述,正确的是:
|
||||
|
||||
1. 建立索引的主要目的是减少冗余数据,使数据表占用更少的空间,并且提高查询的速度
|
||||
1. 一个表上可以建立一个或者多个索引
|
||||
1. 组合索引可以有效提高查询的速度,比单字段索引更高效,所以,我们应该创建一个由所有的字段组成的组合索引,这样就可以解决所有问题了
|
||||
1. 因为索引可以提高查询效率,所以索引建得越多越好
|
||||
|
||||
解析:这道题的正确答案是选项2,我们来分析一下其他选项。
|
||||
|
||||
- 选项1说对了一半,索引可以提高查询效率,但是创建索引不能减少冗余数据,而且索引还要占用额外的存储空间,所以选项1不对。
|
||||
- 选项3不对的原因有2个。第一,组合索引不一定比单字段索引高效,因为组合索引的字段是有序的,遵循左对齐的原则。如果查询的筛选条件不包含组合索引中最左边的字段,那么组合索引就完全不能用。第二,创建索引也是有成本的,需要占用额外的存储空间。用所有的字段创建组合索引的存储成本比较高,而且利用率比较低,完全用上的可能性几乎不存在,所以很少有人会这样做。而且一旦更改任何一个字段的数据,就必须要改索引,这样操作成本也比较高。
|
||||
- 选项4错误,因为索引有成本,很少作为筛选条件的字段,没有必要创建索引。
|
||||
|
||||
如果这道题你回答错了,一定要回去复习下[第11讲](https://time.geekbang.org/column/article/357312)的内容。
|
||||
|
||||
## 第二题
|
||||
|
||||
假设我们有这样一份学生成绩单,所有同学的成绩都各不相同,请编写一个简单的SQL语句,查询分数排在第三名的同学的成绩:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/36/d1/3618ee4c82a592bb7954c4d63d9c1dd1.jpeg" alt="">
|
||||
|
||||
解析:这道题考查的是我们对查询语句的掌握情况。针对题目中的场景,可以分两步来进行查询。
|
||||
|
||||
第一步,按照成绩高低进行排序:
|
||||
|
||||
```
|
||||
mysql> SELECT *
|
||||
-> FROM demo.test1
|
||||
-> ORDER BY score DESC; -- DESC表示降序排列
|
||||
+----+------+-------+
|
||||
| id | name | score |
|
||||
+----+------+-------+
|
||||
| 2 | 李四 | 90.00 |
|
||||
| 4 | 赵六 | 88.00 |
|
||||
| 1 | 张三 | 80.00 |
|
||||
| 3 | 王五 | 76.00 |
|
||||
| 5 | 孙七 | 67.00 |
|
||||
+----+------+-------+
|
||||
5 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
第二步,找出排名第三的同学和对应的成绩。我们可以用[第4讲](https://time.geekbang.org/column/article/351225)里学过的对返回记录进行限定的关键字LIMIT:
|
||||
|
||||
```
|
||||
mysql> SELECT *
|
||||
-> FROM demo.test1
|
||||
-> ORDER BY score DESC
|
||||
-> LIMIT 2,1;
|
||||
+----+------+-------+
|
||||
| id | name | score |
|
||||
+----+------+-------+
|
||||
| 1 | 张三 | 80.00 |
|
||||
+----+------+-------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
在MySQL中,LIMIT后面可以跟2个参数,第一个参数表示记录的起始位置(第一个记录的位置是0),第二个参数表示返回几条记录。因此,“LIMIT 2,1”就表示从第3条记录开始,返回1条记录。这样,就可以查出排名第三的同学的成绩了。
|
||||
|
||||
## 第三题
|
||||
|
||||
现在我们有两个表,分别是人员表(demo.person)和地址表(demo.address),要求你使用关联查询查出完整信息。无论有没有地址信息,人员的信息必须全部包含在结果集中。
|
||||
|
||||
人员表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b6/90/b6ed67787cebbdb0786a42c47326a390.jpeg" alt="">
|
||||
|
||||
地址表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/57/ea/5758763942c2a0dc59125bd73f3134ea.jpeg" alt="">
|
||||
|
||||
解析: 这个是典型的外查询,咱们在[第6讲](https://time.geekbang.org/column/article/353464)里学过。题目要求我们查出人员表中的全部信息,而地址表中信息则可以为空,就可以用下面的查询代码:
|
||||
|
||||
```
|
||||
mysql> SELECT *
|
||||
-> FROM demo.person AS a
|
||||
-> LEFT JOIN demo.address AS b -- 左连接,确保demo.person中的记录全部包括在结果集中
|
||||
-> ON (a.id=b.id);
|
||||
+----+-------+-------+------+---------+------+-----------+
|
||||
| id | fname | lname | id | country | city | address |
|
||||
+----+-------+-------+------+---------+------+-----------+
|
||||
| 1 | 张 | 三 | 1 | 中国 | 北京 | 海淀123 |
|
||||
| 2 | 李 | 四 | 2 | 美国 | 纽约 | 奥巴尼333 |
|
||||
| 3 | 王 | 五 | NULL | NULL | NULL | NULL |
|
||||
+----+-------+-------+------+---------+------+-----------+
|
||||
3 rows in set (0.02 sec)
|
||||
|
||||
```
|
||||
|
||||
## 第四题
|
||||
|
||||
假设有这样一个教学表(demo.teach),里面包含了人员编号(id)、姓名(fname)和对应的老师的人员编号(teacherid)。如果一个人是学生,那么他一定有对应的老师编号,通过这个编号,就可以找到老师的信息;如果一个人是老师,那么他对应的老师编号就是空。比如说,下表中李四的老师编号是101,我们就可以通过搜索人员编号,找到101的名称是张三,那么李四的老师就是张三;而张三自己就是老师,所以他对应的老师编号是空。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8d/7c/8d9de75354c0af5bc99e01767b87a57c.jpeg" alt="">
|
||||
|
||||
要求:请写一个SQL语句,查询出至少有2名学生的老师姓名。
|
||||
|
||||
说明一下,在刚刚的数据表中,张三有3名学生,分别是李四、王五和周八。赵六有一名学生,是孙七。因此,正确的SQL语句的查询结果应该是:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/40/e515868404495d2cea1c271b0d0ec440.jpeg" alt="">
|
||||
|
||||
解析:
|
||||
|
||||
针对这道题,我们可以按照这样的思路去做:
|
||||
|
||||
1. 通过统计学生对应的老师编号,就可以获取至少有2个学生的老师的编号。
|
||||
1. 通过关联查询和自连接获取需要的信息。所谓的自连接,就是数据表与自身进行连接。你可以认为是把数据表复制成一模一样的2个表,通过给数据表起不同的名字来区分它们,这样方便对表进行操作,然后就可以对这2个表进行连接操作了。
|
||||
1. 通过使用条件语句WHERE和HAVING对数据进行筛选:先用WHERE筛选出所有的老师编号,再用HAVING筛选出有2个以上学生的老师编号。
|
||||
|
||||
首先,我们来获取老师编号,如下:
|
||||
|
||||
```
|
||||
mysql> SELECT teacherid
|
||||
-> FROM demo.teach
|
||||
-> WHERE teacherid is not NULL -- 用WHERE筛选出所有的老师编号
|
||||
-> GROUP BY teacherid
|
||||
-> HAVING COUNT(*)>=2; -- 用HAVING筛选出有2个以上学生的老师编号
|
||||
+-----------+
|
||||
| teacherid |
|
||||
+-----------+
|
||||
| 101 |
|
||||
+-----------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
接着,通过自连接,来获取老师的姓名:
|
||||
|
||||
```
|
||||
mysql> SELECT a.id,a.fname
|
||||
-> FROM demo.teach AS a
|
||||
-> JOIN
|
||||
-> (
|
||||
-> SELECT teacherid
|
||||
-> FROM demo.teach
|
||||
-> WHERE teacherid IS NOT NULL
|
||||
-> GROUP BY teacherid
|
||||
-> HAVING COUNT(*)>=2
|
||||
-> ) AS b
|
||||
-> ON (a.id = b.teacherid);
|
||||
+-----+-------+
|
||||
| id | fname |
|
||||
+-----+-------+
|
||||
| 101 | 张三 |
|
||||
+-----+-------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
## 第五题
|
||||
|
||||
假设某中学高三年级有多位同学,分成多个班,我们有统一记录学生成绩的表(demo.student)和班级信息表(demo.class),具体信息如下所示:
|
||||
|
||||
学生成绩表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9a/20/9ae0eyy03386f24d568b8507d2dd6f20.jpeg" alt="">
|
||||
|
||||
班级信息表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/b9/6c4d85c4dff2c626d55fbaf9671bccb9.jpeg" alt="">
|
||||
|
||||
要求:写一个SQL查询语句,查出每个班级前三名的同学。
|
||||
|
||||
说明一下,针对上面的数据,正确的SQL查询应该得出下面的结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/cd/5c6fc34826c367f5a0cdf38610b26ecd.jpeg" alt="">
|
||||
|
||||
解析:
|
||||
|
||||
1. 从题目给出的查询结果看,不需要考虑并列的情况。那么,现在要选出分数排名前三的同学,其实只要找出3个最好的分数以及对应的同学就可以了。
|
||||
1. 这道题需要用到我们讲过的关联查询和子查询的知识。
|
||||
1. WHERE语句的筛选条件表达式中,也可以包括一个子查询的结果。
|
||||
|
||||
第一步,我们假设有一个分数X,就是那个第N好的分数,算一下有多少个同学的成绩优于这个分数:
|
||||
|
||||
```
|
||||
SELECT COUNT(DISTINCT b.points)
|
||||
FROM demo.student AS b
|
||||
WHERE b.points > X;
|
||||
|
||||
```
|
||||
|
||||
这个查询的结果小于3的话,就代表这个分数X是排名第三的分数了。
|
||||
|
||||
第二步,查询出哪些同学满足成绩排名前三的这个档次:
|
||||
|
||||
```
|
||||
mysql> SELECT a.stdname,a.points
|
||||
-> FROM demo.student AS a
|
||||
-> WHERE 3 > -- 比这个成绩好的不超过3,说明这是第三好的成绩
|
||||
-> (
|
||||
-> SELECT COUNT(DISTINCT b.points) -- 统计一下有几个成绩
|
||||
-> FROM demo.student AS b
|
||||
-> WHERE b.points > a.points -- 比这个成绩好
|
||||
-> );
|
||||
+---------+--------+
|
||||
| stdname | points |
|
||||
+---------+--------+
|
||||
| 张三 | 85 |
|
||||
| 李四 | 80 |
|
||||
| 赵六 | 90 |
|
||||
| 周八 | 85 |
|
||||
+---------+--------+
|
||||
4 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
第三步,与班级表关联,按班级统计前三名同学的成绩,并且获取班级信息:
|
||||
|
||||
```
|
||||
mysql> SELECT c.classname,a.stdname,a.points
|
||||
-> FROM demo.student AS a
|
||||
-> JOIN demo.class AS c
|
||||
-> ON (a.classid = c.id) -- 关联班级信息
|
||||
-> WHERE 3 >
|
||||
-> (
|
||||
-> SELECT COUNT(DISTINCT b.points)
|
||||
-> FROM demo.student AS b
|
||||
-> WHERE b.points > a.points
|
||||
-> AND b.classid = a.classid -- 按班级分别查询
|
||||
-> )
|
||||
-> ORDER BY c.classname,a.points DESC;
|
||||
+-----------+---------+--------+
|
||||
| classname | stdname | points |
|
||||
+-----------+---------+--------+
|
||||
| 创新班 | 赵六 | 90 |
|
||||
| 创新班 | 张三 | 85 |
|
||||
| 创新班 | 周八 | 85 |
|
||||
| 创新班 | 郑九 | 70 |
|
||||
| 普通班 | 李四 | 80 |
|
||||
| 普通班 | 王五 | 65 |
|
||||
+-----------+---------+--------+
|
||||
6 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
今天,我们借助几个面试题,回顾了索引的概念、查询、子查询和关联查询的知识,以及条件语句WHERE和HAVING的不同使用方法。如果你发现哪些内容掌握得还没有那么牢固,一定要及时回去复习一下。
|
||||
|
||||
在真正的面试中,很少有单纯考查知识点本身的题目,更多的是考查你在解决实际问题的过程中,对知识的灵活运用能力。所以,在学习每一节课时,你一定要结合我给出的实际项目,去真正实践一下,这样才能以不变应万变,在面试中有好的表现。
|
||||
356
极客时间专栏/geek/MySQL 必知必会/特别放送/特别放送(三)| MySQL 8 都有哪些新特征?.md
Normal file
356
极客时间专栏/geek/MySQL 必知必会/特别放送/特别放送(三)| MySQL 8 都有哪些新特征?.md
Normal file
@@ -0,0 +1,356 @@
|
||||
<audio id="audio" title="特别放送(三)| MySQL 8 都有哪些新特征?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/02/6e/02ca96e1f5961b2f740a41bf2557126e.mp3"></audio>
|
||||
|
||||
你好,我是朱晓峰。今天,我来和你聊一聊MySQL 8的新特征。
|
||||
|
||||
作为应用最广泛的三大关系型数据库之一,MySQL的背后有一个强大的开发团队,使MySQL能够持续迭代和创新,满足不断变化的用户需求。在MySQL 8中,就有很多新特征。
|
||||
|
||||
今天,我就给你介绍两个重要的新特征:窗口函数和公用表表达式(Common Table Expressions,简称CTE)。它们可以帮助我们用相对简单的查询语句,实现更加强大的查询功能。
|
||||
|
||||
## 什么是窗口函数?
|
||||
|
||||
窗口函数的作用类似于在查询中对数据进行分组,不同的是,分组操作会把分组的结果聚合成一条记录,而窗口函数是将结果置于每一条数据记录中。一会儿我会借助一个例子来对比下,在此之前,你要先掌握窗口函数的语法结构。
|
||||
|
||||
窗口函数的语法结构是:
|
||||
|
||||
```
|
||||
函数 OVER([PARTITION BY 字段])
|
||||
|
||||
```
|
||||
|
||||
或者是:
|
||||
|
||||
```
|
||||
函数 OVER 窗口名 … WINDOW 窗口名 AS ([PARTITION BY 字段名])
|
||||
|
||||
```
|
||||
|
||||
现在,我借助一个小例子来解释一下窗口函数的用法。
|
||||
|
||||
假设我现在有这样一个数据表,它显示了某购物网站在每个城市每个区的销售额:
|
||||
|
||||
```
|
||||
mysql> SELECT * FROM demo.test1;
|
||||
+----+------+--------+------------+
|
||||
| id | city | county | salesvalue |
|
||||
+----+------+--------+------------+
|
||||
| 1 | 北京 | 海淀 | 10.00 |
|
||||
| 2 | 北京 | 朝阳 | 20.00 |
|
||||
| 3 | 上海 | 黄埔 | 30.00 |
|
||||
| 4 | 上海 | 长宁 | 10.00 |
|
||||
+----+------+--------+------------+
|
||||
4 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
现在我想计算一下,这个网站在每个城市的销售总额、在全国的销售总额、每个区的销售额占所在城市销售额中的比率,以及占总销售额中的比率。
|
||||
|
||||
如果用分组和聚合函数,就需要分好几步来计算。
|
||||
|
||||
第一步,计算总销售金额,并存入临时表demo.a:
|
||||
|
||||
```
|
||||
mysql> CREATE TEMPORARY TABLE demo.a -- 创建临时表
|
||||
-> SELECT SUM(salesvalue) AS salesvalue -- 计算总计金额
|
||||
-> FROM demo.test1;
|
||||
Query OK, 1 row affected (0.02 sec)
|
||||
Records: 1 Duplicates: 0 Warnings: 0
|
||||
|
||||
```
|
||||
|
||||
我们查看一下临时表demo.a的内容,来验证一下计算结果:
|
||||
|
||||
```
|
||||
mysql> SELECT * FROM demo.a;
|
||||
+------------+
|
||||
| salesvalue |
|
||||
+------------+
|
||||
| 70.00 |
|
||||
+------------+
|
||||
1 row in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
结果显示,总计金额已经存入临时表demo.a中了。
|
||||
|
||||
第二步,计算每个城市的销售总额并存入临时表demo.b:
|
||||
|
||||
```
|
||||
mysql> CREATE TEMPORARY TABLE demo.b -- 创建临时表
|
||||
-> SELECT city,SUM(salesvalue) AS salesvalue -- 计算城市销售合计
|
||||
-> FROM demo.test1
|
||||
-> GROUP BY city;
|
||||
Query OK, 2 rows affected (0.01 sec)
|
||||
Records: 2 Duplicates: 0 Warnings: 0
|
||||
|
||||
```
|
||||
|
||||
我们查看一下临时表demo.b的内容,验证一下计算的结果:
|
||||
|
||||
```
|
||||
mysql> SELECT * FROM demo.b;
|
||||
+------+------------+
|
||||
| city | salesvalue |
|
||||
+------+------------+
|
||||
| 北京 | 30.00 |
|
||||
| 上海 | 40.00 |
|
||||
+------+------------+
|
||||
2 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
结果显示,每个城市的销售总计金额已经计算成功了。
|
||||
|
||||
第三步,计算各区的销售占所在城市的总计金额的比例,和占全部销售总计金额的比例。我们可以通过下面的连接查询获得需要的结果:
|
||||
|
||||
```
|
||||
mysql> SELECT a.city AS 城市,a.county AS 区,a.salesvalue AS 区销售额,
|
||||
-> b.salesvalue AS 市销售额,a.salesvalue/b.salesvalue AS 市比率,
|
||||
-> c.salesvalue AS 总销售额,a.salesvalue/c.salesvalue AS 总比率
|
||||
-> FROM demo.test1 AS a
|
||||
-> JOIN demo.b AS b ON (a.city=b.city) -- 连接市统计结果临时表
|
||||
-> JOIN demo.a AS c -- 连接总计金额临时表
|
||||
-> ORDER BY a.city,a.county;
|
||||
+------+------+----------+----------+----------+----------+----------+
|
||||
| 城市 | 区 | 区销售额 | 市销售额 | 市比率 | 总销售额 | 总比率 |
|
||||
+------+------+----------+----------+----------+----------+----------+
|
||||
| 上海 | 长宁 | 10.00 | 40.00 | 0.250000 | 70.00 | 0.142857 |
|
||||
| 上海 | 黄埔 | 30.00 | 40.00 | 0.750000 | 70.00 | 0.428571 |
|
||||
| 北京 | 朝阳 | 20.00 | 30.00 | 0.666667 | 70.00 | 0.285714 |
|
||||
| 北京 | 海淀 | 10.00 | 30.00 | 0.333333 | 70.00 | 0.142857 |
|
||||
+------+------+----------+----------+----------+----------+----------+
|
||||
4 rows in set (0.01 sec)
|
||||
|
||||
```
|
||||
|
||||
结果显示:市销售金额、市销售占比、总销售金额、总销售占比都计算出来了。
|
||||
|
||||
同样的查询,如果用窗口函数,就简单多了。我们可以用下面的代码来实现:
|
||||
|
||||
```
|
||||
mysql> SELECT city AS 城市,county AS 区,salesvalue AS 区销售额,
|
||||
-> SUM(salesvalue) OVER(PARTITION BY city) AS 市销售额, -- 计算市销售额
|
||||
-> salesvalue/SUM(salesvalue) OVER(PARTITION BY city) AS 市比率,
|
||||
-> SUM(salesvalue) OVER() AS 总销售额, -- 计算总销售额
|
||||
-> salesvalue/SUM(salesvalue) OVER() AS 总比率
|
||||
-> FROM demo.test1
|
||||
-> ORDER BY city,county;
|
||||
+------+------+----------+----------+----------+----------+----------+
|
||||
| 城市 | 区 | 区销售额 | 市销售额 | 市比率 | 总销售额 | 总比率 |
|
||||
+------+------+----------+----------+----------+----------+----------+
|
||||
| 上海 | 长宁 | 10.00 | 40.00 | 0.250000 | 70.00 | 0.142857 |
|
||||
| 上海 | 黄埔 | 30.00 | 40.00 | 0.750000 | 70.00 | 0.428571 |
|
||||
| 北京 | 朝阳 | 20.00 | 30.00 | 0.666667 | 70.00 | 0.285714 |
|
||||
| 北京 | 海淀 | 10.00 | 30.00 | 0.333333 | 70.00 | 0.142857 |
|
||||
+------+------+----------+----------+----------+----------+----------+
|
||||
4 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
结果显示,我们得到了与上面那种查询同样的结果。
|
||||
|
||||
你看,使用窗口函数,我们只用了一步就完成了查询,过程简单多了。而且,由于没有用到临时表,执行的效率也更高了。很显然,**在这种需要用到分组统计的结果对每一条记录进行计算的场景下,使用窗口函数更好**。
|
||||
|
||||
除了可以进行分组统计,窗口函数还有一些自己独有的函数,可以对分组内的数据进行处理,比较常用的就是排序函数RANK()、DENSE_RANK()和ROW_NUMBER()。
|
||||
|
||||
为了帮助你理解这几个函数的作用,我举个小例子。
|
||||
|
||||
假设我们有这样一张学生成绩表:
|
||||
|
||||
```
|
||||
mysql> SELECT * FROM demo.test2;
|
||||
+----+---------+--------+
|
||||
| id | student | points |
|
||||
+----+---------+--------+
|
||||
| 1 | 张三 | 89 |
|
||||
| 2 | 李四 | 77 |
|
||||
| 3 | 王五 | 88 |
|
||||
| 4 | 赵六 | 90 |
|
||||
| 5 | 孙七 | 90 |
|
||||
| 6 | 周八 | 88 |
|
||||
+----+---------+--------+
|
||||
6 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
如果我们需要对表中的数据进行排序,就可以使用排序函数,代码如下所示:
|
||||
|
||||
```
|
||||
mysql> SELECT student,points,
|
||||
-> RANK() OVER w AS 排序1,
|
||||
-> DENSE_RANK() OVER w AS 排序2,
|
||||
-> ROW_NUMBER() OVER w AS 排序3
|
||||
-> FROM demo.test2
|
||||
-> WINDOW w AS (ORDER BY points DESC);
|
||||
+---------+--------+-------+-------+-------+
|
||||
| student | points | 排序1 | 排序2 | 排序3 |
|
||||
+---------+--------+-------+-------+-------+
|
||||
| 赵六 | 90 | 1 | 1 | 1 |
|
||||
| 孙七 | 90 | 1 | 1 | 2 |
|
||||
| 张三 | 89 | 3 | 2 | 3 |
|
||||
| 王五 | 88 | 4 | 3 | 4 |
|
||||
| 周八 | 88 | 4 | 3 | 5 |
|
||||
| 李四 | 77 | 6 | 4 | 6 |
|
||||
+---------+--------+-------+-------+-------+
|
||||
6 rows in set (0.01 sec)
|
||||
|
||||
```
|
||||
|
||||
结果显示:
|
||||
|
||||
- RANK()函数把并列计算在内,并且并列影响排位;
|
||||
- DENSE_RANK()函数也计算并列,但是并列不影响排位;
|
||||
- ROW_NUMBER()函数不计算并列,只是简单排序。
|
||||
|
||||
因此,我们就可以根据这些函数的特点,计算分组中的排位信息。如果不计算并列,就用ROW_NUMBER()函数;计算并列但不占用位次,就用DENSE_RANK()函数;计算并列且占用位次,就用RANK()函数。
|
||||
|
||||
接下来,我们再来学习MySQL 8的另一个重要新特征:公用表表达式。
|
||||
|
||||
## 什么是公用表表达式?
|
||||
|
||||
公用表表达式是一个命名的临时结果集。它存在于单个查询语句中,主要作用就是可以代替子查询,并且可以被后面的查询多次引用。
|
||||
|
||||
依据语法结构和执行方式的不同,公用表表达式分为普通公用表表达式和递归公用表表达式2种。
|
||||
|
||||
### 什么是普通公用表表达式?
|
||||
|
||||
普通公用表表达式的语法结构是:
|
||||
|
||||
```
|
||||
WITH
|
||||
CTE名称 AS (子查询)
|
||||
SELECT|DELETE|UPDATE 语句;
|
||||
|
||||
```
|
||||
|
||||
普通公用表表达式类似于子查询,不过,跟子查询不同的是,它可以被多次引用,而且可以被其他的普通公用表表达式所引用。
|
||||
|
||||
举个小例子,假设我们有一个商品信息表(demo.goodsmaster),它保存的是商品信息,还有一个每日销售统计表(demo.dailystatistics),保存的是每日的销售统计信息。现在超市经营者想要查出都卖了什么商品,我们就可以先通过子查询查出所有销售过的商品的商品编号,然后查出这些商品的商品信息,代码如下所示:
|
||||
|
||||
```
|
||||
mysql> SELECT * FROM demo.goodsmaster
|
||||
-> WHERE itemnumber IN
|
||||
-> (SELECT DISTINCT itemnumber -- 子查询,查出所有销售过的商品的商品编号
|
||||
-> FROM demo.dailystatistics);
|
||||
+------------+---------+-----------+---------------+------+------------+
|
||||
| itemnumber | barcode | goodsname | specification | unit | salesprice |
|
||||
+------------+---------+-----------+---------------+------+------------+
|
||||
| 1 | 0001 | 书 | 16开 | 本 | 89.00 |
|
||||
| 2 | 0002 | 笔 | 黑色 | 支 | 3.00 |
|
||||
| 3 | 0003 | 胶水 | 无机 | 瓶 | 15.00 |
|
||||
+------------+---------+-----------+---------------+------+------------+
|
||||
3 rows in set (0.01 sec)
|
||||
|
||||
```
|
||||
|
||||
这个查询也可以用普通公用表表达式的方式完成:
|
||||
|
||||
```
|
||||
mysql> WITH
|
||||
-> cte AS (SELECT DISTINCT itemnumber FROM demo.dailystatistics)
|
||||
-> SELECT *
|
||||
-> FROM demo.goodsmaster a JOIN cte
|
||||
-> ON (a.itemnumber = cte.itemnumber);
|
||||
+------------+---------+-----------+---------------+------+------------+------------+
|
||||
| itemnumber | barcode | goodsname | specification | unit | salesprice | itemnumber |
|
||||
+------------+---------+-----------+---------------+------+------------+------------+
|
||||
| 1 | 0001 | 书 | 16开 | 本 | 89.00 | 1 |
|
||||
| 2 | 0002 | 笔 | 黑色 | 支 | 3.00 | 2 |
|
||||
| 3 | 0003 | 胶水 | 无机 | 瓶 | 15.00 | 3 |
|
||||
+------------+---------+-----------+---------------+------+------------+------------+
|
||||
3 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
可以看到,普通公用表表达式代替了第一种查询方式中的子查询,并且得到了同样的结果。
|
||||
|
||||
这个例子说明,公用表表达式可以起到子查询的作用。以后如果遇到需要使用子查询的场景,你可以在查询之前,先定义公用表表达式,然后在查询中用它来代替子查询。而且,跟子查询相比,公用表表达式有一个优点,就是定义过公用表表达式之后的查询,可以像一个表一样多次引用公用表表达式,而子查询则不能。
|
||||
|
||||
好了,我们再来学习下递归公用表表达式。
|
||||
|
||||
### 什么是递归公用表表达式?
|
||||
|
||||
递归公用表表达式也是一种公用表表达式,只不过,除了普通公用表表达式的特点以外,它还有自己的特点,就是**可以调用自己**。它的语法结构是:
|
||||
|
||||
```
|
||||
WITH RECURSIVE
|
||||
CTE名称 AS (子查询)
|
||||
SELECT|DELETE|UPDATE 语句;
|
||||
|
||||
```
|
||||
|
||||
递归公用表表达式由2部分组成,分别是种子查询和递归查询,中间通过关键字UION [ALL]进行连接。这里的**种子查询,意思就是获得递归的初始值**。这个查询只会运行一次,以创建初始数据集,之后递归查询会一直执行,直到没有任何新的查询数据产生,递归返回。
|
||||
|
||||
同样,为了帮助你理解递归公用表表达式的工作原理,我来举个小例子。
|
||||
|
||||
假设我们有这样一张人员信息表(demo.teach),里面包含人员编号、名称和老师编号。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a0/ba/a033830c05999ed5c3b5aa2f56710aba.jpeg" alt="">
|
||||
|
||||
如果甲是乙的老师,那么,我们可以把乙叫做甲的徒子,如果同时乙又是丙的老师,那么丙就是乙的徒子,是甲的徒孙。
|
||||
|
||||
下面我们尝试用查询语句列出所有具有徒孙身份的人员信息。
|
||||
|
||||
如果用我们之前学过的知识来解决,会比较复杂,至少要进行4次查询才能搞定:
|
||||
|
||||
- 第一步,先找出初代老师,就是不以任何人为老师的人,把结果存入临时表;
|
||||
- 第二步,找出所有以初代老师为师的人,得到一个徒子集,把结果存入临时表;
|
||||
- 第三步,找出所有以徒子为师的人,得到一个徒孙集,把结果存入临时表。
|
||||
- 第四步,找出所有以徒孙为师的人,得到一个结果集。
|
||||
|
||||
如果第四步的结果集为空,则计算结束,第三步的结果集就是我们需要的徒孙集了,否则就必须继续进行第四步,一直到结果集为空为止。比如上面的这个数据表,就需要到第五步,才能得到空结果集。而且,最后还要进行第六步:把第三步和第四步的结果集合并,这样才能最终获得我们需要的结果集。
|
||||
|
||||
如果用递归公用表表达式,就非常简单了。我介绍下具体的思路。
|
||||
|
||||
- 用递归公用表表达式中的种子查询,找出初代老师。字段n表示代次,初始值为1,表示是第一代老师。
|
||||
- 用递归公用表表达式中的递归查询,查出以这个递归公用表表达式中的人为老师的人,并且代次的值加1。直到没有人以这个递归公用表表达式中的人为老师了,递归返回。
|
||||
- 在最后的查询中,选出所有代次大于等于3的人,他们肯定是第三代及以上代次的学生了,也就是徒孙了。这样就得到了我们需要的结果集。
|
||||
|
||||
这里看似也是3步,实际上是一个查询的3个部分,只需要执行一次就可以了。而且也不需要用临时表保存中间结果,比刚刚的方法简单多了。
|
||||
|
||||
下面是具体的代码:
|
||||
|
||||
```
|
||||
mysql> WITH RECURSIVE
|
||||
-> cte AS (
|
||||
-> SELECT id,fname,teacherid,1 AS n FROM demo.teach WHERE id = 101 -- 种子查询,找到第一代老师
|
||||
-> UNION ALL
|
||||
-> SELECT a.id,a.fname,a.teacherid,n+1 FROM demo.teach AS a JOIN cte
|
||||
-> ON (a.teacherid = cte.id) -- 递归查询,找出以递归公用表表达式的人为老师的人
|
||||
-> )
|
||||
-> SELECT id,fname FROM cte WHERE n>=3; -- 从结果集中筛选代次大于等于3的,得到所有徒孙集
|
||||
+------+-------+
|
||||
| id | fname |
|
||||
+------+-------+
|
||||
| 103 | 王五 |
|
||||
| 105 | 孙七 |
|
||||
| 106 | 周八 |
|
||||
+------+-------+
|
||||
3 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
结果显示,王五、孙七和周八是徒孙。结果显然是正确的。
|
||||
|
||||
总之,递归公用表表达式对于查询一个有共同的根节点的树形结构数据,非常有用。它可以不受层级的限制,轻松查出所有节点的数据。如果用其他的查询方式,就比较复杂了。
|
||||
|
||||
## 总结
|
||||
|
||||
这节课,我们学习了MySQL 8的2个重要新功能:窗口函数和公用表表达式。
|
||||
|
||||
窗口函数的特点是可以分组,而且可以在分组内排序。另外,窗口函数不会因为分组而减少原表中的行数,这对我们在原表数据的基础上进行统计和排序非常有用。
|
||||
|
||||
公用表表达式的作用是可以替代子查询,而且可以被多次引用。递归公用表表达式对查询有一个共同根节点的树形结构数据非常高效,可以轻松搞定其他查询方式难以处理的查询。
|
||||
|
||||
当然,除了今天学习的窗口函数和公用表表达式,MySQL 8还有许多其他的新特征,比如,完善了对空间位置信息的处理;支持对表的DDL操作(创建、修改和删除表)的原子性,使得`CREATE TABLE ...SELECT`语句能够成为一个原子操作,提高了数据安全性,等等。
|
||||
|
||||
如果你想要从旧版本切换到MySQL 8,课下可以点击这个[链接](https://dev.mysql.com/doc/refman/8.0/en/mysql-nutshell.html)进一步了解一下。
|
||||
|
||||
## 思考题
|
||||
|
||||
假设我有一个会员销售统计表(demo.memtrans),其中包括会员名称、商品名称和销售金额,具体数据如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f8/53/f8d3900a108cff5680e5c7b9d9ff0e53.jpeg" alt="">
|
||||
|
||||
请使用窗口函数查询会员名称、商品名称、销售金额、总计金额和销售占比。
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你把它分享给你的朋友或同事,我们下节课见。
|
||||
176
极客时间专栏/geek/MySQL 必知必会/特别放送/特别放送(二)| 经典面试题讲解第二弹.md
Normal file
176
极客时间专栏/geek/MySQL 必知必会/特别放送/特别放送(二)| 经典面试题讲解第二弹.md
Normal file
@@ -0,0 +1,176 @@
|
||||
<audio id="audio" title="特别放送(二)| 经典面试题讲解第二弹" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7f/bc/7f9585f59083444c3c3c044c7210ebbc.mp3"></audio>
|
||||
|
||||
你好,我是朱晓峰。
|
||||
|
||||
到这里,“进阶篇”的内容咱们就学完了。今天,我给你准备了7道面试题。这些面试题涵盖了这个模块的核心内容,我们一起借助面试题来复习一下。我不仅会给你提供正确答案,还会带你深入分析这些问题,让你真正能够做到举一反三。
|
||||
|
||||
好了,话不多说,我们现在开始。
|
||||
|
||||
## 第一题
|
||||
|
||||
**日志文件对数据库的故障恢复至关重要。下面这些关于日志文件的描述,正确的是:**
|
||||
|
||||
1. MySQL日志记录的顺序可以与并发操作的执行顺序不一致
|
||||
1. 为了确保数据库是可恢复的,必须确保先写数据库后写日志
|
||||
1. 日志文件中检查点的主要作用是提高系统出现故障后恢复的效率
|
||||
1. 系统故障恢复必须使用日志文件以保证数据库系统重启时能正常恢复,事务故障恢复不一定需要使用日志文件
|
||||
|
||||
答案:选项3。
|
||||
|
||||
解析:
|
||||
|
||||
选项1是错误的。MySQL日志记录的顺序是严格按照操作执行的顺序记录的,不会出现日志记录的顺序与并发操作执行的顺序不一致的情况。
|
||||
|
||||
选项2也是错误的。MySQL的日志系统遵循WAL(Write-Ahead Logging)机制,也就是所谓的先写日志,后写数据库的机制。由于记录日志是顺序写入磁盘,而写入数据库的磁盘操作需要对磁头定位,因而写入日志的速度要远比写入数据快。为了提高执行的效率,MySQL执行的是先写日志,日志写入成功之后,就回复客户端操作成功,对数据库的磁盘写入则在之后的某个阶段执行。这样,即便遇到系统故障,由于有了日志记录,就可以通过日志对数据库进行恢复了。WAL机制包括3个规则:
|
||||
|
||||
- 对数据的修改操作必须在写入数据库之前写入到日志文件中;
|
||||
- 日志必须严格按序记录,就是说,如果A操作发生在B操作之前,那么在日志中,A操作的记录必须在B操作的记录之前;
|
||||
- 事务提交之后,必须在日志写入成功之后,才能回复事务处理成功。
|
||||
|
||||
选项3是正确的,检查点的作用是加快数据恢复的效率。当修改数据时,为了提高存取效率,采用的策略是先记录日志,实际的数据修改则发生在内存中,这些数据修改是没有写入数据库的,叫做脏页。MySQL把这些脏页分成小的批次,逐步写入磁盘中。因为如果把所有的脏页都一次性写入磁盘,会导致磁盘写入时间过长,影响到用户SQL操作的执行。检查点的作用就是标记哪些脏页已经写入磁盘。这样,当遇到故障时,MySQL可以从检查点的位置开始,执行日志记录的SQL操作,而不是把整个日志都检查一遍,所以,大大提高了故障恢复的效率。
|
||||
|
||||
选项4也是错误的。系统故障恢复必须使用日志文件以保证数据库系统重启时能正常恢复,这个表述是正确的,但后面的表述“事务故障恢复不一定需要使用日志文件”则是错误的。事务故障的恢复也必须要用到日志文件,事务故障恢复需要用到的日志文件有3个,分别是回滚日志、重做日志和二进制日志。
|
||||
|
||||
- 回滚日志:如果事务发生故障,可以借助回滚日志,恢复到故障前的状态,所以回滚日志对事务故障恢复有用。
|
||||
- 重做日志:事务中的操作对数据更新进行提交时,会记录到重做日志,对数据的更新则只会发生在内存中,实际的数据更新写入磁盘,则会由后台的其他进程异步执行。如果这个时候事务故障,内存中的数据丢失,就必须要借助重做日志来找回。所以,重做日志对事务故障恢复有用。
|
||||
- 二进制日志:在主从服务器架构的模式下,从服务器完全依赖二进制日志同步主服务器的操作,如果事务发生故障,从服务器只能依靠主服务器的二进制日志恢复。
|
||||
|
||||
## 第二题
|
||||
|
||||
**MySQL支持事务处理吗?**
|
||||
|
||||
参考答案:
|
||||
|
||||
这个问题跟数据表用的是什么存储引擎有关。如果用的是Innodb,则支持事务处理;如果用的是MyISAM,则不支持事务处理。
|
||||
|
||||
MySQL 8.0 默认的存储引擎是Innodb,Innodb是支持事务处理的。在默认的情况下,MySQL启用AUTOCOMMIT模式,也就是每一个SQL操作都是一个事务操作,如果操作执行没有返回错误,MySQL会提交事务;如果操作返回错误,MySQL会执行事务回滚。
|
||||
|
||||
你也可以通过执行“START TRANSACTION”或者“BEGIN”来开始一个事务,这种情况下,就可以在事务处理中包含多个SQL操作语句,一直到“COMMIT”语句提交事务,或者是“ROLLBACK”语句回滚事务,来结束一个事务操作。
|
||||
|
||||
MyISAM存储引擎是不支持事务操作的,如果你对一个存储引擎是MyISAM的数据表执行事务操作,不管你是否执行“COMMIT”或者是“ROLLBACK”,都不会影响操作的结果。你可以通过下面的SQL语句来查看表的存储引擎:
|
||||
|
||||
```
|
||||
mysql> SHOW CREATE TABLE demo.test;
|
||||
+-------+------------------------------------------------------------------------------------------------------------------+
|
||||
| Table | Create Table |
|
||||
+-------+------------------------------------------------------------------------------------------------------------------+
|
||||
| test | CREATE TABLE `test` (
|
||||
`aa` int DEFAULT NULL
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
|
||||
+-------+------------------------------------------------------------------------------------------------------------------+
|
||||
1 row in set (0.02 sec)
|
||||
|
||||
```
|
||||
|
||||
## 第三题
|
||||
|
||||
**下面这些关于MySQL视图的描述中,错误的是:**
|
||||
|
||||
1. 视图是MySQL提供给用户以多种角度观察数据库中数据的重要机制
|
||||
1. 视图可以对重构数据库提供一定程度的逻辑独立性
|
||||
1. 所有视图都是可查询和可更新的
|
||||
1. 对视图的一切操作都会转换成对数据表的操作
|
||||
|
||||
答案:选项3。
|
||||
|
||||
解析:
|
||||
|
||||
选项1的描述是正确的。视图其实就是存储在服务器中的查询语句,可以通过编写不同的查询语句,从数据库中抽取需要的数据。也就相当于从多种角度观察数据库中的数据。
|
||||
|
||||
选项2的描述是正确的。视图通过查询语句从数据库中抽取需要的数据,如果遇到数据表结构的变更,可以通过修改查询语句,使视图返回的结果集中的字段保持不变。因此,视图可以一定程度不受实际数据表结构的变化的影响,具备一定的逻辑独立性。
|
||||
|
||||
选项3的描述是错误的,所有的视图都可以查询,但不是所有的视图都可以更新。比如,定义视图的查询语句如果包含分组,由于无法对实际数据表中的记录定位,所以这样的视图是不支持数据更新操作的。
|
||||
|
||||
选项4的描述也是正确的。视图不过是一段查询语句,视图并不保存实际数据,对视图的任何操作,最终都会转换到实际数据表。
|
||||
|
||||
## 第四题
|
||||
|
||||
**下面关于存储过程的描述中,不正确的是:**
|
||||
|
||||
1. 存储过程实际上是一组SQL语句
|
||||
1. 存储过程预先被编译存放在数据库服务器上
|
||||
1. 存储过程与数据库无关
|
||||
1. 存储过程可以完成某一特定的业务逻辑
|
||||
|
||||
答案:选项3。
|
||||
|
||||
解析:
|
||||
|
||||
选项1的描述正确,存储过程的程序体就是一组SQL语句,调用存储过程,实际上就是执行了一组SQL操作。
|
||||
|
||||
选项2的描述正确,存储过程创建完成之后,就是经过了编译的过程,以服务可以执行的形式存放在服务器中。
|
||||
|
||||
选项3的描述是错误的,存储过程是数据库的一部分,不能独立于数据库而存在。
|
||||
|
||||
选项4的描述是正确的,创建存储过程的目的,就是为了高效地完成某一特定的业务逻辑。
|
||||
|
||||
## 第五题
|
||||
|
||||
在MySQL中,有学生表(学号,姓名,年龄,所在系),其中学号是主键。在这个表上建有视图V1,V1的定义语句是:
|
||||
|
||||
```
|
||||
CREATE VIEW V1 AS SELECT 姓名,年龄,所在系
|
||||
FROM 学生表 WHERE 年龄>=(SELECT AVG(年龄) FROM 学生表);
|
||||
|
||||
```
|
||||
|
||||
有下列操作语句:
|
||||
|
||||
```
|
||||
A.UPDATE V1 SET 年龄=年龄-1;
|
||||
B.SELECT * FROM V1 WHERE 年龄>20;
|
||||
C.DELETE FROM V1 WHERE 年龄>20;
|
||||
D.INSERT INTO V1 VALUES (‘张三’,20,’计算机系’);
|
||||
|
||||
```
|
||||
|
||||
以上语句能够正确执行的是:
|
||||
|
||||
1. A、B和D
|
||||
1. 仅B
|
||||
1. 仅B和C
|
||||
1. 仅A、B和C
|
||||
|
||||
答案:选项2。
|
||||
|
||||
解析:这道题考核的要点是,如果定义视图的SQL语句中包含了聚合函数,就不能对视图中的数据进行DML操作。
|
||||
|
||||
## 第六题
|
||||
|
||||
**什么是游标?如何知道游标已经走到了最后?**
|
||||
|
||||
参考答案:
|
||||
|
||||
由SELECT语句返回的完整行集(包括满足WHERE语句中条件的所有行)称为结果集。MySQL中的SQL操作,会对整个结果集起作用。应用程序,特别是交互式的联机应用程序,并不总能将整个结果集作为一个单元来进行有效的处理,这些应用程序需要一种机制,以便每次处理一行或者一部分行。游标就是提供这种机制的,是对结果集的一种扩展。
|
||||
|
||||
具体而言,MySQL的游标有以下特点:
|
||||
|
||||
- 可以逐条查看数据集中的记录;
|
||||
- 一次只能查看一条记录;
|
||||
- 只能向一个方向扫描数据,并且不能跳跃;
|
||||
- 游标是只读的。
|
||||
|
||||
可以使用条件处理语句“DECLARE CONTINUE HANDLER FOR NOT FOUND”,来判断游标到达了结果集的最后。
|
||||
|
||||
## 第七题
|
||||
|
||||
下面关于触发器的叙述中,错误的是:
|
||||
|
||||
1. 触发器是一种特殊的存储程序
|
||||
1. 触发器必须创建在一个特殊的表中
|
||||
1. 触发器通过CALL调用
|
||||
1. 触发器有助于实现数据库的完整性、安全性和主动性
|
||||
|
||||
答案:选项3。
|
||||
|
||||
解析:
|
||||
|
||||
触发器也是存储在服务器端,是一种存储程序,触发器监控的对象是一个特定的表,触发器可以记录日志、进行安全性检查,并且无需应用程序的控制,可以由数据操作的事件驱动,所以选项1、2、4都是正确的。
|
||||
|
||||
触发器不能由CALL调用,而是由事件驱动,所以3是错误的。
|
||||
|
||||
## 总结
|
||||
|
||||
今天,我们借助几个面试题,回顾了日志、游标、视图、存储过程和触发器的相关内容。如果你发现回答这些试题有困难,一定要及时回去复习一下。
|
||||
|
||||
刷题不是目的,真正的目的是对你掌握知识的熟练程度进行检验,发现问题,查漏补缺。只有深入了解相关的知识背景、工具的运行机制和原理,才能牢固掌握,在实践中灵活运用。
|
||||
246
极客时间专栏/geek/MySQL 必知必会/特别放送/特别放送(四)| 位置信息:如何进行空间定位?.md
Normal file
246
极客时间专栏/geek/MySQL 必知必会/特别放送/特别放送(四)| 位置信息:如何进行空间定位?.md
Normal file
@@ -0,0 +1,246 @@
|
||||
<audio id="audio" title="特别放送(四)| 位置信息:如何进行空间定位?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/db/7584e459a5404f7b86e93147e08273db.mp3"></audio>
|
||||
|
||||
你好,我是朱晓峰。今天,我来和你聊一聊怎么进行空间定位。
|
||||
|
||||
我们每天都会用到空间数据,比如你在网上购买一件商品,你手机上的App就能够算出你是不是需要负担运费,负担多少运费。这其实就是因为手机获取到了你的空间位置信息,发送到网购平台,然后根据你所在的位置是否属于偏远地区,来决定你是否需要负担运费,如果需要的话,应该负担多少。
|
||||
|
||||
而从应用开发者的角度出发,我们需要知道怎么进行空间定位,获取用户的空间位置信息,以及如何计算发货点与客户地址的距离,这些都要借助与空间数据相关的技术才能解决。
|
||||
|
||||
今天,我还是借助一个真实的项目,来给你介绍下空间数据类型、空间数据处理函数,以及如何为空间数据创建索引,帮助你提升开发基于空间数据应用的能力。
|
||||
|
||||
在我们超市项目的实施过程中,超市经营者提出了这样一个要求:要给距离门店5公里范围内的、从线上下单的客户提供送货上门的服务。要想解决这个问题,就需要用到空间数据了。
|
||||
|
||||
## 空间数据类型与空间函数
|
||||
|
||||
我先给你介绍下空间数据类型和空间函数。
|
||||
|
||||
MySQL支持的空间数据类型分为2类:
|
||||
|
||||
- 一类是包含单个值的几何类型(GEOMETRY)、点类型(POINT)、线类型(LINESTRINIG)和多边形类型(POLYGON);
|
||||
- 另一类是包含多个值的多点类型(MULTIPOINT)、多线类型(MULTILINESTRING)、多多边形类型(MULTIPOLYGON)和几何集类型(GEOMETRYCOLLECTION)。
|
||||
|
||||
我简单说明一下这几种空间数据类型的特点。
|
||||
|
||||
几何类型是一个通用的空间数据类型,你可以把点类型、线类型和多边形类型数据的值赋予几何类型数据。但是点类型、线类型和多边形类型数据则不具备这种通用性,你只能赋予它们各自类型数据的值。
|
||||
|
||||
几何集类型数据可以保存点类型数据、线类型数据和多边形类型数据值的集合。多点类型、多线类型和多多边形类型则分别只能保存点类型数据、线类型数据和多边形类型数据值的集合。
|
||||
|
||||
下面我们重点介绍一下点类型,因为这种类型是最简单、最基础的空间类型,也最常用。
|
||||
|
||||
### 点类型(POINT)
|
||||
|
||||
点类型是最简单的空间数据类型,代表了坐标空间中的单个位置。在不同比例尺的坐标空间中,一个点可以有不同的含义。例如,在较大比例尺的世界地图中,一个点可能代表一座城市;而在较小比例尺的城市地图中,一个点可能只代表一个车站。
|
||||
|
||||
点类型数据的属性有2种:
|
||||
|
||||
- 坐标空间中的X轴的值(比如地理坐标中的经度值);
|
||||
- 坐标空间中的Y轴的值(比如地理坐标中的纬度值)。
|
||||
|
||||
点类型数据的维度是0,边界为空。
|
||||
|
||||
### 空间函数
|
||||
|
||||
MySQL支持的空间函数有一百多种,我们没有必要全部都掌握。所以,我给你重点介绍几个比较常用的空间函数ST_Distance_Sphere()、MBRContains()、MBRWithin()和ST_GeomFromText()。
|
||||
|
||||
**1.ST_Distance_Sphere()函数**
|
||||
|
||||
我们先从计算空间距离的函数ST_Distance_Sphere()说起,这个函数的语法结构和功能如下所示:
|
||||
|
||||
- ST_Distance_Sphere(g1,g2):g1与g2为2个点,函数返回球体上2个点g1与g2之间的最小球面距离。
|
||||
|
||||
**2.MBRContains()和MBRWithin()函数**
|
||||
|
||||
在学习MBRContains()和MBRWithin()函数之前,我们要先了解一个概念,也就是最小边界矩形(MBR,Minimum Bounding Rectangle )。
|
||||
|
||||
最小边界矩形是指以二维坐标表示的若干二维形状(例如点、直线、多边形)的最大范围,即以给定的二维形状各顶点中的最大横坐标、最小横坐标、最大纵坐标、最小纵坐标决定的边界的矩形。
|
||||
|
||||
知道了这个概念,你就能更好地理解这两个函数了。
|
||||
|
||||
- MBRContains(g1,g2):如果几何图形g1的最小边界矩形包含了几何图形g2的最小边界矩形,则返回1,否则返回0。
|
||||
- MBRWithin(g1,g2):与MBRContains(g1,g2)函数正好相反,MBRWithin(g1,g2)表示,如果几何图形g1的最小边界矩形,包含在几何图形g2的最小边界矩形之内,则返回1,否则返回0。
|
||||
|
||||
**3.ST_GeomFromText()**
|
||||
|
||||
这个函数的作用是通过WKT形式创建几何图形。而ST_GeomFromText(WKT,SRID)就表示,返回用WKT形式和SRID指定的参照系表达的几何图形。
|
||||
|
||||
这里的WKT是一种文本标记语言,用来表示几何对象。SRID(Spatial Reference Identifier)是空间参照标识符,默认是0,表示平面坐标系。我们平时常用的SRID是4326,是目前世界通用的以地球质心为原点的地心坐标系。
|
||||
|
||||
知道了这些基础知识,我们就可以着手解决超市经营者提出的需求了。
|
||||
|
||||
这家超市有很多门店,该怎么计算是否应该送货上门呢?如果应该送货上门,应该从哪家门店送货呢?我带你分析下具体的思路。
|
||||
|
||||
- 第一步,把门店的位置信息录入数据表中;
|
||||
- 第二步,根据下单客户的送货地址,获取到地理位置信息;
|
||||
- 第三步,计算各门店位置与送货地址的距离,找出最近的门店安排送货,如果没有一家门店与客户的距离在5公里以内,则提示不能送货。
|
||||
|
||||
下面我们就来实际操作一下。
|
||||
|
||||
首先,我们创建一个门店表(demo.mybranch),包含门店编号、名称、位置等信息。
|
||||
|
||||
```
|
||||
mysql> CREATE TABLE demo.mybranch
|
||||
-> (
|
||||
-> branchid SMALLINT PRIMARY KEY,
|
||||
-> branchname VARCHAR(50) NOT NULL,
|
||||
-> address GEOMETRY NOT NULL SRID 4326
|
||||
-> );
|
||||
Query OK, 0 rows affected (0.07 sec)
|
||||
|
||||
```
|
||||
|
||||
这里需要注意一下,我这里的address字段,定义的空间数据类型是GEOMETRY,SRID是4326。因为GEOMETRY类型比较通用,可以赋予任何类型的空间数据值,而且方便后面创建索引。SRID值为4326,表示采用地心坐标系,这样计算出来的距离才比较准确。当然,你完全可以使用空间数据类型POINT,也能达到同样的效果。
|
||||
|
||||
现在,我们把门店位置信息录入表中:
|
||||
|
||||
```
|
||||
mysql> INSERT INTO demo.mybranch VALUES
|
||||
-> (1,'西直门店',ST_GeomFromText('POINT(39.938099 116.350266)', 4326)), -- 西直门店的经纬度信息
|
||||
-> (2,'东直门店',ST_GeomFromText('POINT(39.941143 116.433769)', 4326)),
|
||||
-> (3,'崇文门店',ST_GeomFromText('POINT(39.896877 116.416977)', 4326)),
|
||||
-> (4,'五道口店',ST_GeomFromText('POINT(39.9921 116.34584)', 4326)),
|
||||
-> (5,'清河店',ST_GeomFromText('POINT(39.743378 116.332878)', 4326));
|
||||
Query OK, 5 rows affected (0.03 sec)
|
||||
Records: 5 Duplicates: 0 Warnings: 0
|
||||
|
||||
```
|
||||
|
||||
结果显示,数据插入成功了。这里有2个问题需要你注意。
|
||||
|
||||
第一,我是用门店的经度和纬度值,来表示门店的地理位置。要获得门店的地理位置,你可以通过地图数据获得,但是这样做成本比较高。还有一种办法,就是通过大厂提供的免费的API接口获取,比如百度地图API,这样比较简单。
|
||||
|
||||
第二,WKT格式表达一个点的时候,在关键字POINT后面的括号中,要先写这个点的纬度,后写这个点的经度。这与一般的习惯相反,不要搞错。而且,经度值与纬度值之间用空格隔开,而不是用逗号。
|
||||
|
||||
准备好各门店的位置信息之后,我们就可以通过空间函数来计算距离了。
|
||||
|
||||
假设我们获取到客户所在位置的地理坐标为:纬度是39.994671,经度是116.330788,那么,我们就可以通过下面的SQL语句查询到这个位置与各个门店的距离:
|
||||
|
||||
```
|
||||
mysql> SELECT branchid,branchname,st_distance_sphere(ST_GeomFromText('POINT(39.994671 116.330788)',4326),address) AS distance
|
||||
-> FROM demo.mybranch;
|
||||
+----------+------------+--------------------+
|
||||
| branchid | branchname | distance |
|
||||
+----------+------------+--------------------+
|
||||
| 1 | 西直门店 | 6505.859589677078 |
|
||||
| 2 | 东直门店 | 10604.07854447186 |
|
||||
| 3 | 崇文门店 | 13123.76779555601 |
|
||||
| 4 | 五道口店 | 1313.741752971374 |
|
||||
| 5 | 清河店 | 27943.114458834025 |
|
||||
+----------+------------+--------------------+
|
||||
5 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
结果显示,所有门店与客户位置之间的距离,都已经计算出来了。
|
||||
|
||||
需要注意的是,这个结果中查出来的距离是以米为单位的。根据这个查询的结果,五道口店的球面最短距离只有1313米,也就是1.3公里,满足送货上门的条件。其他门店的最短距离都在5公里以上。因此,应该从五道口店送货上门。到这里,超市经营者的要求就得到了满足。
|
||||
|
||||
好了,到这里,我们已经知道了如何定位一个空间位置,以及如何计算2个位置之间的距离。接下来,我们就再来了解下如何通过创建索引来提升空间数据的查询效率。
|
||||
|
||||
## 用空间数据创建索引
|
||||
|
||||
对于空间数据的查询,一般分为2种:一种是查询包括一个点的空间对象;另外一种是查询与某一个区域有交集的空间对象。
|
||||
|
||||
为了提高查询的速度,就可以用空间数据字段创建空间索引。MySQL支持使用InnoDB存储引擎,或者是MyISAM存储引擎的数据表,创建空间索引。
|
||||
|
||||
我们有三种创建空间索引的方式。
|
||||
|
||||
第一,我们可以在创建数据表时创建空间索引,语法结构是:
|
||||
|
||||
```
|
||||
CREATE TABLE 表名 (字段名 GEOMETY NOT NULL SRID 4326,SPATIAL INDEX(空间数据字段名));
|
||||
|
||||
```
|
||||
|
||||
第二种是在修改表时创建空间索引,语法结构是:
|
||||
|
||||
```
|
||||
ALTER TABLE 表名 ADD SPATIAL INDEX (空间数据字段名);
|
||||
|
||||
```
|
||||
|
||||
第三种是单独创建空间索引,语法结构是:
|
||||
|
||||
```
|
||||
CREATE SPATIAL INDEX 索引名 ON 表名(空间数据字段名);
|
||||
|
||||
```
|
||||
|
||||
这里要提醒你注意的是:空间索引与普通索引不同,必须要用关键字SPATIAL,而且,创建空间索引的空间数据字段不能为空。空间索引创建一个R树索引,支持区域扫描,对提升空间数据查询的效率很有帮助。
|
||||
|
||||
我还是以刚才的超市门店位置数据为例,来简单说明一下如何用空间类型字段创建空间索引。我我们先用下面的代码,单独创建一下空间索引:
|
||||
|
||||
```
|
||||
mysql> CREATE SPATIAL INDEX index_address ON demo.mybranch(address);
|
||||
Query OK, 0 rows affected, 1 warning (0.04 sec)
|
||||
Records: 0 Duplicates: 0 Warnings: 1
|
||||
|
||||
```
|
||||
|
||||
结果显示,创建成功了。现在我们来确认一下,刚才创建的空间索引能不能起到优化查询的作用。
|
||||
|
||||
在MySQL中,只有在WHERE条件筛选语句中包含类似MBRContains()和MBRWithin()这样的函数,空间索引才会起作用。
|
||||
|
||||
现在,我们来借助一个小例子验证一下,我们创建的空间索引能不能对空间数据的查询起到优化的作用。
|
||||
|
||||
假设我们创建了一个多边形的地理区域,代码如下所示:
|
||||
|
||||
```
|
||||
mysql> SET @poly =
|
||||
-> 'Polygon((
|
||||
'> 40.016712 116.319618,
|
||||
'> 40.016712 116.412773,
|
||||
'> 39.907024 116.412773,
|
||||
'> 39.907024 116.319618,
|
||||
'> 40.016712 116.319618))';
|
||||
Query OK, 0 rows affected (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
这里有个坑,你一定要注意,多边形的区域起点和终点一定要一致,否则就不是一个封闭的区域,MySQL就会提示非法的地理位置数据。
|
||||
|
||||
然后,我们查询下有多少门店在这个区域中。你可以用下面的代码来实现:
|
||||
|
||||
```
|
||||
mysql> SELECT branchid,branchname FROM demo.mybranch
|
||||
-> WHERE MBRContains(ST_GeomFromText(@poly,4326),address);
|
||||
+----------+------------+
|
||||
| branchid | branchname |
|
||||
+----------+------------+
|
||||
| 1 | 西直门店 |
|
||||
| 4 | 五道口店 |
|
||||
+----------+------------+
|
||||
2 rows in set (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
结果显示,有2个门店在这个地理区域范围内。下面我们用查询分析语句来分析一下这个查询,看看有没有用到空间索引:
|
||||
|
||||
```
|
||||
mysql> EXPLAIN SELECT * FROM demo.mybranch
|
||||
-> WHERE MBRContains(ST_GeomFromText(@poly,4326),address);
|
||||
+----+-------------+----------+------------+-------+---------------+---------------+---------+------+------+----------+-------------+
|
||||
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|
||||
+----+-------------+----------+------------+-------+---------------+---------------+---------+------+------+----------+-------------+
|
||||
| 1 | SIMPLE | mybranch | NULL | range | index_address | index_address | 34 | NULL | 5 | 100.00 | Using where |
|
||||
+----+-------------+----------+------------+-------+---------------+---------------+---------+------+------+----------+-------------+
|
||||
1 row in set, 1 warning (0.00 sec)
|
||||
|
||||
```
|
||||
|
||||
结果显示,我们创建的索引起了作用,MySQL优化器使用空间索引进行了区域扫描,提高了查询的效率。
|
||||
|
||||
总之,MySQL为空间数据提供了一套完整的解决方案。从空间数据类型到空间函数,再到空间索引,可以让我们像处理普通数据那样,来存储、处理和查询空间数据。这样一来,开发基于空间数据的应用就十分方便了。
|
||||
|
||||
## 总结
|
||||
|
||||
这节课,我给你介绍了MySQL的空间数据,包括空间数据类型POINT,空间数据处理函数ST_Distance_Sphere()、MBRContains()、MBRWithin()和ST_GeomFromText(),以及创建空间索引的方法。
|
||||
|
||||
MySQL的空间数据是非常有用的数据类型,通过各种空间数据处理函数,可以开发出路径规划、线路导航、自动驾驶等各种应用。虽然现在还存在数据量大、查询效率比较低等问题,但是通过不断使用新的技术,比如空间索引中引入R树索引等,进步是非常明显的。
|
||||
|
||||
如果你在实际工作中,需要开发基于空间数据的应用,课下可以再参考下[链接](https://dev.mysql.com/doc/refman/8.0/en/spatial-types.html)中的内容。
|
||||
|
||||
## 思考题
|
||||
|
||||
在这节课中,我定义的门店表(demo.mybranch)中,地址的空间数据类型是几何类型GEOMETRY,请你改用点类型POINT,完成从创建表到查询最近门店的全部操作。
|
||||
|
||||
欢迎在留言区写下你的思考和答案,我们一起交流讨论。如果你觉得今天的内容对你有所帮助,也欢迎你分享给你的朋友或同事,我们下节课见。
|
||||
Reference in New Issue
Block a user