This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

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