This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View 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 &gt;= 7000 AND role = '法师'
```
SQL语句直观到你就算没有SQL基础也能凭借英语基础猜出它的大致意思。这就是SQL最大的特点。
假如你是一名运营人员想要看下7天内的新增用户数有多少该怎么做呢首先我们需要获取现在的时间使用NOW()函数即可然后把它转化成天数与用户的注册时间进行对比小于7天的时间即是我们的筛选条件最后就可以得到想要的数据了
```
SELECT COUNT(*) as num FROM new_user WHERE TO_DAYS(NOW())-TO_DAYS(regist_time)&lt;=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学习之旅呢

View 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)

View 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这门语言。

View 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就是关系型数据库的查询语言。
相比于SQLNoSQL泛指非关系型数据库包括了榜单上的键值型数据库、文档型数据库、搜索引擎和列存储等除此以外还包括图形数据库。
键值型数据库通过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功能的演变
1970NoSQL = We have no SQL
1980NoSQL = Know SQL
2000NoSQL = No SQL!
2005NoSQL = Not only SQL
2013NoSQL = 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的分支项目MariaDBMariaDB在绝大部分情况下都是与MySQL兼容的并且增加了许多新的特性比如支持更多的存储引擎类型。许多企业也由原来的MySQL纷纷转向了MariaDB。
SQL Server是微软开发的商业数据库诞生于1989年。实际上微软还推出了Access数据库它是一种桌面数据库同时具备后台存储和前台界面开发的功能更加轻量级适合小型的应用场景。因为后台的存储空间有限一般只有2GAccess的优势在于可以在前台便捷地进行界面开发。而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>
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事。

View 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&gt; select * from player where player_id = 10001;
```
你也可以使用绑定变量,如:
```
SQL&gt; 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&gt; select @@profiling;
```
<img src="https://static001.geekbang.org/resource/image/bc/c1/bcbfdd58b908dc8820fb57d00ff4dcc1.png" alt=""><br>
profiling=0代表关闭我们需要把profiling打开即设置为1
```
mysql&gt; set profiling=1;
```
然后我们执行一个SQL查询你可以执行任何一个SQL查询
```
mysql&gt; select * from wucai.heros;
```
查看当前会话所产生的所有profiles
<img src="https://static001.geekbang.org/resource/image/d9/bf/d9445abcde0f3b38488afe21aca8e9bf.png" alt=""><br>
你会发现我们刚才执行了两次查询Query ID分别为1和2。如果我们想要获取上一次查询的执行时间可以使用
```
mysql&gt; show profile;
```
<img src="https://static001.geekbang.org/resource/image/09/7d/09ef901a55ffcd32ed263d82e3cf1f7d.png" alt=""><br>
当然你也可以查询指定的Query ID比如
```
mysql&gt; 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)下载高清大图。

View 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约束必须≥03`CHECK(height&gt;=0 AND height&lt;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>
我们在创建数据表的时候,会对数据表设置主键、外键和索引。你能说下这三者的作用和区别吗?
欢迎你在评论区写下你的答案,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事。

View 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后面加上这个列的字段名即可。比如我们想要检索数据表中都有哪些英雄。
```
SQLSELECT name FROM heros
```
运行结果69条记录见下图你可以看到这样就等于单独输出了name这一列。
<img src="https://static001.geekbang.org/resource/image/e5/f9/e5e240cbbe9b9309d39d919bb10f93f9.png" alt="">
我们也可以对多个列进行检索,在列名之间用逗号(,)分割即可。比如我们想要检索有哪些英雄,他们的最大生命、最大法力、最大物攻和最大物防分别是多少。
```
SQLSELECT 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 * 帮我们检索出所有的列:
```
SQLSELECT * FROM heros
```
运行结果69条记录
<img src="https://static001.geekbang.org/resource/image/5a/38/5a1d231ada76cf93db734de757810c38.png" alt=""><br>
我们在做数据探索的时候,`SELECT *`还是很有用的这样我们就不需要写很长的SELECT语句了。但是在生产环境时要尽量避免使用`SELECT*`,具体原因我会在后面讲。
### 起别名
我们在使用SELECT查询的时候还有一些技巧可以使用比如你可以给列名起别名。我们在进行检索的时候可以给英雄名、最大生命、最大法力、最大物攻和最大物防等取别名
```
SQLSELECT 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`,这个字段固定值为“王者荣耀”,可以这样写:
```
SQLSELECT '王者荣耀' as platform, name FROM heros
```
运行结果69条记录
<img src="https://static001.geekbang.org/resource/image/19/3d/1975e19d4e21914a7ebee73daf240f3d.png" alt=""><br>
在这个SQL语句中我们虚构了一个`platform`字段,并且把它设置为固定值“王者荣耀”。
需要说明的是如果常数是个字符串那么使用单引号就非常重要了比如王者荣耀。单引号说明引号中的字符串是个常数否则SQL会把王者荣耀当成列名进行查询但实际上数据表里没有这个列名就会引起错误。如果常数是英文字母比如`'WZRY'`也需要加引号。如果常数是个数字,就可以直接写数字,不需要单引号,比如:
```
SQLSELECT 123 as platform, name FROM heros
```
运行结果69条记录
<img src="https://static001.geekbang.org/resource/image/41/99/41ed73cef49e445d64b8cb748a82c299.png" alt="">
### 去除重复行
关于单个表的SELECT查询还有一个非常实用的操作就是从结果中去掉重复的行。使用的关键字是DISTINCT。比如我们想要看下heros表中关于攻击范围的取值都有哪些
```
SQLSELECT DISTINCT attack_range FROM heros
```
这是运行结果2条记录这样我们就能直观地看到攻击范围其实只有两个值那就是近战和远程。
<img src="https://static001.geekbang.org/resource/image/e6/15/e67c0d2f7b977cb0ff87891eb9adf615.png" alt=""><br>
如果我们带上英雄名称,会是怎样呢:
```
SQLSELECT 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数据表进行排序。
假设我们想要显示英雄名称及最大生命值,按照最大生命值从高到低的方式进行排序:
```
SQLSELECT name, hp_max FROM heros ORDER BY hp_max DESC
```
运行结果69条记录
<img src="https://static001.geekbang.org/resource/image/67/08/67714aae8a6ac8e9b356f6975324be08.png" alt="">
如果想要显示英雄名称及最大生命值,按照第一排序最大法力从低到高,当最大法力值相等的时候则按照第二排序进行,即最大生命值从高到低的方式进行排序:
```
SQLSELECT 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条记录即可。
```
SQLSELECT 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关键字比如
```
SQLSELECT TOP 5 name, hp_max FROM heros ORDER BY hp_max DESC
```
如果是DB2使用`FETCH FIRST 5 ROWS ONLY`这样的关键字:
```
SQLSELECT name, hp_max FROM heros ORDER BY hp_max DESC FETCH FIRST 5 ROWS ONLY
```
如果是Oracle你需要基于ROWNUM来统计行数
```
SQLSELECT name, hp_max FROM heros WHERE ROWNUM &lt;=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 &lt;=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 &gt; WHERE &gt; GROUP BY &gt; HAVING &gt; SELECT的字段 &gt; DISTINCT &gt; ORDER BY &gt; 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 &gt; 1.80 #顺序2
GROUP BY player.team_id #顺序3
HAVING num &gt; 2 #顺序4
ORDER BY num DESC #顺序6
LIMIT 2 #顺序7
```
在SELECT语句执行这些步骤的时候每个步骤都会产生一个虚拟表然后将这个虚拟表传入下一个步骤中作为输入。需要注意的是这些步骤隐含在SQL的执行过程中对于我们来说是不可见的。
我来详细解释一下SQL的执行原理。
首先你可以注意到SELECT是先执行FROM这一步的。在这个阶段如果是多张表联查还会经历下面的几个步骤
1. 首先先通过CROSS JOIN求笛卡尔积相当于得到虚拟表 vtvirtual table1-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语句。
欢迎你把这篇文章分享给你的朋友或者同事,与他们一起来分析一下王者荣耀的数据,互相切磋交流。

View 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="">
实际上你能看到,同样的含义可能会有多种表达方式,比如小于等于,可以是(&lt;=),也可以是不大于(!&gt;)。同样不等于,可以用(&lt;&gt;),也可以用(!=),它们的含义都是相同的,但这些符号的顺序都不能颠倒,比如你不能写(=&lt;。需要注意的是你需要查看使用的DBMS是否支持不同的DBMS支持的运算符可能是不同的比如Access不支持!=),不等于应该使用(&lt;&gt;。在MySQL中不支持!&gt;!&lt;)等。
我在上一篇文章中使用了heros数据表今天还是以这张表格做练习。下面我们通过比较运算符对王者荣耀的英雄属性进行条件筛选。
WHERE子句的基本格式是`SELECT ……(列名) FROM ……(表名) WHERE ……(子句条件)`
比如我们想要查询所有最大生命值大于6000的英雄
```
SQLSELECT name, hp_max FROM heros WHERE hp_max &gt; 6000
```
运行结果41条记录
<img src="https://static001.geekbang.org/resource/image/9f/c1/9f639dfe0bd9dbfc63944447f92e47c1.png" alt="">
想要查询所有最大生命值在5399到6811之间的英雄
```
SQLSELECT 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`字段进行空值检查。
```
SQLSELECT 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的英雄然后按照最大生命值和最大法力值之和从高到低进行排序。
```
SQLSELECT name, hp_max, mp_max FROM heros WHERE hp_max &gt; 6000 AND mp_max &gt; 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的英雄。
```
SQLSELECT name, hp_max, mp_max FROM heros WHERE (hp_max+mp_max) &gt; 8000 OR hp_max &gt; 6000 AND mp_max &gt; 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两边的条件增加一个括号结果会是怎样的呢
```
SQLSELECT name, hp_max, mp_max FROM heros WHERE ((hp_max+mp_max) &gt; 8000 OR hp_max &gt; 6000) AND mp_max &gt; 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操作符。
如果我们想要匹配任意字符串出现的任意次数,需要使用(%)通配符。比如我们想要查找英雄名中包含“太”字的英雄都有哪些:
```
SQLSELECT 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以及它们的配置。
如果我们想要匹配单个字符,就需要使用下划线(**)通配符。(%)和(**)的区别在于,(%代表零个或多个字符_只代表一个字符。比如我们想要查找英雄名除了第一个字以外包含字的英雄有哪些。
```
SQLSELECT 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的英雄并且按照最大生命和最大法力之和从高到底的顺序进行排序。
欢迎你在评论区写下你的思考,也欢迎点击请朋友读,把这篇文章分享给你的朋友或者同事。

View File

@@ -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函数。
```
SQLSELECT 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)`
```
SQLSELECT MAX(hp_max) FROM heros
```
运行结果为9328。
假如我们想要知道最大生命值最大的是哪个英雄,以及对应的数值,就需要分成两个步骤来处理:首先找到英雄的最大生命值的最大值,即`SELECT MAX(hp_max) FROM heros`,然后再筛选最大生命值等于这个最大值的英雄,如下所示。
```
SQLSELECT 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`函数。
```
SQLSELECT 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)&gt;'2016-10-01'`,然后再显示符合要求的全部字段信息,即:
```
SQL SELECT * FROM heros WHERE DATE(birthdate)&gt;'2016-10-01'
```
需要注意的是下面这种写法是不安全的:
```
SELECT * FROM heros WHERE birthdate&gt;'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)&gt;'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)&gt;'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年之前上线的英雄如果英雄没有统计上线日期则不显示。
欢迎你在评论区与我分享你的答案,也欢迎点击”请朋友读“,把这篇文章分享给你的朋友或者同事。

View 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的英雄数量。
```
SQLSELECT COUNT(*) FROM heros WHERE hp_max &gt; 6000
```
运行结果为41。
如果想要查询最大生命值大于6000且有次要定位的英雄数量需要使用COUNT函数。
```
SQLSELECT COUNT(role_assist) FROM heros WHERE hp_max &gt; 6000
```
运行结果是 23。
需要说明的是有些英雄没有次要定位即role_assist为NULL这时`COUNT(role_assist)`会忽略值为NULL的数据行而COUNT(*)只是统计数据行数不管某个字段是否为NULL。
如果我们想要查询射手主要定位或者次要定位是射手的最大生命值的最大值是多少需要使用MAX函数。
```
SQLSELECT 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的顺序排列越往后数值越大。如果是汉字则按照全拼拼音进行排列。比如
```
SQLSELECT 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 &gt; 5`,然后按照英雄数量从高到低进行排序,即`ORDER BY num DESC`
```
SQL: SELECT COUNT(*) as num, role_main, role_assist FROM heros GROUP BY role_main, role_assist HAVING num &gt; 5 ORDER BY num DESC
```
运行结果4条记录
<img src="https://static001.geekbang.org/resource/image/ca/61/ca9747ad58e8cf637fe352fb0cfd5761.png" alt=""><br>
你能看到还是上面这个分组只不过我们按照数量进行了过滤筛选了数量大于5的分组进行输出。如果把HAVING替换成了WHERESQL则会报错。对于分组的筛选我们一定要用HAVING而不是WHERE。另外你需要知道的是HAVING支持所有WHERE的操作因此所有需要WHERE子句实现的功能你都可以使用HAVING对分组进行筛选。
我们再来看个例子通过这个例子查看一下WHERE和HAVING进行条件过滤的区别。筛选最大生命值大于6000的英雄按照主要定位、次要定位进行分组并且显示分组中英雄数量大于5的分组按照数量从高到低进行排序。
```
SQL: SELECT COUNT(*) as num, role_main, role_assist FROM heros WHERE hp_max &gt; 6000 GROUP BY role_main, role_assist HAVING num &gt; 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的英雄按照攻击范围来进行分组显示分组的英雄数量以及分组英雄的最大生命值与法力值之和的平均值、最大值和最小值并按照分组英雄数从高到低进行排序其中聚集函数的结果包括小数点后两位。
欢迎你在评论区与我分享你的答案,如果你觉得这篇文章有帮助,欢迎把它分享给你的朋友或者同事,一起切磋交流一下。

View File

@@ -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 &gt; (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)`,然后将它作为筛选的条件,实际上也是关联子查询,即:
```
SQLSELECT 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都需要使用比较符比较符包括了&gt;=&lt;&gt;=&lt;=)和(&lt;&gt;)等。
如果我们想要查询球员表中比印第安纳步行者对应的team_id为1002中任意一个球员身高高的球员信息并且输出他们的球员ID、球员姓名和球员身高该怎么写呢首先我们需要找出所有印第安纳步行者队中的球员身高`SELECT height FROM player WHERE team_id = 1002`然后使用ANY子查询即
```
SQL: SELECT player_id, player_name, height FROM player WHERE height &gt; 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 &gt; 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信息。
欢迎在评论区写下你的思考,也欢迎点击请朋友读把这篇文章分享给你的朋友或者同事。

View File

@@ -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中是如何使用连接的
相比于SQL99SQL92规则更简单更适合入门。在这篇文章中我会先讲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字段如果想要知道每个球员的身高的级别可以采用非等值连接查询。
```
SQLSELECT 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中只有左外连接和右外连接没有全外连接。
什么是左外连接,什么是右外连接呢?
左外连接,就是指左边的表是主表,需要显示左边表的全部行,而右侧的表是从表,(+)表示哪个是从表。
```
SQLSELECT * FROM player, team where player.team_id = team.team_id(+)
```
相当于SQL99中的
```
SQLSELECT * FROM player LEFT JOIN team on player.team_id = team.team_id
```
右外连接,指的就是右边的表是主表,需要显示右边表的全部行,而左侧的表是从表。
```
SQLSELECT * FROM player, team where player.team_id(+) = team.team_id
```
相当于SQL99中的
```
SQLSELECT * FROM player RIGHT JOIN team on player.team_id = team.team_id
```
需要注意的是LEFT JOIN和RIGHT JOIN只存在于SQL99及以后的标准中在SQL92中不存在只能用+)表示。
### 自连接
自连接可以对多个表进行操作,也可以对同一个表进行操作。也就是说查询条件使用了当前表的字段。
比如我们想要查看比布雷克·格里芬高的球员都有谁,以及他们的对应身高:
```
SQLSELECT b.player_name, b.height FROM player as a , player as b WHERE a.player_name = '布雷克-格里芬' and a.height &lt; b.height
```
运行结果6条记录
<img src="https://static001.geekbang.org/resource/image/05/94/05e4bf92df00e243601ca2d763fabb94.png" alt=""><br>
如果不用自连接的话需要采用两次SQL查询。首先需要查询布雷克·格里芬的身高。
```
SQLSELECT height FROM player WHERE player_name = '布雷克-格里芬'
```
运行结果为2.08。
然后再查询比2.08高的球员都有谁,以及他们的对应身高:
```
SQLSELECT player_name, height FROM player WHERE height &gt; 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语句显示出所有可能的比赛组合。
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,与他们一起交流一下。

View File

@@ -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两张表
```
SQL99SELECT 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标准的查询结果一样。
```
SQL92SELECT 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 &lt; b.height
```
**SQL99**
```
SELECT b.player_name, b.height FROM player as a JOIN player as b ON a.player_name = '布雷克-格里芬' and a.height &lt; 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表
欢迎你在评论区写下你的答案,我会在评论区与你一起讨论。也欢迎把这篇文章分享给你的朋友或者同事。

View 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 &gt; (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 &gt; (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 &gt; (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 &gt;= 1.90 AND height &lt;= 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>
今天我们对视图进行了讲解,你能用自己的语言来说下视图的优缺点么?另外视图在更新的时候会影响到数据表吗?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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 &lt;= 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 &lt;= 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…ENDBEGIN…END中间包含了多个语句每个语句都以;)号为结束符。
1. DECLAREDECLARE用来声明变量使用的位置在于BEGIN…END语句中间而且需要在其他语句使用之前进行变量的声明。
1. SET赋值语句用于对变量进行赋值。
1. SELECT…INTO把从数据表中查询的结果存放到变量中也就是为变量赋值。
除了上面这些用到的流控制语句以外,还有一些常用的流控制语句:
1.IF…THEN…ENDIF条件判断语句我们还可以在IF…THEN…ENDIF中使用ELSE和ELSEIF来进行条件判断。<br>
2.CASECASE语句用于多条件的分支判断使用的语法是下面这样的。
```
CASE
WHEN expression1 THEN ...
WHEN expression2 THEN ...
...
ELSE
--ELSE语句可以加也可以不加。加的话代表的所有条件都不满足时采用的方式。
END
```
3.LOOP、LEAVE和ITERATELOOP是循环语句使用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用来得到某一类型英雄主要定位为某一类型即可的最大生命值的总和。
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -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&gt; set autocommit =0; //关闭自动提交
```
```
mysql&gt; 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中都有哪些存储引擎支持事务通过什么命令可以查看它们吗另外你是如何理解事务的特性的
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。

View File

@@ -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&gt; BEGIN;
SQL&gt; INSERT INTO heros_temp values(4, '吕布');
```
当小张还没有提交该事务的时候,小李又对数据表进行了访问,他想看下这张英雄表里都有哪些英雄:
```
SQL&gt; SELECT * FROM heros_temp;
```
这时,小李看到的结果如下:
<img src="https://static001.geekbang.org/resource/image/1c/a8/1c4c7b7d7223739eb5346e3159bb34a8.png" alt=""><br>
你有没有发现什么异常?这个时候小张还没有提交事务,但是小李却读到了小张还没有提交的数据,这种现象我们称之为“脏读”。
那么什么是不可重复读呢?
第二天小张想查看id=1的英雄是谁于是他进行了SQL查询
```
SQL&gt; BEGIN;
SQL&gt; 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&gt; BEGIN;
SQL&gt; UPDATE heros_temp SET name = '张翼德' WHERE id = 1;
```
然后小张再一次进行查询同样也是查看id=1的英雄是谁
```
SQL&gt; 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&gt; SELECT * FROM heros_temp;
```
<img src="https://static001.geekbang.org/resource/image/21/41/213f1601abb1629689d6d8477bd16641.png" alt=""><br>
这时当小张执行完之后,小李又开始了一个事务,往数据库里插入一个新的英雄“吕布”:
```
SQL&gt; BEGIN;
SQL&gt; INSERT INTO heros_temp values(4, '吕布');
```
不巧的是,小张这时忘记了英雄都有哪些,又重新执行了一遍查询:
```
SQL&gt; 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&gt; SHOW VARIABLES LIKE 'transaction_isolation';
```
然后你能看到当前的隔离级别是REPEATABLE-READ也就是可重复读。
<img src="https://static001.geekbang.org/resource/image/fa/5f/faaf3196f842d3331e40364fa331925f.png" alt="">
现在我们把隔离级别降到最低设置为READ UNCOMMITTED读未提交
```
mysql&gt; 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&gt; 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>
今天的内容到这里就结束了,你能思考一下为什么隔离级别越高,就越影响系统的并发性能吗?以及不可重复读和幻读的区别是什么?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事。

View 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 &gt; 8500
```
查询结果4条数据
<img src="https://static001.geekbang.org/resource/image/04/c0/046f997b7d1b2ce6a64e65b728cca4c0.jpg" alt="">
这里我们就可以通过游标来操作数据行,如图所示此时游标所在的行是“白起”的记录,我们也可以在结果集上滚动游标,指向结果集中的任意一行。
## 如何使用游标?
游标实际上是一种控制数据集的更加灵活的处理方式。
如果我们想要使用游标一般需要经历五个步骤。不同DBMS中使用游标的语法可能略有不同。
第一步,定义游标。
```
DECLARE cursor_name CURSOR FOR select_statement
```
这个语法适用于MySQLSQL ServerDB2和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就需要有DEALLOCATEDEALLOCATE的作用是释放游标。我们一定要养成释放游标的习惯否则游标会一直存在于内存中直到进程结束后才会自动释放。当你不需要使用游标的时候释放游标可以减少资源浪费。
上面就是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 &lt; 5 THEN
IF temp_diff &gt; 200 THEN
SET temp_growth = temp_growth * 1.1;
ELSEIF temp_diff &gt;= 150 AND temp_diff &lt;=200 THEN
SET temp_growth = temp_growth * 1.08;
ELSEIF temp_diff &lt; 150 THEN
SET temp_growth = temp_growth * 1.07;
END IF;
ELSEIF temp_growth &gt;=5 AND temp_growth &lt;=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的基础上进行面向过程的开发完成较为复杂的功能你能说一下面向过程和面向集合这两种编程方式的区别吗
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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 APIMySQL官方提供了驱动器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=&quot;localhost&quot;,
user=&quot;root&quot;,
passwd=&quot;XXX&quot;, # 写上你的数据库密码
database='wucai',
auth_plugin='mysql_native_password'
)
# 获取操作游标
cursor = db.cursor()
# 执行SQL语句
cursor.execute(&quot;SELECT VERSION()&quot;)
# 获取一条数据
data = cursor.fetchone()
print(&quot;MySQL版本: %s &quot; % data)
# 关闭游标&amp;数据库连接
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 = &quot;INSERT INTO player (team_id, player_name, height) VALUES (%s, %s, %s)&quot;
val = (1003, &quot;约翰-科林斯&quot;, 2.08)
cursor.execute(sql, val)
db.commit()
print(cursor.rowcount, &quot;记录插入成功。&quot;)
```
我们使用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&gt;=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, &quot;约翰-科林斯&quot;)
cursor.execute(sql, val)
db.commit()
print(cursor.rowcount, &quot;记录被修改。&quot;)
```
最后我们看下如何删除约翰·科林斯这个球员的数据,代码如下:
```
sql = 'DELETE FROM player WHERE player_name = %s'
val = (&quot;约翰-科林斯&quot;,)
cursor.execute(sql, val)
db.commit()
print(cursor.rowcount, &quot;记录删除成功。&quot;)
```
最后都执行完了,我们来关闭游标和数据库的连接,使用以下代码即可:
```
cursor.close()
db.close()
```
针对上面的操作过程你可以模拟下数据的CRUD操作但有几点你需要注意。
1.打开数据库连接以后,如果不再使用,则需要关闭数据库连接,以免造成资源浪费。<br>
2.在对数据进行增加、删除和修改的时候,可能会出现异常,这时就需要用`try...except`捕获异常信息。比如针对插入球员约翰·科林斯这个操作,你可以写成下面这样:
```
import traceback
try:
sql = &quot;INSERT INTO player (team_id, player_name, height) VALUES (%s, %s, %s)&quot;
val = (1003, &quot;约翰-科林斯&quot;, 2.08)
cursor.execute(sql, val)
db.commit()
print(cursor.rowcount, &quot;记录插入成功。&quot;)
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的英雄进行查询并且输出相应的属性值。
欢迎在评论区写下你的答案,我会与你一起交流。也欢迎把这篇文章分享给你的朋友或者同事,与它们一起交流一下。

View File

@@ -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是MySQLSQL ServerPostgreSQL还是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 = &quot;约翰-科林斯&quot;, 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
# 查询身高&gt;=2.08的球员有哪些
rows = session.query(Player).filter(Player.height &gt;= 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 &gt;=2.08, Player.height &lt;=2.10).all()
```
如果是OR的关系比如我们想要查询身高 ≥ 2.08,或者身高 ≤ 2.10的球员,可以写成这样:
```
rows = session.query(Player).filter(or_(Player.height &gt;=2.08, Player.height &lt;=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)&gt;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)&gt;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。
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起交流。

View 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)`&gt; `COUNT(字段)`。我们尽量使用`COUNT(*)`,当然如果你要统计的是某个字段的非空数据行数,则另当别论,毕竟比较执行效率的前提是结果一样才可以。
1. 如果要统计`COUNT(*)`,尽量在数据表上建立二级索引,系统会自动采用`key_len`小的二级索引进行扫描,这样当我们使用`SELECT COUNT(*)`的时候效率就会提升,有时候可以提升几倍甚至更高。
### 答疑2
在MySQL中`LIMIT`关键词是最后执行的,如果可以确定只有一条结果,那么就起不到查询优化的效果了吧,因为`LIMIT`是对最后的结果集过滤,如果结果集本来就只有一条,那就没有什么用了。
### 解答
如果你可以确定结果集只有一条,那么加上`LIMIT 1`的时候当找到一条结果的时候就不会继续扫描了这样会加快查询速度。这里指的查询优化针对的是会扫描全表的SQL语句如果数据表已经对字段建立了唯一索引那么可以通过索引进行查询不会全表扫描的话就不需要加上`LIMIT 1`了。
## 关于通配符的解释
关于查询语句中通配符的使用理解我举了一个查询英雄名除了第一个字以外包含“太”字的英雄都有谁的例子使用的SQL语句是
```
SQL&gt; 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表小就用EXISTSB表小就用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;
```
欢迎你在评论区写下你的思考,我会与你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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>
第二步。选择可用的表和列,在左侧面板中勾选我们想要导入的数据表及相应的列,点击 &gt; 按钮导入到右侧的面板中,然后点击下一步。
<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>
然后我们在右侧面板中选择“数据透视表字段”以便对数据透视表中的字段进行管理比如我们勾选numrole_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 &gt; 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。
欢迎你在评论区写下你的体会与思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。

View 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');
}
完整代码如下:
&lt;!DOCTYPE HTML&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;title&gt;SQL必知必会&lt;/title&gt;
&lt;script type=&quot;text/javascript&quot;&gt;
if (!window.openDatabase) {
alert('浏览器不支持WebSQL');
}
&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div id=&quot;status&quot; name=&quot;status&quot;&gt;WebSQL Test&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
```
如果浏览器不支持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, &quot;夏侯惇&quot;, 7350, 1746, &quot;坦克&quot;)');
});
```
这里执行的事务就是一个方法包括两条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, &quot;夏侯惇&quot;, 7350, 1746, &quot;坦克&quot;)');
```
## 在浏览器端做一个王者荣耀英雄的查询页面
刚才我讲解了WebSQL的基本语法现在我们就来用刚学到的东西做一个小练习在浏览器端做一个王者荣耀英雄的创建和查询页面。
具体步骤如下:
1. 初始化数据我们需要在HTML中设置一个id为datatable的table表格然后在JavaScript中创建init()函数获取id为datatable的元素。
1. 创建showData方法参数为查询出来的数据rowshowData方法可以方便地展示查询出来的一行数据我们在数据表中的字段为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))下载):
```
&lt;!DOCTYPE HTML&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;
&lt;title&gt;SQL必知必会&lt;/title&gt;
&lt;script type=&quot;text/javascript&quot;&gt;
// 初始化
function init() {
datatable = document.getElementById(&quot;datatable&quot;);
}
// 显示每个英雄的数据
function showData(row){
var tr = document.createElement(&quot;tr&quot;);
var td1 = document.createElement(&quot;td&quot;);
var td2 = document.createElement(&quot;td&quot;);
var td3 = document.createElement(&quot;td&quot;);
var td4 = document.createElement(&quot;td&quot;);
var td5 = document.createElement(&quot;td&quot;);
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, &quot;夏侯惇&quot;, 7350, 1746, &quot;坦克&quot;)');
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10001, &quot;钟无艳&quot;, 7000, 1760, &quot;战士&quot;)');
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10002, &quot;张飞&quot;, 8341, 100, &quot;坦克&quot;)');
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10003, &quot;牛魔&quot;, 8476, 1926, &quot;坦克&quot;)');
tx.executeSql('INSERT INTO heros (id, name, hp_max, mp_max, role_main) VALUES (10004, &quot;吕布&quot;, 7344, 0, &quot;战士&quot;)');
msg = '&lt;p&gt;heros数据表创建成功一共插入5条数据。&lt;/p&gt;';
document.querySelector('#status').innerHTML = msg;
});
// 查询数据
db.transaction(function (tx) {
tx.executeSql('SELECT * FROM heros', [], function (tx, data) {
var len = data.rows.length;
msg = &quot;&lt;p&gt;查询记录条数: &quot; + len + &quot;&lt;/p&gt;&quot;;
document.querySelector('#status').innerHTML += msg;
// 将查询的英雄数据放到 datatable中
for (i = 0; i &lt; len; i++){
showData(data.rows.item(i));
}
});
});
&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div id=&quot;status&quot; name=&quot;status&quot;&gt;状态信息&lt;/div&gt;
&lt;table border=&quot;1&quot; id=&quot;datatable&quot;&gt;&lt;/table&gt;
&lt;/body&gt;
&lt;/html&gt;
```
演示结果如下:
<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>
欢迎你在评论区写下你的答案,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,与他们一起交流一下。

View 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(&quot;wucai.db&quot;)
```
这里我们连接的是wucai.db这个文件如果没有这个文件存储上面的调用会自动在相应的工程路径里进行创建然后我们可以使用conn操作连接通过会话连接conn来创建游标
```
cur = conn.cursor()
```
通过这一步我们得到了游标cur然后可以使用execute()方法来执行各种DML比如插入删除更新等当然我们也可以进行SQL查询用的同样是execute()方法。
比如我们想要创建heros数据表以及相应的字段id、name、hp_max、mp_max、role_main可以写成下面这样
```
cur.execute(&quot;CREATE TABLE IF NOT EXISTS heros (id int primary key, name text, hp_max real, mp_max real, role_main text)&quot;)
```
在创建之后我们可以使用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(&quot;SELECT id, name, hp_max, mp_max, role_main FROM heros&quot;)
```
这时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(&quot;wucai.db&quot;)
# 获取游标
cur = conn.cursor()
# 创建数据表
cur.execute(&quot;CREATE TABLE IF NOT EXISTS heros (id int primary key, name text, hp_max real, mp_max real, role_main text)&quot;)
# 插入英雄数据
cur.executemany('insert into heros values(?, ?, ?, ?, ?)',
((10000, '夏侯惇', 7350, 1746, '坦克'),
(10001, '钟无艳', 7000, 1760, '战士'),
(10002, '张飞', 8341, 100, '坦克'),
(10003, '牛魔', 8476, 1926, '坦克'),
(10004, '吕布', 7344, 0, '战士')))
cur.execute(&quot;SELECT id, name, hp_max, mp_max, role_main FROM heros&quot;)
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进行操作并输出结果。
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -0,0 +1,224 @@
<audio id="audio" title="41丨初识RedisRedis为什么会这么快" 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的规则是“只提供你想要的”数据模型灵活查询效率高成本低。但同时相比RDBMSNoSQL数据库没有统一的架构和标准语言每种数据库之间差异较大各有所长。
今天我们要讲解的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的数据类型
相比MemcachedRedis有一个非常大的优势就是支持多种数据类型。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为zhangfeiage为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版本之后增加了基数统计HyperLogLog3.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(&quot;users&quot;+str(i), data)
# 统计写时间
delta_time = time.time()-time1
print(delta_time)
# 统计当前时间
time1 = time.time()
# 1万次读
for i in range(10000):
result = r.hmget(&quot;users&quot;+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即使采用单线程模式效率也很高呢
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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模式。
RDBRedis DataBase持久化可以把当前进程的数据生成快照保存到磁盘上触发RDB持久化的方式分为手动触发和自动触发。因为持久化操作与命令操作不是同步进行的所以无法保证事务的持久性。
AOFAppend 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=&quot;ticket_count&quot;
# 模拟第i个用户进行抢票
def sell(i):
# 初始化 pipe
pipe = r.pipeline()
while True:
try:
# 监视票数
pipe.watch(KEY)
# 查看票数
c = int(pipe.get(KEY))
if c &gt; 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__ == &quot;__main__&quot;:
# 初始化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>
欢迎你在评论区写下你的思考,我会和你一起交流,也欢迎你把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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 &gt; 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 &quot;math.randomseed(ARGV[1]); local temp = math.random(1,112); redis.call('SET', KEYS[1], temp); return 'ok';&quot; 1 score 30
```
这条语句代表的意思是我们传入KEY的个数为1参数是scorearg参数为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分成了两个部分整数部分为实际的得分小数部分为注册时间。例子中给出的严格排行榜是在分数相同的情况下按照注册时间的长短进行的排名注册时间长的排名靠前。如果我们将规则进行调整同样是在分数相同的情况下如果注册时间长的排名靠后又该如何编写代码呢
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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(&quot;DROP TABLE heros&quot;,[],
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(&quot;FONT_PATH&quot;, os.path.join(os.path.dirname(__file__), &quot;simhei.ttf&quot;))
f = remove_stop_words(f)
cut_text = &quot; &quot;.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(&quot;wordcloud.jpg&quot;)
# 显示词云文件
plt.imshow(wordcloud)
plt.axis(&quot;off&quot;)
plt.show()
def get_content_from_weixin():
# 创建数据库连接
conn = sqlite3.connect(&quot;weixin.db&quot;)
# 获取游标
cur = conn.cursor()
# 创建数据表
# 查询当前数据库中的所有数据表
sql = &quot;SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'Chat\_%' escape '\\\'&quot;
cur.execute(sql)
tables = cur.fetchall()
content = ''
for table in tables:
sql = &quot;SELECT Message FROM &quot; + 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'&lt;[^&gt;]+&gt;',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=&quot;ticket_count&quot;
# 模拟第i个用户进行抢购
def sell(i):
# 使用decr对KEY减1
temp = r.decr(KEY)
if temp &gt;= 0:
print('用户 {} 抢票成功,当前票数 {}'.format(i, temp))
else:
print('用户 {} 抢票失败,票卖完了'.format(i))
if __name__ == &quot;__main__&quot;:
# 初始化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存储的数据都是热点数据呢
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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中常用的有OracleSQL 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类型的数据还支持ListSetHash等数据结构。 当我们有持久化需求或者是更高级的数据处理需求的时候就可以使用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的查询优化后两个维度在于外援技术。你可以说一说你对这些维度的理解吗
欢迎你在评论区分享你的心得,也欢迎把这篇文章分享给你的朋友或者同事。

View 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和3NFBCNF我们放在后面讲。
**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种键这些键的核心作用就是标识。
在这些概念的基础上我又讲了1NF2NF和3NF。我们经常会与这三种范式打交道利用它们建立冗余度小、结构合理的数据库。
有一点需要注意的是,这些范式只是提出了设计的标准,实际上设计数据表时,未必要符合这些原则。一方面是因为这些范式本身存在一些问题,可能会带来插入,更新,删除等的异常情况(这些会在下一讲举例说明),另一方面,它们也可能降低会查询的效率。这是为什么呢?因为范式等级越高,设计出来的数据表就越多,进行数据查询的时候就可能需要关联多张表,从而影响查询效率。
<img src="https://static001.geekbang.org/resource/image/e7/11/e775113e733020a7810196afd4f58711.jpg" alt=""><br>
2NF和3NF相对容易混淆根据今天的内容你能说下这两个范式之间的区别吗另外如果我们现在有一张学生选课表包含的属性有学号、姓名、课程名称、分数、系别和系主任如果要改成符合3NF要求的设计需要怎么修改呢
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。

View File

@@ -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。 你能看到,在数据量大的情况下,查询效率会有显著的提升。
## 反范式存在的问题&amp;适用场景
从上面的例子中可以看出,反范式可以通过空间换时间,提升查询的效率,但是反范式也会带来一些新问题。
在数据量小的情况下,反范式不能体现性能的优势,可能还会让数据库的设计更加复杂。比如采用存储过程来支持数据的更新、删除等额外操作,很容易增加系统的维护成本。
比如用户每次更改昵称的时候,都需要执行存储过程来更新,如果昵称更改频繁,会非常消耗系统资源。
那么反范式优化适用于哪些场景呢?
在现实生活中,我们经常需要一些冗余信息,比如订单中的收货人信息,包括姓名、电话和地址等。每次发生的订单收货信息都属于历史快照,需要进行保存,但用户可以随时修改自己的信息,这时保存这些冗余信息是非常有必要的。
当冗余信息有价值或者能大幅度提高查询效率的时候,我们就可以采取反范式的优化。
此外反范式优化也常用在数据仓库的设计中,因为数据仓库通常存储历史数据,对增删改的实时性要求不强,对历史数据的分析需求强。这时适当允许数据的冗余度,更方便进行数据分析。
我简单总结下数据仓库和数据库在使用上的区别:
1. 数据库设计的目的在于捕获数据,而数据仓库设计的目的在于分析数据;
1. 数据库对数据的增删改实时性要求强,需要存储在线的用户数据,而数据仓库存储的一般是历史数据;
1. 数据库设计需要尽量避免冗余,但为了提高查询效率也允许一定的冗余度,而数据仓库在设计上更偏向采用反范式设计。
## 总结
今天我们讲了BCNF它是基于3NF进行的改进。你能看到设计范式越高阶数据表就会越精细数据的冗余度也就越少在一定程度上可以让数据库在内部关联上更好地组织数据。但有时候我们也需要采用反范进行优化通过空间来换取时间。
范式本身没有优劣之分,只有适用场景不同。没有完美的设计,只有合适的设计,我们在数据表的设计中,还需要根据需求将范式和反范式混合使用。
<img src="https://static001.geekbang.org/resource/image/ac/fb/acbb07c269c85683cc981c7f677d32fb.jpg" alt=""><br>
我们今天举了一个反范式设计的例子,你在工作中是否有做过反范式设计的例子?欢迎你在评论区与我们一起分享,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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或10代表女性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>
今天的内容到这里就结束了,给你留个问题。关于联合索引的最左原则指的是什么?在使用联合索引时,有哪些需要注意的地方呢?
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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等于根节点也就是找到了这个节点返回根节点即可。
举个例子我们对数列3422895237791创造出来的二分查找树如下图所示
<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&gt;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&gt;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&gt;2有以下的特性
1. 根节点的儿子数的范围是[2,M]。
1. 每个中间节点包含k-1个关键字和k个孩子孩子的数量=关键字的数量+1k的取值范围为[ceil(M/2), M]。
1. 叶子节点包括k-1个关键字叶子节点没有孩子k的取值范围为[ceil(M/2), M]。
1. 假设中间节点节点的关键字为Key[1], Key[2], …, Key[k-1]且关键字按照升序排序即Key[i]&lt;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里面的关键字为812它有3个孩子(35)(910) 和 (1315),你能看到(35)小于8(910)在8和12之间而(1315)大于12刚好符合刚才我们给出的特征。
然后我们来看下如何用B树进行查找。假设我们想要查找的关键字是9那么步骤可以分为以下几步
1. 我们与根节点的关键字(1735进行比较9小于17那么得到指针P1
1. 按照指针P1找到磁盘块2关键字为812因为9在8和12之间所以我们得到指针P2
1. 按照指针P2找到磁盘块6关键字为910然后我们找到了关键字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分别是子节点1814182431354153中的最小值。每一层父节点的关键字都会出现在下一层的子节点的关键字中因此在叶子节点中包括了所有的关键字信息并且每一个叶子节点都有一个指向下一个节点的指针这样就形成了一个链表。
<img src="https://static001.geekbang.org/resource/image/55/32/551171d94a69fbbfc00889f8b1f45932.jpg" alt=""><br>
比如我们想要查找关键字16B+树会自顶向下逐层进行查找:
1. 与根节点的关键字(11835)进行比较16在1和18之间得到指针P1指向磁盘块2
1. 找到磁盘块2关键字为1814因为16大于14所以得到指针P3指向磁盘块7
1. 找到磁盘块7关键字为141617然后我们找到了关键字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树在构造和查询性能上有什么差异呢
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。

View 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为01Y为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值相同时会发生什么
欢迎你在评论区写下你的思考,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -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
```
运行结果(运行时间&gt;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…进行连接的话即使使用了各种优化手段总的运行时间也会很长&gt;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) &gt;= '2018-10-01 10:00:00' AND comment_time &lt;= '2018-10-02 10:00:00'
```
你可以想一下这时候索引是否会失效,为什么?如果失效的话,要进行查询重写,应该怎样写?
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。

View 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 &gt; 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&gt; 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支持的块大小为2KB4KB8KB16KB32KB和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个槽的编号分别为01234我想查找主键为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+树进行记录的检索流程是怎样的?
欢迎你在评论区写下你的思考,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。

View 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 &gt; 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 &gt; 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大小的页来说一次可以顺序读取256040MB/16KB个页相当于一个页的读取时间为0.4ms。采用批量读取的方式,即使是从磁盘上进行读取,效率也比从内存中只单独读取一个页的效率要高。
## 通过last_query_cost统计SQL语句的查询成本
我们先前已经讲过一条SQL查询语句在执行前需要确定查询计划如果存在多种查询计划的话MySQL会计算每个查询计划所需要的成本从中选择成本最小的一个作为最终执行的查询计划。
如果我们想要查看某条SQL语句的查询成本可以在执行完这条SQL语句之后通过查看当前会话中的last_query_cost变量值来得到当前查询的成本。这个查询成本对应的是SQL语句所需要读取的页的数量。
我以product_comment表为例如果我们想要查询comment_id=900001的记录然后看下查询成本我们可以直接在聚集索引上进行查找
```
mysql&gt; 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&gt; 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&gt; 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&gt; 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>
最后给你留两道思考题吧。你能解释下相比于单个页面的随机读,为什么顺序读取时平均一个页面的加载效率会提高吗?另外,对于今天学习的缓冲池机制和数据页加载的方式,你有什么心得体会吗?
欢迎在评论区写下你的答案,如果你觉得这篇文章有帮助,不妨把它分享给你的朋友或者同事吧。

View 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
```
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。

View 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存储引擎时为什么对某行数据添加排它锁之前会在数据表上添加意向排他锁呢
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来进步。

View 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的集合活跃的最大事务IDlow_limit_id为trx8活跃的最小事务IDup_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 &lt; 活跃的最小事务IDup_limit_id也就是说这个行记录在这些活跃的事务创建之前就已经提交了那么这个行记录对该事务是可见的。
如果trx_id &gt; 活跃的最大事务IDlow_limit_id这说明该行记录在这些活跃的事务创建之后才创建那么这个行记录对当前事务不可见。
如果up_limit_id &lt; trx_id &lt; 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&gt;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策略有何不同
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,让我们一起来学习进步。

View 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>
在这两个阶段中,查询重写属于代数级、语法级的优化,也就是属于逻辑范围内的优化,而基于代价的估算模型是从连接路径中选择代价最小的路径,属于物理层面的优化。
## 查询优化器的两种优化方式
查询优化器的目的就是生成最佳的执行计划,而生成最佳执行计划的策略通常有以下两种方式。
第一种是基于规则的优化器RBORule-Based Optimizer规则就是人们以往的经验或者是采用已经被证明是有效的方式。通过在优化器里面嵌入规则来判断SQL查询符合哪种规则就按照相应的规则来制定执行计划同时采用启发式规则去掉明显不好的存取路径。
第二种是基于代价的优化器CBOCost-Based Optimizer这里会根据代价评估模型计算每条可能的执行计划的代价也就是COST从中选择代价最小的作为执行计划。相比于RBO来说CBO对数据更敏感因为它会利用数据表中的统计信息来做判断针对不同的数据表查询得到的执行计划可能是不同的因此制定出来的执行计划也更符合数据表的实际情况。
但我们需要记住SQL是面向集合的语言并没有指定执行的方式因此在优化器中会存在各种组合的可能。我们需要通过优化器来制定数据表的扫描方式、连接方式以及连接顺序从而得到最佳的SQL执行计划。
你能看出来RBO的方式更像是一个出租车老司机凭借自己的经验来选择从A到B的路径。而CBO更像是手机导航通过数据驱动来选择最佳的执行路径。
## CBO是如何统计代价的
大部分RDBMS都支持基于代价的优化器CBOCBO随着版本的迭代也越来越成熟但是CBO依然存在缺陷。通过对CBO工作原理的了解我们可以知道CBO可能存在的不足有哪些有助于让我们知道优化器是如何确定执行计划的。
### 能调整的代价模型的参数有哪些
首先我们先来了解下MySQL中的`COST Model``COST Model`就是优化器用来统计各种步骤的代价模型在5.7.10版本之后MySQL会引入两张数据表里面规定了各种步骤预估的代价Cost Value ,我们可以从`mysql.server_cost``mysql.engine_cost`这两张表中获得这些步骤的代价:
```
SQL &gt; 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 &gt; 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也存在不足你能用自己的话描述一下其中的原因吗
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来学习进步。

View 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 &gt; 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 &gt; set global slow_query_log='ON';
```
然后我们再来查看下慢查询日志是否开启,以及慢查询日志文件的位置:
<img src="https://static001.geekbang.org/resource/image/20/11/20d327118c221ada2bb123a4ce975e11.png" alt=""><br>
你能看到这时慢查询分析已经开启同时文件保存在DESKTOP-4BK02RP-slow文件中。
接下来我们来看下慢查询的时间阈值设置,使用如下命令:
```
mysql &gt; show variables like '%long_query_time%';
```
<img src="https://static001.geekbang.org/resource/image/a7/7a/a752b54a3fa38af449b55ccd0e628e7a.png" alt="">
这里如果我们想把时间缩短比如设置为3秒可以这样设置
```
mysql &gt; 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 &quot;C:\ProgramData\MySQL\MySQL Server 8.0\Data\DESKTOP-4BK02RP-slow.log&quot;
```
<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 &lt; index &lt; range &lt; index_merge &lt; ref &lt; eq_ref &lt; const/system。我们在查看执行计划的时候通常希望执行计划至少可以使用到range级别以上的连接方式如果只使用到了all或者index连接方式我们可以从SQL语句和索引设计的角度上进行改进。
## 使用SHOW PROFILE查看SQL的具体执行成本
SHOW PROFILE相比EXPLAIN能看到更进一步的执行解析包括SQL都做了什么、所花费的时间等。默认情况下profiling是关闭的我们可以在会话级别开启这个功能。
```
mysql &gt; show variables like 'profiling';
```
<img src="https://static001.geekbang.org/resource/image/db/96/db06756d6d8c0614c2a7574b0f6ba896.png" alt=""><br>
通过设置`profiling='ON'`来开启show profile
```
mysql &gt; set profiling = 'ON';
```
<img src="https://static001.geekbang.org/resource/image/11/bd/113b83ee204c9e9bbd4d8bd406abafbd.png" alt=""><br>
我们可以看下当前会话都有哪些profiles使用下面这条命令
```
mysql &gt; show profiles;
```
<img src="https://static001.geekbang.org/resource/image/d0/80/d0f2ceae31f260f7b3e5f4e2a96e7280.png" alt=""><br>
你能看到当前会话一共有2个查询如果我们想要查看上一个查询的开销可以使用
```
mysql &gt; 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 这三种类型的区别吗?查询效率有何不同?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流进步。

View 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索引仅能满足=&lt;&gt;和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&gt; 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&gt;8 AND z=7”的时候如果建立了(x,y,z)顺序的索引这时候z是用不上索引的。这是因为MySQL在匹配联合索引最左前缀的时候如果遇到了范围查询比如&lt;&gt;和between等就会停止匹配。索引列最多作用于一个范围列对于后面的Z来说就没法使用到索引了。
通过这个我们也可以知道联合索引的最左前缀匹配原则针对的是创建的联合索引中的顺序如果创建了联合索引x,y,z那么这个索引的使用顺序就很重要了。如果在条件语句中只有y和z那么就用不上联合索引。
此外SQL条件语句中的字段顺序并不重要因为在逻辑查询优化阶段会自动进行查询重写。
最后你需要记住,如果我们遇到了范围条件查询,比如(&lt;&lt;=&gt;&gt;=和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&gt;1 AND z=3
```
B
```
SELECT x, y, z FROM table WHERE y=2 AND x=1 AND z&gt;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&gt;2 AND x=1 AND z=3
```
欢迎你在评论区写下你的答案,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -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组复制
组复制技术简称MGRMySQL 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的理解。
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View File

@@ -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&lt;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>
今天的内容到这里就结束了,我想问问,在日常工作中,你是否遇到过误操作的情况呢?你又是如何解决的?除了我上面介绍的机制外,还有哪些备份的机制可以增强数据的安全性?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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=&quot;SELECT * FROM users WHERE id='$id' LIMIT 0,1&quot;;
$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=&quot;SELECT ... FROM ... WHERE id='$id' LIMIT 0,1&quot;;
```
两处省略号的地方分别代表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 &quot;http://localhost/sqli-labs-master/Less-1/?id=1&quot; --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 &quot;http://localhost/sqli-labs-master/Less-1/?id=1&quot; --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 &quot;http://localhost/sqli-labs-master/Less-1/?id=1&quot; --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 &quot;http://localhost/sqli-labs-master/Less-1/?id=1&quot; --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 &quot;http://localhost/sqli-labs-master/Less-1/?id=1&quot; -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注入呢
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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_num177
Cabin_null_num687
Embarked_null_num2
Fare_null_num0
Name_null_num0
Parch_null_num0
PassengerId_null_num0
Pclass_null_num0
Sex_null_num0
SibSp_null_num0
Survived_null_num0
Ticket_null_num0
```
为了浏览方便我调整了运行结果的格式你能看到在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>
今天讲解的数据清洗的实例比较简单,实际上数据清洗是个反复的过程,有时候我们需要几天时间才能把数据完整清洗好。你在工作中,会使用哪些工具进行数据清洗呢?
另外,数据缺失问题在数据清洗中非常常见,我今天列举了三种填充数据缺失的方式,分别是删除、均值和高频的方式。实际上缺失值的处理方式不局限于这三种,你可以思考下,如果数据量非常大,某个字段的取值分布也很广,那么对这个字段中的缺失值该采用哪种方式来进行数据填充呢?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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完成这个转换呢
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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.02min_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} -&gt; {coffee}, {cookies} -&gt; {coffee}, {hot chocolate} -&gt; {coffee}, {juice} -&gt; {coffee}, {medialuna} -&gt; {coffee}, {pastry} -&gt; {coffee}, {sandwich} -&gt; {coffee}, {toast} -&gt; {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>
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。

View 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来进行获取。
## 每天积累一点,做重要不紧急的事
DTData Technology全栈在互联网的下半场会发挥越来越重要的作用如果你未来想从事数据相关的工作那么SQL是这个DT大厦中非常重要且使用频率很高的基石。在此基础上我们还可以掌握各种数据分析的能力以及数据可视化的能力。
如果我们把事情按照重要程度和紧急程度划分成4个象限的话我希望能在重要不紧急这个象限上你可以每天积累一点点。
在重要&amp;紧急这个象限上的事情会让你压力无限大无法做长远的学习积累。比如明天马上要交一个工作汇报程序Bug马上需要fix掉然后上线等。
重要&amp;不紧急这个象限上的事情能让你做更长远的计划,像是做投资一样播下一棵种子,在未来会有新的收获。
不重要&amp;紧急,会让我们忙碌且盲目。不重要&amp;不紧急,让我们浪费生命时间。
这两个象限的事情,做与不做其实区别不大。
<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)