mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2026-05-10 19:54:28 +08:00
mod
This commit is contained in:
80
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/01丨了解SQL:一门半衰期很长的语言.md
Normal file
80
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/01丨了解SQL:一门半衰期很长的语言.md
Normal file
@@ -0,0 +1,80 @@
|
||||
<audio id="audio" title="01丨了解SQL:一门半衰期很长的语言" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/40/c3/407ef2c27870f4c4b30739688debf0c3.mp3"></audio>
|
||||
|
||||
在我们的日常工作中,使用的是类似MySQL、Oracle这种的数据库管理系统,实际上这些数据库管理系统都遵循SQL语言,这就意味着,我们在使用这些数据库的时候,都是通过SQL语言与它们打交道。所以对于从事编程或者互联网行业的人来说,最具有中台能力的语言便是SQL语言。自从SQL加入了TIOBE编程语言排行榜,就一直保持在Top 10。
|
||||
|
||||
1. SQL语言无处不在,它对于不同职位的人来说都有价值。SQL已经不仅仅是技术人员需要掌握的技能,产品经理、运营人员也同样需要掌握SQL。
|
||||
1. SQL语言从诞生到现在,很少变化。这就意味着一旦你掌握了它,就可以一劳永逸,至少在你的职业生涯中,它都可以发挥作用。
|
||||
1. SQL入门并不难。
|
||||
|
||||
## 半衰期很长的SQL
|
||||
|
||||
可以说在整个数字化的世界中,最重要而且最通用的元基础就是数据,而直接与数据打交道的语言就是SQL语言。很多人忽视了SQL语言的重要性,认为它不就是SELECT语句吗,掌握它应该是数据分析师的事。事实上在实际工作中,你不应该低估SQL的作用。如今互联网的很多业务处理离不开SQL,因为它们都需要与数据打交道。
|
||||
|
||||
SQL在各种技术和业务中无处不在,它的情况又是怎样的呢?45年前,也就是1974年,IBM研究员发布了一篇揭开数据库技术的论文《SEQUEL:一门结构化的英语查询语言》,直到今天这门结构化的查询语言并没有太大的变化,相比于其他语言,SQL的半衰期可以说是非常长了。
|
||||
|
||||
SQL有两个重要的标准,分别是SQL92和SQL99,它们分别代表了92年和99年颁布的SQL标准,我们今天使用的SQL语言依然遵循这些标准。要知道92年是Windows3.1发布的时间,如今还有多少人记得它,但如果你从事数据分析,或者和数据相关的工作,依然会用到SQL语言。
|
||||
|
||||
作为技术和互联网行业的从业人员,我们总是希望能找到一个通用性强,变化相对少,上手相对容易的语言,SQL正是为数不多的,可以满足这三个条件的语言。
|
||||
|
||||
## 入门SQL并不难
|
||||
|
||||
SQL功能这么强大,那么学起来会很难吗?一点也不。SQL不需要像其他语言那样,学习起来需要大量的程序语言基础,SQL更像是一门英语,有一些简单的英语单词,当你使用它的时候,就好像在用英语与数据库进行对话。
|
||||
|
||||
我们可以把SQL语言按照功能划分成以下的4个部分:
|
||||
|
||||
1. DDL,英文叫做Data Definition Language,也就是数据定义语言,它用来定义我们的数据库对象,包括数据库、数据表和列。通过使用DDL,我们可以创建,删除和修改数据库和表结构。
|
||||
1. DML,英文叫做Data Manipulation Language,数据操作语言,我们用它操作和数据库相关的记录,比如增加、删除、修改数据表中的记录。
|
||||
1. DCL,英文叫做Data Control Language,数据控制语言,我们用它来定义访问权限和安全级别。
|
||||
1. DQL,英文叫做Data Query Language,数据查询语言,我们用它查询想要的记录,它是SQL语言的重中之重。在实际的业务中,我们绝大多数情况下都是在和查询打交道,因此学会编写正确且高效的查询语句,是学习的重点。
|
||||
|
||||
学习SQL就像学习英文语法一样。SQL 是为数不多的声明性语言,这种语言的特点就是,你只需要告诉计算机,你想从原始数据中获取什么样的数据结果即可。比如我想找主要角色定位是战士的英雄,以及他们的英雄名和最大生命值,就可以输入下面的语言:
|
||||
|
||||
```
|
||||
SELECT name, hp_max FROM heros WHERE role_main = '战士'
|
||||
|
||||
```
|
||||
|
||||
这里我定义了heros数据表,包括了name、hp_max、role_main等字段,具体的数据表我会在后面的课程中作为示例讲解,这里只是做个简单的说明。
|
||||
|
||||
你能从这段代码看出,我并没有告诉计算机该如何执行才能得到结果,这也是声明性语言最大的便捷性。我们不需要指定具体的执行步骤,比如先执行哪一步,再执行哪一步,在执行前是否要检查是否满足条件A等等这些传统的编程思维。
|
||||
|
||||
SQL语言定义了我们的需求,而不同的DBMS(数据库管理系统)则会按照指定的SQL帮我们提取想要的结果,这样是不是很棒!
|
||||
|
||||
## 开启SQL之旅
|
||||
|
||||
SQL是我们与DBMS交流的语言,我们在创建DBMS之前,还需要对它进行设计,对于RDBMS来说采用的是ER图(Entity Relationship Diagram),即实体-关系图的方式进行设计。
|
||||
|
||||
ER图评审通过后,我们再用SQL语句或者可视化管理工具(如Navicat)创建数据表。
|
||||
|
||||
实体-关系图有什么用呢?它是我们用来描述现实世界的概念模型,在这个模型中有3个要素:实体、属性、关系。
|
||||
|
||||
实体就是我们要管理的对象,属性是标识每个实体的属性,关系则是对象之间的关系。比如我们创建了“英雄”这个实体,那么它下面的属性包括了姓名、职业、最大生命值、初始生命值、最大魔法值、初始魔法值和攻击范围等。同时,我们还有“用户”这个实体,它下面的属性包括用户ID、登录名、密码、性别和头像等。
|
||||
|
||||
“英雄”和“用户”这两个实体之间就是多对多的关系,也就是说一个英雄可以从属多个用户,而一个用户也可以拥有多个英雄。
|
||||
|
||||
除了多对多之外,也有一对一和一对多的关系。
|
||||
|
||||
创建完数据表之后,我们就可以用SQL操作了。你能看到很多SQL语句的大小写不统一,虽然大小写不会影响SQL的执行,不过我还是推荐你采用统一的书写规范,因为好的代码规范是提高效率的关键。
|
||||
|
||||
关于SQL大小写的问题,我总结了下面两点:
|
||||
|
||||
1. 表名、表别名、字段名、字段别名等都小写;
|
||||
1. SQL保留字、函数名、绑定变量等都大写。
|
||||
|
||||
比如下面这个SQL语句:
|
||||
|
||||
```
|
||||
SELECT name, hp_max FROM heros WHERE role_main = '战士'
|
||||
|
||||
```
|
||||
|
||||
你能看到SELECT、FROM、WHERE这些常用的SQL保留字都采用了大写,而name、hp_max、role_main这些字段名,表名都采用了小写。此外在数据表的字段名推荐采用下划线命名,比如role_main这种。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我带你初步了解了SQL语言,当然,SQL再简单,也还是需要你一步一步,从点滴做起,先掌握基本的DDL、DML、DCL和DQL语法,再了解不同的DBMS中的SQL语法差异,然后再来看如何优化,提升SQL的效率。要想写出高性能的SQL,首先要了解它的原理,其次就是做大量的练习。
|
||||
|
||||
SQL的价值在于通用性强(市场需求普遍),半衰期长(一次学习终身受用),入门不难。实际上,很多事情的价值都可以按照这三点来进行判断,比如一个产品的市场价值。如果你是一名产品经理,你是喜欢通用性更强的产品,还是喜欢更个性的产品。今天的文章只是简单预热,你可能也会有一些感悟,不妨说说你对一个产品或者语言的市场价值的理解。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ca/11/ca5cb085cbb961ac1569d3e22b404e11.jpg" alt=""><br>
|
||||
欢迎你在评论区写下你的心得,也欢迎把这篇文章分享给你的朋友或者同事,让更多人了解SQL这门语言。
|
||||
88
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/02丨DBMS的前世今生.md
Normal file
88
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/02丨DBMS的前世今生.md
Normal file
@@ -0,0 +1,88 @@
|
||||
<audio id="audio" title="02丨DBMS的前世今生" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/82/46/82aba5e8d81c9ac11c675c79d2d7b246.mp3"></audio>
|
||||
|
||||
上一讲我们介绍过两个IBM研究员在1974年发表了一篇有关结构化英语查询语言的论文,并将这门语言命名为SEQUEL。SEQUEL的语言标准是开放的,但是围绕它的商业化竞争却从来没有停止过。首先因为商标之争,SEQUEL改名为SQL,后来有一个重量级的公司基于那篇论文研发出了商业化的数据库管理软件,这就是Oracle。自此之后,又诞生了一堆大家熟知的DBMS,比如MySQL、SQL Server、PostgreSQL、DB2和MongoDB等。
|
||||
|
||||
我们今天了解一下这些数据库管理软件,也就是DBMS。为什么需要了解它们呢?因为在工作中,我们通常都是和这些数据库管理软件打交道,只不过SQL是它们的通用语言。
|
||||
|
||||
今天我们先从SQL语言中跳脱出来,来分析下这些DBMS。关于今天的内容,你可以从以下几个方面进行思考:
|
||||
|
||||
1. 主流的DBMS都有哪些,它们各自都有哪些特点;
|
||||
1. 既然SQL是通用的标准语言,为什么能存在这么多DBMS;
|
||||
1. 从这些DBMS的发展史中,你有哪些感悟。
|
||||
|
||||
## DB、DBS和DBMS的区别是什么
|
||||
|
||||
说到DBMS,有一些概念你需要了解。
|
||||
|
||||
DBMS的英文全称是DataBase Management System,数据库管理系统,实际上它可以对多个数据库进行管理,所以你可以理解为DBMS = 多个数据库(DB) + 管理程序。
|
||||
|
||||
DB的英文是DataBase,也就是数据库。数据库是存储数据的集合,你可以把它理解为多个数据表。
|
||||
|
||||
DBS的英文是DataBase System,数据库系统。它是更大的概念,包括了数据库、数据库管理系统以及数据库管理人员DBA。
|
||||
|
||||
这里需要注意的是,虽然我们有时候把Oracle、MySQL等称之为数据库,但确切讲,它们应该是数据库管理系统,即DBMS。
|
||||
|
||||
## 排名前20的DBMS都有哪些?
|
||||
|
||||
了解了DBMS的概念之后,我们来看下当前主流的DBMS都有哪些。下面这张表是2019年5月DB-Engines公布的DBMS的排名(每年的排名会有更新,主要依据这些DBMS在搜索引擎上的热度):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a7/91/a7237ddbe4ca69353bd21a6eff35d391.png" alt="">
|
||||
|
||||
从排名中我们能看出来,关系型数据库绝对是DBMS的主流,其中使用最多的DBMS分别是Oracle、MySQL和SQL Server。
|
||||
|
||||
关系型数据库(RDBMS)就是建立在关系模型基础上的数据库,SQL就是关系型数据库的查询语言。
|
||||
|
||||
相比于SQL,NoSQL泛指非关系型数据库,包括了榜单上的键值型数据库、文档型数据库、搜索引擎和列存储等,除此以外还包括图形数据库。
|
||||
|
||||
键值型数据库通过Key-Value键值的方式来存储数据,其中Key和Value可以是简单的对象,也可以是复杂的对象。Key作为唯一的标识符,优点是查找速度快,在这方面明显优于关系型数据库,同时缺点也很明显,它无法像关系型数据库一样自由使用条件过滤(比如WHERE),如果你不知道去哪里找数据,就要遍历所有的键,这就会消耗大量的计算。键值型数据库典型的使用场景是作为内容缓存。Redis是最流行的键值型数据库。
|
||||
|
||||
文档型数据库用来管理文档,在数据库中文档作为处理信息的基本单位,一个文档就相当于一条记录,MongoDB是最流行的文档型数据库。
|
||||
|
||||
搜索引擎也是数据库检索中的重要应用,常见的全文搜索引擎有Elasticsearch、Splunk和Solr。虽然关系型数据库采用了索引提升检索效率,但是针对全文索引效率却较低。搜索引擎的优势在于采用了全文搜索的技术,核心原理是“倒排索引”。
|
||||
|
||||
列式数据库是相对于行式存储的数据库,Oracle、MySQL、SQL Server等数据库都是采用的行式存储(Row-based),而列式数据库是将数据按照列存储到数据库中,这样做的好处是可以大量降低系统的I/O,适合于分布式文件系统,不足在于功能相对有限。
|
||||
|
||||
图形数据库,利用了图这种数据结构存储了实体(对象)之间的关系。最典型的例子就是社交网络中人与人的关系,数据模型主要是以节点和边(关系)来实现,特点在于能高效地解决复杂的关系问题。
|
||||
|
||||
## SQL阵营与NoSQL阵营
|
||||
|
||||
NoSQL的分类很多,刚才提到的键值型、文档型、搜索引擎、列式存储和图形数据库等都属于NoSQL阵营。也只有用NoSQL一词才能将这些技术囊括进来。即便如此,在DBMS排名中,还是SQL阵营的比重更大,影响力前5的DBMS中有4个是关系型数据库,而排名前20的DBMS中也有12个是关系型数据库。所以说,掌握SQL是非常有必要的。
|
||||
|
||||
由于SQL一直称霸DBMS,因此许多人在思考是否有一种数据库技术能远离SQL,于是NoSQL诞生了,但是随着发展却发现越来越离不开SQL。到目前为止NoSQL阵营中的DBMS都会有实现类似SQL的功能。下面是“NoSQL”这个名词在不同时期的诠释,从这些释义的变化中可以看出NoSQL功能的演变:
|
||||
|
||||
1970:NoSQL = We have no SQL
|
||||
|
||||
1980:NoSQL = Know SQL
|
||||
|
||||
2000:NoSQL = No SQL!
|
||||
|
||||
2005:NoSQL = Not only SQL
|
||||
|
||||
2013:NoSQL = No, SQL!
|
||||
|
||||
NoSQL对SQL做出了很好的补充,它可以让我们在云计算时代,更好地使用数据库技术,比如快速读写,这样可以用低廉的成本,更方便进行扩展。整个专栏的学习也将围绕SQL展开,同时你还需要了解SQL阵营中不同的DBMS之间的使用差异。这些DBMS除了支持SQL标准以外,还会有自己的“方言”,也就是自己独有的语法。在专栏中,我也会对近些年热门的NoSQL进行讲解,方便你在后续使用中更快上手。
|
||||
|
||||
## SQL阵营中的DBMS
|
||||
|
||||
如果我们把数据互通作为当今数字化社会发展的大中台能力,那么DBMS无疑是一个巨大的市场。在这个市场中,排名前20的DBMS有12个属于SQL阵营,其中排名前3名的DBMS均为SQL阵营,它们分别是Oracle、MySQL和SQL Server。这三家的市场份额远超其他DBMS的市场份额。
|
||||
|
||||
下面,我们来简单介绍下这三个主流DBMS的发展。
|
||||
|
||||
1979年,Oracle 2诞生,它是第一个商用的RDBMS(关系型数据库管理系统),随后被卖给了军方客户。随着Oracle软件的名气越来越大,公司也改叫Oracle公司。20世纪90年代,Oracle的创始人埃里森成为继比尔·盖茨之后第二富有的人,可以说IBM缔造了两个帝国,一个是软件业的霸主微软,另一个是企业软件市场的霸主Oracle。如今Oracle的年收入达到了400亿美金,足以证明商用数据库软件的价值。从这点我们也能看出,如果选择了一个大的赛道,就要尽早商业化,占据大型企业客户完全可以创建巨大的商业价值,也足以证明一个软件企业不需要靠卖硬件也可以挣到很多钱。
|
||||
|
||||
MySQL是1995年诞生的开源数据库管理系统,因为免费开源的特性,得到了开发者的喜爱,用户量迅速增长,成为开源数据库的No.1。但在发展过程中,MySQL先后两次被易手,先是在2008年被SUN收购,然后在2010年SUN被Oracle收购,于是Oracle同时拥有了MySQL的管理权,至此Oracle在数据库领域中成为绝对的领导者。从这里我们也能看到,虽然MySQL是免费的产品,但是使用人数多,就足以证明巨大的用户价值。一个有巨大用户价值的产品,即使没有直接的商业价值,但作为基础设施也会被商业巨头看上。
|
||||
|
||||
不过在Oracle收购MySQL的同时,MySQL的创造者担心MySQL有闭源的风险,因此创建了MySQL的分支项目MariaDB,MariaDB在绝大部分情况下都是与MySQL兼容的,并且增加了许多新的特性,比如支持更多的存储引擎类型。许多企业也由原来的MySQL纷纷转向了MariaDB。
|
||||
|
||||
SQL Server是微软开发的商业数据库,诞生于1989年。实际上微软还推出了Access数据库,它是一种桌面数据库,同时具备后台存储和前台界面开发的功能,更加轻量级,适合小型的应用场景。因为后台的存储空间有限,一般只有2G,Access的优势在于可以在前台便捷地进行界面开发。而SQL Server是大型数据库,用于后台的存储和查询,不具备界面开发的功能。从这里我们也能看出,即使SQL语言是通用的,但是为了满足不同用户的使用场景,会存在多个DBMS。比如Oracle更适合大型跨国企业的使用,因为他们对费用不敏感,但是对性能要求以及安全性有更高的要求,而MySQL更受到许多互联网公司,尤其是早期创业公司的青睐。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们简单梳理了DBMS的发展。1974年,SEQUEL论文发表,1979年,第一个商用关系型数据库Oracle 2诞生,1995年,MySQL开源数据库诞生,如今,NoSQL得到了发展,并且围绕SQL标准展开的DBMS竞赛从来没有停止过。在这段发展史中,既有SQL阵营,又有NoSQL阵营,既有商业数据库软件,又有开源产品,在不同的应用场景下,同一家公司也会有不同的DBMS布局。
|
||||
|
||||
如果说不同的DBMS代表了不同公司的利益,那么作为使用者的我们更应该注重的是这些DBMS的使用场景。比如Oracle作为市场占有率最高的商用数据库软件,适合大型的跨国企业,而针对轻量级的桌面数据库,我们采用Access就可以了。对于免费开源的产品来说,可以选用MySQL或者MariaDB。同时在NoSQL阵营中,我们也需要了解键值型、文档型、搜索引擎、列式数据库和图形数据库的区别。
|
||||
|
||||
我在文章中列举了排名前20的DBMS,你都使用过哪些呢?可以说说你的使用体会吗?另外你有没有想过,虽然SQL是通用的标准语言,但为什么能存在这么多DBMS呢?
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fc/d0/fcb0eba7fe352cb1dd5a17efc99d81d0.jpg" alt=""><br>
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事。
|
||||
175
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/03丨学会用数据库的方式思考SQL是如何执行的.md
Normal file
175
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/03丨学会用数据库的方式思考SQL是如何执行的.md
Normal file
@@ -0,0 +1,175 @@
|
||||
<audio id="audio" title="03丨学会用数据库的方式思考SQL是如何执行的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ad/07/ad590188d5b924bf34f75dd6f0324a07.mp3"></audio>
|
||||
|
||||
通过上一篇文章对不同的DBMS的介绍,你应该对它们有了一些基础的了解。虽然SQL是声明式语言,我们可以像使用英语一样使用它,不过在RDBMS(关系型数据库管理系统)中,SQL的实现方式还是有差别的。今天我们就从数据库的角度来思考一下SQL是如何被执行的。
|
||||
|
||||
关于今天的内容,你会从以下几个方面进行学习:
|
||||
|
||||
1. Oracle中的SQL是如何执行的,什么是硬解析和软解析;
|
||||
1. MySQL中的SQL是如何执行的,MySQL的体系结构又是怎样的;
|
||||
1. 什么是存储引擎,MySQL的存储引擎都有哪些?
|
||||
|
||||
## Oracle中的SQL是如何执行的
|
||||
|
||||
我们先来看下SQL在Oracle中的执行过程:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/70/4b43aeaf9bb0fe2d576757d3fef50070.png" alt=""><br>
|
||||
从上面这张图中可以看出,SQL语句在Oracle中经历了以下的几个步骤。
|
||||
|
||||
<li>
|
||||
语法检查:检查SQL拼写是否正确,如果不正确,Oracle会报语法错误。
|
||||
</li>
|
||||
<li>
|
||||
语义检查:检查SQL中的访问对象是否存在。比如我们在写SELECT语句的时候,列名写错了,系统就会提示错误。语法检查和语义检查的作用是保证SQL语句没有错误。
|
||||
</li>
|
||||
<li>
|
||||
权限检查:看用户是否具备访问该数据的权限。
|
||||
</li>
|
||||
<li>
|
||||
共享池检查:共享池(Shared Pool)是一块内存池,最主要的作用是缓存SQL语句和该语句的执行计划。Oracle通过检查共享池是否存在SQL语句的执行计划,来判断进行软解析,还是硬解析。那软解析和硬解析又该怎么理解呢?
|
||||
在共享池中,Oracle首先对SQL语句进行Hash运算,然后根据Hash值在库缓存(Library Cache)中查找,如果存在SQL语句的执行计划,就直接拿来执行,直接进入“执行器”的环节,这就是软解析。
|
||||
如果没有找到SQL语句和执行计划,Oracle就需要创建解析树进行解析,生成执行计划,进入“优化器”这个步骤,这就是硬解析。
|
||||
</li>
|
||||
<li>
|
||||
优化器:优化器中就是要进行硬解析,也就是决定怎么做,比如创建解析树,生成执行计划。
|
||||
</li>
|
||||
<li>
|
||||
执行器:当有了解析树和执行计划之后,就知道了SQL该怎么被执行,这样就可以在执行器中执行语句了。
|
||||
</li>
|
||||
|
||||
共享池是Oracle中的术语,包括了库缓存,数据字典缓冲区等。我们上面已经讲到了库缓存区,它主要缓存SQL语句和执行计划。而数据字典缓冲区存储的是Oracle中的对象定义,比如表、视图、索引等对象。当对SQL语句进行解析的时候,如果需要相关的数据,会从数据字典缓冲区中提取。
|
||||
|
||||
库缓存这一个步骤,决定了SQL语句是否需要进行硬解析。为了提升SQL的执行效率,我们应该尽量避免硬解析,因为在SQL的执行过程中,创建解析树,生成执行计划是很消耗资源的。
|
||||
|
||||
你可能会问,如何避免硬解析,尽量使用软解析呢?在Oracle中,绑定变量是它的一大特色。绑定变量就是在SQL语句中使用变量,通过不同的变量取值来改变SQL的执行结果。这样做的好处是能提升软解析的可能性,不足之处在于可能会导致生成的执行计划不够优化,因此是否需要绑定变量还需要视情况而定。
|
||||
|
||||
举个例子,我们可以使用下面的查询语句:
|
||||
|
||||
```
|
||||
SQL> select * from player where player_id = 10001;
|
||||
|
||||
```
|
||||
|
||||
你也可以使用绑定变量,如:
|
||||
|
||||
```
|
||||
SQL> select * from player where player_id = :player_id;
|
||||
|
||||
```
|
||||
|
||||
这两个查询语句的效率在Oracle中是完全不同的。如果你在查询player_id = 10001之后,还会查询10002、10003之类的数据,那么每一次查询都会创建一个新的查询解析。而第二种方式使用了绑定变量,那么在第一次查询之后,在共享池中就会存在这类查询的执行计划,也就是软解析。
|
||||
|
||||
因此我们可以通过使用绑定变量来减少硬解析,减少Oracle的解析工作量。但是这种方式也有缺点,使用动态SQL的方式,因为参数不同,会导致SQL的执行效率不同,同时SQL优化也会比较困难。
|
||||
|
||||
## MySQL中的SQL是如何执行的
|
||||
|
||||
Oracle中采用了共享池来判断SQL语句是否存在缓存和执行计划,通过这一步骤我们可以知道应该采用硬解析还是软解析。那么在MySQL中,SQL是如何被执行的呢?
|
||||
|
||||
首先MySQL是典型的C/S架构,即Client/Server架构,服务器端程序使用的mysqld。整体的MySQL流程如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c4/9e/c4b24ef2377e0d233af69925b0d7139e.png" alt=""><br>
|
||||
你能看到MySQL由三层组成:
|
||||
|
||||
1. 连接层:客户端和服务器端建立连接,客户端发送SQL至服务器端;
|
||||
1. SQL层:对SQL语句进行查询处理;
|
||||
1. 存储引擎层:与数据库文件打交道,负责数据的存储和读取。
|
||||
|
||||
其中SQL层与数据库文件的存储方式无关,我们来看下SQL层的结构:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/30/79/30819813cc9d53714c08527e282ede79.jpg" alt="">
|
||||
|
||||
1. 查询缓存:Server如果在查询缓存中发现了这条SQL语句,就会直接将结果返回给客户端;如果没有,就进入到解析器阶段。需要说明的是,因为查询缓存往往效率不高,所以在MySQL8.0之后就抛弃了这个功能。
|
||||
1. 解析器:在解析器中对SQL语句进行语法分析、语义分析。
|
||||
1. 优化器:在优化器中会确定SQL语句的执行路径,比如是根据全表检索,还是根据索引来检索等。
|
||||
1. 执行器:在执行之前需要判断该用户是否具备权限,如果具备权限就执行SQL查询并返回结果。在MySQL8.0以下的版本,如果设置了查询缓存,这时会将查询结果进行缓存。
|
||||
|
||||
你能看到SQL语句在MySQL中的流程是:SQL语句→缓存查询→解析器→优化器→执行器。在一部分中,MySQL和Oracle执行SQL的原理是一样的。
|
||||
|
||||
与Oracle不同的是,MySQL的存储引擎采用了插件的形式,每个存储引擎都面向一种特定的数据库应用环境。同时开源的MySQL还允许开发人员设置自己的存储引擎,下面是一些常见的存储引擎:
|
||||
|
||||
1. InnoDB存储引擎:它是MySQL 5.5版本之后默认的存储引擎,最大的特点是支持事务、行级锁定、外键约束等。
|
||||
1. MyISAM存储引擎:在MySQL 5.5版本之前是默认的存储引擎,不支持事务,也不支持外键,最大的特点是速度快,占用资源少。
|
||||
1. Memory存储引擎:使用系统内存作为存储介质,以便得到更快的响应速度。不过如果mysqld进程崩溃,则会导致所有的数据丢失,因此我们只有当数据是临时的情况下才使用Memory存储引擎。
|
||||
1. NDB存储引擎:也叫做NDB Cluster存储引擎,主要用于MySQL Cluster分布式集群环境,类似于Oracle的RAC集群。
|
||||
1. Archive存储引擎:它有很好的压缩机制,用于文件归档,在请求写入时会进行压缩,所以也经常用来做仓库。
|
||||
|
||||
需要注意的是,数据库的设计在于表的设计,而在MySQL中每个表的设计都可以采用不同的存储引擎,我们可以根据实际的数据处理需要来选择存储引擎,这也是MySQL的强大之处。
|
||||
|
||||
## 数据库管理系统也是一种软件
|
||||
|
||||
我们刚才了解了SQL语句在Oracle和MySQL中的执行流程,实际上完整的Oracle和MySQL结构图要复杂得多:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/74/d99e951b69a692c7f075dd21116d3574.png" alt=""><br>
|
||||
<img src="https://static001.geekbang.org/resource/image/9b/7f/9b515e012856099b05d9dc3a5eaabe7f.png" alt=""><br>
|
||||
如果你只是简单地把MySQL和Oracle看成数据库管理系统软件,从外部看难免会觉得“晦涩难懂”,毕竟组织结构太多了。我们在学习的时候,还需要具备抽象的能力,抓取最核心的部分:SQL的执行原理。因为不同的DBMS的SQL的执行原理是相通的,只是在不同的软件中,各有各的实现路径。
|
||||
|
||||
既然一条SQL语句会经历不同的模块,那我们就来看下,在不同的模块中,SQL执行所使用的资源(时间)是怎样的。下面我来教你如何在MySQL中对一条SQL语句的执行时间进行分析。
|
||||
|
||||
首先我们需要看下profiling是否开启,开启它可以让MySQL收集在SQL执行时所使用的资源情况,命令如下:
|
||||
|
||||
```
|
||||
mysql> select @@profiling;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/bc/c1/bcbfdd58b908dc8820fb57d00ff4dcc1.png" alt=""><br>
|
||||
profiling=0代表关闭,我们需要把profiling打开,即设置为1:
|
||||
|
||||
```
|
||||
mysql> set profiling=1;
|
||||
|
||||
```
|
||||
|
||||
然后我们执行一个SQL查询(你可以执行任何一个SQL查询):
|
||||
|
||||
```
|
||||
mysql> select * from wucai.heros;
|
||||
|
||||
```
|
||||
|
||||
查看当前会话所产生的所有profiles:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/bf/d9445abcde0f3b38488afe21aca8e9bf.png" alt=""><br>
|
||||
你会发现我们刚才执行了两次查询,Query ID分别为1和2。如果我们想要获取上一次查询的执行时间,可以使用:
|
||||
|
||||
```
|
||||
mysql> show profile;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/09/7d/09ef901a55ffcd32ed263d82e3cf1f7d.png" alt=""><br>
|
||||
当然你也可以查询指定的Query ID,比如:
|
||||
|
||||
```
|
||||
mysql> show profile for query 2;
|
||||
|
||||
```
|
||||
|
||||
查询SQL的执行时间结果和上面是一样的。
|
||||
|
||||
在8.0版本之后,MySQL不再支持缓存的查询,原因我在上文已经说过。一旦数据表有更新,缓存都将清空,因此只有数据表是静态的时候,或者数据表很少发生变化时,使用缓存查询才有价值,否则如果数据表经常更新,反而增加了SQL的查询时间。
|
||||
|
||||
你可以使用select version()来查看MySQL的版本情况。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/08/1a/0815cf2a78889b947cb498622377c21a.png" alt="">
|
||||
|
||||
## 总结
|
||||
|
||||
我们在使用SQL的时候,往往只见树木,不见森林,不会注意到它在各种数据库软件中是如何执行的,今天我们从全貌的角度来理解这个问题。你能看到不同的RDBMS之间有相同的地方,也有不同的地方。
|
||||
|
||||
相同的地方在于Oracle和MySQL都是通过解析器→优化器→执行器这样的流程来执行SQL的。
|
||||
|
||||
但Oracle和MySQL在进行SQL的查询上面有软件实现层面的差异。Oracle提出了共享池的概念,通过共享池来判断是进行软解析,还是硬解析。而在MySQL中,8.0以后的版本不再支持查询缓存,而是直接执行解析器→优化器→执行器的流程,这一点从MySQL中的show profile里也能看到。同时MySQL的一大特色就是提供了各种存储引擎以供选择,不同的存储引擎有各自的使用场景,我们可以针对每张表选择适合的存储引擎。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/02/f1/02719a80d54a174dec8672d1f87295f1.jpg" alt=""><br>
|
||||
今天的内容到这里就结束了,你能说一下Oracle中的绑定变量是什么,使用它有什么优缺点吗?MySQL的存储引擎是一大特色,其中MyISAM和InnoDB都是常用的存储引擎,这两个存储引擎的特性和使用场景分别是什么?
|
||||
|
||||
最后留一道选择题吧,解析后的SQL语句在Oracle的哪个区域中进行缓存?
|
||||
|
||||
A. 数据缓冲区<br>
|
||||
B. 日志缓冲区<br>
|
||||
C. 共享池<br>
|
||||
D. 大池
|
||||
|
||||
欢迎你在评论区写下你的思考,我会在评论区与你一起交流,如果这篇文章帮你理顺了Oracle和MySQL执行SQL的过程,欢迎你把它分享给你的朋友或者同事。
|
||||
|
||||
※注:本篇文章出现的图片请点击[这里](http://github.com/cystanford/SQL-XMind)下载高清大图。
|
||||
192
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/04丨使用DDL创建数据库&数据表时需要注意什么?.md
Normal file
192
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/04丨使用DDL创建数据库&数据表时需要注意什么?.md
Normal file
@@ -0,0 +1,192 @@
|
||||
<audio id="audio" title="04丨使用DDL创建数据库&数据表时需要注意什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/48/2d/48ebb7342670d6de72e6c79ae1b5d92d.mp3"></audio>
|
||||
|
||||
DDL是DBMS的核心组件,也是SQL的重要组成部分,DDL的正确性和稳定性是整个SQL运行的重要基础。面对同一个需求,不同的开发人员创建出来的数据库和数据表可能千差万别,那么在设计数据库的时候,究竟什么是好的原则?我们在创建数据表的时候需要注意什么?
|
||||
|
||||
今天的内容,你可以从以下几个角度来学习:
|
||||
|
||||
1. 了解DDL的基础语法,它如何定义数据库和数据表;
|
||||
1. 使用DDL定义数据表时,都有哪些约束性;
|
||||
1. 使用DDL设计数据库时,都有哪些重要原则。
|
||||
|
||||
## DDL的基础语法及设计工具
|
||||
|
||||
DDL的英文全称是Data Definition Language,中文是数据定义语言。它定义了数据库的结构和数据表的结构。
|
||||
|
||||
在DDL中,我们常用的功能是增删改,分别对应的命令是CREATE、DROP和ALTER。需要注意的是,在执行DDL的时候,不需要COMMIT,就可以完成执行任务。
|
||||
|
||||
1.**对数据库进行定义**
|
||||
|
||||
```
|
||||
CREATE DATABASE nba; // 创建一个名为nba的数据库
|
||||
DROP DATABASE nba; // 删除一个名为nba的数据库
|
||||
|
||||
```
|
||||
|
||||
2.**对数据表进行定义**
|
||||
|
||||
创建表结构的语法是这样的:
|
||||
|
||||
```
|
||||
CREATE TABLE [table_name](字段名 数据类型,......)
|
||||
|
||||
```
|
||||
|
||||
### 创建表结构
|
||||
|
||||
比如我们想创建一个球员表,表名为player,里面有两个字段,一个是player_id,它是int类型,另一个player_name字段是`varchar(255)`类型。这两个字段都不为空,且player_id是递增的。
|
||||
|
||||
那么创建的时候就可以写为:
|
||||
|
||||
```
|
||||
CREATE TABLE player (
|
||||
player_id int(11) NOT NULL AUTO_INCREMENT,
|
||||
player_name varchar(255) NOT NULL
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,语句最后以分号(;)作为结束符,最后一个字段的定义结束后没有逗号。数据类型中int(11)代表整数类型,显示长度为11位,括号中的参数11代表的是最大有效显示长度,与类型包含的数值范围大小无关。`varchar(255)`代表的是最大长度为255的可变字符串类型。`NOT NULL`表明整个字段不能是空值,是一种数据约束。`AUTO_INCREMENT`代表主键自动增长。
|
||||
|
||||
实际上,我们通常很少自己写DDL语句,可以使用一些可视化工具来创建和操作数据库和数据表。在这里我推荐使用Navicat,它是一个数据库管理和设计工具,跨平台,支持很多种数据库管理软件,比如MySQL、Oracle、MariaDB等。基本上专栏讲到的数据库软件都可以使用Navicat来管理。
|
||||
|
||||
假如还是针对player这张表,我们想设计以下的字段:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/dd/4925e0c2a1342f4b19d29f0d665afbdd.png" alt=""><br>
|
||||
其中player_id是数据表player的主键,且自动增长,也就是player_id会从1开始,然后每次加1。player_id、team_id、player_name 这三个字段均不为空,height字段可以为空。
|
||||
|
||||
按照上面的设计需求,我们可以使用Navicat软件进行设计,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cc/ef/cca134810dee3f5de5413482103367ef.png" alt=""><br>
|
||||
然后,我们还可以对player_name字段进行索引,索引类型为`Unique`。使用Navicat设置如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9a/5b/9a806e7be2f61c64a034ee8a422ea55b.png" alt=""><br>
|
||||
这样一张player表就通过可视化工具设计好了。我们可以把这张表导出来,可以看看这张表对应的SQL语句是怎样的。方法是在Navicat左侧用右键选中player这张表,然后选择“转储SQL文件”→“仅结构”,这样就可以看到导出的SQL文件了,代码如下:
|
||||
|
||||
```
|
||||
DROP TABLE IF EXISTS `player`;
|
||||
CREATE TABLE `player` (
|
||||
`player_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`team_id` int(11) NOT NULL,
|
||||
`player_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
`height` float(3, 2) NULL DEFAULT 0.00,
|
||||
PRIMARY KEY (`player_id`) USING BTREE,
|
||||
UNIQUE INDEX `player_name`(`player_name`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
```
|
||||
|
||||
你能看到整个SQL文件中的DDL处理,首先先删除player表(如果数据库中存在该表的话),然后再创建player表,里面的数据表和字段都使用了反引号,这是为了避免它们的名称与MySQL保留字段相同,对数据表和字段名称都加上了反引号。
|
||||
|
||||
其中player_name字段的字符编码是utf8,排序规则是`utf8_general_ci`,代表对大小写不敏感,如果设置为`utf8_bin`,代表对大小写敏感,还有许多其他排序规则这里不进行介绍。
|
||||
|
||||
因为player_id设置为了主键,因此在DDL中使用`PRIMARY KEY`进行规定,同时索引方法采用BTREE。
|
||||
|
||||
因为我们对player_name字段进行索引,在设置字段索引时,我们可以设置为`UNIQUE INDEX`(唯一索引),也可以设置为其他索引方式,比如`NORMAL INDEX`(普通索引),这里我们采用`UNIQUE INDEX`。唯一索引和普通索引的区别在于它对字段进行了唯一性的约束。在索引方式上,你可以选择`BTREE`或者`HASH`,这里采用了`BTREE`方法进行索引。我会在后面介绍`BTREE`和`HASH`索引方式的区别。
|
||||
|
||||
整个数据表的存储规则采用InnoDB。之前我们简单介绍过InnoDB,它是MySQL5.5版本之后默认的存储引擎。同时,我们将字符编码设置为utf8,排序规则为`utf8_general_ci`,行格式为`Dynamic`,就可以定义数据表的最后约定了:
|
||||
|
||||
```
|
||||
ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
```
|
||||
|
||||
你能看出可视化工具还是非常方便的,它能直接帮我们将数据库的结构定义转化成SQL语言,方便数据库和数据表结构的导出和导入。不过在使用可视化工具前,你首先需要了解对于DDL的基础语法,至少能清晰地看出来不同字段的定义规则、索引方法,以及主键和外键的定义。
|
||||
|
||||
### 修改表结构
|
||||
|
||||
在创建表结构之后,我们还可以对表结构进行修改,虽然直接使用Navicat可以保证重新导出的数据表就是最新的,但你也有必要了解,如何使用DDL命令来完成表结构的修改。
|
||||
|
||||
1.添加字段,比如我在数据表中添加一个age字段,类型为`int(11)`
|
||||
|
||||
```
|
||||
ALTER TABLE player ADD (age int(11));
|
||||
|
||||
```
|
||||
|
||||
2.修改字段名,将age字段改成`player_age`
|
||||
|
||||
```
|
||||
ALTER TABLE player RENAME COLUMN age to player_age
|
||||
|
||||
```
|
||||
|
||||
3.修改字段的数据类型,将`player_age`的数据类型设置为`float(3,1)`
|
||||
|
||||
```
|
||||
ALTER TABLE player MODIFY (player_age float(3,1));
|
||||
|
||||
```
|
||||
|
||||
4.删除字段, 删除刚才添加的`player_age`字段
|
||||
|
||||
```
|
||||
ALTER TABLE player DROP COLUMN player_age;
|
||||
|
||||
```
|
||||
|
||||
## 数据表的常见约束
|
||||
|
||||
当我们创建数据表的时候,还会对字段进行约束,约束的目的在于保证RDBMS里面数据的准确性和一致性。下面,我们来看下常见的约束有哪些。
|
||||
|
||||
首先是主键约束。
|
||||
|
||||
主键起的作用是唯一标识一条记录,不能重复,不能为空,即UNIQUE+NOT NULL。一个数据表的主键只能有一个。主键可以是一个字段,也可以由多个字段复合组成。在上面的例子中,我们就把player_id设置为了主键。
|
||||
|
||||
其次还有外键约束。
|
||||
|
||||
外键确保了表与表之间引用的完整性。一个表中的外键对应另一张表的主键。外键可以是重复的,也可以为空。比如player_id在player表中是主键,如果你想设置一个球员比分表即player_score,就可以在player_score中设置player_id为外键,关联到player表中。
|
||||
|
||||
除了对键进行约束外,还有字段约束。
|
||||
|
||||
唯一性约束。
|
||||
|
||||
唯一性约束表明了字段在表中的数值是唯一的,即使我们已经有了主键,还可以对其他字段进行唯一性约束。比如我们在player表中给player_name设置唯一性约束,就表明任何两个球员的姓名不能相同。需要注意的是,唯一性约束和普通索引(NORMAL INDEX)之间是有区别的。唯一性约束相当于创建了一个约束和普通索引,目的是保证字段的正确性,而普通索引只是提升数据检索的速度,并不对字段的唯一性进行约束。
|
||||
|
||||
NOT NULL约束。对字段定义了NOT NULL,即表明该字段不应为空,必须有取值。
|
||||
|
||||
DEFAULT,表明了字段的默认值。如果在插入数据的时候,这个字段没有取值,就设置为默认值。比如我们将身高height字段的取值默认设置为0.00,即`DEFAULT 0.00`。
|
||||
|
||||
CHECK约束,用来检查特定字段取值范围的有效性,CHECK约束的结果不能为FALSE,比如我们可以对身高height的数值进行CHECK约束,必须≥0,且<3,即`CHECK(height>=0 AND height<3)`。
|
||||
|
||||
## 设计数据表的原则
|
||||
|
||||
我们在设计数据表的时候,经常会考虑到各种问题,比如:用户都需要什么数据?需要在数据表中保存哪些数据?哪些数据是经常访问的数据?如何提升检索效率?
|
||||
|
||||
如何保证数据表中数据的正确性,当插入、删除、更新的时候该进行怎样的约束检查?
|
||||
|
||||
如何降低数据表的数据冗余度,保证数据表不会因为用户量的增长而迅速扩张?
|
||||
|
||||
如何让负责数据库维护的人员更方便地使用数据库?
|
||||
|
||||
除此以外,我们使用数据库的应用场景也各不相同,可以说针对不同的情况,设计出来的数据表可能千差万别。那么有没有一种设计原则可以让我们来借鉴呢?这里我整理了一个“**三少一多**”原则:
|
||||
|
||||
1.**数据表的个数越少越好**
|
||||
|
||||
RDBMS的核心在于对实体和联系的定义,也就是E-R图(Entity Relationship Diagram),数据表越少,证明实体和联系设计得越简洁,既方便理解又方便操作。
|
||||
|
||||
2.**数据表中的字段个数越少越好**
|
||||
|
||||
字段个数越多,数据冗余的可能性越大。设置字段个数少的前提是各个字段相互独立,而不是某个字段的取值可以由其他字段计算出来。当然字段个数少是相对的,我们通常会在数据冗余和检索效率中进行平衡。
|
||||
|
||||
3.**数据表中联合主键的字段个数越少越好**
|
||||
|
||||
设置主键是为了确定唯一性,当一个字段无法确定唯一性的时候,就需要采用联合主键的方式(也就是用多个字段来定义一个主键)。联合主键中的字段越多,占用的索引空间越大,不仅会加大理解难度,还会增加运行时间和索引空间,因此联合主键的字段个数越少越好。
|
||||
|
||||
4.**使用主键和外键越多越好**
|
||||
|
||||
数据库的设计实际上就是定义各种表,以及各种字段之间的关系。这些关系越多,证明这些实体之间的冗余度越低,利用度越高。这样做的好处在于不仅保证了数据表之间的独立性,还能提升相互之间的关联使用率。
|
||||
|
||||
你应该能看出来“三少一多”原则的核心就是简单可复用。简单指的是用更少的表、更少的字段、更少的联合主键字段来完成数据表的设计。可复用则是通过主键、外键的使用来增强数据表之间的复用率。因为一个主键可以理解是一张表的代表。键设计得越多,证明它们之间的利用率越高。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们学习了DDL的基础语法,比如如何对数据库和数据库表进行定义,也了解了使用Navicat可视化管理工具来辅助我们完成数据表的设计,省去了手写SQL的工作量。
|
||||
|
||||
在创建数据表的时候,除了对字段名及数据类型进行定义以外,我们考虑最多的就是关于字段的约束,我介绍了7种常见的约束,它们都是数据表设计中会用到的约束:主键、外键、唯一性、NOT NULL、DEFAULT、CHECK约束等。
|
||||
|
||||
当然,了解了如何操作创建数据表之后,你还需要动脑思考,怎样才能设计出一个好的数据表?设计的原则都有哪些?针对这个,我整理出了“三少一多”原则,在实际使用过程中,你需要灵活掌握,因为这个原则并不是绝对的,有时候我们需要牺牲数据的冗余度来换取数据处理的效率。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/80/c1/80aecedfad59aad06cc08bb9bca721c1.jpg" alt=""><br>
|
||||
我们在创建数据表的时候,会对数据表设置主键、外键和索引。你能说下这三者的作用和区别吗?
|
||||
|
||||
欢迎你在评论区写下你的答案,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事。
|
||||
275
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/05丨检索数据:你还在SELECT * 么?.md
Normal file
275
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/05丨检索数据:你还在SELECT * 么?.md
Normal file
@@ -0,0 +1,275 @@
|
||||
<audio id="audio" title="05丨检索数据:你还在SELECT * 么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ed/80/ed8254f88dba7a70d210713610061380.mp3"></audio>
|
||||
|
||||
今天我们主要学习如何使用SQL检索数据。如果你已经有了一定的SQL基础,这节课可以跳过,也可以把它当做是个快速的复习。
|
||||
|
||||
SELECT可以说是SQL中最常用的语句了。你可以把SQL语句看作是英语语句,SELECT就是SQL中的关键字之一,除了SELECT之外,还有INSERT、DELETE、UPDATE等关键字,这些关键字是SQL的保留字,这样可以很方便地帮助我们分析理解SQL语句。我们在定义数据库表名、字段名和变量名时,要尽量避免使用这些保留字。
|
||||
|
||||
SELECT的作用是从一个表或多个表中检索出想要的数据行。今天我主要讲解SELECT的基础查询,后面我会讲解如何通过多个表的连接操作进行复杂的查询。
|
||||
|
||||
在这篇文章中,你需要重点掌握以下几方面的内容:
|
||||
|
||||
1. SELECT查询的基础语法;
|
||||
1. 如何排序检索数据;
|
||||
1. 什么情况下用`SELECT*`,如何提升SELECT查询效率?
|
||||
|
||||
## SELECT查询的基础语法
|
||||
|
||||
SELECT可以帮助我们从一个表或多个表中进行数据查询。我们知道一个数据表是由列(字段名)和行(数据行)组成的,我们要返回满足条件的数据行,就需要在SELECT后面加上我们想要查询的列名,可以是一列,也可以是多个列。如果你不知道所有列名都有什么,也可以检索所有列。
|
||||
|
||||
我创建了一个王者荣耀英雄数据表,这张表里一共有69个英雄,23个属性值(不包括英雄名name)。SQL文件见[Github地址](https://github.com/cystanford/sql_heros_data)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/00/38/002726e066b15cb91e96828c82825f38.png" alt="">
|
||||
|
||||
数据表中这24个字段(除了id以外),分别代表的含义见下图。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/50/44/50284b67a5c5d3f6ecc541f8dbb5ea44.png" alt="">
|
||||
|
||||
### 查询列
|
||||
|
||||
如果我们想要对数据表中的某一列进行检索,在SELECT后面加上这个列的字段名即可。比如我们想要检索数据表中都有哪些英雄。
|
||||
|
||||
```
|
||||
SQL:SELECT name FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录)见下图,你可以看到这样就等于单独输出了name这一列。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/f9/e5e240cbbe9b9309d39d919bb10f93f9.png" alt="">
|
||||
|
||||
我们也可以对多个列进行检索,在列名之间用逗号(,)分割即可。比如我们想要检索有哪些英雄,他们的最大生命、最大法力、最大物攻和最大物防分别是多少。
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max, mp_max, attack_max, defense_max FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/95/4b1e0691f2fbc80fce8d06ff1bdf7a95.png" alt="">
|
||||
|
||||
这个表中一共有25个字段,除了id和英雄名name以外,还存在23个属性值,如果我们记不住所有的字段名称,可以使用SELECT * 帮我们检索出所有的列:
|
||||
|
||||
```
|
||||
SQL:SELECT * FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5a/38/5a1d231ada76cf93db734de757810c38.png" alt=""><br>
|
||||
我们在做数据探索的时候,`SELECT *`还是很有用的,这样我们就不需要写很长的SELECT语句了。但是在生产环境时要尽量避免使用`SELECT*`,具体原因我会在后面讲。
|
||||
|
||||
### 起别名
|
||||
|
||||
我们在使用SELECT查询的时候,还有一些技巧可以使用,比如你可以给列名起别名。我们在进行检索的时候,可以给英雄名、最大生命、最大法力、最大物攻和最大物防等取别名:
|
||||
|
||||
```
|
||||
SQL:SELECT name AS n, hp_max AS hm, mp_max AS mm, attack_max AS am, defense_max AS dm FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果和上面多列检索的运行结果是一样的,只是将列名改成了n、hm、mm、am和dm。当然这里的列别名只是举例,一般来说起别名的作用是对原有名称进行简化,从而让SQL语句看起来更精简。同样我们也可以对表名称起别名,这个在多表连接查询的时候会用到。
|
||||
|
||||
### 查询常数
|
||||
|
||||
SELECT查询还可以对常数进行查询。对的,就是在SELECT查询结果中增加一列固定的常数列。这列的取值是我们指定的,而不是从数据表中动态取出的。你可能会问为什么我们还要对常数进行查询呢?SQL中的SELECT语法的确提供了这个功能,一般来说我们只从一个表中查询数据,通常不需要增加一个固定的常数列,但如果我们想整合不同的数据源,用常数列作为这个表的标记,就需要查询常数。
|
||||
|
||||
比如说,我们想对heros数据表中的英雄名进行查询,同时增加一列字段`platform`,这个字段固定值为“王者荣耀”,可以这样写:
|
||||
|
||||
```
|
||||
SQL:SELECT '王者荣耀' as platform, name FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果:(69条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/19/3d/1975e19d4e21914a7ebee73daf240f3d.png" alt=""><br>
|
||||
在这个SQL语句中,我们虚构了一个`platform`字段,并且把它设置为固定值“王者荣耀”。
|
||||
|
||||
需要说明的是,如果常数是个字符串,那么使用单引号(‘’)就非常重要了,比如‘王者荣耀’。单引号说明引号中的字符串是个常数,否则SQL会把王者荣耀当成列名进行查询,但实际上数据表里没有这个列名,就会引起错误。如果常数是英文字母,比如`'WZRY'`也需要加引号。如果常数是个数字,就可以直接写数字,不需要单引号,比如:
|
||||
|
||||
```
|
||||
SQL:SELECT 123 as platform, name FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果:(69条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/99/41ed73cef49e445d64b8cb748a82c299.png" alt="">
|
||||
|
||||
### 去除重复行
|
||||
|
||||
关于单个表的SELECT查询,还有一个非常实用的操作,就是从结果中去掉重复的行。使用的关键字是DISTINCT。比如我们想要看下heros表中关于攻击范围的取值都有哪些:
|
||||
|
||||
```
|
||||
SQL:SELECT DISTINCT attack_range FROM heros
|
||||
|
||||
```
|
||||
|
||||
这是运行结果(2条记录),这样我们就能直观地看到攻击范围其实只有两个值,那就是近战和远程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e6/15/e67c0d2f7b977cb0ff87891eb9adf615.png" alt=""><br>
|
||||
如果我们带上英雄名称,会是怎样呢:
|
||||
|
||||
```
|
||||
SQL:SELECT DISTINCT attack_range, name FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/01/2a/0105eb3f0b74d0ed5e6c2fafca38292a.png" alt=""><br>
|
||||
这里有两点需要注意:
|
||||
|
||||
1. DISTINCT需要放到所有列名的前面,如果写成`SELECT name, DISTINCT attack_range FROM heros`会报错。
|
||||
1. DISTINCT其实是对后面所有列名的组合进行去重,你能看到最后的结果是69条,因为这69个英雄名称不同,都有攻击范围(attack_range)这个属性值。如果你想要看都有哪些不同的攻击范围(attack_range),只需要写`DISTINCT attack_range`即可,后面不需要再加其他的列名了。
|
||||
|
||||
## 如何排序检索数据
|
||||
|
||||
当我们检索数据的时候,有时候需要按照某种顺序进行结果的返回,比如我们想要查询所有的英雄,按照最大生命从高到底的顺序进行排列,就需要使用ORDER BY子句。使用ORDER BY子句有以下几个点需要掌握:
|
||||
|
||||
1. 排序的列名:ORDER BY后面可以有一个或多个列名,如果是多个列名进行排序,会按照后面第一个列先进行排序,当第一列的值相同的时候,再按照第二列进行排序,以此类推。
|
||||
1. 排序的顺序:ORDER BY后面可以注明排序规则,ASC代表递增排序,DESC代表递减排序。如果没有注明排序规则,默认情况下是按照ASC递增排序。我们很容易理解ORDER BY对数值类型字段的排序规则,但如果排序字段类型为文本数据,就需要参考数据库的设置方式了,这样才能判断A是在B之前,还是在B之后。比如使用MySQL在创建字段的时候设置为BINARY属性,就代表区分大小写。
|
||||
1. 非选择列排序:ORDER BY可以使用非选择列进行排序,所以即使在SELECT后面没有这个列名,你同样可以放到ORDER BY后面进行排序。
|
||||
1. ORDER BY的位置:ORDER BY通常位于SELECT语句的最后一条子句,否则会报错。
|
||||
|
||||
在了解了ORDER BY的使用语法之后,我们来看下如何对heros数据表进行排序。
|
||||
|
||||
假设我们想要显示英雄名称及最大生命值,按照最大生命值从高到低的方式进行排序:
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros ORDER BY hp_max DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/67/08/67714aae8a6ac8e9b356f6975324be08.png" alt="">
|
||||
|
||||
如果想要显示英雄名称及最大生命值,按照第一排序最大法力从低到高,当最大法力值相等的时候则按照第二排序进行,即最大生命值从高到低的方式进行排序:
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros ORDER BY mp_max, hp_max DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:(69条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e6/3e/e622aa87b86051b0512cb36ba7daf43e.png" alt="">
|
||||
|
||||
## 约束返回结果的数量
|
||||
|
||||
另外在查询过程中,我们可以约束返回结果的数量,使用LIMIT关键字。比如我们想返回英雄名称及最大生命值,按照最大生命值从高到低排序,返回5条记录即可。
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros ORDER BY hp_max DESC LIMIT 5
|
||||
|
||||
```
|
||||
|
||||
运行结果(5条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/21/b3/21c4f6e69fd3429b796146675389bbb3.png" alt=""><br>
|
||||
有一点需要注意,约束返回结果的数量,在不同的DBMS中使用的关键字可能不同。在MySQL、PostgreSQL、MariaDB和SQLite中使用LIMIT关键字,而且需要放到SELECT语句的最后面。如果是SQL Server和Access,需要使用TOP关键字,比如:
|
||||
|
||||
```
|
||||
SQL:SELECT TOP 5 name, hp_max FROM heros ORDER BY hp_max DESC
|
||||
|
||||
```
|
||||
|
||||
如果是DB2,使用`FETCH FIRST 5 ROWS ONLY`这样的关键字:
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros ORDER BY hp_max DESC FETCH FIRST 5 ROWS ONLY
|
||||
|
||||
```
|
||||
|
||||
如果是Oracle,你需要基于ROWNUM来统计行数:
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros WHERE ROWNUM <=5 ORDER BY hp_max DESC
|
||||
|
||||
```
|
||||
|
||||
需要说明的是,这条语句是先取出来前5条数据行,然后再按照hp_max从高到低的顺序进行排序。但这样产生的结果和上述方法的并不一样。我会在后面讲到子查询,你可以使用`SELECT name, hp_max FROM (SELECT name, hp_max FROM heros ORDER BY hp_max) WHERE ROWNUM <=5`得到与上述方法一致的结果。
|
||||
|
||||
约束返回结果的数量可以减少数据表的网络传输量,也可以提升查询效率。如果我们知道返回结果只有1条,就可以使用`LIMIT 1`,告诉SELECT语句只需要返回一条记录即可。这样的好处就是SELECT不需要扫描完整的表,只需要检索到一条符合条件的记录即可返回。
|
||||
|
||||
## SELECT的执行顺序
|
||||
|
||||
查询是RDBMS中最频繁的操作。我们在理解SELECT语法的时候,还需要了解SELECT执行时的底层原理。只有这样,才能让我们对SQL有更深刻的认识。
|
||||
|
||||
其中你需要记住SELECT查询时的两个顺序:
|
||||
|
||||
1.关键字的顺序是不能颠倒的:
|
||||
|
||||
```
|
||||
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ...
|
||||
|
||||
```
|
||||
|
||||
2.SELECT语句的执行顺序(在MySQL和Oracle中,SELECT执行顺序基本相同):
|
||||
|
||||
```
|
||||
FROM > WHERE > GROUP BY > HAVING > SELECT的字段 > DISTINCT > ORDER BY > LIMIT
|
||||
|
||||
```
|
||||
|
||||
比如你写了一个SQL语句,那么它的关键字顺序和执行顺序是下面这样的:
|
||||
|
||||
```
|
||||
SELECT DISTINCT player_id, player_name, count(*) as num #顺序5
|
||||
FROM player JOIN team ON player.team_id = team.team_id #顺序1
|
||||
WHERE height > 1.80 #顺序2
|
||||
GROUP BY player.team_id #顺序3
|
||||
HAVING num > 2 #顺序4
|
||||
ORDER BY num DESC #顺序6
|
||||
LIMIT 2 #顺序7
|
||||
|
||||
```
|
||||
|
||||
在SELECT语句执行这些步骤的时候,每个步骤都会产生一个虚拟表,然后将这个虚拟表传入下一个步骤中作为输入。需要注意的是,这些步骤隐含在SQL的执行过程中,对于我们来说是不可见的。
|
||||
|
||||
我来详细解释一下SQL的执行原理。
|
||||
|
||||
首先,你可以注意到,SELECT是先执行FROM这一步的。在这个阶段,如果是多张表联查,还会经历下面的几个步骤:
|
||||
|
||||
1. 首先先通过CROSS JOIN求笛卡尔积,相当于得到虚拟表 vt(virtual table)1-1;
|
||||
1. 通过ON进行筛选,在虚拟表vt1-1的基础上进行筛选,得到虚拟表 vt1-2;
|
||||
1. 添加外部行。如果我们使用的是左连接、右链接或者全连接,就会涉及到外部行,也就是在虚拟表vt1-2的基础上增加外部行,得到虚拟表vt1-3。
|
||||
|
||||
当然如果我们操作的是两张以上的表,还会重复上面的步骤,直到所有表都被处理完为止。这个过程得到是我们的原始数据。
|
||||
|
||||
当我们拿到了查询数据表的原始数据,也就是最终的虚拟表vt1,就可以在此基础上再进行WHERE阶段。在这个阶段中,会根据vt1表的结果进行筛选过滤,得到虚拟表vt2。
|
||||
|
||||
然后进入第三步和第四步,也就是GROUP和 HAVING阶段。在这个阶段中,实际上是在虚拟表vt2的基础上进行分组和分组过滤,得到中间的虚拟表vt3和vt4。
|
||||
|
||||
当我们完成了条件筛选部分之后,就可以筛选表中提取的字段,也就是进入到SELECT和DISTINCT阶段。
|
||||
|
||||
首先在SELECT阶段会提取想要的字段,然后在DISTINCT阶段过滤掉重复的行,分别得到中间的虚拟表vt5-1和vt5-2。
|
||||
|
||||
当我们提取了想要的字段数据之后,就可以按照指定的字段进行排序,也就是ORDER BY阶段,得到虚拟表vt6。
|
||||
|
||||
最后在vt6的基础上,取出指定行的记录,也就是LIMIT阶段,得到最终的结果,对应的是虚拟表vt7。
|
||||
|
||||
当然我们在写SELECT语句的时候,不一定存在所有的关键字,相应的阶段就会省略。
|
||||
|
||||
同时因为SQL是一门类似英语的结构化查询语言,所以我们在写SELECT语句的时候,还要注意相应的关键字顺序,所谓底层运行的原理,就是我们刚才讲到的执行顺序。
|
||||
|
||||
## 什么情况下用SELECT*,如何提升SELECT查询效率?
|
||||
|
||||
当我们初学SELECT语法的时候,经常会使用`SELECT *`,因为使用方便。实际上这样也增加了数据库的负担。所以如果我们不需要把所有列都检索出来,还是先指定出所需的列名,因为写清列名,可以减少数据表查询的网络传输量,而且考虑到在实际的工作中,我们往往不需要全部的列名,因此你需要养成良好的习惯,写出所需的列名。
|
||||
|
||||
如果我们只是练习,或者对数据表进行探索,那么是可以使用`SELECT *`的。它的查询效率和把所有列名都写出来再进行查询的效率相差并不大。这样可以方便你对数据表有个整体的认知。但是在生产环境下,不推荐你直接使用`SELECT *`进行查询。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我对SELECT的基础语法进行了讲解,SELECT是SQL的基础。但不同阶段看SELECT都会有新的体会。当你第一次学习的时候,关注的往往是如何使用它,或者语法是否正确。再看的时候,可能就会更关注SELECT的查询效率,以及不同DBMS之间的差别。
|
||||
|
||||
在我们的日常工作中,很多人都可以写出SELECT语句,但是执行的效率却相差很大。产生这种情况的原因主要有两个,一个是习惯的培养,比如大部分初学者会经常使用`SELECT *`,而好的习惯则是只查询所需要的列;另一个对SQL查询的执行顺序及查询效率的关注,比如当你知道只有1条记录的时候,就可以使用`LIMIT 1`来进行约束,从而提升查询效率。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c8/a8/c88258e72728957b43dc2441d3f381a8.jpg" alt="">
|
||||
|
||||
最后留两道思考题吧,我今天对单表的SELECT查询进行了讲解,你之前可能也有学习使用的经验,可以说下你对SELECT使用的理解吗?另外,我今天使用heros数据表进行了举例,请你编写SQL语句,对英雄名称和最大法力进行查询,按照最大生命从高到低排序,只返回5条记录即可。你可以说明下使用的DBMS及相应的SQL语句。
|
||||
|
||||
欢迎你把这篇文章分享给你的朋友或者同事,与他们一起来分析一下王者荣耀的数据,互相切磋交流。
|
||||
170
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/06丨数据过滤:SQL数据过滤都有哪些方法?.md
Normal file
170
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/06丨数据过滤:SQL数据过滤都有哪些方法?.md
Normal file
@@ -0,0 +1,170 @@
|
||||
<audio id="audio" title="06丨数据过滤:SQL数据过滤都有哪些方法?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ae/a2/ae0d6b886ac142fd30a931ad13cea0a2.mp3"></audio>
|
||||
|
||||
我在上篇文章中讲到过,提升查询效率的一个很重要的方式,就是约束返回结果的数量,还有一个很有效的方式,就是指定筛选条件,进行过滤。过滤可以筛选符合条件的结果,并进行返回,减少不必要的数据行。
|
||||
|
||||
那么在今天的内容里,我们来学习如何对SQL数据进行过滤,这里主要使用的就是WHERE子句。
|
||||
|
||||
你可能已经使用过WHERE子句,说起来SQL其实很简单,只要能把满足条件的内容筛选出来即可,但在实际使用过程中,不同人写出来的WHERE子句存在很大差别,比如执行效率的高低,有没有遇到莫名的报错等。
|
||||
|
||||
在今天的学习中,你重点需要掌握以下几方面的内容:
|
||||
|
||||
1. 学会使用WHERE子句,如何使用比较运算符对字段的数值进行比较筛选;
|
||||
1. 如何使用逻辑运算符,进行多条件的过滤;
|
||||
1. 学会使用通配符对数据条件进行复杂过滤。
|
||||
|
||||
## 比较运算符
|
||||
|
||||
在SQL中,我们可以使用WHERE子句对条件进行筛选,在此之前,你需要了解WHERE子句中的比较运算符。这些比较运算符的含义你可以参见下面这张表格:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3a/e0/3a2667784b4887ef15becc7056f3d3e0.png" alt="">
|
||||
|
||||
实际上你能看到,同样的含义可能会有多种表达方式,比如小于等于,可以是(<=),也可以是不大于(!>)。同样不等于,可以用(<>),也可以用(!=),它们的含义都是相同的,但这些符号的顺序都不能颠倒,比如你不能写(=<)。需要注意的是,你需要查看使用的DBMS是否支持,不同的DBMS支持的运算符可能是不同的,比如Access不支持(!=),不等于应该使用(<>)。在MySQL中,不支持(!>)(!<)等。
|
||||
|
||||
我在上一篇文章中使用了heros数据表,今天还是以这张表格做练习。下面我们通过比较运算符对王者荣耀的英雄属性进行条件筛选。
|
||||
|
||||
WHERE子句的基本格式是:`SELECT ……(列名) FROM ……(表名) WHERE ……(子句条件)`
|
||||
|
||||
比如我们想要查询所有最大生命值大于6000的英雄:
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros WHERE hp_max > 6000
|
||||
|
||||
```
|
||||
|
||||
运行结果(41条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9f/c1/9f639dfe0bd9dbfc63944447f92e47c1.png" alt="">
|
||||
|
||||
想要查询所有最大生命值在5399到6811之间的英雄:
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros WHERE hp_max BETWEEN 5399 AND 6811
|
||||
|
||||
```
|
||||
|
||||
运行结果:(41条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4b/60/4b11a5f32f3f2807c8278f8d5637d460.png" alt="">
|
||||
|
||||
需要注意的是`hp_max`可以取值到最小值和最大值,即5399和6811。
|
||||
|
||||
我们也可以对heros表中的`hp_max`字段进行空值检查。
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros WHERE hp_max IS NULL
|
||||
|
||||
```
|
||||
|
||||
运行结果为空,说明heros表中的`hp_max`字段没有存在空值的数据行。
|
||||
|
||||
## 逻辑运算符
|
||||
|
||||
我刚才介绍了比较运算符,如果我们存在多个WHERE条件子句,可以使用逻辑运算符:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ae/c1/aeed170c57ae1e5378fbee9f8fb6a8c1.png" alt="">
|
||||
|
||||
我们还是通过例子来看下这些逻辑运算符的使用,同样采用heros这张表的数据查询。
|
||||
|
||||
假设想要筛选最大生命值大于6000,最大法力大于1700的英雄,然后按照最大生命值和最大法力值之和从高到低进行排序。
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max, mp_max FROM heros WHERE hp_max > 6000 AND mp_max > 1700 ORDER BY (hp_max+mp_max) DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:(23条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/85/4e/859e7e9fcf28a30f9189ef81cfe7284e.png" alt="">
|
||||
|
||||
如果AND和OR同时存在WHERE子句中会是怎样的呢?假设我们想要查询最大生命值加最大法力值大于8000的英雄,或者最大生命值大于6000并且最大法力值大于1700的英雄。
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max, mp_max FROM heros WHERE (hp_max+mp_max) > 8000 OR hp_max > 6000 AND mp_max > 1700 ORDER BY (hp_max+mp_max) DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:(33条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/05/ae/052c1fb7031d025d5e0c0027177187ae.png" alt="">
|
||||
|
||||
你能看出来相比于上一个条件查询,这次的条件查询多出来了10个英雄,这是因为我们放宽了条件,允许最大生命值+最大法力值大于8000的英雄显示出来。另外你需要注意到,当WHERE子句中同时存在OR和AND的时候,AND执行的优先级会更高,也就是说SQL会优先处理AND操作符,然后再处理OR操作符。
|
||||
|
||||
如果我们对这条查询语句OR两边的条件增加一个括号,结果会是怎样的呢?
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max, mp_max FROM heros WHERE ((hp_max+mp_max) > 8000 OR hp_max > 6000) AND mp_max > 1700 ORDER BY (hp_max+mp_max) DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1a/c9/1a41124faad3aac6e8170a72e65de5c9.png" alt="">
|
||||
|
||||
所以当WHERE子句中同时出现AND和OR操作符的时候,你需要考虑到执行的先后顺序,也就是两个操作符执行的优先级。一般来说()优先级最高,其次优先级是AND,然后是OR。
|
||||
|
||||
如果我想要查询主要定位或者次要定位是法师或是射手的英雄,同时英雄的上线时间不在2016-01-01到2017-01-01之间。
|
||||
|
||||
```
|
||||
SQL:
|
||||
SELECT name, role_main, role_assist, hp_max, mp_max, birthdate
|
||||
FROM heros
|
||||
WHERE (role_main IN ('法师', '射手') OR role_assist IN ('法师', '射手'))
|
||||
AND DATE(birthdate) NOT BETWEEN '2016-01-01' AND '2017-01-01'
|
||||
ORDER BY (hp_max + mp_max) DESC
|
||||
|
||||
```
|
||||
|
||||
你能看到我把WHERE子句分成了两个部分。第一部分是关于主要定位和次要定位的条件过滤,使用的是`role_main in ('法师', '射手') OR role_assist in ('法师', '射手')`。这里用到了IN逻辑运算符,同时`role_main`和`role_assist`是OR(或)的关系。
|
||||
|
||||
第二部分是关于上线时间的条件过滤。NOT代表否,因为我们要找到不在2016-01-01到2017-01-01之间的日期,因此用到了`NOT BETWEEN '2016-01-01' AND '2017-01-01'`。同时我们是在对日期类型数据进行检索,所以使用到了DATE函数,将字段birthdate转化为日期类型再进行比较。关于日期的操作,我会在下一篇文章中再作具体介绍。
|
||||
|
||||
这是运行结果(6条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/70/8e/7048f6ff11215b6d0113b2370103828e.png" alt="">
|
||||
|
||||
## 使用通配符进行过滤
|
||||
|
||||
刚才讲解的条件过滤都是对已知值进行的过滤,还有一种情况是我们要检索文本中包含某个词的所有数据,这里就需要使用通配符。通配符就是我们用来匹配值的一部分的特殊字符。这里我们需要使用到LIKE操作符。
|
||||
|
||||
如果我们想要匹配任意字符串出现的任意次数,需要使用(%)通配符。比如我们想要查找英雄名中包含“太”字的英雄都有哪些:
|
||||
|
||||
```
|
||||
SQL:SELECT name FROM heros WHERE name LIKE '%太%'
|
||||
|
||||
```
|
||||
|
||||
运行结果:(2条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b1/18/b18de17c2517d7c06c56f324309c4c18.png" alt=""><br>
|
||||
需要说明的是不同DBMS对通配符的定义不同,在Access中使用的是(*)而不是(%)。另外关于字符串的搜索可能是需要区分大小写的,比如`'liu%'`就不能匹配上`'LIU BEI'`。具体是否区分大小写还需要考虑不同的DBMS以及它们的配置。
|
||||
|
||||
如果我们想要匹配单个字符,就需要使用下划线(**)通配符。(%)和(**)的区别在于,(%)代表零个或多个字符,而(_)只代表一个字符。比如我们想要查找英雄名除了第一个字以外,包含‘太’字的英雄有哪些。
|
||||
|
||||
```
|
||||
SQL:SELECT name FROM heros WHERE name LIKE '_%太%'
|
||||
|
||||
```
|
||||
|
||||
运行结果(1条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ab/65/ab30c809327a81ee00ce4989e7815065.png" alt="">
|
||||
|
||||
因为太乙真人的太是第一个字符,而`_%太%`中的太不是在第一个字符,所以匹配不到“太乙真人”,只可以匹配上“东皇太一”。
|
||||
|
||||
同样需要说明的是,在Access中使用(?)来代替(`_`),而且在DB2中是不支持通配符(`_`)的,因此你需要在使用的时候查阅相关的DBMS文档。
|
||||
|
||||
你能看出来通配符还是很有用的,尤其是在进行字符串匹配的时候。不过在实际操作过程中,我还是建议你尽量少用通配符,因为它需要消耗数据库更长的时间来进行匹配。即使你对LIKE检索的字段进行了索引,索引的价值也可能会失效。如果要让索引生效,那么LIKE后面就不能以(%)开头,比如使用`LIKE '%太%'`或`LIKE '%太'`的时候就会对全表进行扫描。如果使用`LIKE '太%'`,同时检索的字段进行了索引的时候,则不会进行全表扫描。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我对SQL语句中的WHERE子句进行了讲解,你可以使用比较运算符、逻辑运算符和通配符这三种方式对检索条件进行过滤。
|
||||
|
||||
比较运算符是对数值进行比较,不同的DBMS支持的比较运算符可能不同,你需要事先查阅相应的DBMS文档。逻辑运算符可以让我们同时使用多个WHERE子句,你需要注意的是AND和OR运算符的执行顺序。通配符可以让我们对文本类型的字段进行模糊查询,不过检索的代价也是很高的,通常都需要用到全表扫描,所以效率很低。只有当LIKE语句后面不用通配符,并且对字段进行索引的时候才不会对全表进行扫描。
|
||||
|
||||
你可能认为学习SQL并不难,掌握这些语法就可以对数据进行筛选查询。但实际工作中不同人写的SQL语句的查询效率差别很大,保持高效率的一个很重要的原因,就是要避免全表扫描,所以我们会考虑在WHERE及ORDER BY涉及到的列上增加索引。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fb/50/fbd79c2c90a58891b498e7f29d935050.jpg" alt="">
|
||||
|
||||
你能说一下WHERE子句中比较运算符、逻辑运算符和通配符这三者各自的作用吗?以heros数据表为例,请你编写SQL语句,对英雄名称、主要定位、次要定位、最大生命和最大法力进行查询,筛选条件为:主要定位是坦克或者战士,并且次要定位不为空,同时满足最大生命值大于8000或者最大法力小于1500的英雄,并且按照最大生命和最大法力之和从高到底的顺序进行排序。
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎点击请朋友读,把这篇文章分享给你的朋友或者同事。
|
||||
235
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/07丨什么是SQL函数?为什么使用SQL函数可能会带来问题?.md
Normal file
235
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/07丨什么是SQL函数?为什么使用SQL函数可能会带来问题?.md
Normal 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函数。
|
||||
|
||||
```
|
||||
SQL:SELECT name, ROUND(attack_growth,1) FROM heros
|
||||
|
||||
```
|
||||
|
||||
代码中,`ROUND(attack_growth,1)`中的`attack_growth`代表想要处理的数据,“1”代表四舍五入的位数,也就是我们这里需要精确到的位数。
|
||||
|
||||
运行结果为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fb/ed/fb55a715543e1ed3245ae37210ad75ed.png" alt=""><br>
|
||||
假设我们想显示英雄最大生命值的最大值,就需要用到MAX函数。在数据中,“最大生命值”对应的列数为`hp_max`,在代码中的格式为`MAX(hp_max)`。
|
||||
|
||||
```
|
||||
SQL:SELECT MAX(hp_max) FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果为9328。
|
||||
|
||||
假如我们想要知道最大生命值最大的是哪个英雄,以及对应的数值,就需要分成两个步骤来处理:首先找到英雄的最大生命值的最大值,即`SELECT MAX(hp_max) FROM heros`,然后再筛选最大生命值等于这个最大值的英雄,如下所示。
|
||||
|
||||
```
|
||||
SQL:SELECT name, hp_max FROM heros WHERE hp_max = (SELECT MAX(hp_max) FROM heros)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/20/9371fdcee4d1f7bdfdd71bc0a58aac20.png" alt="">
|
||||
|
||||
假如我们想显示英雄的名字,以及他们的名字字数,需要用到`CHAR_LENGTH`函数。
|
||||
|
||||
```
|
||||
SQL:SELECT CHAR_LENGTH(name), name FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/41/8c/415aa09e2fdc121861e3c96bd8a2af8c.png" alt="">
|
||||
|
||||
假如想要提取英雄上线日期(对应字段birthdate)的年份,只显示有上线日期的英雄即可(有些英雄没有上线日期的数据,不需要显示),这里我们需要使用EXTRACT函数,提取某一个时间元素。所以我们需要筛选上线日期不为空的英雄,即`WHERE birthdate is not null`,然后再显示他们的名字和上线日期的年份,即:
|
||||
|
||||
```
|
||||
SQL: SELECT name, EXTRACT(YEAR FROM birthdate) AS birthdate FROM heros WHERE birthdate is NOT NULL
|
||||
|
||||
```
|
||||
|
||||
或者使用如下形式:
|
||||
|
||||
```
|
||||
SQL: SELECT name, YEAR(birthdate) AS birthdate FROM heros WHERE birthdate is NOT NULL
|
||||
|
||||
```
|
||||
|
||||
运行结果为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/26/16/26cacf4d619d9f177a1f5b22059f9916.png" alt="">
|
||||
|
||||
假设我们需要找出在2016年10月1日之后上线的所有英雄。这里我们可以采用DATE函数来判断birthdate的日期是否大于2016-10-01,即`WHERE DATE(birthdate)>'2016-10-01'`,然后再显示符合要求的全部字段信息,即:
|
||||
|
||||
```
|
||||
SQL: SELECT * FROM heros WHERE DATE(birthdate)>'2016-10-01'
|
||||
|
||||
```
|
||||
|
||||
需要注意的是下面这种写法是不安全的:
|
||||
|
||||
```
|
||||
SELECT * FROM heros WHERE birthdate>'2016-10-01'
|
||||
|
||||
```
|
||||
|
||||
因为很多时候你无法确认birthdate的数据类型是字符串,还是datetime类型,如果你想对日期部分进行比较,那么使用`DATE(birthdate)`来进行比较是更安全的。
|
||||
|
||||
运行结果为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e5/22/e5696b5ff0aae0fd910463b1f8e6ed22.png" alt="">
|
||||
|
||||
假设我们需要知道在2016年10月1日之后上线英雄的平均最大生命值、平均最大法力和最高物攻最大值。同样我们需要先筛选日期条件,即`WHERE DATE(birthdate)>'2016-10-01'`,然后再选择`AVG(hp_max), AVG(mp_max), MAX(attack_max)`字段进行显示。
|
||||
|
||||
```
|
||||
SQL: SELECT AVG(hp_max), AVG(mp_max), MAX(attack_max) FROM heros WHERE DATE(birthdate)>'2016-10-01'
|
||||
|
||||
```
|
||||
|
||||
运行结果为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8f/6b/8f559dc1be7d62e4c58402ebe2e7856b.png" alt="">
|
||||
|
||||
## 为什么使用SQL函数会带来问题
|
||||
|
||||
尽管SQL函数使用起来会很方便,但我们使用的时候还是要谨慎,因为你使用的函数很可能在运行环境中无法工作,这是为什么呢?
|
||||
|
||||
如果你学习过编程语言,就会知道语言是有不同版本的,比如Python会有2.7版本和3.x版本,不过它们之间的函数差异不大,也就在10%左右。但我们在使用SQL语言的时候,不是直接和这门语言打交道,而是通过它使用不同的数据库软件,即DBMS。DBMS之间的差异性很大,远大于同一个语言不同版本之间的差异。实际上,只有很少的函数是被DBMS同时支持的。比如,大多数DBMS使用(||)或者(+)来做拼接符,而在MySQL中的字符串拼接函数为`Concat()`。大部分DBMS会有自己特定的函数,这就意味着采用SQL函数的代码可移植性是很差的,因此在使用函数的时候需要特别注意。
|
||||
|
||||
## 关于大小写的规范
|
||||
|
||||
细心的人可能会发现,我在写SELECT语句的时候用的是大写,而你在网上很多地方,包括你自己写的时候可能用的是小写。实际上在SQL中,关键字和函数名是不用区分字母大小写的,比如SELECT、WHERE、ORDER、GROUP BY等关键字,以及ABS、MOD、ROUND、MAX等函数名。
|
||||
|
||||
不过在SQL中,你还是要确定大小写的规范,因为在Linux和Windows环境下,你可能会遇到不同的大小写问题。
|
||||
|
||||
比如MySQL在Linux的环境下,数据库名、表名、变量名是严格区分大小写的,而字段名是忽略大小写的。
|
||||
|
||||
而MySQL在Windows的环境下全部不区分大小写。
|
||||
|
||||
这就意味着如果你的变量名命名规范没有统一,就可能产生错误。这里有一个有关命名规范的建议:
|
||||
|
||||
1. 关键字和函数名称全部大写;
|
||||
1. 数据库名、表名、字段名称全部小写;
|
||||
1. SQL语句必须以分号结尾。
|
||||
|
||||
虽然关键字和函数名称在SQL中不区分大小写,也就是如果小写的话同样可以执行,但是数据库名、表名和字段名在Linux MySQL环境下是区分大小写的,因此建议你统一这些字段的命名规则,比如全部采用小写的方式。同时将关键词和函数名称全部大写,以便于区分数据库名、表名、字段名。
|
||||
|
||||
## 总结
|
||||
|
||||
函数对于一门语言的重要性毋庸置疑,我们在写Python代码的时候,会自己编写函数,也会使用Python内置的函数。在SQL中,使用函数的时候需要格外留意。不过如果工程量不大,使用的是同一个DBMS的话,还是可以使用函数简化操作的,这样也能提高代码效率。只是在系统集成,或者在多个DBMS同时存在的情况下,使用函数的时候就需要慎重一些。
|
||||
|
||||
比如`CONCAT()`是字符串拼接函数,在MySQL和Oracle中都有这个函数,但是在这两个DBMS中作用却不一样,`CONCAT`函数在MySQL中可以连接多个字符串,而在Oracle中`CONCAT`函数只能连接两个字符串,如果要连接多个字符串就需要用(||)连字符来解决。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/c9/8c5e316b466e8fa65789a9c6a220ebc9.jpg" alt=""><br>
|
||||
讲完了SQL函数的使用,我们来做一道练习题。还是根据王者荣耀英雄数据表,请你使用SQL函数作如下的练习:计算英雄的最大生命平均值;显示出所有在2017年之前上线的英雄,如果英雄没有统计上线日期则不显示。
|
||||
|
||||
欢迎你在评论区与我分享你的答案,也欢迎点击”请朋友读“,把这篇文章分享给你的朋友或者同事。
|
||||
185
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/08丨什么是SQL的聚集函数,如何利用它们汇总表的数据?.md
Normal file
185
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/08丨什么是SQL的聚集函数,如何利用它们汇总表的数据?.md
Normal file
@@ -0,0 +1,185 @@
|
||||
<audio id="audio" title="08丨什么是SQL的聚集函数,如何利用它们汇总表的数据?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/35/09/3537c5286d494ac21a3ba0a1cc12a509.mp3"></audio>
|
||||
|
||||
我们上节课讲到了SQL函数,包括算术函数、字符串函数、日期函数和转换函数。实际上SQL函数还有一种,叫做聚集函数,它是对一组数据进行汇总的函数,输入的是一组数据的集合,输出的是单个值。通常我们可以利用聚集函数汇总表的数据,如果稍微复杂一些,我们还需要先对数据做筛选,然后再进行聚集,比如先按照某个条件进行分组,对分组条件进行筛选,然后得到筛选后的分组的汇总信息。
|
||||
|
||||
有关今天的内容,你重点需要掌握以下几个方面:
|
||||
|
||||
1. 聚集函数都有哪些,能否在一条SELECT语句中使用多个聚集函数;
|
||||
1. 如何对数据进行分组,并进行聚集统计;
|
||||
1. 如何使用HAVING过滤分组,HAVING和WHERE的区别是什么。
|
||||
|
||||
## 聚集函数都有哪些
|
||||
|
||||
SQL中的聚集函数一共包括5个,可以帮我们求某列的最大值、最小值和平均值等,它们分别是:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d1/15/d101026459ffa96504ba3ebb85054415.png" alt="">
|
||||
|
||||
这些函数你可能已经接触过,我们再来简单复习一遍。我们继续使用heros数据表,对王者荣耀的英雄数据进行聚合。
|
||||
|
||||
如果我们想要查询最大生命值大于6000的英雄数量。
|
||||
|
||||
```
|
||||
SQL:SELECT COUNT(*) FROM heros WHERE hp_max > 6000
|
||||
|
||||
```
|
||||
|
||||
运行结果为41。
|
||||
|
||||
如果想要查询最大生命值大于6000,且有次要定位的英雄数量,需要使用COUNT函数。
|
||||
|
||||
```
|
||||
SQL:SELECT COUNT(role_assist) FROM heros WHERE hp_max > 6000
|
||||
|
||||
```
|
||||
|
||||
运行结果是 23。
|
||||
|
||||
需要说明的是,有些英雄没有次要定位,即role_assist为NULL,这时`COUNT(role_assist)`会忽略值为NULL的数据行,而COUNT(*)只是统计数据行数,不管某个字段是否为NULL。
|
||||
|
||||
如果我们想要查询射手(主要定位或者次要定位是射手)的最大生命值的最大值是多少,需要使用MAX函数。
|
||||
|
||||
```
|
||||
SQL:SELECT MAX(hp_max) FROM heros WHERE role_main = '射手' or role_assist = '射手'
|
||||
|
||||
```
|
||||
|
||||
运行结果为6014。
|
||||
|
||||
你能看到,上面的例子里,都是在一条SELECT语句中使用了一次聚集函数,实际上我们也可以在一条SELECT语句中进行多项聚集函数的查询,比如我们想知道射手(主要定位或者次要定位是射手)的英雄数、平均最大生命值、法力最大值的最大值、攻击最大值的最小值,以及这些英雄总的防御最大值等汇总数据。
|
||||
|
||||
如果想要知道英雄的数量,我们使用的是COUNT(*)函数,求平均值、最大值、最小值,以及总的防御最大值,我们分别使用的是AVG、MAX、MIN和SUM函数。另外我们还需要对英雄的主要定位和次要定位进行筛选,使用的是`WHERE role_main = '射手' or role_assist = '射手'`。
|
||||
|
||||
```
|
||||
SQL: SELECT COUNT(*), AVG(hp_max), MAX(mp_max), MIN(attack_max), SUM(defense_max) FROM heros WHERE role_main = '射手' or role_assist = '射手'
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/9d/b9cbaa2af34e0b1eb74e76b60e7eaf9d.png" alt=""><br>
|
||||
需要说明的是AVG、MAX、MIN等聚集函数会自动忽略值为NULL的数据行,MAX和MIN函数也可以用于字符串类型数据的统计,如果是英文字母,则按照A—Z的顺序排列,越往后,数值越大。如果是汉字则按照全拼拼音进行排列。比如:
|
||||
|
||||
```
|
||||
SQL:SELECT MIN(CONVERT(name USING gbk)), MAX(CONVERT(name USING gbk)) FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e1/71/e1fa345ebcb78e686d3dd16cb075e871.png" alt=""><br>
|
||||
需要说明的是,我们需要先把name字段统一转化为gbk类型,使用`CONVERT(name USING gbk)`,然后再使用MIN和MAX取最小值和最大值。
|
||||
|
||||
我们也可以对数据行中不同的取值进行聚集,先用DISTINCT函数取不同的数据,然后再使用聚集函数。比如我们想要查询不同的生命最大值的英雄数量是多少。
|
||||
|
||||
```
|
||||
SQL: SELECT COUNT(DISTINCT hp_max) FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果为61。
|
||||
|
||||
实际上在heros这个数据表中,一共有69个英雄数量,生命最大值不一样的英雄数量是61个。
|
||||
|
||||
假如我们想要统计不同生命最大值英雄的平均生命最大值,保留小数点后两位。首先需要取不同生命最大值,即`DISTINCT hp_max`,然后针对它们取平均值,即`AVG(DISTINCT hp_max)`,最后再针对这个值保留小数点两位,也就是`ROUND(AVG(DISTINCT hp_max), 2)`。
|
||||
|
||||
```
|
||||
SQL: SELECT ROUND(AVG(DISTINCT hp_max), 2) FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果为6653.84。
|
||||
|
||||
你能看到,如果我们不使用DISTINCT函数,就是对全部数据进行聚集统计。如果使用了DISTINCT函数,就可以对数值不同的数据进行聚集。一般我们使用MAX和MIN函数统计数据行的时候,不需要再额外使用DISTINCT,因为使用DISTINCT和全部数据行进行最大值、最小值的统计结果是相等的。
|
||||
|
||||
## 如何对数据进行分组,并进行聚集统计
|
||||
|
||||
我们在做统计的时候,可能需要先对数据按照不同的数值进行分组,然后对这些分好的组进行聚集统计。对数据进行分组,需要使用GROUP BY子句。
|
||||
|
||||
比如我们想按照英雄的主要定位进行分组,并统计每组的英雄数量。
|
||||
|
||||
```
|
||||
SQL: SELECT COUNT(*), role_main FROM heros GROUP BY role_main
|
||||
|
||||
```
|
||||
|
||||
运行结果(6条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6b/9c/6ba58294f85edbb37499d3f28f60719c.png" alt="">
|
||||
|
||||
如果我们想要对英雄按照次要定位进行分组,并统计每组英雄的数量。
|
||||
|
||||
```
|
||||
SELECT COUNT(*), role_assist FROM heros GROUP BY role_assist
|
||||
|
||||
```
|
||||
|
||||
运行结果:(6条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/08/c0/0802ba993844e2875bd6cbbc3d4fa5c0.png" alt="">
|
||||
|
||||
你能看出如果字段为NULL,也会被列为一个分组。在这个查询统计中,次要定位为NULL,即只有一个主要定位的英雄是40个。
|
||||
|
||||
我们也可以使用多个字段进行分组,这就相当于把这些字段可能出现的所有的取值情况都进行分组。比如,我们想要按照英雄的主要定位、次要定位进行分组,查看这些英雄的数量,并按照这些分组的英雄数量从高到低进行排序。
|
||||
|
||||
```
|
||||
SELECT COUNT(*) as num, role_main, role_assist FROM heros GROUP BY role_main, role_assist ORDER BY num DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:(19条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/35/8f/3504fa2cf288a50eb39028d1768d5c8f.png" alt="">
|
||||
|
||||
## 如何使用HAVING过滤分组,它与WHERE的区别是什么?
|
||||
|
||||
当我们创建出很多分组的时候,有时候就需要对分组进行过滤。你可能首先会想到WHERE子句,实际上过滤分组我们使用的是HAVING。HAVING的作用和WHERE一样,都是起到过滤的作用,只不过WHERE是用于数据行,而HAVING则作用于分组。
|
||||
|
||||
比如我们想要按照英雄的主要定位、次要定位进行分组,并且筛选分组中英雄数量大于5的组,最后按照分组中的英雄数量从高到低进行排序。
|
||||
|
||||
首先我们需要获取的是英雄的数量、主要定位和次要定位,即`SELECT COUNT(*) as num, role_main, role_assist`。然后按照英雄的主要定位和次要定位进行分组,即`GROUP BY role_main, role_assist`,同时我们要对分组中的英雄数量进行筛选,选择大于5的分组,即`HAVING num > 5`,然后按照英雄数量从高到低进行排序,即`ORDER BY num DESC`。
|
||||
|
||||
```
|
||||
SQL: SELECT COUNT(*) as num, role_main, role_assist FROM heros GROUP BY role_main, role_assist HAVING num > 5 ORDER BY num DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:(4条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ca/61/ca9747ad58e8cf637fe352fb0cfd5761.png" alt=""><br>
|
||||
你能看到还是上面这个分组,只不过我们按照数量进行了过滤,筛选了数量大于5的分组进行输出。如果把HAVING替换成了WHERE,SQL则会报错。对于分组的筛选,我们一定要用HAVING,而不是WHERE。另外你需要知道的是,HAVING支持所有WHERE的操作,因此所有需要WHERE子句实现的功能,你都可以使用HAVING对分组进行筛选。
|
||||
|
||||
我们再来看个例子,通过这个例子查看一下WHERE和HAVING进行条件过滤的区别。筛选最大生命值大于6000的英雄,按照主要定位、次要定位进行分组,并且显示分组中英雄数量大于5的分组,按照数量从高到低进行排序。
|
||||
|
||||
```
|
||||
SQL: SELECT COUNT(*) as num, role_main, role_assist FROM heros WHERE hp_max > 6000 GROUP BY role_main, role_assist HAVING num > 5 ORDER BY num DESC
|
||||
|
||||
```
|
||||
|
||||
运行结果:(2条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7b/3a/7b62b2a12ec9e66675b3da8b5b54093a.png" alt="">
|
||||
|
||||
你能看到,还是针对上一个例子的查询,只是我们先增加了一个过滤条件,即筛选最大生命值大于6000的英雄。这里我们就需要先使用WHERE子句对最大生命值大于6000的英雄进行条件过滤,然后再使用GROUP BY进行分组,使用HAVING进行分组的条件判断,然后使用ORDER BY进行排序。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我对SQL的聚集函数进行了讲解。通常我们还会对数据先进行分组,然后再使用聚集函数统计不同组的数据概况,比如数据行数、平均值、最大值、最小值以及求和等。我们也可以使用HAVING对分组进行过滤,然后通过ORDER BY按照某个字段的顺序进行排序输出。有时候你能看到在一条SELECT语句中,可能会包括多个子句,用WHERE进行数据量的过滤,用GROUP BY进行分组,用HAVING进行分组过滤,用ORDER BY进行排序……
|
||||
|
||||
**你要记住,在SELECT查询中,关键字的顺序是不能颠倒的,它们的顺序是:**
|
||||
|
||||
```
|
||||
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ...
|
||||
|
||||
```
|
||||
|
||||
另外需要注意的是,使用GROUP BY进行分组,如果想让输出的结果有序,可以在GROUP BY后使用ORDER BY。因为GROUP BY只起到了分组的作用,排序还是需要通过ORDER BY来完成。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3a/de/3aa2b0626f5cfc64b4a7175de938d1de.png" alt=""><br>
|
||||
我今天对SQL的聚集函数以及SQL查询中的关键字顺序进行了讲解,但你还是需要通过训练加深理解,基于heros数据表,请你写出下面2个SQL查询语句:
|
||||
|
||||
1. 筛选最大生命值大于6000的英雄,按照主要定位进行分组,选择分组英雄数量大于5的分组,按照分组英雄数从高到低进行排序,并显示每个分组的英雄数量、主要定位和平均最大生命值。
|
||||
1. 筛选最大生命值与最大法力值之和大于7000的英雄,按照攻击范围来进行分组,显示分组的英雄数量,以及分组英雄的最大生命值与法力值之和的平均值、最大值和最小值,并按照分组英雄数从高到低进行排序,其中聚集函数的结果包括小数点后两位。
|
||||
|
||||
欢迎你在评论区与我分享你的答案,如果你觉得这篇文章有帮助,欢迎把它分享给你的朋友或者同事,一起切磋交流一下。
|
||||
|
||||
|
||||
182
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/09丨子查询:子查询的种类都有哪些,如何提高子查询的性能?.md
Normal file
182
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/09丨子查询:子查询的种类都有哪些,如何提高子查询的性能?.md
Normal 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 > (SELECT avg(height) FROM player AS b WHERE a.team_id = b.team_id)
|
||||
|
||||
```
|
||||
|
||||
运行结果:(18条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a5/ac/a53fc11e20a44453176bea25d3e789ac.png" alt="">
|
||||
|
||||
这里我们将player表复制成了表a和表b,每次计算的时候,需要将表a中的team_id传入从句,作为已知值。因为每次表a中的team_id可能是不同的,所以是关联子查询。如果是非关联子查询,那么从句计算的结果是固定的才可以。
|
||||
|
||||
## EXISTS子查询
|
||||
|
||||
关联子查询通常也会和EXISTS一起来使用,EXISTS子查询用来判断条件是否满足,满足的话为True,不满足为False。
|
||||
|
||||
比如我们想要看出场过的球员都有哪些,并且显示他们的姓名、球员ID和球队ID。在这个统计中,是否出场是通过player_score这张表中的球员出场表现来统计的,如果某个球员在player_score中有出场记录则代表他出场过,这里就使用到了EXISTS子查询,即`EXISTS (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)`,然后将它作为筛选的条件,实际上也是关联子查询,即:
|
||||
|
||||
```
|
||||
SQL:SELECT player_id, team_id, player_name FROM player WHERE EXISTS (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)
|
||||
|
||||
```
|
||||
|
||||
运行结果:(19条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a6/d3/a6a447a53b9cc158db5f4ffb905aaed3.png" alt=""><br>
|
||||
同样,NOT EXISTS就是不存在的意思,我们也可以通过NOT EXISTS查询不存在于player_score表中的球员信息,比如主表中的player_id不在子表player_score中,判断语句为`NOT EXISTS (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)`。整体的SQL语句为:
|
||||
|
||||
```
|
||||
SQL: SELECT player_id, team_id, player_name FROM player WHERE NOT EXISTS (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)
|
||||
|
||||
```
|
||||
|
||||
运行结果:(18条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fb/e0/fb0a34e0144a6c0ba9a48d1da4dfade0.png" alt="">
|
||||
|
||||
## 集合比较子查询
|
||||
|
||||
集合比较子查询的作用是与另一个查询结果集进行比较,我们可以在子查询中使用IN、ANY、ALL和SOME操作符,它们的含义和英文意义一样:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d3/2c/d3867c22616cbdf88ed83865604e8e2c.png" alt="">
|
||||
|
||||
还是通过上面那个例子,假设我们想要看出场过的球员都有哪些,可以采用IN子查询来进行操作:
|
||||
|
||||
```
|
||||
SELECT player_id, team_id, player_name FROM player WHERE player_id in (SELECT player_id FROM player_score WHERE player.player_id = player_score.player_id)
|
||||
|
||||
```
|
||||
|
||||
你会发现运行结果和上面的是一样的,那么问题来了,既然IN和EXISTS都可以得到相同的结果,那么我们该使用IN还是EXISTS呢?
|
||||
|
||||
我们可以把这个模式抽象为:
|
||||
|
||||
```
|
||||
SELECT * FROM A WHERE cc IN (SELECT cc FROM B)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
SELECT * FROM A WHERE EXIST (SELECT cc FROM B WHERE B.cc=A.cc)
|
||||
|
||||
```
|
||||
|
||||
实际上在查询过程中,在我们对cc列建立索引的情况下,我们还需要判断表A和表B的大小。在这里例子当中,表A指的是player表,表B指的是player_score表。如果表A比表B大,那么IN子查询的效率要比EXIST子查询效率高,因为这时B表中如果对cc列进行了索引,那么IN子查询的效率就会比较高。
|
||||
|
||||
同样,如果表A比表B小,那么使用EXISTS子查询效率会更高,因为我们可以使用到A表中对cc列的索引,而不用从B中进行cc列的查询。
|
||||
|
||||
了解了IN查询后,我们来看下ANY和ALL子查询。刚才讲到了ANY和ALL都需要使用比较符,比较符包括了(>)(=)(<)(>=)(<=)和(<>)等。
|
||||
|
||||
如果我们想要查询球员表中,比印第安纳步行者(对应的team_id为1002)中任意一个球员身高高的球员信息,并且输出他们的球员ID、球员姓名和球员身高,该怎么写呢?首先我们需要找出所有印第安纳步行者队中的球员身高,即`SELECT height FROM player WHERE team_id = 1002`,然后使用ANY子查询即:
|
||||
|
||||
```
|
||||
SQL: SELECT player_id, player_name, height FROM player WHERE height > ANY (SELECT height FROM player WHERE team_id = 1002)
|
||||
|
||||
```
|
||||
|
||||
运行结果:(35条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/45/da/4547b4671d2727eb5075e0c050eac4da.png" alt=""><br>
|
||||
运行结果为35条,你发现有2个人的身高是不如印第安纳步行者的所有球员的。
|
||||
|
||||
同样,如果我们想要知道比印第安纳步行者(对应的team_id为1002)中所有球员身高都高的球员的信息,并且输出球员ID、球员姓名和球员身高,该怎么写呢?
|
||||
|
||||
```
|
||||
SQL: SELECT player_id, player_name, height FROM player WHERE height > ALL (SELECT height FROM player WHERE team_id = 1002)
|
||||
|
||||
```
|
||||
|
||||
运行结果:(1条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/b5/b910c7a40a8cfbde7d47409afe5171b5.png" alt=""><br>
|
||||
我们能看到比印第安纳步行者所有球员都高的球员,在player这张表(一共37个球员)中只有索恩·马克。
|
||||
|
||||
需要强调的是ANY、ALL关键字必须与一个比较操作符一起使用。因为如果你不使用比较操作符,就起不到集合比较的作用,那么使用ANY和ALL就没有任何意义。
|
||||
|
||||
## 将子查询作为计算字段
|
||||
|
||||
我刚才讲了子查询的几种用法,实际上子查询也可以作为主查询的计算字段。比如我想查询每个球队的球员数,也就是对应team这张表,我需要查询相同的team_id在player这张表中所有的球员数量是多少。
|
||||
|
||||
```
|
||||
SQL: SELECT team_name, (SELECT count(*) FROM player WHERE player.team_id = team.team_id) AS player_num FROM team
|
||||
|
||||
```
|
||||
|
||||
运行结果:(3条记录)
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b3/ae/b39cee43eb0545592e54c5ce533cd8ae.png" alt=""><br>
|
||||
你能看到,在player表中只有底特律活塞和印第安纳步行者的球员数据,所以它们的player_num不为0,而亚特兰大老鹰的player_num等于0。在查询的时候,我将子查询`SELECT count(*) FROM player WHERE player.team_id = team.team_id`作为了计算字段,通常我们需要给这个计算字段起一个别名,这里我用的是player_num,因为子查询的语句比较长,使用别名更容易理解。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我讲解了子查询的使用,按照子查询执行的次数,我们可以将子查询分成关联子查询和非关联子查询,其中非关联子查询与主查询的执行无关,只需要执行一次即可,而关联子查询,则需要将主查询的字段值传入子查询中进行关联查询。
|
||||
|
||||
同时,在子查询中你可能会使用到EXISTS、IN、ANY、ALL和SOME等关键字。在某些情况下使用EXISTS和IN可以得到相同的效果,具体使用哪个执行效率更高,则需要看字段的索引情况以及表A和表B哪个表更大。同样,IN、ANY、ALL、SOME这些关键字是用于集合比较的,SOME是ANY的别名,当我们使用ANY或ALL的时候,一定要使用比较操作符。
|
||||
|
||||
最后,我讲解了如何使用子查询作为计算字段,把子查询的结果作为主查询的列。
|
||||
|
||||
SQL中,子查询的使用大大增强了SELECT查询的能力,因为很多时候查询需要从结果集中获取数据,或者需要从同一个表中先计算得出一个数据结果,然后与这个数据结果(可能是某个标量,也可能是某个集合)进行比较。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/67/48/67dffabba0619fa4d311929c5d1c0f48.png" alt=""><br>
|
||||
我今天讲解了子查询的使用,其中讲到了EXISTS和IN子查询效率的比较,当查询字段进行了索引时,主表A大于从表B,使用IN子查询效率更高,相反主表A小于从表B时,使用EXISTS子查询效率更高,同样,如果使用NOT IN子查询和NOT EXISTS子查询,在什么情况下,哪个效率更高呢?
|
||||
|
||||
最后请你使用子查询,编写SQL语句,得到场均得分大于20的球员。场均得分从player_score表中获取,同时你需要输出球员的ID、球员姓名以及所在球队的ID信息。
|
||||
|
||||
欢迎在评论区写下你的思考,也欢迎点击请朋友读把这篇文章分享给你的朋友或者同事。
|
||||
196
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/10丨常用的SQL标准有哪些,在SQL92中是如何使用连接的?.md
Normal file
196
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/10丨常用的SQL标准有哪些,在SQL92中是如何使用连接的?.md
Normal 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中是如何使用连接的
|
||||
|
||||
相比于SQL99,SQL92规则更简单,更适合入门。在这篇文章中,我会先讲SQL92是如何对连接表进行操作的,下一篇文章再讲SQL99,到时候你可以对比下这两者之间有什么区别。
|
||||
|
||||
在进行连接之前,我们需要用数据表做举例。这里我创建了NBA球员和球队两张表,SQL文件你可以从[GitHub](https://github.com/cystanford/sql_nba_data)上下载。
|
||||
|
||||
其中player表为球员表,一共有37个球员,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e3/1b/e327a3eeeb7a7195a7ae0703ebd8e51b.png" alt=""><br>
|
||||
team表为球队表,一共有3支球队,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/39/b5228a60a4ccffa5b2848fe82d575239.png" alt=""><br>
|
||||
有了这两个数据表之后,我们再来看下SQL92中的5种连接方式,它们分别是笛卡尔积、等值连接、非等值连接、外连接(左连接、右连接)和自连接。
|
||||
|
||||
### 笛卡尔积
|
||||
|
||||
笛卡尔乘积是一个数学运算。假设我有两个集合X和Y,那么X和Y的笛卡尔积就是X和Y的所有可能组合,也就是第一个对象来自于X,第二个对象来自于Y的所有可能。
|
||||
|
||||
我们假定player表的数据是集合X,先进行SQL查询:
|
||||
|
||||
```
|
||||
SELECT * FROM player
|
||||
|
||||
```
|
||||
|
||||
再假定team表的数据为集合Y,同样需要进行SQL查询:
|
||||
|
||||
```
|
||||
SELECT * FROM team
|
||||
|
||||
```
|
||||
|
||||
你会看到运行结果会显示出上面的两张表格。
|
||||
|
||||
接着我们再来看下两张表的笛卡尔积的结果,这是笛卡尔积的调用方式:
|
||||
|
||||
```
|
||||
SQL: SELECT * FROM player, team
|
||||
|
||||
```
|
||||
|
||||
运行结果(一共37*3=111条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/37/2e66048cba86811a740a85f68d81c537.png" alt=""><br>
|
||||
笛卡尔积也称为交叉连接,英文是CROSS JOIN,它的作用就是可以把任意表进行连接,即使这两张表不相关。但我们通常进行连接还是需要筛选的,因此你需要在连接后面加上WHERE子句,也就是作为过滤条件对连接数据进行筛选。比如后面要讲到的等值连接。
|
||||
|
||||
### 等值连接
|
||||
|
||||
两张表的等值连接就是用两张表中都存在的列进行连接。我们也可以对多张表进行等值连接。
|
||||
|
||||
针对player表和team表都存在team_id这一列,我们可以用等值连接进行查询。
|
||||
|
||||
```
|
||||
SQL: SELECT player_id, player.team_id, player_name, height, team_name FROM player, team WHERE player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
运行结果(一共37条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/28/d9/282aa15e7d02c60e9ebba8a0cc9134d9.png" alt=""><br>
|
||||
我们在进行等值连接的时候,可以使用表的别名,这样会让SQL语句更简洁:
|
||||
|
||||
```
|
||||
SELECT player_id, a.team_id, player_name, height, team_name FROM player AS a, team AS b WHERE a.team_id = b.team_id
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,如果我们使用了表的别名,在查询字段中就只能使用别名进行代替,不能使用原有的表名,比如下面的SQL查询就会报错:
|
||||
|
||||
```
|
||||
SELECT player_id, player.team_id, player_name, height, team_name FROM player AS a, team AS b WHERE a.team_id = b.team_id
|
||||
|
||||
```
|
||||
|
||||
### 非等值连接
|
||||
|
||||
当我们进行多表查询的时候,如果连接多个表的条件是等号时,就是等值连接,其他的运算符连接就是非等值查询。
|
||||
|
||||
这里我创建一个身高级别表height_grades,如下所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cf/68/cf5ea984ba0c4501c5a4e1eec19e5b68.png" alt=""><br>
|
||||
我们知道player表中有身高height字段,如果想要知道每个球员的身高的级别,可以采用非等值连接查询。
|
||||
|
||||
```
|
||||
SQL:SELECT p.player_name, p.height, h.height_level
|
||||
FROM player AS p, height_grades AS h
|
||||
WHERE p.height BETWEEN h.height_lowest AND h.height_highest
|
||||
|
||||
```
|
||||
|
||||
运行结果(37条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fa/84/fa049e7e186978e7086eb8e157fdc284.png" alt="">
|
||||
|
||||
### 外连接
|
||||
|
||||
除了查询满足条件的记录以外,外连接还可以查询某一方不满足条件的记录。两张表的外连接,会有一张是主表,另一张是从表。如果是多张表的外连接,那么第一张表是主表,即显示全部的行,而第剩下的表则显示对应连接的信息。在SQL92中采用(+)代表从表所在的位置,而且在SQL92中,只有左外连接和右外连接,没有全外连接。
|
||||
|
||||
什么是左外连接,什么是右外连接呢?
|
||||
|
||||
左外连接,就是指左边的表是主表,需要显示左边表的全部行,而右侧的表是从表,(+)表示哪个是从表。
|
||||
|
||||
```
|
||||
SQL:SELECT * FROM player, team where player.team_id = team.team_id(+)
|
||||
|
||||
```
|
||||
|
||||
相当于SQL99中的:
|
||||
|
||||
```
|
||||
SQL:SELECT * FROM player LEFT JOIN team on player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
右外连接,指的就是右边的表是主表,需要显示右边表的全部行,而左侧的表是从表。
|
||||
|
||||
```
|
||||
SQL:SELECT * FROM player, team where player.team_id(+) = team.team_id
|
||||
|
||||
```
|
||||
|
||||
相当于SQL99中的:
|
||||
|
||||
```
|
||||
SQL:SELECT * FROM player RIGHT JOIN team on player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
需要注意的是,LEFT JOIN和RIGHT JOIN只存在于SQL99及以后的标准中,在SQL92中不存在,只能用(+)表示。
|
||||
|
||||
### 自连接
|
||||
|
||||
自连接可以对多个表进行操作,也可以对同一个表进行操作。也就是说查询条件使用了当前表的字段。
|
||||
|
||||
比如我们想要查看比布雷克·格里芬高的球员都有谁,以及他们的对应身高:
|
||||
|
||||
```
|
||||
SQL:SELECT b.player_name, b.height FROM player as a , player as b WHERE a.player_name = '布雷克-格里芬' and a.height < b.height
|
||||
|
||||
```
|
||||
|
||||
运行结果(6条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/05/94/05e4bf92df00e243601ca2d763fabb94.png" alt=""><br>
|
||||
如果不用自连接的话,需要采用两次SQL查询。首先需要查询布雷克·格里芬的身高。
|
||||
|
||||
```
|
||||
SQL:SELECT height FROM player WHERE player_name = '布雷克-格里芬'
|
||||
|
||||
```
|
||||
|
||||
运行结果为2.08。
|
||||
|
||||
然后再查询比2.08高的球员都有谁,以及他们的对应身高:
|
||||
|
||||
```
|
||||
SQL:SELECT player_name, height FROM player WHERE height > 2.08
|
||||
|
||||
```
|
||||
|
||||
运行结果和采用自连接的运行结果是一致的。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我讲解了常用的SQL标准以及SQL92中的连接操作。SQL92和SQL99是经典的SQL标准,也分别叫做SQL-2和SQL-3标准。也正是在这两个标准发布之后,SQL影响力越来越大,甚至超越了数据库领域。现如今SQL已经不仅仅是数据库领域的主流语言,还是信息领域中信息处理的主流语言。在图形检索、图像检索以及语音检索中都能看到SQL语言的使用。
|
||||
|
||||
除此以外,我们使用的主流RDBMS,比如MySQL、Oracle、SQL Sever、DB2、PostgreSQL等都支持SQL语言,也就是说它们的使用符合大部分SQL标准,但很难完全符合,因为这些数据库管理系统都在SQL语言的基础上,根据自身产品的特点进行了扩充。即使这样,SQL语言也是目前所有语言中半衰期最长的,在1992年,Windows3.1发布,SQL92标准也同时发布,如今我们早已不使用Windows3.1操作系统,而SQL92标准却一直持续至今。
|
||||
|
||||
当然我们也要注意到SQL标准的变化,以及不同数据库管理系统使用时的差别,比如Oracle对SQL92支持较好,而MySQL则不支持SQL92的外连接。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e4/0d/e473b216f11cfa7696371bfeadba220d.jpg" alt=""><br>
|
||||
我今天讲解了SQL的连接操作,你能说说内连接、外连接和自连接指的是什么吗?另外,你不妨拿案例中的team表做一道动手题,表格中一共有3支球队,现在这3支球队需要进行比赛,请用一条SQL语句显示出所有可能的比赛组合。
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,与他们一起交流一下。
|
||||
273
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/11丨SQL99是如何使用连接的,与SQL92的区别是什么?.md
Normal file
273
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/11丨SQL99是如何使用连接的,与SQL92的区别是什么?.md
Normal 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两张表:
|
||||
|
||||
```
|
||||
SQL99:SELECT p.player_name, p.height, h.height_level
|
||||
FROM player as p JOIN height_grades as h
|
||||
ON height BETWEEN h.height_lowest AND h.height_highest
|
||||
|
||||
|
||||
```
|
||||
|
||||
这个语句的运行结果和我们之前采用SQL92标准的查询结果一样。
|
||||
|
||||
```
|
||||
SQL92:SELECT p.player_name, p.height, h.height_level
|
||||
FROM player AS p, height_grades AS h
|
||||
WHERE p.height BETWEEN h.height_lowest AND h.height_highest
|
||||
|
||||
```
|
||||
|
||||
一般来说在SQL99中,我们需要连接的表会采用JOIN进行连接,ON指定了连接条件,后面可以是等值连接,也可以采用非等值连接。
|
||||
|
||||
### USING连接
|
||||
|
||||
当我们进行连接的时候,可以用USING指定数据表里的同名字段进行等值连接。比如:
|
||||
|
||||
```
|
||||
SELECT player_id, team_id, player_name, height, team_name FROM player JOIN team USING(team_id)
|
||||
|
||||
```
|
||||
|
||||
你能看出与自然连接NATURAL JOIN不同的是,USING指定了具体的相同的字段名称,你需要在USING的括号()中填入要指定的同名字段。同时使用JOIN USING可以简化JOIN ON的等值连接,它与下面的SQL查询结果是相同的:
|
||||
|
||||
```
|
||||
SELECT player_id, player.team_id, player_name, height, team_name FROM player JOIN team ON player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
### 外连接
|
||||
|
||||
SQL99的外连接包括了三种形式:
|
||||
|
||||
1. 左外连接:LEFT JOIN 或 LEFT OUTER JOIN
|
||||
1. 右外连接:RIGHT JOIN 或 RIGHT OUTER JOIN
|
||||
1. 全外连接:FULL JOIN 或 FULL OUTER JOIN
|
||||
|
||||
我们在SQL92中讲解了左外连接、右外连接,在SQL99中还有全外连接。全外连接实际上就是左外连接和右外连接的结合。在这三种外连接中,我们一般省略OUTER不写。
|
||||
|
||||
1.左外连接
|
||||
|
||||
**SQL92**
|
||||
|
||||
```
|
||||
SELECT * FROM player, team where player.team_id = team.team_id(+)
|
||||
|
||||
```
|
||||
|
||||
**SQL99**
|
||||
|
||||
```
|
||||
SELECT * FROM player LEFT JOIN team ON player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
2.右外连接
|
||||
|
||||
**SQL92**
|
||||
|
||||
```
|
||||
SELECT * FROM player, team where player.team_id(+) = team.team_id
|
||||
|
||||
```
|
||||
|
||||
**SQL99**
|
||||
|
||||
```
|
||||
SELECT * FROM player RIGHT JOIN team ON player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
3.全外连接
|
||||
|
||||
**SQL99**
|
||||
|
||||
```
|
||||
SELECT * FROM player FULL JOIN team ON player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
需要注意的是MySQL不支持全外连接,否则的话全外连接会返回左表和右表中的所有行。当表之间有匹配的行,会显示内连接的结果。当某行在另一个表中没有匹配时,那么会把另一个表中选择的列显示为空值。
|
||||
|
||||
也就是说,全外连接的结果=左右表匹配的数据+左表没有匹配到的数据+右表没有匹配到的数据。
|
||||
|
||||
### 自连接
|
||||
|
||||
自连接的原理在SQL92和SQL99中都是一样的,只是表述方式不同。
|
||||
|
||||
比如我们想要查看比布雷克·格里芬身高高的球员都有哪些,在两个SQL标准下的查询如下。
|
||||
|
||||
**SQL92**
|
||||
|
||||
```
|
||||
SELECT b.player_name, b.height FROM player as a , player as b WHERE a.player_name = '布雷克-格里芬' and a.height < b.height
|
||||
|
||||
```
|
||||
|
||||
**SQL99**
|
||||
|
||||
```
|
||||
SELECT b.player_name, b.height FROM player as a JOIN player as b ON a.player_name = '布雷克-格里芬' and a.height < b.height
|
||||
|
||||
```
|
||||
|
||||
运行结果(6条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c7/e0/c79ecee3e5368ee73bfe7edb8a80a6e0.png" alt="">
|
||||
|
||||
## SQL99和SQL92的区别
|
||||
|
||||
至此我们讲解完了SQL92和SQL99标准下的连接查询,它们都对连接进行了定义,只是操作的方式略有不同。我们再来回顾下,这些连接操作基本上可以分成三种情况:
|
||||
|
||||
1. 内连接:将多个表之间满足连接条件的数据行查询出来。它包括了等值连接、非等值连接和自连接。
|
||||
1. 外连接:会返回一个表中的所有记录,以及另一个表中匹配的行。它包括了左外连接、右外连接和全连接。
|
||||
1. 交叉连接:也称为笛卡尔积,返回左表中每一行与右表中每一行的组合。在SQL99中使用的CROSS JOIN。
|
||||
|
||||
不过SQL92在这三种连接操作中,和SQL99还存在着明显的区别。
|
||||
|
||||
首先我们看下SQL92中的WHERE和SQL99中的JOIN。
|
||||
|
||||
你能看出在SQL92中进行查询时,会把所有需要连接的表都放到FROM之后,然后在WHERE中写明连接的条件。而SQL99在这方面更灵活,它不需要一次性把所有需要连接的表都放到FROM之后,而是采用JOIN的方式,每次连接一张表,可以多次使用JOIN进行连接。
|
||||
|
||||
另外,我建议多表连接使用SQL99标准,因为层次性更强,可读性更强,比如:
|
||||
|
||||
```
|
||||
SELECT ...
|
||||
FROM table1
|
||||
JOIN table2 ON table1和table2的连接条件
|
||||
JOIN table3 ON table2和table3的连接条件
|
||||
|
||||
```
|
||||
|
||||
它的嵌套逻辑类似我们使用的FOR循环:
|
||||
|
||||
```
|
||||
for t1 in table1:
|
||||
for t2 in table2:
|
||||
if condition1:
|
||||
for t3 in table3:
|
||||
if condition2:
|
||||
output t1 + t2 + t3
|
||||
|
||||
```
|
||||
|
||||
SQL99采用的这种嵌套结构非常清爽,即使再多的表进行连接也都清晰可见。如果你采用SQL92,可读性就会大打折扣。
|
||||
|
||||
最后一点就是,SQL99在SQL92的基础上提供了一些特殊语法,比如NATURAL JOIN和JOIN USING。它们在实际中是比较常用的,省略了ON后面的等值条件判断,让SQL语句更加简洁。
|
||||
|
||||
## 不同DBMS中使用连接需要注意的地方
|
||||
|
||||
SQL连接具有通用性,但是不同的DBMS在使用规范上会存在差异,在标准支持上也存在不同。在实际工作中,你需要参考你正在使用的DBMS文档,这里我整理了一些需要注意的常见的问题。
|
||||
|
||||
**1.不是所有的DBMS都支持全外连接**
|
||||
|
||||
虽然SQL99标准提供了全外连接,但不是所有的DBMS都支持。不仅MySQL不支持,Access、SQLite、MariaDB等数据库软件也不支持。不过在Oracle、DB2、SQL Server中是支持的。
|
||||
|
||||
**2.Oracle没有表别名AS**
|
||||
|
||||
为了让SQL查询语句更简洁,我们经常会使用表别名AS,不过在Oracle中是不存在AS的,使用表别名的时候,直接在表名后面写上表别名即可,比如player p,而不是player AS p。
|
||||
|
||||
**3.SQLite的外连接只有左连接**
|
||||
|
||||
SQLite是一款轻量级的数据库软件,在外连接上只支持左连接,不支持右连接,不过如果你想使用右连接的方式,比如`table1 RIGHT JOIN table2`,在SQLite你可以写成`table2 LEFT JOIN table1`,这样就可以得到相同的效果。
|
||||
|
||||
除了一些常见的语法问题,还有一些关于连接的性能问题需要你注意:
|
||||
|
||||
**1.控制连接表的数量**
|
||||
|
||||
多表连接就相当于嵌套for循环一样,非常消耗资源,会让SQL查询性能下降得很严重,因此不要连接不必要的表。在许多DBMS中,也都会有最大连接表的限制。
|
||||
|
||||
**2.在连接时不要忘记WHERE语句**
|
||||
|
||||
多表连接的目的不是为了做笛卡尔积,而是筛选符合条件的数据行,因此在多表连接的时候不要忘记了WHERE语句,这样可以过滤掉不必要的数据行返回。
|
||||
|
||||
**3.使用自连接而不是子查询**
|
||||
|
||||
我们在查看比布雷克·格里芬高的球员都有谁的时候,可以使用子查询,也可以使用自连接。一般情况建议你使用自连接,因为在许多DBMS的处理过程中,对于自连接的处理速度要比子查询快得多。你可以这样理解:子查询实际上是通过未知表进行查询后的条件判断,而自连接是通过已知的自身数据表进行条件判断,因此在大部分DBMS中都对自连接处理进行了优化。
|
||||
|
||||
## 总结
|
||||
|
||||
连接可以说是SQL中的核心操作,通过两篇文章的学习,你已经从多个维度对连接进行了了解。同时,我们对SQL的两个重要标准SQL92和SQL99进行了学习,在我们需要进行外连接的时候,建议采用SQL99标准,这样更适合阅读。
|
||||
|
||||
此外我还想强调一下,我们在进行连接的时候,使用的关系型数据库管理系统,之所以存在关系是因为各种数据表之间存在关联,它们并不是孤立存在的。在实际工作中,尤其是做业务报表的时候,我们会用到SQL中的连接操作(JOIN),因此我们需要理解和熟练掌握SQL标准中连接的使用,以及不同DBMS中对连接的语法规范。剩下要做的,就是通过做练习和实战来增强你的经验了,做的练习多了,也就自然有感觉了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/44/5b/443181aea770ba5844efac6b02e02c5b.jpg" alt=""><br>
|
||||
我今天讲解了SQL99的连接操作,不妨请你做一个小练习。请你编写SQL查询语句,查询不同身高级别(对应height_grades表)对应的球员数量(对应player表)。
|
||||
|
||||
欢迎你在评论区写下你的答案,我会在评论区与你一起讨论。也欢迎把这篇文章分享给你的朋友或者同事。
|
||||
224
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/12丨视图在SQL中的作用是什么,它是怎样工作的?.md
Normal file
224
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/12丨视图在SQL中的作用是什么,它是怎样工作的?.md
Normal file
@@ -0,0 +1,224 @@
|
||||
<audio id="audio" title="12丨视图在SQL中的作用是什么,它是怎样工作的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7c/ba/7ccceb296d99aaf83bbc8de24d9d46ba.mp3"></audio>
|
||||
|
||||
我们之前对SQL中的数据表查询进行了讲解,今天我们来看下如何对视图进行查询。视图,也就是我们今天要讲的虚拟表,本身是不具有数据的,它是SQL中的一个重要概念。从下面这张图中,你能看到,虚拟表的创建连接了一个或多个数据表,不同的查询应用都可以建立在虚拟表之上。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/e8/6c7cd968b0bd24ce5689a08c052eade8.jpg" alt="">
|
||||
|
||||
视图一方面可以帮我们使用表的一部分而不是所有的表,另一方面也可以针对不同的用户制定不同的查询视图。比如,针对一个公司的销售人员,我们只想给他看部分数据,而某些特殊的数据,比如采购的价格,则不会提供给他。
|
||||
|
||||
刚才讲的只是视图的一个使用场景,实际上视图还有很多作用,今天我们就一起学习下。今天的文章里,你将重点掌握以下的内容:
|
||||
|
||||
1. 什么是视图?如何创建、更新和删除视图?
|
||||
1. 如何使用视图来简化我们的SQL操作?
|
||||
1. 视图和临时表的区别是什么,它们各自有什么优缺点?
|
||||
|
||||
## 如何创建,更新和删除视图
|
||||
|
||||
视图作为一张虚拟表,帮我们封装了底层与数据表的接口。它相当于是一张表或多张表的数据结果集。视图的这一特点,可以帮我们简化复杂的SQL查询,比如在编写视图后,我们就可以直接重用它,而不需要考虑视图中包含的基础查询的细节。同样,我们也可以根据需要更改数据格式,返回与底层数据表格式不同的数据。
|
||||
|
||||
通常情况下,小型项目的数据库可以不使用视图,但是在大型项目中,以及数据表比较复杂的情况下,视图的价值就凸显出来了,它可以帮助我们把经常查询的结果集放到虚拟表中,提升使用效率。理解和使用起来都非常方便。
|
||||
|
||||
### 创建视图:CREATE VIEW
|
||||
|
||||
那么该如何创建视图呢?创建视图的语法是:
|
||||
|
||||
```
|
||||
CREATE VIEW view_name AS
|
||||
SELECT column1, column2
|
||||
FROM table
|
||||
WHERE condition
|
||||
|
||||
```
|
||||
|
||||
实际上就是我们在SQL查询语句的基础上封装了视图VIEW,这样就会基于SQL语句的结果集形成一张虚拟表。其中view_name为视图名称,column1、column2代表列名,condition代表查询过滤条件。
|
||||
|
||||
我们以NBA球员数据表为例。我们想要查询比NBA球员平均身高高的球员都有哪些,显示他们的球员ID和身高。假设我们给这个视图起个名字player_above_avg_height,那么创建视图可以写成:
|
||||
|
||||
```
|
||||
CREATE VIEW player_above_avg_height AS
|
||||
SELECT player_id, height
|
||||
FROM player
|
||||
WHERE height > (SELECT AVG(height) from player)
|
||||
|
||||
```
|
||||
|
||||
视图查询结果(18条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a0/35/a05ac6169562c0f95bf8387df3577635.png" alt=""><br>
|
||||
当视图创建之后,它就相当于一个虚拟表,可以直接使用:
|
||||
|
||||
```
|
||||
SELECT * FROM player_above_avg_height
|
||||
|
||||
```
|
||||
|
||||
运行结果和上面一样。
|
||||
|
||||
### 嵌套视图
|
||||
|
||||
当我们创建好一张视图之后,还可以在它的基础上继续创建视图,比如我们想在虚拟表player_above_avg_height的基础上,找到比这个表中的球员平均身高高的球员,作为新的视图player_above_above_avg_height,那么可以写成:
|
||||
|
||||
```
|
||||
CREATE VIEW player_above_above_avg_height AS
|
||||
SELECT player_id, height
|
||||
FROM player
|
||||
WHERE height > (SELECT AVG(height) from player_above_avg_height)
|
||||
|
||||
```
|
||||
|
||||
视图查询结果(11条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6b/5b/6b7416b24d91786c023bf10eee50355b.png" alt="">
|
||||
|
||||
你能看到这个视图的数据记录数为11个,比之前的记录少了7个。
|
||||
|
||||
### 修改视图:ALTER VIEW
|
||||
|
||||
修改视图的语法是:
|
||||
|
||||
```
|
||||
ALTER VIEW view_name AS
|
||||
SELECT column1, column2
|
||||
FROM table
|
||||
WHERE condition
|
||||
|
||||
```
|
||||
|
||||
你能看出来它的语法和创建视图一样,只是对原有视图的更新。比如我们想更新视图player_above_avg_height,增加一个player_name字段,可以写成:
|
||||
|
||||
```
|
||||
ALTER VIEW player_above_avg_height AS
|
||||
SELECT player_id, player_name, height
|
||||
FROM player
|
||||
WHERE height > (SELECT AVG(height) from player)
|
||||
|
||||
```
|
||||
|
||||
这样的话,下次再对视图进行查询的时候,视图结果就进行了更新。
|
||||
|
||||
```
|
||||
SELECT * FROM player_above_avg_height
|
||||
|
||||
```
|
||||
|
||||
运行结果(18条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/bb/d91a3d7978b12fc52c194d1ce58410bb.png" alt="">
|
||||
|
||||
### 删除视图:DROP VIEW
|
||||
|
||||
删除视图的语法是:
|
||||
|
||||
```
|
||||
DROP VIEW view_name
|
||||
|
||||
```
|
||||
|
||||
比如我们想把刚才创建的视图删除,可以使用:
|
||||
|
||||
```
|
||||
DROP VIEW player_above_avg_height
|
||||
|
||||
```
|
||||
|
||||
需要说明的是,SQLite不支持视图的修改,仅支持只读视图,也就是说你只能使用CREATE VIEW和DROP VIEW,如果想要修改视图,就需要先DROP然后再CREATE。
|
||||
|
||||
## 如何使用视图简化SQL操作
|
||||
|
||||
从上面这个例子中,你能看出视图就是对SELECT语句进行了封装,方便我们重用它们。下面我们再来看几个视图使用的例子。
|
||||
|
||||
### 利用视图完成复杂的连接
|
||||
|
||||
我在讲解SQL99标准连接操作的时候,举了一个NBA球员和身高等级连接的例子,有两张表,分别为player和height_grades。其中height_grades记录了不同身高对应的身高等级。这里我们可以通过创建视图,来完成球员以及对应身高等级的查询。
|
||||
|
||||
首先我们对player表和height_grades表进行连接,关联条件是球员的身高height(在身高等级表规定的最低身高和最高身高之间),这样就可以得到这个球员对应的身高等级,对应的字段为height_level。然后我们通过SELECT得到我们想要查询的字段,分别为球员姓名player_name、球员身高height,还有对应的身高等级height_level。然后把取得的查询结果集放到视图player_height_grades中,即:
|
||||
|
||||
```
|
||||
CREATE VIEW player_height_grades AS
|
||||
SELECT p.player_name, p.height, h.height_level
|
||||
FROM player as p JOIN height_grades as h
|
||||
ON height BETWEEN h.height_lowest AND h.height_highest
|
||||
|
||||
```
|
||||
|
||||
运行结果(37条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/31/d2/3185f62845a19162c19b22673da6c8d2.png" alt="">
|
||||
|
||||
以后我们进行查询的时候,可以直接通过视图查询,比如我想查询身高介于1.90m和2.08m之间的球员及他们对应的身高:
|
||||
|
||||
```
|
||||
SELECT * FROM player_height_grades WHERE height >= 1.90 AND height <= 2.08
|
||||
|
||||
```
|
||||
|
||||
运行结果(26条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/89/8c060eb06386b95cb31ff43c95948a89.png" alt="">
|
||||
|
||||
这样就把一个相对复杂的连接查询转化成了视图查询。
|
||||
|
||||
### 利用视图对数据进行格式化
|
||||
|
||||
我们经常需要输出某个格式的内容,比如我们想输出球员姓名和对应的球队,对应格式为player_name(team_name),就可以使用视图来完成数据格式化的操作:
|
||||
|
||||
```
|
||||
CREATE VIEW player_team AS
|
||||
SELECT CONCAT(player_name, '(' , team.team_name , ')') AS player_team FROM player JOIN team WHERE player.team_id = team.team_id
|
||||
|
||||
```
|
||||
|
||||
首先我们将player表和team表进行连接,关联条件是相同的team_id。我们想要的格式是`player_name(team_name)`,因此我们使用CONCAT函数,即`CONCAT(player_name, '(' , team.team_name , ')')`,将player_name字段和team_name字段进行拼接,得到了拼接值被命名为player_team的字段名,将它放到视图player_team中。
|
||||
|
||||
这样的话,我们直接查询视图,就可以得到格式化后的结果:
|
||||
|
||||
```
|
||||
SELECT * FROM player_team
|
||||
|
||||
```
|
||||
|
||||
运行结果(37条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/28/0d/280a22627fd84cd8450245041a3bba0d.png" alt="">
|
||||
|
||||
### 使用视图与计算字段
|
||||
|
||||
我们在数据查询中,有很多统计的需求可以通过视图来完成。正确地使用视图可以帮我们简化复杂的数据处理。
|
||||
|
||||
我以球员比赛成绩表为例,对应的是player_score表。这张表中一共有19个字段,它们代表的含义如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8a/d2/8a77858d8c9633c7c4128dd454ad38d2.png" alt=""><br>
|
||||
如果我想要统计每位球员在每场比赛中的二分球、三分球和罚球的得分,可以通过创建视图完成:
|
||||
|
||||
```
|
||||
CREATE VIEW game_player_score AS
|
||||
SELECT game_id, player_id, (shoot_hits-shoot_3_hits)*2 AS shoot_2_points, shoot_3_hits*3 AS shoot_3_points, shoot_p_hits AS shoot_p_points, score FROM player_score
|
||||
|
||||
```
|
||||
|
||||
然后通过查询视图就可以完成。
|
||||
|
||||
```
|
||||
SELECT * FROM game_player_score
|
||||
|
||||
```
|
||||
|
||||
运行结果(19条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b0/dc/b0edf8453df44d018315f68e89f7e3dc.png" alt="">
|
||||
|
||||
你能看出正确使用视图可以简化复杂的SQL查询,让SQL更加清爽易用。不过有一点需要注意,视图是虚拟表,它只是封装了底层的数据表查询接口,因此有些RDBMS不支持对视图创建索引(有些RDBMS则支持,比如新版本的SQL Server)。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我讲解了视图的使用,包括创建,修改和删除视图。使用视图有很多好处,比如安全、简单清晰。
|
||||
|
||||
1. 安全性:虚拟表是基于底层数据表的,我们在使用视图时,一般不会轻易通过视图对底层数据进行修改,即使是使用单表的视图,也会受到限制,比如计算字段,类型转换等是无法通过视图来对底层数据进行修改的,这也在一定程度上保证了数据表的数据安全性。同时,我们还可以针对不同用户开放不同的数据查询权限,比如人员薪酬是个敏感的字段,那么只给某个级别以上的人员开放,其他人的查询视图中则不提供这个字段。
|
||||
1. 简单清晰:视图是对SQL查询的封装,它可以将原本复杂的SQL查询简化,在编写好查询之后,我们就可以直接重用它而不必要知道基本的查询细节。同时我们还可以在视图之上再嵌套视图。这样就好比我们在进行模块化编程一样,不仅结构清晰,还提升了代码的复用率。
|
||||
|
||||
另外,我们也需要了解到视图是虚拟表,本身不存储数据,如果想要通过视图对底层数据表的数据进行修改也会受到很多限制,通常我们是把视图用于查询,也就是对SQL查询的一种封装。那么它和临时表又有什么区别呢?在实际工作中,我们可能会见到各种临时数据。比如你可能会问,如果我在做一个电商的系统,中间会有个购物车的功能,需要临时统计购物车中的商品和金额,那该怎么办呢?这里就需要用到临时表了,临时表是真实存在的数据表,不过它不用于长期存放数据,只为当前连接存在,关闭连接后,临时表就会自动释放。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8a/30/8afa99e7d1ac1de2c802cf0c61004b30.jpg" alt=""><br>
|
||||
今天我们对视图进行了讲解,你能用自己的语言来说下视图的优缺点么?另外视图在更新的时候会影响到数据表吗?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
176
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/13丨什么是存储过程,在实际项目中用得多么?.md
Normal file
176
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/13丨什么是存储过程,在实际项目中用得多么?.md
Normal file
@@ -0,0 +1,176 @@
|
||||
<audio id="audio" title="13丨什么是存储过程,在实际项目中用得多么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7a/cb/7af0629b30613af937de2c126ce081cb.mp3"></audio>
|
||||
|
||||
上一节我介绍了视图,它是SQL中的一个重要应用,使用视图对SQL查询进行封装,可以让SQL的代码结构更清晰,让用户权限管理更安全。
|
||||
|
||||
今天我来讲一下SQL的存储过程,它是SQL中另一个重要应用,和视图一样,都是对SQL代码进行封装,可以反复利用。它和视图有着同样的优点,清晰、安全,还可以减少网络传输量。不过它和视图不同,视图是虚拟表,通常不对底层数据表直接操作,而存储过程是程序化的SQL,可以直接操作底层数据表,相比于面向集合的操作方式,能够实现一些更复杂的数据处理。存储过程可以说是由SQL语句和流控制语句构成的语句集合,它和我们之前学到的函数一样,可以接收输入参数,也可以返回输出参数给调用者,返回计算结果。
|
||||
|
||||
今天有关存储过程的内容,你将重点掌握以下几个部分:
|
||||
|
||||
1. 什么是存储过程,如何创建一个存储过程?
|
||||
1. 流控制语句都有哪些,如何使用它们?
|
||||
1. 各大公司是如何看待存储过程的?在实际工作中,我们该如何使用存储过程?
|
||||
|
||||
## 什么是存储过程,如何创建一个存储过程
|
||||
|
||||
存储过程的英文是Stored Procedure。它的思想很简单,就是SQL语句的封装。一旦存储过程被创建出来,使用它就像使用函数一样简单,我们直接通过调用存储过程名即可。我在前面讲过,存储过程实际上由SQL语句和流控制语句共同组成。流控制语句都有哪些呢?这个我稍后讲解。
|
||||
|
||||
我们先来看下如何定义一个存储过程:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE 存储过程名称([参数列表])
|
||||
BEGIN
|
||||
需要执行的语句
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
在这里,我们使用CREATE PROCEDURE创建一个存储过程,后面是存储过程的名称,以及过程所带的参数,可以包括输入参数和输出参数。最后由BEGIN和END来定义我们所要执行的语句块。
|
||||
|
||||
和视图一样,我们可以删除已经创建的存储过程,使用的是DROP PROCEDURE。如果要更新存储过程,我们需要使用ALTER PROCEDURE。
|
||||
|
||||
讲完了如何创建,更新和删除一个存储过程,下面我们来看下如何实现一个简单的存储过程。比如我想做一个累加运算,计算1+2+…+n等于多少,我们可以通过参数n来表示想要累加的个数,那么如何用存储过程实现这一目的呢?这里我做一个add_num的存储过程,具体的代码如下:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `add_num`(IN n INT)
|
||||
BEGIN
|
||||
DECLARE i INT;
|
||||
DECLARE sum INT;
|
||||
|
||||
SET i = 1;
|
||||
SET sum = 0;
|
||||
WHILE i <= n DO
|
||||
SET sum = sum + i;
|
||||
SET i = i +1;
|
||||
END WHILE;
|
||||
SELECT sum;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
当我们需要再次使用这个存储过程的时候,直接使用 `CALL add_num(50);`即可。这里我传入的参数为50,也就是统计1+2+…+50的积累之和,查询结果为:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e4/1e/e4e29b71160980a9208c8da4dc2a6f1e.png" alt=""><br>
|
||||
这就是一个简单的存储过程,除了理解1+2+…+n的实现过程,还有两点你需要理解,一个是DELIMITER定义语句的结束符,另一个是存储过程的三种参数类型。
|
||||
|
||||
我们先来看下DELIMITER的作用。如果你使用Navicat这个工具来管理MySQL执行存储过程,那么直接执行上面这段代码就可以了。如果用的是MySQL,你还需要用DELIMITER来临时定义新的结束符。因为默认情况下SQL采用(;)作为结束符,这样当存储过程中的每一句SQL结束之后,采用(;)作为结束符,就相当于告诉SQL可以执行这一句了。但是存储过程是一个整体,我们不希望SQL逐条执行,而是采用存储过程整段执行的方式,因此我们就需要临时定义新的DELIMITER,新的结束符可以用(//)或者($$)。如果你用的是MySQL,那么上面这段代码,应该写成下面这样:
|
||||
|
||||
```
|
||||
DELIMITER //
|
||||
CREATE PROCEDURE `add_num`(IN n INT)
|
||||
BEGIN
|
||||
DECLARE i INT;
|
||||
DECLARE sum INT;
|
||||
|
||||
SET i = 1;
|
||||
SET sum = 0;
|
||||
WHILE i <= n DO
|
||||
SET sum = sum + i;
|
||||
SET i = i +1;
|
||||
END WHILE;
|
||||
SELECT sum;
|
||||
END //
|
||||
DELIMITER ;
|
||||
|
||||
```
|
||||
|
||||
首先我用(//)作为结束符,又在整个存储过程结束后采用了(//)作为结束符号,告诉SQL可以执行了,然后再将结束符还原成默认的(;)。
|
||||
|
||||
需要注意的是,如果你用的是Navicat工具,那么在编写存储过程的时候,Navicat会自动设置DELIMITER为其他符号,我们不需要再进行DELIMITER的操作。
|
||||
|
||||
我们再来看下存储过程的3种参数类型。在刚才的存储过程中,我们使用了IN类型的参数,另外还有OUT类型和INOUT类型,作用如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/29/8c12ca959dbc6cfe1d62459481454429.png" alt=""><br>
|
||||
IN和OUT的结合,既用于存储过程的传入参数,同时又可以把计算结果放到参数中,调用者可以得到返回值。
|
||||
|
||||
你能看到,IN参数必须在调用存储过程时指定,而在存储过程中修改该参数的值不能被返回。而OUT参数和INOUT参数可以在存储过程中被改变,并可返回。
|
||||
|
||||
举个例子,这里会用到我们之前讲过的王者荣耀的英雄数据表heros。假设我想创建一个存储类型get_hero_scores,用来查询某一类型英雄中的最大的最大生命值,最小的最大魔法值,以及平均最大攻击值,那么该怎么写呢?
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `get_hero_scores`(
|
||||
OUT max_max_hp FLOAT,
|
||||
OUT min_max_mp FLOAT,
|
||||
OUT avg_max_attack FLOAT,
|
||||
s VARCHAR(255)
|
||||
)
|
||||
BEGIN
|
||||
SELECT MAX(hp_max), MIN(mp_max), AVG(attack_max) FROM heros WHERE role_main = s INTO max_max_hp, min_max_mp, avg_max_attack;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
你能看到我定义了4个参数类型,其中3个为OUT类型,分别为max_max_hp、min_max_mp和avg_max_attack,另一个参数s为IN类型。
|
||||
|
||||
这里我们从heros数据表中筛选主要英雄定位为s的英雄数据,即筛选条件为role_main=s,提取这些数据中的最大的最大生命值,最小的最大魔法值,以及平均最大攻击值,分别赋值给变量max_max_hp、min_max_mp和avg_max_attack。
|
||||
|
||||
然后我们就可以调用存储过程,使用下面这段代码即可:
|
||||
|
||||
```
|
||||
CALL get_hero_scores(@max_max_hp, @min_max_mp, @avg_max_attack, '战士');
|
||||
SELECT @max_max_hp, @min_max_mp, @avg_max_attack;
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7f/e9/7f059f3cb9c345530c67db42af5d04e9.png" alt="">
|
||||
|
||||
## 流控制语句
|
||||
|
||||
流控制语句是用来做流程控制的,我刚才讲了两个简单的存储过程的例子,一个是1+2+…+n的结果计算,一个是王者荣耀的数据查询,你能看到这两个例子中,我用到了下面的流控制语句:
|
||||
|
||||
1. BEGIN…END:BEGIN…END中间包含了多个语句,每个语句都以(;)号为结束符。
|
||||
1. DECLARE:DECLARE用来声明变量,使用的位置在于BEGIN…END语句中间,而且需要在其他语句使用之前进行变量的声明。
|
||||
1. SET:赋值语句,用于对变量进行赋值。
|
||||
1. SELECT…INTO:把从数据表中查询的结果存放到变量中,也就是为变量赋值。
|
||||
|
||||
除了上面这些用到的流控制语句以外,还有一些常用的流控制语句:
|
||||
|
||||
1.IF…THEN…ENDIF:条件判断语句,我们还可以在IF…THEN…ENDIF中使用ELSE和ELSEIF来进行条件判断。<br>
|
||||
2.CASE:CASE语句用于多条件的分支判断,使用的语法是下面这样的。
|
||||
|
||||
```
|
||||
CASE
|
||||
WHEN expression1 THEN ...
|
||||
WHEN expression2 THEN ...
|
||||
...
|
||||
ELSE
|
||||
--ELSE语句可以加,也可以不加。加的话代表的所有条件都不满足时采用的方式。
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
3.LOOP、LEAVE和ITERATE:LOOP是循环语句,使用LEAVE可以跳出循环,使用ITERATE则可以进入下一次循环。如果你有面向过程的编程语言的使用经验,你可以把LEAVE理解为BREAK,把ITERATE理解为CONTINUE。<br>
|
||||
4.REPEAT…UNTIL…END REPEAT:这是一个循环语句,首先会执行一次循环,然后在UNTIL中进行表达式的判断,如果满足条件就退出,即END REPEAT;如果条件不满足,则会就继续执行循环,直到满足退出条件为止。<br>
|
||||
5.WHILE…DO…END WHILE:这也是循环语句,和REPEAT循环不同的是,这个语句需要先进行条件判断,如果满足条件就进行循环,如果不满足条件就退出循环。
|
||||
|
||||
我们之前说过SQL是声明型语言,使用SQL就像在使用英语,简单直接。今天讲的存储过程,尤其是在存储过程中使用到的流控制语句,属于过程性语言,类似于C++语言中函数,这些语句可以帮我们解决复杂的业务逻辑。
|
||||
|
||||
## 关于存储过程使用的争议
|
||||
|
||||
尽管存储过程有诸多优点,但是对于存储过程的使用,一直都存在着很多争议,比如有些公司对于大型项目要求使用存储过程,而有些公司在手册中明确禁止使用存储过程,为什么这些公司对存储过程的使用需求差别这么大呢?
|
||||
|
||||
我们得从存储过程的特点来找答案。
|
||||
|
||||
你能看到存储过程有很多好处。
|
||||
|
||||
首先存储过程可以一次编译多次使用。存储过程只在创造时进行编译,之后的使用都不需要重新编译,这就提升了SQL的执行效率。其次它可以减少开发工作量。将代码封装成模块,实际上是编程的核心思想之一,这样可以把复杂的问题拆解成不同的模块,然后模块之间可以重复使用,在减少开发工作量的同时,还能保证代码的结构清晰。还有一点,存储过程的安全性强,我们在设定存储过程的时候可以设置对用户的使用权限,这样就和视图一样具有较强的安全性。最后它可以减少网络传输量,因为代码封装到存储过程中,每次使用只需要调用存储过程即可,这样就减少了网络传输量。同时在进行相对复杂的数据库操作时,原本需要使用一条一条的SQL语句,可能要连接多次数据库才能完成的操作,现在变成了一次存储过程,只需要连接一次即可。
|
||||
|
||||
基于上面这些优点,不少大公司都要求大型项目使用存储过程,比如微软、IBM等公司。但是国内的阿里并不推荐开发人员使用存储过程,这是为什么呢?
|
||||
|
||||
存储过程虽然有诸如上面的好处,但缺点也是很明显的。
|
||||
|
||||
它的可移植性差,存储过程不能跨数据库移植,比如在MySQL、Oracle和SQL Server里编写的存储过程,在换成其他数据库时都需要重新编写。
|
||||
|
||||
其次调试困难,只有少数DBMS支持存储过程的调试。对于复杂的存储过程来说,开发和维护都不容易。
|
||||
|
||||
此外,存储过程的版本管理也很困难,比如数据表索引发生变化了,可能会导致存储过程失效。我们在开发软件的时候往往需要进行版本管理,但是存储过程本身没有版本控制,版本迭代更新的时候很麻烦。
|
||||
|
||||
最后它不适合高并发的场景,高并发的场景需要减少数据库的压力,有时数据库会采用分库分表的方式,而且对可扩展性要求很高,在这种情况下,存储过程会变得难以维护,增加数据库的压力,显然就不适用了。
|
||||
|
||||
了解了存储过程的优缺点之后,我想说的是,存储过程既方便,又有局限性。尽管不同的公司对存储过程的态度不一,但是对于我们开发人员来说,不论怎样,掌握存储过程都是必备的技能之一。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/54/9f/54a0d3b0c0e5336c1da6fc84c909a49f.jpg" alt=""><br>
|
||||
最后我们做一个小练习吧。针对王者荣耀的英雄数据表heros表,请编写存储过程get_sum_score,用来得到某一类型英雄(主要定位为某一类型即可)的最大生命值的总和。
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
@@ -0,0 +1,153 @@
|
||||
<audio id="audio" title="14丨什么是事务处理,如何使用COMMIT和ROLLBACK进行操作?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/27/bf/270a3a064d24b4473da95708fa70d5bf.mp3"></audio>
|
||||
|
||||
我们知道在MySQL 5.5版本之前,默认的存储引擎是MyISAM,在5.5版本之后默认存储引擎是InnoDB。InnoDB和MyISAM区别之一就是InnoDB支持事务,也可以说这是InnoDB取代MyISAM的重要原因。那么什么是事务呢?事务的英文是transaction,从英文中你也能看出来它是进行一次处理的基本单元,要么完全执行,要么都不执行。
|
||||
|
||||
这么讲,你可能觉得有些抽象,我换一种方式讲。
|
||||
|
||||
不知道你是否遇到过这样的情况,你去家门口的小卖铺买东西,已经交了钱,但是老板比较忙接了个电话,忘记你是否交过钱,然后让你重新付款,这时你还要找之前的付款记录证明你已经完成了付款。
|
||||
|
||||
实际上如果我们线下的交易也能支持事务(满足事务的特性),就不会出现交了钱却拿不到商品的烦恼了,同样,对于小卖铺的老板来说,也不存在给出了商品但没有收到款的风险。总之,事务保证了一次处理的完整性,也保证了数据库中的数据一致性。它是一种高级的数据处理方式,如果我们在增加、删除、修改的时候某一个环节出了错,它允许我们回滚还原。正是因为这个特点,事务非常适合应用在安全性高的场景里,比如金融行业等。
|
||||
|
||||
我们今天就来学习下SQL中的事务。今天的课程你将重点掌握以下的内容:
|
||||
|
||||
1. 事务的特性是什么?如何理解它们?
|
||||
1. 如何对事务进行控制?控制的命令都有哪些?
|
||||
1. 为什么我们执行COMMIT、ROLLBACK这些命令的时候,有时会成功,有时会失败?
|
||||
|
||||
## 事务的特性:ACID
|
||||
|
||||
我刚才提到了事务的特性:要么完全执行,要么都不执行。不过要对事务进行更深一步的理解,还要从事务的4个特性说起,这4个特性用英文字母来表达就是ACID。
|
||||
|
||||
1. A,也就是原子性(Atomicity)。原子的概念就是不可分割,你可以把它理解为组成物质的基本单位,也是我们进行数据处理操作的基本单位。
|
||||
1. C,就是一致性(Consistency)。一致性指的就是数据库在进行事务操作后,会由原来的一致状态,变成另一种一致的状态。也就是说当事务提交后,或者当事务发生回滚后,数据库的完整性约束不能被破坏。
|
||||
1. I,就是隔离性(Isolation)。它指的是每个事务都是彼此独立的,不会受到其他事务的执行影响。也就是说一个事务在提交之前,对其他事务都是不可见的。
|
||||
1. 最后一个D,指的是持久性(Durability)。事务提交之后对数据的修改是持久性的,即使在系统出故障的情况下,比如系统崩溃或者存储介质发生故障,数据的修改依然是有效的。因为当事务完成,数据库的日志就会被更新,这时可以通过日志,让系统恢复到最后一次成功的更新状态。
|
||||
|
||||
ACID可以说是事务的四大特性,在这四个特性中,原子性是基础,隔离性是手段,一致性是约束条件,而持久性是我们的目的。原子性和隔离性比较好理解,这里我讲下对一致性的理解(国内很多网站上对一致性的阐述有误,具体你可以参考Wikipedia对[Consistency](https://en.wikipedia.org/wiki/ACID)的阐述)。
|
||||
|
||||
我之前讲到过数据表的7种常见约束([对应04篇](https://time.geekbang.org/column/article/101697))。这里指的一致性本身是由具体的业务定义的,也就是说,任何写入数据库中的数据都需要满足我们事先定义的约束规则。
|
||||
|
||||
比如说,在数据表中我们将姓名字段设置为唯一性约束,这时当事务进行提交或者事务发生回滚的时候,如果数据表中的姓名非唯一,就破坏了事务的一致性要求。所以说,事务操作会让数据表的状态变成另一种一致的状态,如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。
|
||||
|
||||
事务的另一个特点就是持久性,持久性是通过事务日志来保证的。日志包括了回滚日志和重做日志。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。
|
||||
|
||||
## 事务的控制
|
||||
|
||||
当我们了解了事务的特性后,再来看下如何使用事务。我们知道Oracle是支持事务的,而在MySQL中,则需要选择适合的存储引擎才可以支持事务。如果你使用的是MySQL,可以通过SHOW ENGINES命令来查看当前MySQL支持的存储引擎都有哪些,以及这些存储引擎是否支持事务。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dd/45/ddfbb2ec8a03dc4eb3c77ae3dea63945.png" alt=""><br>
|
||||
你能看出在MySQL中,InnoDB是支持事务的,而MyISAM存储引擎不支持事务。
|
||||
|
||||
看到这里,我们已经对事务有了一定的了解,现在我们再来看下事务的常用控制语句都有哪些。
|
||||
|
||||
1. START TRANSACTION或者 BEGIN,作用是显式开启一个事务。
|
||||
1. COMMIT:提交事务。当提交事务后,对数据库的修改是永久性的。
|
||||
1. ROLLBACK或者ROLLBACK TO [SAVEPOINT],意为回滚事务。意思是撤销正在进行的所有没有提交的修改,或者将事务回滚到某个保存点。
|
||||
1. SAVEPOINT:在事务中创建保存点,方便后续针对保存点进行回滚。一个事务中可以存在多个保存点。
|
||||
1. RELEASE SAVEPOINT:删除某个保存点。
|
||||
1. SET TRANSACTION,设置事务的隔离级别。
|
||||
|
||||
需要说明的是,使用事务有两种方式,分别为隐式事务和显式事务。隐式事务实际上就是自动提交,Oracle默认不自动提交,需要手写COMMIT命令,而MySQL默认自动提交,当然我们可以配置MySQL的参数:
|
||||
|
||||
```
|
||||
mysql> set autocommit =0; //关闭自动提交
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
mysql> set autocommit =1; //开启自动提交
|
||||
|
||||
```
|
||||
|
||||
我们看下在MySQL的默认状态下,下面这个事务最后的处理结果是什么:
|
||||
|
||||
```
|
||||
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '关羽';
|
||||
COMMIT;
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '张飞';
|
||||
INSERT INTO test SELECT '张飞';
|
||||
ROLLBACK;
|
||||
SELECT * FROM test;
|
||||
|
||||
```
|
||||
|
||||
运行结果(1行数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a7/26/a7a49569e87dd5c6f5333a81d33c2826.png" alt="">
|
||||
|
||||
在这个事务中,整个SQL一共执行了2个事务,第一个是插入“关羽”,提交后执行成功,第二个是插入两次“张飞”,这里需要注意的是,我们将name设置为了主键,也就是说主键的值是唯一的,那么第二次插入“张飞”时就会产生错误,然后执行ROLLBACK相当于对事务进行了回滚,所以我们看到最终结果只有一行数据,也就是第一个事务执行之后的结果,即“关羽”。
|
||||
|
||||
那么如果我们进行下面的操作又会怎样呢?
|
||||
|
||||
```
|
||||
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '关羽';
|
||||
COMMIT;
|
||||
INSERT INTO test SELECT '张飞';
|
||||
INSERT INTO test SELECT '张飞';
|
||||
ROLLBACK;
|
||||
SELECT * FROM test;
|
||||
|
||||
```
|
||||
|
||||
运行结果(2行数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/df/f7/df437e577ce75363f5eb22dc362dbff7.png" alt=""><br>
|
||||
你能看到这次数据是2行,上一次操作我把两次插入“张飞”放到一个事务里,而这次操作它们不在同一个事务里,那么对于MySQL来说,默认情况下这实际上就是两个事务,因为在autocommit=1的情况下,MySQL会进行隐式事务,也就是自动提交,因此在进行第一次插入“张飞”后,数据表里就存在了两行数据,而第二次插入“张飞”就会报错:`1062 - Duplicate entry '张飞' for key 'PRIMARY'`。
|
||||
|
||||
最后我们在执行ROLLBACK的时候,实际上事务已经自动提交了,就没法进行回滚了。
|
||||
|
||||
同样的我们再来看下这段代码,你又能发现什么不同呢?
|
||||
|
||||
```
|
||||
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
|
||||
SET @@completion_type = 1;
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '关羽';
|
||||
COMMIT;
|
||||
INSERT INTO test SELECT '张飞';
|
||||
INSERT INTO test SELECT '张飞';
|
||||
ROLLBACK;
|
||||
SELECT * FROM test;
|
||||
|
||||
```
|
||||
|
||||
运行结果(1行数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/26/80/26d1f5a4a534eb9b1415ce867f006b80.png" alt="">
|
||||
|
||||
你能看到还是相同的SQL代码,只是我在事务开始之前设置了`SET @@completion_type = 1;`,结果就和我们第一次处理的一样,只有一个“关羽”。这是为什么呢?
|
||||
|
||||
这里我讲解下MySQL中completion_type参数的作用,实际上这个参数有3种可能:
|
||||
|
||||
1. completion=0,这是默认情况。也就是说当我们执行COMMIT的时候会提交事务,在执行下一个事务时,还需要我们使用START TRANSACTION或者BEGIN来开启。
|
||||
1. completion=1,这种情况下,当我们提交事务后,相当于执行了COMMIT AND CHAIN,也就是开启一个链式事务,即当我们提交事务之后会开启一个相同隔离级别的事务(隔离级别会在下一节中进行介绍)。
|
||||
1. completion=2,这种情况下COMMIT=COMMIT AND RELEASE,也就是当我们提交后,会自动与服务器断开连接。
|
||||
|
||||
在上面这段代码里,我使用了completion=1,也就是说当我提交之后,相当于在下一行写了一个START TRANSACTION或BEGIN。这时两次插入“张飞”会被认为是在同一个事务之内的操作,那么第二次插入“张飞”就会导致事务失败,而回滚也将这次事务进行了撤销,所以你能看到的结果就只有一个“关羽”。
|
||||
|
||||
通过这样简单的练习,你应该能体会到事务提交和回滚的操作。
|
||||
|
||||
当我们设置autocommit=0时,不论是否采用START TRANSACTION或者BEGIN的方式来开启事务,都需要用COMMIT进行提交,让事务生效,使用ROLLBACK对事务进行回滚。
|
||||
|
||||
当我们设置autocommit=1时,每条SQL语句都会自动进行提交。<br>
|
||||
不过这时,如果你采用START TRANSACTION或者BEGIN的方式来显式地开启事务,那么这个事务只有在COMMIT时才会生效,在ROLLBACK时才会回滚。
|
||||
|
||||
## 总结
|
||||
|
||||
关于SQL中的事务处理,内容相对比较多,因此我会采用两节来进行讲解。今天我们对事务的概念进行了理解,并进行了简单的事务操作。我们在做数据库操作的时候,可能会失败,但正是因为有事务的存在,即使在数据库操作失败的情况下,也能保证数据的一致性。同样,多个应用程序访问数据库的时候,事务可以提供隔离,保证事务之间不被干扰。最后,事务一旦提交,结果就会是永久性的,这就意味着,即使系统崩溃了,数据库也可以对数据进行恢复。
|
||||
|
||||
在使用事务的过程中,我们会采用控制流语句对事务进行操作,不过在实际操作中,不一定每次使用COMMIT或ROLLBACK都会成功,你还需要知道当前系统的事务执行方式,也就是一些常用的参数情况,比如MySQL中的autocommit和completion_type等。
|
||||
|
||||
事务是数据库区别于文件系统的重要特性之一,当我们有了事务就会让数据库始终保持一致性,同时我们还能通过事务的机制恢复到某个时间点,这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6d/ae/6d9e3a6db17b2a4f52120ae67dece3ae.png" alt=""><br>
|
||||
今天的内容到这里就结束了,你能说一下MySQL中都有哪些存储引擎支持事务,通过什么命令可以查看它们吗?另外,你是如何理解事务的特性的?
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起来交流。
|
||||
|
||||
|
||||
253
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/15丨初识事务隔离:隔离的级别有哪些,它们都解决了哪些异常问题?.md
Normal file
253
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/15丨初识事务隔离:隔离的级别有哪些,它们都解决了哪些异常问题?.md
Normal 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> BEGIN;
|
||||
SQL> INSERT INTO heros_temp values(4, '吕布');
|
||||
|
||||
```
|
||||
|
||||
当小张还没有提交该事务的时候,小李又对数据表进行了访问,他想看下这张英雄表里都有哪些英雄:
|
||||
|
||||
```
|
||||
SQL> SELECT * FROM heros_temp;
|
||||
|
||||
```
|
||||
|
||||
这时,小李看到的结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1c/a8/1c4c7b7d7223739eb5346e3159bb34a8.png" alt=""><br>
|
||||
你有没有发现什么异常?这个时候小张还没有提交事务,但是小李却读到了小张还没有提交的数据,这种现象我们称之为“脏读”。
|
||||
|
||||
那么什么是不可重复读呢?
|
||||
|
||||
第二天,小张想查看id=1的英雄是谁,于是他进行了SQL查询:
|
||||
|
||||
```
|
||||
SQL> BEGIN;
|
||||
SQL> SELECT name FROM heros_temp WHERE id = 1;
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e3/74/e31d7f286ac5d77a82fb6ece2d1d6174.png" alt=""><br>
|
||||
然而此时,小李开始了一个事务操作,他对id=1的英雄姓名进行了修改,把原来的“张飞”改成了“张翼德”:
|
||||
|
||||
```
|
||||
SQL> BEGIN;
|
||||
SQL> UPDATE heros_temp SET name = '张翼德' WHERE id = 1;
|
||||
|
||||
```
|
||||
|
||||
然后小张再一次进行查询,同样也是查看id=1的英雄是谁:
|
||||
|
||||
```
|
||||
SQL> SELECT name FROM heros_temp WHERE id = 1;
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e3/74/e3f6e70119376869f9a39d739c842674.png" alt=""><br>
|
||||
这个时候你会发现,两次查询的结果并不一样。小张会想这是怎么回事呢?他明明刚执行了一次查询,马上又进行了一次查询,结果两次的查询结果不同。实际上小张遇到的情况我们称之为“不可重复读”,也就是同一条记录,两次读取的结果不同。
|
||||
|
||||
从这个例子中,我们能看到小张和小李,分别开启了两个事务,针对客户端A和客户端B,我用时间顺序的方式展示下他们各自执行的内容:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c3/b8/c3361eaa98016638fc47af65ce12edb8.png" alt="">
|
||||
|
||||
那什么是幻读呢?
|
||||
|
||||
第三天,小张想要看下数据表里都有哪些英雄,他开始执行下面这条语句:
|
||||
|
||||
```
|
||||
SQL> SELECT * FROM heros_temp;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/21/41/213f1601abb1629689d6d8477bd16641.png" alt=""><br>
|
||||
这时当小张执行完之后,小李又开始了一个事务,往数据库里插入一个新的英雄“吕布”:
|
||||
|
||||
```
|
||||
SQL> BEGIN;
|
||||
SQL> INSERT INTO heros_temp values(4, '吕布');
|
||||
|
||||
```
|
||||
|
||||
不巧的是,小张这时忘记了英雄都有哪些,又重新执行了一遍查询:
|
||||
|
||||
```
|
||||
SQL> SELECT * FROM heros_temp;
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/13/ab/134a542eae8b47b9c5da5b74aa96d3ab.png" alt=""><br>
|
||||
他发现这一次查询多了一个英雄,原来只有3个,现在变成了4个。这种异常情况我们称之为“幻读”。
|
||||
|
||||
我来总结下这三种异常情况的特点:
|
||||
|
||||
1. 脏读:读到了其他事务还没有提交的数据。
|
||||
1. 不可重复读:对某数据进行读取,发现两次读取的结果不同,也就是说没有读到相同的内容。这是因为有其他事务对这个数据同时进行了修改或删除。
|
||||
1. 幻读:事务A根据条件查询得到了N条数据,但此时事务B更改或者增加了M条符合事务A查询条件的数据,这样当事务A再次进行查询的时候发现会有N+M条数据,产生了幻读。
|
||||
|
||||
## 事务隔离的级别有哪些?
|
||||
|
||||
脏读、不可重复读和幻读这三种异常情况,是在SQL-92标准中定义的,同时SQL-92标准还定义了4种隔离级别来解决这些异常情况。
|
||||
|
||||
解决异常数量从少到多的顺序(比如读未提交可能存在3种异常,可串行化则不会存在这些异常)决定了隔离级别的高低,这四种隔离级别从低到高分别是:读未提交(READ UNCOMMITTED )、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)和可串行化(SERIALIZABLE)。这些隔离级别能解决的异常情况如下表所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b0/95/b07103c5f5486aec5e2daf1dacfd6f95.png" alt=""><br>
|
||||
你能看到可串行化能避免所有的异常情况,而读未提交则允许异常情况发生。
|
||||
|
||||
关于这四种级别,我来简单讲解下。
|
||||
|
||||
读未提交,也就是允许读到未提交的数据,这种情况下查询是不会使用锁的,可能会产生脏读、不可重复读、幻读等情况。
|
||||
|
||||
读已提交就是只能读到已经提交的内容,可以避免脏读的产生,属于RDBMS中常见的默认隔离级别(比如说Oracle和SQL Server),但如果想要避免不可重复读或者幻读,就需要我们在SQL查询的时候编写带加锁的SQL语句(我会在进阶篇里讲加锁)。
|
||||
|
||||
可重复读,保证一个事务在相同查询条件下两次查询得到的数据结果是一致的,可以避免不可重复读和脏读,但无法避免幻读。MySQL默认的隔离级别就是可重复读。
|
||||
|
||||
可串行化,将事务进行串行化,也就是在一个队列中按照顺序执行,可串行化是最高级别的隔离等级,可以解决事务读取中所有可能出现的异常情况,但是它牺牲了系统的并发性。
|
||||
|
||||
## 使用MySQL客户端来模拟三种异常
|
||||
|
||||
我在讲解这三种异常的时候举了一个英雄数据表查询的例子,你还可以自己写SQL来模拟一下这三种异常。
|
||||
|
||||
首先我们需要一个英雄数据表heros_temp,具体表结构和数据,你可以从[GitHub](https://github.com/cystanford/sql_heros_data)上下载heros_temp.sql文件。
|
||||
|
||||
你也可以执行下面的SQL文件,来完成heros_temp数据表的创建。
|
||||
|
||||
```
|
||||
-- ----------------------------
|
||||
-- Table structure for heros_temp
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `heros_temp`;
|
||||
CREATE TABLE `heros_temp` (
|
||||
`id` int(11) NOT NULL,
|
||||
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of heros_temp
|
||||
-- ----------------------------
|
||||
INSERT INTO `heros_temp` VALUES (1, '张飞');
|
||||
INSERT INTO `heros_temp` VALUES (2, '关羽');
|
||||
INSERT INTO `heros_temp` VALUES (3, '刘备');
|
||||
|
||||
```
|
||||
|
||||
模拟的时候我们需要开两个MySQL客户端,分别是客户端1和客户端2。
|
||||
|
||||
在客户端1中,我们先来查看下当前会话的隔离级别,使用命令:
|
||||
|
||||
```
|
||||
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
|
||||
|
||||
```
|
||||
|
||||
然后你能看到当前的隔离级别是REPEATABLE-READ,也就是可重复读。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fa/5f/faaf3196f842d3331e40364fa331925f.png" alt="">
|
||||
|
||||
现在我们把隔离级别降到最低,设置为READ UNCOMMITTED(读未提交)。
|
||||
|
||||
```
|
||||
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
|
||||
|
||||
```
|
||||
|
||||
然后再查看下当前会话(SESSION)下的隔离级别,结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/25/33/25bc5c5e570315b9c711156cf87f4233.png" alt="">
|
||||
|
||||
因为MySQL默认是事务自动提交,这里我们还需要将autocommit参数设置为0,命令如下:
|
||||
|
||||
```
|
||||
mysql> SET autocommit = 0;
|
||||
|
||||
```
|
||||
|
||||
然后我们再来查看SESSION中的autocommit取值,结果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/e3/8c584071af1c37cb2ea7835ea94489e3.png" alt=""><br>
|
||||
接着我们以同样的操作启动客户端2,也就是将隔离级别设置为READ UNCOMMITTED(读未提交),autocommit设置为0。
|
||||
|
||||
### 模拟“脏读”
|
||||
|
||||
我们在客户端2中开启一个事务,在heros_temp表中写入一个新的英雄“吕布”,注意这个时候不要提交。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/23/bb/2329718eb5e446a2e2e1e42420818abb.png" alt=""><br>
|
||||
然后我们在客户端1中,查看当前的英雄表:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a7/34/a74922819952a7afc93311c8e3f85834.png" alt="">
|
||||
|
||||
你能发现客户端1中读取了客户端2未提交的新英雄“吕布”,实际上客户端2可能马上回滚,从而造成了“脏读”。
|
||||
|
||||
### 模拟“不可重复读”
|
||||
|
||||
我们用客户端1来查看id=1的英雄:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/39/17/39d6f01fbf926a83e28950bee720c917.png" alt="">
|
||||
|
||||
然后用客户端2对id=1的英雄姓名进行修改:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/43/a6/43e24b5a2c9d861d4fde7ddc3195cda6.png" alt="">
|
||||
|
||||
这时用客户端1再次进行查询:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/38/dd/38a638e9dcca39bc1cfb68279acdcbdd.png" alt="">
|
||||
|
||||
你能发现对于客户端1来说,同一条查询语句出现了“不可重复读”。
|
||||
|
||||
### 模拟“幻读”
|
||||
|
||||
我们先用客户端1查询数据表中的所有英雄:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cd/32/cd47f77c15be231ea0b185c265d4cd32.png" alt="">
|
||||
|
||||
然后用客户端2,开始插入新的英雄“吕布”:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/13/1d/13ffd32b3654ff569e0eef6dbb2de51d.png" alt=""><br>
|
||||
这时,我们再用客户端1重新进行查看:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c3/3e/c3de2074d616530cafc6942e4a592b3e.png" alt=""><br>
|
||||
你会发现数据表多出一条数据。
|
||||
|
||||
如果你是初学者,那么你可以采用heros_temp数据表简单模拟一下以上的过程,加深对脏读、不可重复读以及幻读的理解。对应的,你也会更了解不同的隔离级别解决的异常问题。
|
||||
|
||||
## 总结
|
||||
|
||||
我们今天只是简单讲解了4种隔离级别,以及对应的要解决的三种异常问题。我会在优化篇这一模块里继续讲解隔离级别以及锁的使用。
|
||||
|
||||
你能看到,标准的价值在于,即使是不同的RDBMS都需要达成对异常问题和隔离级别定义的共识。这就意味着一个隔离级别的实现满足了下面的两个条件:
|
||||
|
||||
1. 正确性:只要能满足某一个隔离级别,一定能解决这个隔离级别对应的异常问题。
|
||||
1. 与实现无关:实际上RDBMS种类很多,这就意味着有多少种RDBMS,就有多少种锁的实现方式,因此它们实现隔离级别的原理可能不同,然而一个好的标准不应该限制其实现的方式。
|
||||
|
||||
隔离级别越低,意味着系统吞吐量(并发程度)越大,但同时也意味着出现异常问题的可能性会更大。在实际使用过程中我们往往需要在性能和正确性上进行权衡和取舍,没有完美的解决方案,只有适合与否。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/aa/fb/aa2ae6682a571676b686509623a2a7fb.jpg" alt=""><br>
|
||||
今天的内容到这里就结束了,你能思考一下为什么隔离级别越高,就越影响系统的并发性能吗?以及不可重复读和幻读的区别是什么?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事。
|
||||
277
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/16丨游标:当我们需要逐条处理数据时,该怎么做?.md
Normal file
277
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/16丨游标:当我们需要逐条处理数据时,该怎么做?.md
Normal file
@@ -0,0 +1,277 @@
|
||||
<audio id="audio" title="16丨游标:当我们需要逐条处理数据时,该怎么做?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/da/a7/da00a389262c0d9738859a04b53045a7.mp3"></audio>
|
||||
|
||||
我们在编写SQL语句的时候通常是面向集合进行思考,这种思考方式更让我们关注结果集的特征,而不是具体的实现过程。面向集合的思考方式与面向过程的思考方式各有特点,我们该如何理解它们呢?
|
||||
|
||||
我们用下面这张图开启今天的学习。这张图中一共有9个图形,每个图形有不同的特征,包括形状、纹理、颜色和个数等。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3d/8b/3daf9a9168ac825e9e7943843175bb8b.jpg" alt=""><br>
|
||||
当我们看到这张图时,有时候会不由自主地按照某个属性进行分类,比如说按照红色分类,那么1、4、9就是一类。这实际上就是属于同一个条件下的查询结果集。或者我们也可以按照物体的个数来划分,比如都有3个物体的,那么对应的就是2、5、6、8,这就是对应着“都包括3个物体”的查询结果集。
|
||||
|
||||
你能看出来集合思维更像是从整体的角度来考虑,然后把整个数据集按照不同的属性进行划分,形成不同的子集合。面向集合的思考方式,让我们关注“获取什么”,而不是“如何获取”,这也可以说是SQL与传统编程最大的区别之一,因为SQL本身是以关系模型和集合论为基础的。
|
||||
|
||||
然而也有一些情况,我们不需要对查询结果集中的所有数据行都采用相同的处理方式,需要每次处理一行或者一部分行,这时就需要面向过程的编程方法了。游标就是这种编程方式的体现。如果你之前已经有了一些面向过程的编程经验,那么对于游标的理解也会比较容易。
|
||||
|
||||
关于游标,你需要掌握以下几个方面的内容:
|
||||
|
||||
1. 什么是游标?我们为什么要使用游标?
|
||||
1. 如何使用游标?使用游标的常用步骤都包括哪些?
|
||||
1. 如何使用游标来解决一些常见的问题?
|
||||
|
||||
## 什么是游标?
|
||||
|
||||
在数据库中,游标是个重要的概念,它提供了一种灵活的操作方式,可以让我们从数据结果集中每次提取一条数据记录进行操作。游标让SQL这种面向集合的语言有了面向过程开发的能力。可以说,游标是面向过程的编程方式,这与面向集合的编程方式有所不同。
|
||||
|
||||
在SQL中,游标是一种临时的数据库对象,可以指向存储在数据库表中的数据行指针。这里游标充当了指针的作用,我们可以通过操作游标来对数据行进行操作。
|
||||
|
||||
比如我们查询了heros数据表中最大生命值大于8500的英雄都有哪些:
|
||||
|
||||
```
|
||||
SELECT id, name, hp_max FROM heros WHERE hp_max > 8500
|
||||
|
||||
```
|
||||
|
||||
查询结果(4条数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/04/c0/046f997b7d1b2ce6a64e65b728cca4c0.jpg" alt="">
|
||||
|
||||
这里我们就可以通过游标来操作数据行,如图所示此时游标所在的行是“白起”的记录,我们也可以在结果集上滚动游标,指向结果集中的任意一行。
|
||||
|
||||
## 如何使用游标?
|
||||
|
||||
游标实际上是一种控制数据集的更加灵活的处理方式。
|
||||
|
||||
如果我们想要使用游标,一般需要经历五个步骤。不同DBMS中,使用游标的语法可能略有不同。
|
||||
|
||||
第一步,定义游标。
|
||||
|
||||
```
|
||||
DECLARE cursor_name CURSOR FOR select_statement
|
||||
|
||||
```
|
||||
|
||||
这个语法适用于MySQL,SQL Server,DB2和MariaDB。如果是用Oracle或者PostgreSQL,需要写成:
|
||||
|
||||
```
|
||||
DECLARE cursor_name CURSOR IS select_statement
|
||||
|
||||
```
|
||||
|
||||
要使用SELECT语句来获取数据结果集,而此时还没有开始遍历数据,这里select_statement代表的是SELECT语句。
|
||||
|
||||
下面我用MySQL举例讲解游标的使用,如果你使用的是其他的RDBMS,具体的游标语法可能略有差异。我们定义一个能够存储heros数据表中的最大生命值的游标,可以写为:
|
||||
|
||||
```
|
||||
DECLARE cur_hero CURSOR FOR
|
||||
SELECT hp_max FROM heros;
|
||||
|
||||
```
|
||||
|
||||
第二步,打开游标。
|
||||
|
||||
```
|
||||
OPEN cursor_name
|
||||
|
||||
```
|
||||
|
||||
当我们定义好游标之后,如果想要使用游标,必须先打开游标。打开游标的时候SELECT语句的查询结果集就会送到游标工作区。
|
||||
|
||||
第三步,从游标中取得数据。
|
||||
|
||||
```
|
||||
FETCH cursor_name INTO var_name ...
|
||||
|
||||
```
|
||||
|
||||
这句的作用是使用cursor_name这个游标来读取当前行,并且将数据保存到var_name这个变量中,游标指针指到下一行。如果游标读取的数据行有多个列名,则在INTO关键字后面赋值给多个变量名即可。
|
||||
|
||||
第四步,关闭游标。
|
||||
|
||||
```
|
||||
CLOSE cursor_name
|
||||
|
||||
```
|
||||
|
||||
有OPEN就会有CLOSE,也就是打开和关闭游标。当我们使用完游标后需要关闭掉该游标。关闭游标之后,我们就不能再检索查询结果中的数据行,如果需要检索只能再次打开游标。
|
||||
|
||||
最后一步,释放游标。
|
||||
|
||||
```
|
||||
DEALLOCATE cursor_namec
|
||||
|
||||
```
|
||||
|
||||
有DECLARE就需要有DEALLOCATE,DEALLOCATE的作用是释放游标。我们一定要养成释放游标的习惯,否则游标会一直存在于内存中,直到进程结束后才会自动释放。当你不需要使用游标的时候,释放游标可以减少资源浪费。
|
||||
|
||||
上面就是5个常用的游标步骤。我来举一个简单的例子,假设我想用游标来扫描heros数据表中的数据行,然后累计最大生命值,那么该怎么做呢?
|
||||
|
||||
我先创建一个存储过程calc_hp_max,然后在存储过程中定义游标cur_hero,使用FETCH获取每一行的具体数值,然后赋值给变量hp,再用变量hp_sum做累加求和,最后再输出hp_sum,代码如下:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `calc_hp_max`()
|
||||
BEGIN
|
||||
-- 创建接收游标的变量
|
||||
DECLARE hp INT;
|
||||
-- 创建总数变量
|
||||
DECLARE hp_sum INT DEFAULT 0;
|
||||
-- 创建结束标志变量
|
||||
DECLARE done INT DEFAULT false;
|
||||
-- 定义游标
|
||||
DECLARE cur_hero CURSOR FOR SELECT hp_max FROM heros;
|
||||
|
||||
OPEN cur_hero;
|
||||
read_loop:LOOP
|
||||
FETCH cur_hero INTO hp;
|
||||
SET hp_sum = hp_sum + hp;
|
||||
END LOOP;
|
||||
CLOSE cur_hero;
|
||||
SELECT hp_sum;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
你会发现执行`call calc_hp_max()`这一句的时候系统会提示1329错误,也就是在LOOP中当游标没有取到数据时会报的错误。
|
||||
|
||||
当游标溢出时(也就是当游标指向到最后一行数据后继续执行会报的错误),我们可以定义一个continue的事件,指定这个事件发生时修改变量done的值,以此来判断游标是否已经溢出,即:
|
||||
|
||||
```
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
|
||||
|
||||
```
|
||||
|
||||
同时在循环中我们需要加上对done的判断,如果游标的循环已经结束,就需要跳出read_loop循环,完善的代码如下:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `calc_hp_max`()
|
||||
BEGIN
|
||||
-- 创建接收游标的变量
|
||||
DECLARE hp INT;
|
||||
|
||||
-- 创建总数变量
|
||||
DECLARE hp_sum INT DEFAULT 0;
|
||||
-- 创建结束标志变量
|
||||
DECLARE done INT DEFAULT false;
|
||||
-- 定义游标
|
||||
DECLARE cur_hero CURSOR FOR SELECT hp_max FROM heros;
|
||||
-- 指定游标循环结束时的返回值
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
|
||||
|
||||
OPEN cur_hero;
|
||||
read_loop:LOOP
|
||||
FETCH cur_hero INTO hp;
|
||||
-- 判断游标的循环是否结束
|
||||
IF done THEN
|
||||
LEAVE read_loop;
|
||||
END IF;
|
||||
|
||||
SET hp_sum = hp_sum + hp;
|
||||
END LOOP;
|
||||
CLOSE cur_hero;
|
||||
SELECT hp_sum;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
运行结果(1行数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/25/74/250fa19076fb9e55f801847815eb8674.png" alt="">
|
||||
|
||||
在游标中的循环中,除了使用LOOP循环以外,你还可以使用REPEAT… UNTIL…以及WHILE循环。它们同样需要设置CONTINUE事件来处理游标溢出的情况。
|
||||
|
||||
所以你能看出,使用游标可以让我们对SELECT结果集中的每一行数据进行相同或者不同的操作,从而很精细化地管理结果集中的每一条数据。
|
||||
|
||||
**使用游标来解决一些常见的问题**
|
||||
|
||||
我刚才讲了一个简单的使用案例,实际上如果想要统计hp_sum,完全可以通过SQL语句来完成,比如:
|
||||
|
||||
```
|
||||
SELECT SUM(hp_max) FROM heros
|
||||
|
||||
```
|
||||
|
||||
运行结果(1行数据):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3b/9f/3b26582a9d86399c7b8c240e82369f9f.png" alt=""><br>
|
||||
那么游标都有什么用呢?
|
||||
|
||||
当你需要处理一些复杂的数据行计算的时候,游标就会起到作用了。我举个例子,还是针对heros数据表,假设我们想要对英雄的物攻成长(对应attack_growth)进行升级,在新版本中大范围提升英雄的物攻成长数值,但是针对不同的英雄情况,提升的幅度也不同,具体提升的方式如下。
|
||||
|
||||
如果这个英雄原有的物攻成长小于5,那么将在原有基础上提升7%-10%。如果物攻成长的提升空间(即最高物攻attack_max-初始物攻attack_start)大于200,那么在原有的基础上提升10%;如果物攻成长的提升空间在150到200之间,则提升8%;如果物攻成长的提升空间不足150,则提升7%。
|
||||
|
||||
如果原有英雄的物攻成长在5—10之间,那么将在原有基础上提升5%。
|
||||
|
||||
如果原有英雄的物攻成长大于10,则保持不变。
|
||||
|
||||
以上所有的更新后的物攻成长数值,都需要保留小数点后3位。
|
||||
|
||||
你能看到上面这个计算的情况相对复杂,实际工作中你可能会遇到比这个更加复杂的情况,这时你可以采用面向过程的思考方式来完成这种任务,也就是说先取出每行的数值,然后针对数值的不同情况采取不同的计算方式。
|
||||
|
||||
针对上面这个情况,你自己可以用游标来完成转换,具体的代码如下:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `alter_attack_growth`()
|
||||
BEGIN
|
||||
-- 创建接收游标的变量
|
||||
DECLARE temp_id INT;
|
||||
DECLARE temp_growth, temp_max, temp_start, temp_diff FLOAT;
|
||||
|
||||
-- 创建结束标志变量
|
||||
DECLARE done INT DEFAULT false;
|
||||
-- 定义游标
|
||||
DECLARE cur_hero CURSOR FOR SELECT id, attack_growth, attack_max, attack_start FROM heros;
|
||||
-- 指定游标循环结束时的返回值
|
||||
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
|
||||
|
||||
OPEN cur_hero;
|
||||
FETCH cur_hero INTO temp_id, temp_growth, temp_max, temp_start;
|
||||
REPEAT
|
||||
IF NOT done THEN
|
||||
SET temp_diff = temp_max - temp_start;
|
||||
IF temp_growth < 5 THEN
|
||||
IF temp_diff > 200 THEN
|
||||
SET temp_growth = temp_growth * 1.1;
|
||||
ELSEIF temp_diff >= 150 AND temp_diff <=200 THEN
|
||||
SET temp_growth = temp_growth * 1.08;
|
||||
ELSEIF temp_diff < 150 THEN
|
||||
SET temp_growth = temp_growth * 1.07;
|
||||
END IF;
|
||||
ELSEIF temp_growth >=5 AND temp_growth <=10 THEN
|
||||
SET temp_growth = temp_growth * 1.05;
|
||||
END IF;
|
||||
UPDATE heros SET attack_growth = ROUND(temp_growth,3) WHERE id = temp_id;
|
||||
END IF;
|
||||
FETCH cur_hero INTO temp_id, temp_growth, temp_max, temp_start;
|
||||
UNTIL done = true END REPEAT;
|
||||
|
||||
CLOSE cur_hero;
|
||||
END
|
||||
|
||||
```
|
||||
|
||||
这里我创建了alter_attack_growth这个存储过程,使用了REPEAT…UNTIL…的循环方式,针对不同的情况计算了新的物攻成长temp_growth,然后对原有的attack_growth进行了更新,最后调用call alter_attack_growth();执行存储过程。
|
||||
|
||||
有一点需要注意的是,我们在对数据表进行更新前,需要备份之前的表,我们可以将备份后的表命名为heros_copy1。更新完heros数据表之后,你可以看下两张表在attack_growth字段上的对比,我们使用SQL进行查询:
|
||||
|
||||
```
|
||||
SELECT heros.id, heros.attack_growth, heros_copy1.attack_growth FROM heros JOIN heros_copy1 WHERE heros.id = heros_copy1.id
|
||||
|
||||
```
|
||||
|
||||
运行结果(69条记录):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/62/c9/622c50afb8ff6be7d9682fe3537dedc9.png" alt=""><br>
|
||||
通过前后两张表的attack_growth对比你也能看出来,存储过程通过游标对不同的数据行进行了更新。
|
||||
|
||||
需要说明的是,以上代码适用于MySQL,如果在SQL Server或Oracle中,使用方式会有些差别。
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们讲解了如何在SQL中使用游标,游标实际上是面向过程的思维方式,与面向集合的思维方式不同的地方在于,游标更加关注“如何执行”。我们可以通过游标更加精细、灵活地查询和管理想要的数据行。
|
||||
|
||||
有的时候,我们需要找特定数据,用SQL查询写起来会比较困难,比如两表或多表之间的嵌套循环查找,如果用JOIN会非常消耗资源,效率也可能不高,而用游标则会比较高效。
|
||||
|
||||
虽然在处理某些复杂的数据情况下,使用游标可以更灵活,但同时也会带来一些性能问题,比如在使用游标的过程中,会对数据行进行加锁,这样在业务并发量大的时候,不仅会影响业务之间的效率,还会消耗系统资源,造成内存不足,这是因为游标是在内存中进行的处理。如果有游标的替代方案,我们可以采用替代方案。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dc/11/dca1fadf6625b9699c25104e74fb8d11.jpg" alt=""><br>
|
||||
我们今天讲解了游标,你能用自己的语言介绍下游标的作用吗?另外,我们之前提到过,SQL本身是一门结构化查询语言,但我们也可以在SQL的基础上进行面向过程的开发,完成较为复杂的功能,你能说一下面向过程和面向集合这两种编程方式的区别吗?
|
||||
|
||||
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
216
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/17丨如何使用Python操作MySQL?.md
Normal file
216
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/17丨如何使用Python操作MySQL?.md
Normal file
@@ -0,0 +1,216 @@
|
||||
<audio id="audio" title="17丨如何使用Python操作MySQL?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c1/93/c1ccf5a94d7e547e397069fb5c7f5293.mp3"></audio>
|
||||
|
||||
我们之前都是直接在DBMS里面进行SQL的操作,实际上我们还可以通过后端语言对DBMS进行访问以及进行相应的操作,这样更具有灵活性,可以实现一些较为复杂的操作。作为一个后端开发人员,掌握一些SQL技术是必须的;作为一个数据库管理人员,了解后端语言如何开发和管理数据库也是很有必要的。
|
||||
|
||||
今天我以Python为例,讲解下如何对MySQL数据库进行操作。你需要掌握以下几个方面的内容:
|
||||
|
||||
1. Python的DB API规范是什么,遵守这个规范有什么用?
|
||||
1. 基于DB API,MySQL官方提供了驱动器mysql-connector,如何使用它来完成对数据库管理系统的操作?
|
||||
1. CRUD是最常见的数据库的操作,分别对应数据的增加、读取、修改和删除。在掌握了mysql-connector的使用方法之后,如何完成对数据表的CRUD操作?
|
||||
|
||||
## Python DB API规范
|
||||
|
||||
Python可以支持非常多的数据库管理系统,比如MySQL、Oracle、SQL Server和PostgreSQL等。为了实现对这些DBMS的统一访问,Python需要遵守一个规范,这就是DB API规范。我在下图中列出了DB API规范的作用,这个规范给我们提供了数据库对象连接、对象交互和异常处理的方式,为各种DBMS提供了统一的访问接口。这样做的好处就是如果项目需要切换数据库,Python层的代码移植会比较简单。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/7f/5d8113fc1637d1fe951e985b22e0287f.png" alt=""><br>
|
||||
我们在使用Python对DBMS进行操作的时候,需要经过下面的几个步骤:
|
||||
|
||||
1. 引入API模块;
|
||||
1. 与数据库建立连接;
|
||||
1. 执行SQL语句;
|
||||
1. 关闭数据库连接。
|
||||
|
||||
## 如何使用mysql-connector
|
||||
|
||||
使用Python对数据库进行访问需要基于DB API规范,这里有不少库供我们选择,比如MySQLdb、mysqlclient、PyMySQL、peewee和SQLAIchemy等。今天我讲解的是mysql-connector,它是MySQL 官方提供的驱动器,用来给后端语言,比如Python提供连接。
|
||||
|
||||
下面我们看下如何用Python使用mysql-connector,以完成数据库的连接和使用。
|
||||
|
||||
首先安装mysql-connector。在使用前,你需要先使用下面这句命令进行安装:
|
||||
|
||||
```
|
||||
pip install mysql-connector
|
||||
|
||||
```
|
||||
|
||||
在安装之后,你可以创建数据库连接,然后查看下数据库的版本号,来验证下数据库是否连接成功。代码如下:
|
||||
|
||||
```
|
||||
# -*- coding: UTF-8 -*-
|
||||
import mysql.connector
|
||||
# 打开数据库连接
|
||||
db = mysql.connector.connect(
|
||||
host="localhost",
|
||||
user="root",
|
||||
passwd="XXX", # 写上你的数据库密码
|
||||
database='wucai',
|
||||
auth_plugin='mysql_native_password'
|
||||
)
|
||||
# 获取操作游标
|
||||
cursor = db.cursor()
|
||||
# 执行SQL语句
|
||||
cursor.execute("SELECT VERSION()")
|
||||
# 获取一条数据
|
||||
data = cursor.fetchone()
|
||||
print("MySQL版本: %s " % data)
|
||||
# 关闭游标&数据库连接
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
MySQL版本: 8.0.13
|
||||
|
||||
```
|
||||
|
||||
上面这段代码中有两个重要的对象你需要了解下,分别是Connection和Cursor。
|
||||
|
||||
Connection就是对数据库的当前连接进行管理,我们可以通过它来进行以下操作:
|
||||
|
||||
1. 通过指定host、user、passwd和port等参数来创建数据库连接,这些参数分别对应着数据库IP地址、用户名、密码和端口号;
|
||||
1. 使用db.close()关闭数据库连接;
|
||||
1. 使用db.cursor()创建游标,操作数据库中的数据;
|
||||
1. 使用db.begin()开启事务;
|
||||
1. 使用db.commit()和db.rollback(),对事务进行提交以及回滚。
|
||||
|
||||
当我们通过`cursor = db.cursor()`创建游标后,就可以通过面向过程的编程方式对数据库中的数据进行操作:
|
||||
|
||||
1. 使用`cursor.execute(query_sql)`,执行数据库查询;
|
||||
1. 使用`cursor.fetchone()`,读取数据集中的一条数据;
|
||||
1. 使用`cursor.fetchall()`,取出数据集中的所有行,返回一个元组tuples类型;
|
||||
1. 使用`cursor.fetchmany(n)`,取出数据集中的多条数据,同样返回一个元组tuples;
|
||||
1. 使用`cursor.rowcount`,返回查询结果集中的行数。如果没有查询到数据或者还没有查询,则结果为-1,否则会返回查询得到的数据行数;
|
||||
1. 使用`cursor.close()`,关闭游标。
|
||||
|
||||
## 对数据表进行增删改查
|
||||
|
||||
了解了Connection和Cursor的使用方式之后,我们来看下如何来对heros数据表进行CRUD的操作,即增加、读取、更新和删除。
|
||||
|
||||
首先是增加数据。
|
||||
|
||||
假设我们想在player表中增加一名新球员,姓名为“约翰·科林斯”,球队ID为1003(即亚特兰大老鹰),身高为2.08m。代码如下:
|
||||
|
||||
```
|
||||
# 插入新球员
|
||||
sql = "INSERT INTO player (team_id, player_name, height) VALUES (%s, %s, %s)"
|
||||
val = (1003, "约翰-科林斯", 2.08)
|
||||
cursor.execute(sql, val)
|
||||
db.commit()
|
||||
print(cursor.rowcount, "记录插入成功。")
|
||||
|
||||
```
|
||||
|
||||
我们使用cursor.execute来执行相应的SQL语句,val为SQL语句中的参数,SQL执行后使用db.commit()进行提交。需要说明的是,我们在使用SQL语句的时候,可以向SQL语句传递参数,这时SQL语句里要统一用(%s)进行占位,否则就会报错。不论插入的数值为整数类型,还是浮点类型,都需要统一用(%s)进行占位。
|
||||
|
||||
另外在用游标进行SQL操作之后,还需要使用db.commit()进行提交,否则数据不会被插入。
|
||||
|
||||
然后是读取数据。我们来看下数据是否被插入成功,这里我们查询下身高大于等于2.08m的球员都有哪些,代码如下:
|
||||
|
||||
```
|
||||
# 查询身高大于等于2.08的球员
|
||||
sql = 'SELECT player_id, player_name, height FROM player WHERE height>=2.08'
|
||||
cursor.execute(sql)
|
||||
data = cursor.fetchall()
|
||||
for each_player in data:
|
||||
print(each_player)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
(10003, '安德烈-德拉蒙德', 2.11)
|
||||
(10004, '索恩-马克', 2.16)
|
||||
(10009, '扎扎-帕楚里亚', 2.11)
|
||||
(10010, '乔恩-洛伊尔', 2.08)
|
||||
(10011, '布雷克-格里芬', 2.08)
|
||||
(10015, '亨利-埃伦森', 2.11)
|
||||
(10023, '多曼塔斯-萨博尼斯', 2.11)
|
||||
(10024, '迈尔斯-特纳', 2.11)
|
||||
(10032, 'TJ-利夫', 2.08)
|
||||
(10033, '凯尔-奥奎因', 2.08)
|
||||
(10037, '伊凯·阿尼博古', 2.08)
|
||||
(10038, '约翰-科林斯', 2.08)
|
||||
|
||||
```
|
||||
|
||||
你能看到球员约翰·科林斯被正确插入。
|
||||
|
||||
那么如何修改数据呢?
|
||||
|
||||
假如我想修改刚才插入的球员约翰·科林斯的身高,将身高修改成2.09,代码如下:
|
||||
|
||||
```
|
||||
# 修改球员约翰-科林斯
|
||||
sql = 'UPDATE player SET height = %s WHERE player_name = %s'
|
||||
val = (2.09, "约翰-科林斯")
|
||||
cursor.execute(sql, val)
|
||||
db.commit()
|
||||
print(cursor.rowcount, "记录被修改。")
|
||||
|
||||
```
|
||||
|
||||
最后我们看下如何删除约翰·科林斯这个球员的数据,代码如下:
|
||||
|
||||
```
|
||||
sql = 'DELETE FROM player WHERE player_name = %s'
|
||||
val = ("约翰-科林斯",)
|
||||
cursor.execute(sql, val)
|
||||
db.commit()
|
||||
print(cursor.rowcount, "记录删除成功。")
|
||||
|
||||
```
|
||||
|
||||
最后都执行完了,我们来关闭游标和数据库的连接,使用以下代码即可:
|
||||
|
||||
```
|
||||
cursor.close()
|
||||
db.close()
|
||||
|
||||
```
|
||||
|
||||
针对上面的操作过程,你可以模拟下数据的CRUD操作,但有几点你需要注意。
|
||||
|
||||
1.打开数据库连接以后,如果不再使用,则需要关闭数据库连接,以免造成资源浪费。<br>
|
||||
2.在对数据进行增加、删除和修改的时候,可能会出现异常,这时就需要用`try...except`捕获异常信息。比如针对插入球员约翰·科林斯这个操作,你可以写成下面这样:
|
||||
|
||||
```
|
||||
import traceback
|
||||
try:
|
||||
sql = "INSERT INTO player (team_id, player_name, height) VALUES (%s, %s, %s)"
|
||||
val = (1003, "约翰-科林斯", 2.08)
|
||||
cursor.execute(sql, val)
|
||||
db.commit()
|
||||
print(cursor.rowcount, "记录插入成功。")
|
||||
except Exception as e:
|
||||
# 打印异常信息
|
||||
traceback.print_exc()
|
||||
# 回滚
|
||||
db.rollback()
|
||||
finally:
|
||||
# 关闭数据库连接
|
||||
db.close()
|
||||
|
||||
```
|
||||
|
||||
运行结果告诉我们记录插入成功。
|
||||
|
||||
3.如果你在使用mysql-connector连接的时候,系统报的错误为`authentication plugin caching_sha2`,这时你需要下载最新的版本更新来解决,点击[这里](https://dev.mysql.com/downloads/connector/python/)进行更新。
|
||||
|
||||
## 总结
|
||||
|
||||
我今天讲解了如何使用Python来操作MySQL,这里我们使用的是官方提供的mysql-connector,当然除了它之外,还有很多库可以进行选择。
|
||||
|
||||
在使用基于DB API规范的协议时,重点需要掌握Connection和Cursor这两个对象,Connection就是对数据库的连接进行管理,而Cursor是对数据库的游标进行管理,通过它们,我们可以执行具体的SQL语句,以及处理复杂的数据。
|
||||
|
||||
用Python操作MySQL,还有很多种姿势,mysql-connector只是其中一种,实际上还有另外一种方式,就是采用ORM框架。ORM的英文是Object Relational Mapping,也就是采用对象关系映射的模式,使用这种模式可以将数据库中各种数据表之间的关系映射到程序中的对象。这种模式可以屏蔽底层的数据库的细节,不需要我们与复杂的SQL语句打交道,直接采用操作对象的形式操作就可以。
|
||||
|
||||
不过如果应用数据实体少,其实没有必要使用ORM框架,针对少量对象的管理,自己实现起来也很简单,比如本篇文章中我讲到的采用官方提供的mysql-connector驱动的方式来实现CRUD。引入一个框架的学习成本很高,代码膨胀也很厉害,所以如果是相对简单的操作,完全可以自己动手来实现。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/96/d2/96cf0463992b9843ba02bb7f65cb4ed2.jpg" alt=""><br>
|
||||
使用Python对数据库进行操作,关键在于实战,所以这里我出一个练习题。请你使用Python对heros表中最大生命值大于6000的英雄进行查询,并且输出相应的属性值。
|
||||
|
||||
欢迎在评论区写下你的答案,我会与你一起交流。也欢迎把这篇文章分享给你的朋友或者同事,与它们一起交流一下。
|
||||
@@ -0,0 +1,217 @@
|
||||
<audio id="audio" title="18丨SQLAlchemy:如何使用Python ORM框架来操作MySQL?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/70/47/700dd3410ddbfc57f76a90a305d7f147.mp3"></audio>
|
||||
|
||||
上节课,我介绍了Python DB API规范的作用,以及如何使用MySQL官方的mysql-connector驱动来完成数据库的连接和使用。在项目比较小的时候,我们可以直接使用SQL语句,通过mysql-connector完成与MySQL的交互,但是任何事物都有两面性,随着项目规模的增加,代码会越来越复杂,维护的成本也越来越高,这时mysql-connector就不够用了,我们需要更好的设计模式。
|
||||
|
||||
Python还有另一种方式可以与MySQL进行交互,这种方式采用的是ORM框架。我们今天就来讲解如何使用ORM框架操作MySQL,那么今天的课程你需要掌握以下几个方面的内容:
|
||||
|
||||
1. 什么是ORM框架,以及为什么要使用ORM框架?
|
||||
1. Python中的ORM框架都有哪些?
|
||||
1. 如何使用SQLAlchemy来完成与MySQL的交互?
|
||||
|
||||
## 我们为什么要使用ORM框架?
|
||||
|
||||
在讲解ORM框架之前,我们需要先了解什么是持久化。如下图所示,持久化层在业务逻辑层和数据库层起到了衔接的作用,它可以将内存中的数据模型转化为存储模型,或者将存储模型转化为内存中的数据模型。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b9/5b/b9dafd636ec586704bb8488d9b2faa5b.jpg" alt="">
|
||||
|
||||
你可能会想到,我们在讲事务的4大特性ACID时,提到过持久性。你可以简单地理解为,持久性就是将对象数据永久存储在数据库中。通常我们将数据库的作用理解为永久存储,将内存理解为暂时存储。我们在程序的层面操作数据,其实都是把数据放到内存中进行处理,如果需要数据就会通过持久化层,从数据库中取数据;如果需要保存数据,就是将对象数据通过持久化层存储到数据库中。
|
||||
|
||||
那么ORM解决的是什么问题呢?它提供了一种持久化模式,可以高效地对数据库进行访问。ORM的英文是Object Relation Mapping,中文叫对象关系映射。它是RDBMS和业务实体对象之间的一个映射,从图中你也能看到,它可以把底层的RDBMS封装成业务实体对象,提供给业务逻辑层使用。程序员往往关注业务逻辑层面,而不是底层数据库该如何访问,以及如何编写SQL语句获取数据等等。采用ORM,就可以从数据库的设计层面转化成面向对象的思维。
|
||||
|
||||
我在开篇的时候提到过,随着项目规模的增大,在代码层编写SQL语句访问数据库会降低开发效率,也会提升维护成本,因此越来越多的开发人员会采用基于ORM的方式来操作数据库。这样做的好处就是一旦定义好了对象模型,就可以让它们简单可复用,从而不必关注底层的数据库访问细节,我们只要将注意力集中到业务逻辑层面就可以了。由此还可以带来另一点好处,那就是即便数据库本身进行了更换,在业务逻辑代码上也不会有大的调整。这是因为ORM抽象了数据的存取,同时也兼容多种DBMS,我们不用关心底层采用的到底是哪种DBMS,是MySQL,SQL Server,PostgreSQL还是SQLite。
|
||||
|
||||
但没有一种模式是完美的,采用ORM当然也会付出一些代价,比如性能上的一些损失。面对一些复杂的数据查询,ORM会显得力不从心。虽然可以实现功能,但相比于直接编写SQL查询语句来说,ORM需要编写的代码量和花费的时间会比较多,这种情况下,直接编写SQL反而会更简单有效。
|
||||
|
||||
其实你也能看出来,没有一种方式是一劳永逸的,在实际工作中,我们需要根据需求选择适合的方式。
|
||||
|
||||
## Python中的ORM框架都有哪些
|
||||
|
||||
ORM框架帮我们适配了各种DBMS,同时我们也可以选择不同的ORM框架。如果你用Python的话,有三种主流的ORM框架。
|
||||
|
||||
第一个是Django,它是Python的WEB应用开发框架,本身走大而全的方式。Django采用了MTV的框架模式,包括了Model(模型),View(视图)和Template(模版)。Model模型只是Django的一部分功能,我们可以通过它来实现数据库的增删改查操作。
|
||||
|
||||
一个Model映射到一个数据表,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d9/95/d9ea5e64198554061c729ffb97561795.jpg" alt=""><br>
|
||||
从这张图上你能直观地看到,ORM的作用就是建立了对象关系映射。模型的每个属性代表数据表中的一个字段,我们通过操作类实例对象,对数据表中的数据行进行增删改查等操作。
|
||||
|
||||
第二个是SQLALchemy,它也是Python中常用的ORM框架之一。它提供了SQL工具包及ORM工具,如果你想用支持ORM和支持原生SQL两种方式的工具,那么SQLALchemy是很好的选择。另外SQLALchemy的社区更加活跃,这对项目实施会很有帮助。
|
||||
|
||||
第三个是peewee,这是一个轻量级的ORM框架,简单易用。peewee采用了Model类、Field实例和Model实例来与数据库建立映射关系,从而完成面向对象的管理方式。使用起来方便,学习成本也低。
|
||||
|
||||
## 如何使用SQLAlchemy来操作MySQL
|
||||
|
||||
下面我们来看下如何使用SQLAlchemy工具对player数据表进行增删改查,在使用前,你需要先安装相应的工具包:
|
||||
|
||||
```
|
||||
pip install sqlalchemy
|
||||
初始化数据库连接
|
||||
from sqlalchemy import create_engine
|
||||
# 初始化数据库连接,修改为你的数据库用户名和密码
|
||||
engine = create_engine('mysql+mysqlconnector://root:password@localhost:3306/wucai')
|
||||
|
||||
```
|
||||
|
||||
create_engine的使用方法类似我们在上篇文章中提到的mysql.connector,都需要提供数据库+数据库连接框架,即对应的是`mysql+mysqlconnector`,后面的是用户名:`密码@IP地址:端口号/数据库名称`。
|
||||
|
||||
### 创建模型
|
||||
|
||||
我们已经创建了player数据表,这里需要创建相应的player模型。
|
||||
|
||||
```
|
||||
# 定义Player对象:
|
||||
class Player(Base):
|
||||
# 表的名字:
|
||||
__tablename__ = 'player'
|
||||
|
||||
# 表的结构:
|
||||
player_id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
team_id = Column(Integer)
|
||||
player_name = Column(String(255))
|
||||
height = Column(Float(3,2))
|
||||
|
||||
```
|
||||
|
||||
这里需要说明的是,`__tablename__` 指明了模型对应的数据表名称,即player数据表。同时我们在Player模型中对采用的变量名进行定义,变量名需要和数据表中的字段名称保持一致,否则会找不到数据表中的字段。在SQLAlchemy中,我们采用Column对字段进行定义,常用的数据类型如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d6/42/d6f02460647f34fba692e8a61b80a042.png" alt=""><br>
|
||||
除了指定Column的数据类型以外,我们也可以指定Column的参数,这些参数可以帮我们对对象创建列约束:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/45/dd/458d77c980f2ac7b9e8e34dd75eac8dd.png" alt=""><br>
|
||||
这里需要说明的是,如果你使用相应的数据类型,那么需要提前在SQLAlchemy中进行引用,比如:
|
||||
|
||||
```
|
||||
from sqlalchemy import Column, String, Integer, Float
|
||||
|
||||
```
|
||||
|
||||
### 对数据表进行增删改查
|
||||
|
||||
假设我们想给player表增加一名新球员,姓名为“约翰·科林斯”,球队ID为1003(即亚特兰大老鹰),身高为2.08。代码如下:
|
||||
|
||||
```
|
||||
# 创建DBSession类型:
|
||||
DBSession = sessionmaker(bind=engine)
|
||||
# 创建session对象:
|
||||
session = DBSession()
|
||||
|
||||
|
||||
# 创建Player对象:
|
||||
new_player = Player(team_id = 1003, player_name = "约翰-科林斯", height = 2.08)
|
||||
# 添加到session:
|
||||
session.add(new_player)
|
||||
# 提交即保存到数据库:
|
||||
session.commit()
|
||||
# 关闭session:
|
||||
session.close()
|
||||
|
||||
```
|
||||
|
||||
这里,我们首先需要初始化DBSession,相当于创建一个数据库的会话实例session。通过session来完成新球员的添加。对于新球员的数据,我们可以通过Player类来完成创建,在参数中指定相应的`team_id, player_name, height`即可。
|
||||
|
||||
然后把创建好的对象new_player添加到session中,提交到数据库即可完成添加数据的操作。
|
||||
|
||||
接着,我们来看一下如何查询数据。
|
||||
|
||||
添加完插入的新球员之后,我们可以查询下身高 ≥ 2.08m的球员都有哪些,代码如下:
|
||||
|
||||
```
|
||||
#增加to_dict()方法到Base类中
|
||||
def to_dict(self):
|
||||
return {c.name: getattr(self, c.name, None)
|
||||
for c in self.__table__.columns}
|
||||
#将对象可以转化为dict类型
|
||||
Base.to_dict = to_dict
|
||||
# 查询身高>=2.08的球员有哪些
|
||||
rows = session.query(Player).filter(Player.height >= 2.08).all()
|
||||
print([row.to_dict() for row in rows])
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
[{'player_id': 10003, 'team_id': 1001, 'player_name': '安德烈-德拉蒙德', 'height': Decimal('2.1100000000')}, {'player_id': 10004, 'team_id': 1001, 'player_name': '索恩-马克', 'height': Decimal('2.1600000000')}, {'player_id': 10009, 'team_id': 1001, 'player_name': '扎扎-帕楚里亚', 'height': Decimal('2.1100000000')}, {'player_id': 10010, 'team_id': 1001, 'player_name': '乔恩-洛伊尔', 'height': Decimal('2.0800000000')}, {'player_id': 10011, 'team_id': 1001, 'player_name': '布雷克-格里芬', 'height': Decimal('2.0800000000')}, {'player_id': 10015, 'team_id': 1001, 'player_name': '亨利-埃伦森', 'height': Decimal('2.1100000000')}, {'player_id': 10023, 'team_id': 1002, 'player_name': '多曼塔斯-萨博尼斯', 'height': Decimal('2.1100000000')}, {'player_id': 10024, 'team_id': 1002, 'player_name': '迈尔斯-特纳', 'height': Decimal('2.1100000000')}, {'player_id': 10032, 'team_id': 1002, 'player_name': 'TJ-利夫', 'height': Decimal('2.0800000000')}, {'player_id': 10033, 'team_id': 1002, 'player_name': '凯尔-奥奎因', 'height': Decimal('2.0800000000')}, {'player_id': 10037, 'team_id': 1002, 'player_name': '伊凯·阿尼博古', 'height': Decimal('2.0800000000')}, {'player_id': 10038, 'team_id': 1003, 'player_name': '约翰-科林斯', 'height': Decimal('2.0800000000')}]
|
||||
|
||||
```
|
||||
|
||||
如果我们对整个数据行进行查询,采用的是`session.query(Player)`,相当于使用的是SELECT *。这时如果我们想要在Python中对query结果进行打印,可以对Base类增加`to_dict()`方法,相当于将对象转化成了Python的字典类型。
|
||||
|
||||
在进行查询的时候,我们使用的是filter方法,对应的是SQL中的WHERE条件查询。除此之外,filter也支持多条件查询。
|
||||
|
||||
如果是AND的关系,比如我们想要查询身高 ≥ 2.08,同时身高 ≤ 2.10的球员,可以写成下面这样:
|
||||
|
||||
```
|
||||
rows = session.query(Player).filter(Player.height >=2.08, Player.height <=2.10).all()
|
||||
|
||||
```
|
||||
|
||||
如果是OR的关系,比如我们想要查询身高 ≥ 2.08,或者身高 ≤ 2.10的球员,可以写成这样:
|
||||
|
||||
```
|
||||
rows = session.query(Player).filter(or_(Player.height >=2.08, Player.height <=2.10)).all()
|
||||
|
||||
```
|
||||
|
||||
这里我们使用了SQLAlchemy的or_操作符,在使用它之前你需要进行引入,即:`from sqlalchemy import or_`。
|
||||
|
||||
除了多条件查询,SQLAlchemy也同样支持分组操作、排序和返回指定数量的结果。
|
||||
|
||||
比如我想要按照team_id进行分组,同时筛选分组后数据行数大于5的分组,并且按照分组后数据行数递增的顺序进行排序,显示team_id字段,以及每个分组的数据行数。那么代码如下:
|
||||
|
||||
```
|
||||
from sqlalchemy import func
|
||||
rows = session.query(Player.team_id, func.count(Player.player_id)).group_by(Player.team_id).having(func.count(Player.player_id)>5).order_by(func.count(Player.player_id).asc()).all()
|
||||
print(rows)
|
||||
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
```
|
||||
[(1001, 20), (1002, 17)]
|
||||
|
||||
```
|
||||
|
||||
这里有几点需要注意:
|
||||
|
||||
1. 我们把需要显示的字段Player.team_id, func.count(Player.player_id)作为query的参数,其中我们需要用到sqlalchemy的func类,它提供了各种聚集函数,比如func.count函数。
|
||||
1. 在query()后面使用了group_by()进行分组,参数设置为Player.team_id字段,再使用having对分组条件进行筛选,参数为`func.count(Player.player_id)>5`。
|
||||
1. 使用order_by进行排序,参数为`func.count(Player.player_id).asc()`,也就是按照分组后的数据行数递增的顺序进行排序,最后使用.all()方法需要返回全部的数据。
|
||||
|
||||
你能看到SQLAlchemy使用的规则和使用SELECT语句的规则差不多,只是封装到了类中作为方法进行调用。
|
||||
|
||||
接着,我们再来看下如何删除数据。如果我们想要删除某些数据,需要先进行查询,然后再从session中把这些数据删除掉。
|
||||
|
||||
比如我们想要删除姓名为约翰·科林斯的球员,首先我们需要进行查询,然后从session对象中进行删除,最后进行commit提交,代码如下:
|
||||
|
||||
```
|
||||
row = session.query(Player).filter(Player.player_name=='约翰-科林斯').first()
|
||||
session.delete(row)
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
```
|
||||
|
||||
需要说明的是,判断球员姓名是否为约翰·科林斯,这里需要使用(==)。
|
||||
|
||||
同样,如果我们想要修改某条数据,也需要进行查询,然后再进行修改。比如我想把球员索恩·马克的身高改成2.17,那么执行完之后直接对session对象进行commit操作,代码如下:
|
||||
|
||||
```
|
||||
row = session.query(Player).filter(Player.player_name=='索恩-马克').first()
|
||||
row.height = 2.17
|
||||
session.commit()
|
||||
session.close()
|
||||
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
今天我们使用SQLAlalchemy对MySQL进行了操作,你能看到这些实现并不复杂,只是需要事先掌握一些使用方法,尤其是如何创建seesion对象,以及如何通过session对象来完成对数据的增删改查等操作。建议你把文章里的代码都跑一遍,在运行的过程中一定会有更深入的体会。
|
||||
|
||||
当然除了学习掌握SQLAlalchemy这个Python ORM工具以外,我还希望你能了解到ORM的价值和不足。如果项目本身不大,那么自己动手写SQL语句会比较简单,你可以不使用ORM工具,而是直接使用上节课讲到的mysql-connector。但是随着项目代码量的增加,为了在业务逻辑层与数据库底层进行松耦合,采用ORM框架是更加适合的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6c/f3/6cffd2ac3be05210ace5cd753ee4aff3.jpg" alt=""><br>
|
||||
我今天讲解了SQLAlalchemy工具的使用,为了更好地让你理解,我出一道练习题吧。还是针对player数据表,请你使用SQLAlalchemy工具查询身高为2.08米的球员,并且将这些球员的身高修改为2.09。
|
||||
|
||||
欢迎你在评论区写下你的答案,也欢迎把这篇文章分享给你的朋友或者同事,一起交流。
|
||||
238
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/19丨基础篇总结:如何理解查询优化、通配符以及存储过程?.md
Normal file
238
极客时间专栏/SQL必知必会/第一章:SQL语法基础篇/19丨基础篇总结:如何理解查询优化、通配符以及存储过程?.md
Normal file
@@ -0,0 +1,238 @@
|
||||
<audio id="audio" title="19丨基础篇总结:如何理解查询优化、通配符以及存储过程?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/92/2e/921c35d2baa0e77404aa5537e2e22a2e.mp3"></audio>
|
||||
|
||||
到这一篇的时候,意味着SQL专栏的基础部分正式更新完毕。在文章更新的时候,谢谢大家积极地评论和提问,让专栏增色不少。我总结了一些基础篇的常见问题,希望能对你有所帮助。答疑篇主要包括了DBMS、查询优化、存储过程、事务处理等一些问题。
|
||||
|
||||
## 关于各种DBMS的介绍
|
||||
|
||||
### 答疑1
|
||||
|
||||
文章中有句话不太理解,“列式数据库是将数据按照列存储到数据库中,这样做的好处是可以大量降低系统的 I/O”,可以解释一些“降低系统I/O”是什么意思吗?
|
||||
|
||||
### 解答
|
||||
|
||||
行式存储是把一行的数据都串起来进行存储,然后再存储下一行。同样,列式存储是把一列的数据都串起来进行存储,然后再存储下一列。这样做的话,相邻数据的数据类型都是一样的,更容易压缩,压缩之后就自然降低了I/O。
|
||||
|
||||
我们还需要从数据处理的需求出发,去理解行式存储和列式存储。数据处理可以分为OLTP(联机事务处理)和OLAP(联机分析处理)两大类。
|
||||
|
||||
OLTP一般用于处理客户的事务和进行查询,需要随时对数据表中的记录进行增删改查,对实时性要求高。
|
||||
|
||||
OLAP一般用于市场的数据分析,通常数据量大,需要进行复杂的分析操作,可以对大量历史数据进行汇总和分析,对实时性要求不高。
|
||||
|
||||
那么对于OLTP来说,由于随时需要对数据记录进行增删改查,更适合采用行式存储,因为一行数据的写入会同时修改多个列。传统的RDBMS都属于行式存储,比如Oracle、SQL Server和MySQL等。
|
||||
|
||||
对于OLAP来说,由于需要对大量历史数据进行汇总和分析,则适合采用列式存储,这样的话汇总数据会非常快,但是对于插入(INSERT)和更新(UPDATE)会比较麻烦,相比于行式存储性能会差不少。
|
||||
|
||||
所以说列式存储适合大批量数据查询,可以降低I/O,但如果对实时性要求高,则更适合行式存储。
|
||||
|
||||
## 关于查询优化
|
||||
|
||||
### 答疑1
|
||||
|
||||
在MySQL中统计数据表的行数,可以使用三种方式:`SELECT COUNT(*)`、`SELECT COUNT(1)`和`SELECT COUNT(具体字段)`,使用这三者之间的查询效率是怎样的?之前看到说是:`SELECT COUNT(*)`> `SELECT COUNT(1)`> `SELECT COUNT(具体字段)`。
|
||||
|
||||
### 解答
|
||||
|
||||
在MySQL InnoDB存储引擎中,`COUNT(*)`和`COUNT(1)`都是对所有结果进行`COUNT`。如果有WHERE子句,则是对所有符合筛选条件的数据行进行统计;如果没有WHERE子句,则是对数据表的数据行数进行统计。
|
||||
|
||||
因此`COUNT(*)`和`COUNT(1)`本质上并没有区别,执行的复杂度都是`O(N)`,也就是采用全表扫描,进行循环+计数的方式进行统计。
|
||||
|
||||
如果是MySQL MyISAM存储引擎,统计数据表的行数只需要`O(1)`的复杂度,这是因为每张MyISAM的数据表都有一个meta信息存储了`row_count`值,而一致性则由表级锁来保证。因为InnoDB支持事务,采用行级锁和MVCC机制,所以无法像MyISAM一样,只维护一个`row_count`变量,因此需要采用扫描全表,进行循环+计数的方式来完成统计。
|
||||
|
||||
需要注意的是,在实际执行中,`COUNT(*)`和`COUNT(1)`的执行时间可能略有差别,不过你还是可以把它俩的执行效率看成是相等的。
|
||||
|
||||
另外在InnoDB引擎中,如果采用`COUNT(*)`和`COUNT(1)`来统计数据行数,要尽量采用二级索引。因为主键采用的索引是聚簇索引,聚簇索引包含的信息多,明显会大于二级索引(非聚簇索引)。对于`COUNT(*)`和`COUNT(1)`来说,它们不需要查找具体的行,只是统计行数,系统会自动采用占用空间更小的二级索引来进行统计。
|
||||
|
||||
然而如果想要查找具体的行,那么采用主键索引的效率更高。如果有多个二级索引,会使用key_len小的二级索引进行扫描。当没有二级索引的时候,才会采用主键索引来进行统计。
|
||||
|
||||
这里我总结一下:
|
||||
|
||||
1. 一般情况下,三者执行的效率为 `COUNT(*)`= `COUNT(1)`> `COUNT(字段)`。我们尽量使用`COUNT(*)`,当然如果你要统计的是某个字段的非空数据行数,则另当别论,毕竟比较执行效率的前提是结果一样才可以。
|
||||
1. 如果要统计`COUNT(*)`,尽量在数据表上建立二级索引,系统会自动采用`key_len`小的二级索引进行扫描,这样当我们使用`SELECT COUNT(*)`的时候效率就会提升,有时候可以提升几倍甚至更高。
|
||||
|
||||
### 答疑2
|
||||
|
||||
在MySQL中,`LIMIT`关键词是最后执行的,如果可以确定只有一条结果,那么就起不到查询优化的效果了吧,因为`LIMIT`是对最后的结果集过滤,如果结果集本来就只有一条,那就没有什么用了。
|
||||
|
||||
### 解答
|
||||
|
||||
如果你可以确定结果集只有一条,那么加上`LIMIT 1`的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。这里指的查询优化针对的是会扫描全表的SQL语句,如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上`LIMIT 1`了。
|
||||
|
||||
## 关于通配符的解释
|
||||
|
||||
关于查询语句中通配符的使用理解,我举了一个查询英雄名除了第一个字以外,包含“太”字的英雄都有谁的例子,使用的SQL语句是:
|
||||
|
||||
```
|
||||
SQL> SELECT name FROM heros WHERE name LIKE '_%太%'
|
||||
|
||||
```
|
||||
|
||||
(_)匹配任意一个字符,(%) 匹配大于等于0个任意字符。
|
||||
|
||||
所以通配符`'_%太%'`说明在第一个字符之后需要有“太”字,这里就不能匹配上“太乙真人”,但是可以匹配上“东皇太一”。如果数据表中有“太乙真人太太”,那么结果集中也可以匹配到。
|
||||
|
||||
另外,单独的`LIKE '%'`无法查出NULL值,比如:`SELECT * FROM heros WHERE role_assist LIKE '%'`。
|
||||
|
||||
### 答疑4
|
||||
|
||||
可以理解在WHERE条件字段上加索引,但是为什么在ORDER BY字段上还要加索引呢?这个时候已经通过WHERE条件过滤得到了数据,已经不需要再筛选过滤数据了,只需要根据字段排序就好了。
|
||||
|
||||
### 解答
|
||||
|
||||
在MySQL中,支持两种排序方式,分别是FileSort和Index排序。在Index排序中,索引可以保证数据的有序性,不需要再进行排序,效率更高。而FileSort排序则一般在内存中进行排序,占用CPU较多。如果待排结果较大,会产生临时文件I/O到磁盘进行排序的情况,效率较低。
|
||||
|
||||
所以使用ORDER BY子句时,应该尽量使用Index排序,避免使用FileSort排序。当然你可以使用explain来查看执行计划,看下优化器是否采用索引进行排序。
|
||||
|
||||
优化建议:
|
||||
|
||||
1. SQL中,可以在WHERE子句和ORDER BY子句中使用索引,目的是在WHERE子句中避免全表扫描,在ORDER BY子句避免使用FileSort排序。当然,某些情况下全表扫描,或者FileSort排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。一般情况下,优化器会帮我们进行更好的选择,当然我们也需要建立合理的索引。
|
||||
1. 尽量使用Index完成ORDER BY排序。如果WHERE和ORDER BY后面是相同的列就使用单索引列;如果不同就使用联合索引。
|
||||
1. 无法使用Index时,需要对FileSort方式进行调优。
|
||||
|
||||
### 答疑5
|
||||
|
||||
ORDER BY是对分的组排序还是对分组中的记录排序呢?
|
||||
|
||||
### 解答
|
||||
|
||||
ORDER BY就是对记录进行排序。如果你在ORDER BY前面用到了GROUP BY,实际上这是一种分组的聚合方式,已经把一组的数据聚合成为了一条记录,再进行排序的时候,相当于对分的组进行了排序。
|
||||
|
||||
### 答疑6
|
||||
|
||||
请问下关于SELECT语句内部的执行步骤。
|
||||
|
||||
### 解答
|
||||
|
||||
一条完整的SELECT语句内部的执行顺序是这样的:
|
||||
|
||||
1. FROM子句组装数据(包括通过ON进行连接);
|
||||
1. WHERE子句进行条件筛选;
|
||||
1. GROUP BY分组 ;
|
||||
1. 使用聚集函数进行计算;
|
||||
1. HAVING筛选分组;
|
||||
1. 计算所有的表达式;
|
||||
1. SELECT 的字段;
|
||||
1. ORDER BY排序;
|
||||
1. LIMIT筛选。
|
||||
|
||||
### 答疑7
|
||||
|
||||
不太理解哪种情况下应该使用EXISTS,哪种情况应该用IN。选择的标准是看能否使用表的索引吗?
|
||||
|
||||
### 解答
|
||||
|
||||
索引是个前提,其实选择与否还是要看表的大小。你可以将选择的标准理解为小表驱动大表。在这种方式下效率是最高的。
|
||||
|
||||
比如下面这样:
|
||||
|
||||
```
|
||||
SELECT * FROM A WHERE cc IN (SELECT cc FROM B)
|
||||
SELECT * FROM A WHERE EXISTS (SELECT cc FROM B WHERE B.cc=A.cc)
|
||||
|
||||
```
|
||||
|
||||
当A小于B时,用EXISTS。因为EXISTS的实现,相当于外表循环,实现的逻辑类似于:
|
||||
|
||||
```
|
||||
for i in A
|
||||
for j in B
|
||||
if j.cc == i.cc then ...
|
||||
|
||||
```
|
||||
|
||||
当B小于A时用IN,因为实现的逻辑类似于:
|
||||
|
||||
```
|
||||
for i in B
|
||||
for j in A
|
||||
if j.cc == i.cc then ...
|
||||
|
||||
```
|
||||
|
||||
哪个表小就用哪个表来驱动,A表小就用EXISTS,B表小就用IN。
|
||||
|
||||
## 关于存储过程
|
||||
|
||||
### 答疑1
|
||||
|
||||
在使用存储过程声明变量时,都支持哪些数据类型呢?
|
||||
|
||||
### 解答
|
||||
|
||||
不同的DBMS对数据类型的定义不同,你需要查询相关的DBMS文档。以MySQL为例,常见的数据类型可以分成三类,分别是数值类型、字符串类型和日期/时间类型。
|
||||
|
||||
### 答疑2
|
||||
|
||||
“IN参数必须在调用存储过程时指定”的含义是什么?我查询了MySQL的存储过程定义,可以不包含 IN 参数。当存储过程的定义语句里有 IN 参数时,存储过程的语句中必须用到这个参数吗?
|
||||
|
||||
### 解答
|
||||
|
||||
如果存储过程定义了IN参数,就需要在调用的时候传入。当然在定义存储过程的时候,如果不指定参数类型,就默认是IN类型的参数。因为IN参数在存储过程中是默认值,可以省略不写。比如下面两种定义方式都是一样的:
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `add_num`(IN n INT)
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
CREATE PROCEDURE `add_num`(n INT)
|
||||
|
||||
```
|
||||
|
||||
在存储过程中的语句里,不一定要用到IN参数,只是在调用的时候需要传入这个。另外IN参数在存储过程中进行了修改,也不会进行返回的。如果想要返回参数,需要使用OUT,或者INOUT参数类型。
|
||||
|
||||
## 关于事务处理
|
||||
|
||||
### 答疑1
|
||||
|
||||
如果`INSERT INTO test SELECT '关羽';`之后没有执行COMMIT,结果应该是空。但是我执行出来的结果是`'关羽'`,为什么ROLLBACK没有全部回滚?
|
||||
|
||||
代码如下:
|
||||
|
||||
```
|
||||
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '关羽';
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '张飞';
|
||||
INSERT INTO test SELECT '张飞';
|
||||
ROLLBACK;
|
||||
SELECT * FROM test;
|
||||
|
||||
```
|
||||
|
||||
### 解答
|
||||
|
||||
先解释下连续BEGIN的情况。
|
||||
|
||||
在MySQL中BEGIN用于开启事务,如果是连续BEGIN,当开启了第一个事务,还没有进行COMMIT提交时,会直接进行第二个事务的BEGIN,这时数据库会隐式地COMMIT第一个事务,然后再进入到第二个事务。
|
||||
|
||||
为什么ROLLBACK没有全部回滚呢?
|
||||
|
||||
因为ROLLBACK是针对当前事务的,在BEGIN之后已经开启了第二个事务,当遇到ROLLBACK的时候,第二个事务都进行了回滚,也就得到了第一个事务执行之后的结果即“关羽”。
|
||||
|
||||
关于事务的ACID,以及我们使用COMMIT和ROLLBACK来控制事务的时候,有一个容易出错的地方。
|
||||
|
||||
在一个事务的执行过程中可能会失败。遇到失败的时候是进行回滚,还是将事务执行过程中已经成功操作的来进行提交,这个逻辑是需要开发者自己来控制的。
|
||||
|
||||
这里开发者可以决定,如果遇到了小错误是直接忽略,提交事务,还是遇到任何错误都进行回滚。如果我们强行进行COMMIT,数据库会将这个事务中成功的操作进行提交,它会认为你觉得已经是ACID了(就是你认为可以做COMMIT了,即使遇到了一些小问题也是可以忽略的)。
|
||||
|
||||
我在今天的文章里重点解答了一些问题,还有一些未解答的会留在评论里进行回复。最后出一道思考题吧。
|
||||
|
||||
请你自己写出下面操作的运行结果(你可以把它作为一道笔试题,自己写出结果,再与实际的运行结果进行比对):
|
||||
|
||||
```
|
||||
DROP TABLE IF EXISTS test;
|
||||
CREATE TABLE test(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '关羽';
|
||||
BEGIN;
|
||||
INSERT INTO test SELECT '张飞';
|
||||
INSERT INTO test SELECT '张飞';
|
||||
COMMIT;
|
||||
SELECT * FROM test;
|
||||
|
||||
```
|
||||
|
||||
欢迎你在评论区写下你的思考,我会与你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user