- SQL常见面试题
- 关系型数据库(SQL)
- 非关系型数据库(NoSQL)
- 数据库三大范式
- 主键与外键
- CHAR与VRCHAR数据类型
- 临时表
- 数据库函数、触发过程与存储器
- SQL语句
- SQL语言分类
- DROP、TRUNCATE、DELETE的区别
- sum、count(\*)、count(1)、count(column)的区别
- 自然连接、内连接、外连接、交叉连接的区别
- 事务
- 什么是事务
- 事务的特性(ACID)
- 事务并发导致的问题
- 事务的隔离级别
- 数据库锁
- 锁的级别
- 数据库死锁
- 悲观锁与乐观锁
- 悲观锁
- 乐观锁
- 如何选择乐观锁或悲观锁
- 存储引擎
- InnoDB与MyISAM
- 如何选择引擎
- 索引
- 索引是什么
- 索引的优缺点
- 添加索引的原则
- 如何创建索引
- 索引的类型
- 聚簇索引与非聚簇索引
- 索引的底层实现
- Hash索引
- B-树索引
- B+树索引
- 为什么索引结构默认使用B+树
- 最左前缀匹配原则
- 索引在什么情况下会失效
- 优化
- SQL语言的执行顺序
- SQL优化方法有哪些
- 索引使用技巧
- 其他
- 分析SQL执行情况
SQL常见面试题
关系型数据库(SQL)
关系型数据库是由二维表及其之间的联系所组成的一个数据组织,最典型的数据结构是表,即所有的数据都通过行和列的二元表现形式表示出来
常用的关系型数据库:mysql / oracle / sql server / sqlite
优点:
1、保持了数据的一致性(最大优势),可以进行事务处理
2、支持通用的SQL(结构化查询语言)语句
3、可以在一个表以及多个表之间进行复杂的数据查询,如join
4、二维表结构非常贴近逻辑世界的概念,更容易理解
缺点:
1、高并发读写能力差,一台数据库的最大连接数有限,硬盘 I/O 也有限,不能满足很多人同时连接
2、海量数据情况下读写效率低。对大数据量的表进行读写操作时,需要等待较长的时间
3、数据结构灵活度低,固定的表结构无法快速容纳新的数据类型
非关系型数据库(NoSQL)
非关系型数据库又称NoSQL(Not only SQL ),意为不仅仅是 SQL。其中数据以对象的形式存储在数据库中,而对象之间的关系通过每个对象自身的属性来决定
常见的非关系型数据库:键值数据库Redis、文档数据库MongoDB、图形数据库InfoGrid、列族数据库Bigtable等
优点:
1、格式灵活,存储数据的格式可以是key-value形式、文档形式、图片形式等,而关系型数据库则只支持基础类型
2、查询速度快,NoSQL将数据存储于缓存之中,而关系型数据库将数据存储在硬盘中
3、数据之间没有耦合性,非常容易进行水平扩展
4、高并发、高稳定性、成本低廉、可以实现数据的分布式处理
缺点:
1、不支持 SQL 语句,学习和使用成本高
2、没有事务处理,无法保证数据的完整性和安全性,适合处理海量数据,但不一定安全
3、复杂表关联查询不容易实现
数据库三大范式
-
第一范式:确保每列的原子性
第一范式是最基本的范式,如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式
第一范式的合理遵循需要根据系统的实际需求来定。例如:某些数据库系统中包含“地址”属性,本来直接将“地址”属性设计成一个数据库表的字段即可,但是如果系统经常访问“地址”属性中的“城市”部分,那么就需要将“地址”这个属性重新拆分为省份、城市、详细地址等多个部分进行存储,才算满足了数据库的第一范式:
编号 姓名 性别 年龄 联系电话 省份 城市 详细地址 1 张红欣 男 26 0378-23459876 河南 开封 朝阳区新华路23号 2 李四平 女 32 0751-65432584 广州 广东 白云区天明路148号 3 刘志国 男 21 0371-87659852 河南 郑州 二七区大学路198号 -
第二范式:确保表中每列都与主键相关
第二范式是指在关系表中,不是主键的列都必须完全依赖于主键,而不是主键的一部分(即消除部分依赖,主要是针对联合主键而言)
依赖:对于X的每个值,Y都有一个值与之对应,反过来则不一定不成立,这叫做X函数决定Y,Y函数依赖X
部分依赖:当主键由两个或两个以上字段构成,而表中的某些信息通过主键的一个字段就能唯一确定
例如:在学生选课表中,通过学号和课程号可以唯一确定一条记录,因此用学号和课程号做联合主键。但是表中的姓名、专业通过主键中的学号就能唯一确定,而课程名通过课程号就能唯一确定,这就是部分依赖,这样的设计不符合第二范式:
学号 姓名 专业 课程号 课程名 成绩 001 张三 电子 C01 C++ 80 002 李四 电子 C01 C++ 86 001 张三 电子 C02 操作系统 90 002 李四 电子 C02 操作系统 88 不符合第二范式会带来的问题:
- 数据信息冗余
- 增删改会出现问题,比如有一门《微机原理》没有人选,那么由于缺少学号(主键之一)那么这门课就不能出现在表里。
解决方案:用关系分解的方法消除部分依赖,将上表改成三张表
学号 姓名 专业 001 张三 电子 002 李四 电子 课程号 课程名 C01 C++ C02 操作系统 学号 课程号 成绩 001 C01 80 002 C01 86 001 C02 90 002 C02 88 -
第三范式:确保每列都和主键列直接相关
第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。第三范式的目的是移除那些不是直接依赖于主键的属性,这些属性是借由另一个属性来依赖于主键的(即A依赖于B,B依赖于C,就可以说A依赖C,形成了传递依赖)
例如:下表中有如下决定关系: 学号 –> 姓名,性别;系号 –> 系名;宿舍号 –> 宿舍电话,同时又有学号 –> 系名,学号 –> 宿舍电话,即存在着传递依赖。
学号 姓名 性别 系号 系名 宿舍号 宿舍电话 J001 张三 男 X01 计算机 103 13579 D005 李四 男 X02 电子 205 24680 S005 王五 男 X03 生物 309 12345 这样设计表会带来数据冗余,操作异常等问题。同样可以用关系分解的分解的方法来消除传递依赖,将这张表分成三张表:
系号 系名 X01 计算机 X02 电子 X03 生物 宿舍号 宿舍电话 103 13579 205 24680 309 12345 学号 姓名 性别 系号 宿舍号 J001 张三 男 X01 103 D005 李四 男 X02 205 S005 王五 男 X03 309
主键与外键
- 主键:主键是能确定一条记录的唯一标识,不可重复,不能为空,一张表只能有一个主键
- 外键:外键用来建立与其他表的连接,是另一张表的主键,可以重复,可以为空,一张表可以有多个外键
CHAR与VRCHAR数据类型
-
CHAR的长度是不可变的,VARCHAR的长度是可变的
当定义的是char(10),输入的是"abc"这三个字符时,它们所占空间是10个字节,其中包括7个空字节,当输入的字符长度超过指定的数时,char会截取超出的字符。在取数据时,char类型的要去除多余的空格,而对于varchar类型则不需要
-
CHAR的存取速度比VARCHAR快,因为其长度固定,方便程序的存储与查找
char也为此付出的是空间的代价,因为其长度固定,所以难免会有多余的空格占位符占据空间,可谓是以空间换取时间效率。varchar则是以空间效率为首位
-
CHAR的存储方式是:对英文字符(ASCII)占用1个字节,对一个汉字占用两个字节。
VARCHAR的存储方式是:对每个英文字符占用2个字节,汉字也占用2个字节。 -
两者的存储数据都非unicode的字符数据
临时表
临时表只在当前连接可见,当关闭连接时会被自动删除并释放所有空间,因此可以在不同连接中创建同名的临时表
临时表的创建与语法为CREATE TEMPOPARY TABLE tmp_table
临时表也可以手动删除DELETE TEMPOPARY TABLE IF EXISTS tmp_table
数据库函数、触发过程与存储器
存储过程 | 函数 | |
---|---|---|
参数类型 | 可以使用IN、OUT、IN OUT三种模式的参数 | 只有IN |
返回值 | 可以通过OUT、IN OUT参数返回零个或多个参数值 | 必须有返回值,且必须为单一值或单一对象 |
是否可以单独执行 | 可以 | 必须通过execute执行 |
是否可以通过SQL语句 (DML或SELECT)调用 | 不可以 | 可以,且可以位于FROM关键字的后面 |
数据库函数
数据库中的函数包括内置函数(如SUM()
,COUNT()
,AVG()
)和自定义函数,具有以下特点:
- 函数只有一种参数模式(IN),只能有一条RETURN语句,只能返回单一值
- 函数可以在DML语句或SELECT子句中进行调用。由于函数可以返回一个表对象,因此它可以在查询语句中放在FROM后面
存储过程
存储过程是一组完成特定功能的SQL语句集,经编译后存储在数据库中,具有以下特点:
- 存储过程是预编译过的,执行效率高
- 存储过程的代码直接存放于数据库中,通过存储过程名直接调用,减少网络通讯
- 安全性高,执行存储过程需要有一定权限的用户
- 可以重复使用,可减少数据库开发人员的工作量
- 缺点是可移植性差
调用方法:
- 可以用一个命令对象 execute <过程名> 来调用存储过程
- 可以供外部程序调用,如 java 程序
触发器
触发器是一种特殊的存储过程,主要是通过事件来触发而被执行的。它可以强化约束,来维护数据的完整性和一致。
SQL语句
SQL语言分类
-
数据库定义语句DDL(Data Definition Language)
用于创建、修改和删除数据库对象
DDL语句 功能说明 CREATE 创建数据库结构 ALTER 修改数据库结构 DROP 删除数据库结构 RENAME 更改数据库对象的名称 TRUNCATE 删除表的全部内容 -
数据查询语句DQL(Data Query Language)
数据查询语言DQL基本结构是由
SELECT
子句,FROM
子句,WHERE
子句组成的查询块 -
数据库操纵语句DML(Data Maintain Language)
用来处理数据库中的数据内容
DML语句 功能说明 INSERT 插入数据到表或视图 UPDATE 更新数据 DELETE 删除数据 CALL 调用过程 MERGE 合并(插入或修改) -
数据库控制语句DCL(Data Control Language)
用于修改数据库结构的操作权限
DCL语句 功能说明 CRANT 授予其他用户对数据库结构的访问权限 REVOKE 收回用户访问数据库结构的权限 COMMIT 将当前事务所做的更改永久化(写入数据库) ROLLBACK 取消上次提交以来的所有操作
DROP、TRUNCATE、DELETE的区别
用法 | 类型 | 功能 | |
---|---|---|---|
DROP | DROP TABLE 表名 | DDL | 删除整个表的数据与结构 |
TRUNCATE | TRUNCATE TABLE 表名 | DDL | 删除整张表的数据,但保留表结构 |
DELETE | DELETE TABLE 表名 WHERE 条件 | DML | 既可以删除整张表数据又可以删除单行 |
- 执行速度:DROP > TRUNCATE > DELETE
- DROP 与 TRUNCATE 是DDL语言,操作即生效,不能进行回滚,不能激活触发器
DELETE 是DML语言,会被放入回滚片段中,待事务提交后才生效,如果有相应的触发器,将在执行时激活 - TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同,均删除表中的全部行
但 TRUNCATE 比 DELETE 速度快,且使用的系统和事务日志资源少 - 使用 TRUNCATE 清空表后索将重新设置成初始大小,而 DELETE 则不能
- 对于由 FOREIGN KEY 约束引用的表,不能使用 TRUNCATE TABLE,而应使用不带 WHERE 子句的 DELETE 语句,这是因为TRUNCATE TABLE 不记录在日志中,所以它不能激活触发器
sum、count(*)、count(1)、count(column)的区别
count 用于求行的个数;sum 用于累加求和
- count(*) 对行的数目进行计算,包含NULL
- count(1) 与 count(*) 效果相同
- count(column) 对特定列的行数进行计算,不包含NULL
自然连接、内连接、外连接、交叉连接的区别
table1:
A | B | C |
---|---|---|
1 | 2 | 3 |
5 | 6 | 7 |
table2:
C | D | E |
---|---|---|
3 | 4 | 5 |
8 | 9 | 1 |
-
自然连接(natural join)
自然连接是一种特殊的等值连接,他要求两个关系表中进行连接的必须是相同的属性列(名字相同),无须添加连接条件,并且在结果中消除重复的属性列
SELECT * FROM table1 NATURAL JOIN table2
C A B D E 3 1 2 4 5 -
内连接(INNER JOIN)
内连接与自然连接类似,不同之处在于内连接不要求两属性列同名,而是使用using或on来指定某两列字段相同的连接条件
SELECT * FROM table1 INNER JOIN table2 on table1.A = table2.E
A B C C(table2) D E 1 2 3 8 9 1 5 6 7 3 4 5 -
左外连接(LEFT OUTTER JOIN,OUTER可以省略)
返回左表中的所有行,如果左表中行在右表中没有匹配行,则结果中右表中的列返回空值
SELECT * FROM table1 LEFT JOIN table2 on table1.C = table2.C
A B C C(table2) D E 1 2 3 3 4 5 5 6 7 NULL NULL NULL -
右外连接(RIGHT OUTTER JOIN,OUTER可以省略)
返回右表中的所有行,如果右表中行在左表中没有匹配行,则结果中左表中的列返回空值
SELECT * FROM table1 RIGHT JOIN table2 on table1.C = table2.C
A B C C(table2) D E NULL NULL NULL 8 9 1 1 2 3 3 4 5 -
全外连接(FULL OUTTER JOIN,OUTER可以省略,MySQL不支持)
返回左表和右表中的所有行。当某行在另一表中没有匹配行,则另一表中的列返回空值
-
交叉连接(CROSS JOIN)
相当于笛卡尔积,它不使用任何匹配或者选取条件,而是直接将一个数据源中的每个行与另一个数据源的每个行都一一匹配
A B C C(table2) D E 1 2 3 8 9 1 5 6 7 3 4 5 1 2 3 3 4 5 5 6 7 8 9 1
事务
什么是事务
事务(transaction)是作为一个单元的一组有序的数据库操作集合,用来保持数据的完整性和一致性,事务是一种高级的处理方式,如在一些列增删改中只要哪个出错还可以回滚还原
例如,小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
事务的特性(ACID)
-
原子性(Atomicity)
表示组成一个事务的多个操作是一个不可分割的原子单元,只有当事务中所有操作成功时,才对整个事务进行提交;事务中任何一步的失败都会导致整个事务失败,需要进行回滚来撤销已执行的操作,让数据库回到初始状态
-
一致性(Consistency)
事务开始前和结束后,数据库的完整性约束不能被破坏。例如A向B转账,不管操作成功与否,A和B账户的总和应该是不变的
-
隔离性(Isolation)
隔离性是指当有多个用户并发操作数据库时,数据库为每一个用户开启的事务不能被其他事务的操作所影响,也就是说多个并发事务之间要相互隔离。例如A要向某账户存钱,B要从该账户取钱,那么在A存钱的过程中B不能进行操作
-
持久性(Durability)
持久性是指当某一事务被提交后,那么它对于数据库的改变就是永久性的,即使在系统故障情况下也不会丢失
事务并发导致的问题
-
脏读(Dirty Read)
事务A读取了事务B更新但尚未提交的数据,然后事务B进行了回滚操作,那么事务A读取到的数据就是脏数据
-
不可重复读(Non-repeatable read)
事务A多次读取同一数据,而事务B在这个过程中对数据进行了更新并提交,导致事务A多次读取的结果不一致
-
幻读(Phantom Read)
事务A多次读取数据并统计读取数据的条数,在这个过程中事务B新增了数据行并提交,导致事务A多次读取得到的结果条数不一样
注:不可重复读与幻读的区别在于一个是读取了其他事务更新的数据,一个是读取了其他事务新增的数据。要避免不可重复读可以根据条件锁定行,而避免幻读则需要锁定整张表
事务的隔离级别
为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,可以逐个解决脏读、不可重复读和幻读的问题
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读取未提交 | √ | √ | √ |
读取已提交 | × | √ | √ |
可重复读 | × | × | √ |
串行化 | × | × | × |
-
读取未提交(READ-UNCOMMITED)
最低隔离级别
-
读取已提交(READ-COMMITTED)
仅能够阻止脏读
-
可重复读(REPEATED-READ)
对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,该级别也是MySQL默认采取的隔离级别
-
可串行化(SERIALIZABLE)
最高隔离级别,完全符合ACID要求,即所有事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
注:隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,但在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
数据库锁
锁的级别
MySQL有三种级别的锁:表级、行级、页面级
-
表级锁
开销小,加锁快,不会发生死锁,锁定粒度大,发生锁冲突的概率最高,并发度最低
-
行级锁
开销大,加锁慢,会出现死锁,锁定粒度小,发生锁冲突的概率最低,并发度最高
-
页面级锁
开销和加锁时间介于表级锁和行级锁之间,会出现死锁,锁定粒度和并发度一般
数据库死锁
死锁是指两个或两个以上的进程在执行过程中,因为争夺资源而造成的一种相互等待的现象,若无外界作用,它们都将无法推进下去,死锁的关键在于加锁的顺序不一致
解决办法:
- 查出线程并用kill杀死
- 设置锁的超时时间
- 指定获取锁的顺序
悲观锁与乐观锁
悲观锁
先获取锁,再进行业务操作
即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的“一锁二查三更新”即指的是使用悲观锁。
实现方式:
通常来讲在数据库上的悲观锁需要数据库本身提供支持,即通过常用的select … for update操作来实现。当数据库执行select for update时会获取被select中的数据行的行锁,因此其他并发执行的select for update如果试图选中同一行则会发生排斥(需要等待行锁被释放),达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。
注意:
不同的数据库对select for update的实现和支持都是有所区别的,
- oracle支持select for update no wait,表示如果拿不到锁立刻报错,而不是等待,MySQL就没有no wait这个选项。
- MySQL还有个问题是select for update语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在MySQL中用悲观锁务必要确定走了索引,而不是全表扫描。
乐观锁
先进行业务操作,不到万不得已不去拿锁
乐观锁也叫乐观并发控制,它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,那么当前正在提交的事务会进行回滚。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。
实现方式:
乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号,或者时间戳
-
SELECT data AS old_data, version AS old_version FROM …;
-
根据获取的数据进行业务操作,得到new_data和new_version
-
UPDATE SET data = new_data, version = new_version WHERE version = old_version
if (updated row > 0) {
// 乐观锁获取成功,操作完成 }
else {
// 乐观锁获取失败,回滚并重试 }
注意:
- 乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此适合用在取锁失败概率比较小的场景,可以提升系统并发性能
- 乐观锁还适用于一些比较特殊的场景,例如在业务操作过程中无法和数据库保持连接等悲观锁无法适用的地方。
如何选择乐观锁或悲观锁
悲观锁和乐观锁是数据库用来保证数据并发安全防止更新丢失的两种方法,大部分场景下差异不大,一些独特场景下有一些差别,一般我们可以从如下几个方面来判断。
-
响应速度
如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁
-
冲突频率
如果冲突频率非常高,建议采用悲观锁,保证成功率,如果冲突频率大,乐观锁会需要多次重试才能成功,代价比较大
-
重试代价
如果重试代价大,建议采用悲观锁
存储引擎
MySQL有多种存储引擎,如MyISAM、InnoDB、MERGE、EXAMPLE、CSV、等等,但常用的只有MyISAM与InnoDB两个,其中InNoDB是MySQL的默认引擎
InnoDB与MyISAM
InnoDB | MyISAM | |
---|---|---|
事务 | 支持 | 不支持 |
锁 | 行级锁 | 表级锁 |
主外键 | 支持 | 不支持 |
索引类型 | 聚簇索引 | 非聚簇索引 |
缓存 | 只缓存索引 | 缓存索引和真实数据 |
表空间 | 小 | 大 |
- InnoDB支持事务,而MyISAM不支持,这是两引擎最大的区别
对于InnoDB来说,支持事务是以损失效率来换取的,InnoDB的默认事务隔离级别是可重复读 - InnoDB使用的锁粒度为行级锁,可以支持更高的并发
InnoDB的行锁是有条件的,只是在WHERE的主键是有效的,非主键的WHERE也会锁全表
MyISAM最小的锁粒度是表锁,一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞 - InnoDB支持外键,而MyISAM不支持
对一个包含外键的 InnoDB 表转为 MYISAM 会失败 - InnoDB是聚簇索引,MyISAM 是非聚簇索引
聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB必须要有主键,通过主键索引效率很高
MyISAM索引和数据是分开的,而且其索引经过压缩,可以更好地利用内存,所以它的查询性能更高 - InnoDB 不保存表的具体行数,执行select count(*) from table 时需要全表扫描
MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快 - InnoDB不支持全文索引,MyISAM支持,这可以极大地优化LIKE查询的效率
如何选择引擎
如果考虑使用事务或读写密集型操作,则应选择INNODB引擎
对查询性能要求较高,应使用MyISAM
索引
索引是什么
索引是一种数据结构,数据库索引就是对数据库表中一列或多列的值进行排序的结构,可以帮助数据库实现快速的查询与更新。
索引可以理解为书的目录,字典的拼音、偏旁部首检索,都是为了方便查找内容而建立的
索引的优缺点
优点:
- 可以大大加快数据的检索速度(最大的优点)
- 可以加速表与表之间的连接
- 在使用分组和排序子句进行索引时,同样能显著减少分组与排序的时间
- 通过建立唯一性索引,可以保证表中数据的唯一性
- 通过使用索引,可以在查询过程中使用优化隐藏器,提高系统性能
缺点:
- 时间:创建索引和维护索引都需要耗费时间,具体来说,当对表中数据进行增删查改时,索引也需要动态地维护
- 空间:索引需要占据一定的物理空间
添加索引的原则
应该创建索引的列:
-
经常需要搜索的列
显著加快搜索的速度
-
作为主键的列
强制该列的唯一性和组织表中数据的排列结构
-
经常用作连接的列(通常是外键)
加快表与表之间的连接速度
-
经常需要根据范围搜索的列
索引已经排序,其指定的范围是连续的
-
经常需要排序的列
索引已经排序,这样查询可以利用索引的排序,加快排序查询时间
-
经常使用在WHERE子句上的列
加快条件的判断速度
不应该创建索引的情况
-
在查询中很少使用的列不应建立索引
如果列很少被使用到,那么有索引并不能提高查询速度,反而会降低系统的维护速度和增加空间需求
-
只有很少数据值的列不应建立索引
如信息表中的性别列,只有两种结果值,对其建立索引并不能明显加快检索速度
-
定义为text、image和bit数据类型的列不应该建立索引
这些列的数据要么数据量相当大,要么取值很少
-
当修改性能远大于检索性能时不应建立索引
修改性能与检索性能是相互矛盾的,当增加索引时会降低修改性能
如何创建索引
索引创建有三种方式:
-
在执行
CREATE TABLE
时创建CREATE TABLE user_table(id INT NOT NULL PRIMARY KEY,name VARCHAR(20) NULL,sex VARCHAR(2) NULL,info VARCHAR(20) NULL,INDEX index_id(id), -- 普通索引UNIQUE INDEX index_id(id), -- 唯一索引FULLTEXT INDEX index_id(id), -- 全文索引KEY index_id_name(id,name) -- 组合索引 );
-
使用
ALTER TABLE
增加索引ALTER TABLE user_table ADD INDEX index_id(id) -- 普通索引 ALTER TABLE user_table ADD UNIQUE INDEX index_id(id) -- 唯一索引 ALTER TABLE user_table ADD FULLTEXT INDEX index_id(id) -- 全文索引 ALTER TABLE user_table ADD INDEX index_id_name(id,name) -- 组合索引
-
使用
CREATE INDEX
命令创建CREATE INDEX index_id ON user_table(id) -- 普通索引 CREATE UNIQUE INDEX index_id ON user_table(id) -- 唯一索引 CREATE FULLTEXT INDEX index_id ON user_table(id) -- 全文索引 CREATE INDEX index_id_name ON user_table(id,name) -- 组合索引
索引的类型
从索引的存储形式划分:
- BTree索引(包括B+Tree和B-Tree)
- Hash索引
- Full-index全文索引
- RTree索引
从应用层次划分:
- 主键索引:一张表中只能存在一个,不可重复,不能为NULL
- 组合索引:由多个列值组成的索引
- 唯一索引:不可重复,但可以为空,索引列的值必须是唯一的,如果是组合索引,则组合的列值必须是唯一的
- 全文索引:对文本内容进行搜索
- 普通索引:基本的索引类型
从数据与索引是否分开存储划分:
- 聚簇索引:将数据与索引放在一起储存,索引的叶子结点保存了数据行
- 非聚簇索引:将数据与索引分开储存,索引的叶子结点存储的是指向数据行的地址
聚簇索引与非聚簇索引
聚集索引和非聚集索引的根本区别是表记录的排列顺序和与索引的排列顺序是否一致。或者说,聚集索引的叶节点就是数据节点。而非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块
聚集索引类似新华字典中的拼音排序索引,都是按顺序进行。比如,我们要查“安”字,因为“安”的拼音是“an”,而按照拼音排序汉字的字典是以英文字母“a”开头并以“z”结尾的,那么“安”字就自然地排在字典的前部。
而非聚集索引则类似于偏旁部首索引,索引顺序和物理顺序并不是按顺序存放的。比如查“张”字,可以看到在查部首之后的检字表中“张”的页码是672页,而“张”的上面“驰”字页码是63页,“张”的下面是“弩”字页面是390页
索引的底层实现
Hash索引
Hash索引是基于哈希表实现的,对于每一行数据,都需要通过哈希函数计算其索引列值对应的哈希码,将哈希码的值作为哈希表的key,将指向数据行的指针作为value进行存储,这样查找一个数据的时间复杂度就是o(1),一般多用于精确查找。
B-树索引

B-树(又称为B树)是一种平衡的多叉查找树,也就是说最多可以开m个叉(m>=2),具有以下特点:
- 每个叶子结点的高度一样
- 关键字集合分布在整颗树中
- 任何一个关键字出现且只出现在一个节点中
- 搜索有可能在非叶子节点结束
- 其搜索性能等价于在关键字集合内做一次二分查找
- B树在插入删除新的数据记录会破坏B-Tree的性质,因为在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持B-Tree性质
B树的查询过程和二叉排序树比较类似,从根节点依次比较每个结点,如果查找失败,则会返回叶子节点,即空指针
例如查询图中字母表中的K:
- 从根节点P开始,K的位置在P之前,进入左侧指针
- 左子树中,依次比较C、F、J、M,发现K在J和M之间
- 沿着J和M之间的指针,继续访问子树,并依次进行比较,发现第一个关键字K即为指定查找的值
B+树索引

作为B树的加强版,B+树与B树的差异在于:
- 有n棵子树的节点含有n个关键字(也有认为是n-1个关键字)
- 所有的关键字全部存储在叶子节点上,且叶子节点本身根据关键字自小而大顺序连接
- 非叶子节点可以看成索引部分,节点中仅含有其子树(根节点)中的最大(或最小)关键字
B+树的查找过程与B树类似,只不过如果在非叶子节点上的关键字等于给定值时并不终止,而是继续沿着指针直到叶子节点位置。因此在B+树,不管查找成功与否,每次查找都是走了一条从根到叶子节点的路径。
B+树的特性如下:
- 所有关键字都存储在叶子节上,且链表中的关键字恰好是有序的
- 不可能非叶子节点命中返回
- 非叶子节点相当于叶子节点的索引,叶子节点相当于是存储(关键字)数据的数据层
- 更适合文件索引系统
为什么索引结构默认使用B+树
B+树的优点:
- 磁盘读写代价更低
- 查询效率更加稳定
- 有利于对数据库的扫描
Hash索引的缺点:
- 只支持等值查询,如“=”,“IN”,而不能使用范围查询
- 因为不是按照索引值顺序存储的,因此不能利用索引完成排序
- 始终索引全部列的全部内容,不支持部分索引列的匹配查找
- 在有大量重复键值的情况下,效率会降低
最左前缀匹配原则
最左匹配原则就是最左优先**,在创建多列索引时,应根据业务需求,将WHERE子句中使用最频繁的一列放在最左边**。这是因为MySQL 建立联合索引时,会首先根据联合索引中最左边的、也就是第一个字段进行排序,在第一个字段排序的基础上,再对联合索引中后面的第二个字段进行排序,依此类推。
匹配原则:
-
对于联合索引,MySQL 会一直向右匹配直到遇到范围查询(> , < ,between,like)就停止匹配
比如 a = 3 and b = 4 and c > 5 and d = 6,如果建立的是(a,b,c,d)这种顺序的索引,那么 d 是用不到索引的,但是如果建立的是 (a,b,d,c)这种顺序的索引的话,那么就没问题,而且 a,b,d 的顺序可以随意调换。
-
= 和 in 可以乱序
比如 a = 3 and b = 4 and c = 5 建立 (a,b,c)索引可以任意顺序。
-
如果建立的索引顺序是 (a,b)那么直接采用 where b = 5 这种查询条件是无法利用到索引的,这一条最能体现最左匹配的特性
索引在什么情况下会失效
-
使用
!=
、<>
进行判断会导致索引失效例如
select * from table_name where a != 1
注意:
<>
符号是不等于的意思,和!=
的功能是一样的 -
数据类型的隐式转换会导致索引失效
假设
select * from table_name where a = '1'
会使用到索引,如果没有对字符串加引号,写成select * from table_name where a = 1
则会导致索引失效。 -
在索引中使用函数会导致索引失效
例如
select * from table_name where abs(a) = 1
-
在索引上进行计算会导致索引失效
例如
select * from table_name where a + 1 = 2
-
条件中有
OR
会导致索引失效例如
select * from table_name where a = 1 or b = 3
注意:并不是所有的
OR
都会导致索引失效,如果OR
连接的是同一字段将不会导致失效 -
使用模糊查询时以%开头会导致索引失效
例如
select * from table_name where name LIKE '%表'
优化
SQL语言的执行顺序
SQL 不同于与其他编程语言的最明显特征是处理代码的顺序。在大多数编程语言中,代码按编码顺序被处理,但是在SQL语言中,第一个被处理的子句是FROM子句,尽管SELECT语句第一个出现,但是几乎总是最后被处理
(8) SELECT(9) DISTINCT column,…
选择字段 、去重
(6) AGG_FUNC(column or expression),…
使用聚集函数进行计算
(1) FROM [left_table]
选择表
(3) <join_type> JOIN <right_table>
链接
(2) ON <join_condition>
链接条件
(4) WHERE <where_condition>
基于指定的条件对记录行进行筛选
(5) GROUP BY <group_by_list>
将数据划分为多个分组
(7) HAVING <having_condition>
筛选分组
(10) ORDER BY <order_by_list>
对结果集进行排序
(11) LIMIT count OFFSET count;
分页
每个步骤都会产生一个虚拟表,该虚拟表被用作下一个步骤的输入。这些虚拟表对调用者(客户端应用程序或者外部查询)不可用。只是最后一步生成的表才会返回给调用者。如果没有在查询中指定某一子句,将跳过相应的步骤。
SQL优化方法有哪些
索引使用技巧
-
避免在where子句中使用
!=
或<>
操作符 -
避免数据类型的隐式转换
【反例】 SELECT * FROM student WHERE NAME=123【正例】 SELECT * FROM student WHERE NAME='123'
-
避免在索引列上使用内置函数
【反例】 SELECT * FROM student WHERE DATE(birthday) = NOW()【正例】 SELECT * FROM student WHERE birthday = DATE(NOW())
-
避免在索隐列上使用表达式计算
【反例】 SELECT * FROM student WHERE id=+1-1+1【正例】 SELECT * FROM student WHERE id=1
-
避免使用
OR
来连接过滤条件【反例】 SELECT * FROM student WHERE id=1 OR salary=30000【正例】 SELECT * FROM student WHERE id=1 # 方法一:使用union all UNION ALL SELECT * FROM student WHERE salary=30000 SELECT * FROM student WHERE id=1 # 方法二:分开两条sql写 SELECT * FROM student WHERE salary=30000
-
避免在字段开头模糊查询
【反例】 SELECT id,NAME FROM student WHERE NAME LIKE '%1' SELECT id,NAME FROM student WHERE NAME LIKE '%1%'【正例】 SELECT id,NAME FROM student WHERE NAME LIKE '1%'
-
尽量避免使用
IN
和NOT IN
【反例】 SELECT * FROM t WHERE id IN (2,3) SELECT * FROM t1 WHERE username IN (SELECT username FROM t2)【正例】 SELECT * FROM t WHERE id BETWEEN 2 AND 3 # 如果是连续数值,可以用between代替 SELECT * FROM t1 WHERE EXISTS (SELECT * FROM t2 WHERE t1.username = t2.username) #如果是子查询,可以用exists代替
-
尽量避免进行
NULL
值的判断【反例】 SELECT * FROM t WHERE score IS NULL【正例】 SELECT * FROM t WHERE score = 0 # 可以给字段添加默认值0,对0值进行判断
-
索引并不是越多越好,一个表的索引数最好不要超过5个
其他
-
尽量避免使用
SELECT *
返回无用的字段会降低查询效率
-
去重
DISTINCT
过滤字段要少当查询很多字段时,如果使用distinct,数据库引擎就会对数据进行比较与过滤,需要占用时间
-
批量插入数据
插入是DML语句,默认每条都需要事务开启和事务提交;而批量处理只需要一次事务开启和提交
多条提交: INSERT INTO student (id,NAME) VALUES(4,'name1') INSERT INTO student (id,NAME) VALUES(5,'name2')批量提交: INSERT INTO student (id,NAME) VALUES(4,'name1'),(5,'name2')
-
分批删除数据
一次性删除太多数据可能造成锁表,建议分批操作
【反例】 delete from student where id <100000; # 一次删除10万+for(User user:list){ # 采用单一循环操作,效率低,时间漫长delete from student; }【正例】 for(){ # 分批进行删除,如每次500 delete student where id<500; }
-
提高
GROUP BY
语句的效率【反例】 select job,avg(salary) from employee # 先分组,再过滤 group by job having job ='president' or job = 'managent';【正例】 select job,avg(salary) from employee # 先过滤,后分组 where job ='president' or job = 'managent' group by job;
-
优先使用inner join
三种连接如果结果相同,优先使用inner join,如果使用left join左边表尽量小,即小表驱动大表,小的数据集驱动大的数据集,从而让性能更优
-
尽量使用union all替代union
union和union all的区别是,union会自动去掉多个结果集合中的重复结果,而union all则将所有的结果全部显示出来,不管是不是重复
去重的过程同样会占用时间,而实际大部分应用中是不会产生重复的记录
分析SQL执行情况
在SQL语句前使用EXPLAIN关键字,可以查看该条SQL语句是否使用了索引
扫描类型TYPE性能由低到高依次为:
- all:全表扫描,没有优化,最慢的方式
- index:需要扫描索引上的全部数据,它仅比全表扫描快一点
- range:索引范围扫描,对索引的扫描开始于某一点,返回匹配值的行,常见于
between
、<
、>
等查询 - ref:非唯一性索引扫描,返回匹配某个单独值的所有行,常见于多表查询或普通非唯一索引
- eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于多表查询时命中后表的主键或唯一索引
- const:命中主键或者唯一索引,且查询条件是一个常量值
- system:该表只有一行(相当于系统表)