文章目录
- Mysql数据库高级笔记
- 1. DQL数据查询进阶
- 1.1 子查询
- 1.2 联表查询
- 1.2.1 表关系
- 1.2.2 表联结查询
- 1.2.2.1 内联结(等值连接)——INNER JOIN
- 1.2.2.2 自联结
- 1.2.2.3 外部联结——LEFT/RIGHT JOIN
- 1.3 组合查询
- 2. MySQL事务
- 2.1 事务的ACID特性
- 2.2 事务并发常见问题
- 2.3 事务的隔离级别
- 3. MySQL存储过程
- 4. MySQL触发器
- 5. MySQL的视图
- 6.MySQL索引与优化
- 6.1 索引概述
- 6.2 索引原理-索引与B+Tree
- 6.3 聚簇索引和非聚簇索引
- 6.4 慢查询与SQL优化
- 6.4.1 慢查询与Explain执行计划
- 6.4.2 SQL优化
Mysql数据库高级笔记
1. DQL数据查询进阶
这里所用到的表结构如下图所示:
1.1 子查询
子查询即嵌套在其他查询中的查询。可以利用嵌套关系完成一些复杂情况的查询。
例子 1:利用子查询进行过滤
订单存储在两个表中。对于包含订单号、客户ID、订单日期的每个订单,orders表存储⼀行。 各订单的物品存储在相关的orderitems表中。orders表不存储客户信息。它只存储客户的ID。实际的客户信息存储在customers表中。现在,假如需要列出订购了物品TNT2的所有客户,可以用下面的方式查询
-- 从oderitems表中查询所有包含物品TNT2中的订单编号order_num; -- 将上一步查询出的order_num作为在oders表中查询的条件,查询出符合条件的用户编号cust_id; -- 再将上一步查询出的cust_id作为在customers表中查询的条件,就可以查询出订购了物品TNT2的所有客户啦
SELECT cust_name,cust_contact
FROM customers
WHERE cust_id IN (
SELECT cust_id
FROM orders
WHERE order_num IN (
SELECT order_numFROM orderitems WHERE prod_id = 'TNT2'
)
);
-- 查询结果:
+----------------+--------------+
| cust_name | cust_contact |
+----------------+--------------+
| Coyote Inc. | Y Lee |
| Yosemite Place | Y Sam |
+----------------+--------------+
例子 2:作为计算字段使用子查询
假如需要显示customers表中每个客户的订单总数。订单与相应的客户ID存储在orders表中。
-- 从customers表中检索客户列表。对于检索出的每个客户,统计其在orders表中的订单数目。
SELECT cust_id,cust_name,
(SELECT COUNT(*)
FROM orders
WHERE orders.cust_id = customers.cust_id) as ordersNum
FROM customers
ORDER BY cust_name;
-- 查询结果
+---------+----------------+-----------+
| cust_id | cust_name | ordersNum |
+---------+----------------+-----------+
| 10001 | Coyote Inc. | 2 |
| 10005 | E Fudd | 1 |
| 10002 | Mouse House | 0 |
| 10003 | Wascals | 1 |
| 10004 | Yosemite Place | 1 |
+---------+----------------+-----------+
这条SELECT 语句对customers 表中每个客户返回3 列:cust_name、cust_state和ordersNum。ordersNum是一个计算字段,它是由圆括号中的子查询
SELECT COUNT(*) FROM orders WHERE orders.cust_id = customers.cust_id) as ordersNum)
建立的。该子查询对检索出的每个客户执行一次。在此例子中,该子查询执行了5次,因为检索出了5个客户。子查询中的WHERE子句与前面使用的WHERE子句稍有不同,因为它使用了完全限定列名。这种方式叫做相关子查询,它是涉及外部查询的子查询。任何时候只要列名可能有多义性,就必须使用这种语法。因为有两个cust_id列,一个在customers中,另一个在orders中,需要⽐比较这两个列列以正确地把订单与它们相应的顾客匹配
WHERE orders.cust_id = customers.cust_id
1.2 联表查询
SQL最强大的功能之⼀就是能在数据检索查询的执行中联结(join)表。在能够有效地使⽤用联结前,必须了了解关系表以及关系数据库设计的⼀些基础知识
1.2.1 表关系
MySQL作为一个关系型数据,最大的特点便是通过表与表之间的关系,可以有效地存储和方便地处理数据
关系表的设计就是要保证把信息分解成多个表,一类数据一个表。各表通过某些常用的值(即关系设计中的关系(relational))互相关联。
表与表之间的关系,通过外键进行关联。外键存在于两个拥有相同字段的表中,其中一个表的主键在另一个表中作为外键查找数据时,可以通过外键确定数据对应关系。
表关系又下面三种
- 一对一关系:就是在一个表A中的一条数据,对应着另外一张表B中的一条数据,反过来也是。
- 一对多(多对一)关系:在一个表A中的一条数据对应着另外一个表B中的多条数据,但反过来另一个表B中的一条数据也只能对应表A中的一条数据,比如,爸爸和儿女,一个爸爸可以有很多儿女,但一个儿女只能有一个爸爸。
- 多对多关系:在一个表A中的一条数据对应另一个表B中的多条数据,同时,表B中的一条数据也对应着表A中的多条数据
有关表关系更详细的内容可以看这篇文章
https://cloud.tencent.com/developer/article/1585927
1.2.2 表联结查询
如果数据存储在多个表中,怎样⽤用单条SELECT语句句检索出数据?
答案是使⽤用联结。简单地说,联结是⼀种机制,用来在一条SELECT语句中关联表,因此称之为联
结。
使⽤用特殊的语法,可以联结多个表返回一组输出,联结在运行时关联表中正确的行。
1.2.2.1 内联结(等值连接)——INNER JOIN
获取两个表中字段匹配关系的记录。
SELECT 表名1.字段名1,表名2.字段名2... FROM 表名1 INNER JOIN 表名2 ON 表名1.字段名3 = 表名2.字段名3;
也可以省略 INNER ,只写一个 JOIN。
举例:我们需要查询出所有的商品及对应的供应商信息:
SELECT vend_name,prod_name,prod_price
FROM vendors INNER JOIN products
ON vendors.vend_id = products.vend_id
-- 查询结果
+-------------+----------------+------------+
| vend_name | prod_name | prod_price |
+-------------+----------------+------------+
| ACME | Bird seed | 10.00 |
| ACME | Carrots | 2.50 |
| ACME | Detonator | 13.00 |
| ACME | Safe | 50.00 |
| ACME | Sling | 4.49 |
| ACME | TNT (1 stick) | 2.50 |
| ACME | TNT (5 sticks) | 10.00 |
| Anvils R Us | .5 ton anvil | 5.99 |
| Anvils R Us | 1 ton anvil | 9.99 |
| Anvils R Us | 2 ton anvil | 14.99 |
| Jet Set | JetPack 1000 | 35.00 |
| Jet Set | JetPack 2000 | 55.00 |
| LT Supplies | Fuses | 3.42 |
| LT Supplies | Oil can | 8.99 |
+-------------+----------------+------------+
上面语句等价于:
SELECT vend_name,prod_name,prod_price
FROM vendors,products
WHERE vendors.vend_id = products.vend_id
多表联结:SQL对⼀一条SELECT语句句中可以联结的表的数目没有限制。
SELECT 表名1.字段名1,表名2.字段名2...
FROM 表名1
INNER JOIN 表名2 ON 表名1.字段名3 = 表名2.字段名3
INNER JOIN 表名3 ON 表名2.字段名4 = 表名3.字段名4
...
(WHERE 过滤条件);
-- ON 后面跟联结条件
1.2.2.2 自联结
自联结在查询一些特定数据时可以作为子嵌套查询的替代
这时候,就需要自己与自己进行联结,为了区分,需要用AS取一个别名
举例:
假如你发现某物品(其ID为DTNTR)存在问题,因此想知道生产该物品的供应商生产的其他物品是否也存在这些问题。此查询要求首先找到生产ID为DTNTR的物品的供应商,然后找出这个供应商生产的其他物品。
-- 1.自联结 JOIN
SELECT p1.prod_id,p1.prod_name
FROM products AS p1
JOIN products AS p2
ON p1.vend_id = p2.vend_id
WHERE p2.prod_id = 'DTNTR';
-- 2.自联结 WHERE
SELECT p1.prod_id,p1.prod_name
FROM products AS p1,products AS p2
WHERE p1.vend_id = p2.vend_id AND p2.prod_id = 'DTNTR';
-- 3.子查询
SELECT prod_id,prod_name
FROM products
WHERE vend_id = (SELECT vend_id FROM products WHERE prod_id = 'DTNTR');
-- 查询结果
+---------+----------------+
| prod_id | prod_name |
+---------+----------------+
| DTNTR | Detonator |
| FB | Bird seed |
| FC | Carrots |
| SAFE | Safe |
| SLING | Sling |
| TNT1 | TNT (1 stick) |
| TNT2 | TNT (5 sticks) |
+---------+----------------+
自联结通常作为外部语句用来替代从相同表中检索数据时使用的子查询语句。虽然最终的结果是相同的,但有时候处理联结远比处理子查询快得多。
1.2.2.3 外部联结——LEFT/RIGHT JOIN
- 左联结 LEFT JOIN:以左侧表为基准,去关联右侧的表进行联结,如果有未关联的数据,那么结果为null
- 右联结 RIGHT JOIN:以右侧表为基准,去关联左侧的表进行联结,如果有未关联的数据,那么结果为null
例如:我们要对每个客户下了多少订单进行计数,包括那些至今尚未下订单的客户;
-- 1.内部联结。它检索所有客户及其订单:
SELECT customers.cust_id,orders.order_num
FROM customers INNER JOIN orders
ONcustomers.cust_id = orders.cust_id;
+---------+-----------+
| cust_id | order_num |
+---------+-----------+
| 10001 | 20005 |
| 10001 | 20009 |
| 10003 | 20006 |
| 10004 | 20007 |
| 10005 | 20008 |
+---------+-----------+
--
明显这种方式不能达到我们的要求,它不包含至今尚未下订单的客户 10002 -- 2.外部联结。检索所有客户,包括那些没有订单的客户
SELECT customers.cust_id,orders.order_num
FROM customers LEFT JOIN orders
ON customers.cust_id = orders.cust_id;
-- 也可以写成这样
SELECT customers.cust_id,orders.order_num
FROM orders RIGHT JOIN customers
ON customers.cust_id = orders.cust_id;
-- 查询结果
+---------+-----------+
| cust_id | order_num |
+---------+-----------+
| 10001 | 20005 |
| 10001 | 20009 |
| 10002 | NULL |
| 10003 | 20006 |
| 10004 | 20007 |
| 10005 | 20008 |
+---------+-----------+
-- 加入订单量计数
SELECT customers.cust_id,customers.cust_name,COUNT(orders.order_num) AS num_ord
FROM customers LEFT JOIN orders
ON customers.cust_id = orders.cust_id
GROUP BY customers.cust_id;
-- 查询结果
+---------+----------------+---------+
| cust_id | cust_name | num_ord |
+---------+----------------+---------+
| 10001 | Coyote Inc. | 2 |
| 10002 | Mouse House | 0 |
| 10003 | Wascals | 1 |
| 10004 | Yosemite Place | 1 |
| 10005 | E Fudd | 1 |
+---------+----------------+---------+
-- 列出所有产品以及订购数量量,包括没有⼈人订购的产品;
select products.prod_id,products.prod_name,count(orderitems.order_num) AS num_ord
from products left join orderitems
on products.prod_id = orderitems.prod_id
group by products.prod_id;
表联结注意事项
- 保证使⽤用正确的联结条件,否则将返回不不正确的数据。
- 应该总是提供联结条件,否则会得出笛卡⼉儿积。
- 在一个联结中可以包含多个表,甚至对于每个联结可以采⽤用不同的联结类型。虽然这样做是合法的,一般也很有用,但应该在一起测试它们前,分别测试每个联结。这将使故障排除更为简单
1.3 组合查询
MySQL也允许执行多个查询(多条SELECT语句句),并将结果作为单个查询结果集返回。这些组合查询通常称为并(union)或复合查询(compound query)。
UNION规则
- UNION必须由两条或两条以上的SELECT语句组成,语句之间用关键字UNION分隔(因此,如果组合4条SELECT语句句,将要使用3个UNION关键字)。
- UNION中的每个查询必须包含相同的列、表达式或聚集函数(不过各个列不需要以相同的次序列出)
- 列数据类型必须兼容:类型不必完全相同,但必须是DBMS可以隐含地转换的类型(例如,不同的数值类型或不同的日期类型)。
举例:假如需要价格小于等于5的所有物品的一个列表,而且还想包括供应商1001和1002生产的所有物品。
SELECT vend_id,prod_id,prod_price FROM products WHERE prod_price <= 5
UNION
SELECT vend_id,prod_id,prod_price FROM products WHERE vend_id in(1001,1002);
-- 查询结果
+---------+---------+------------+
| vend_id | prod_id | prod_price |
+---------+---------+------------+
| 1003 | FC | 2.50 |
| 1002 | FU1 | 3.42 |
| 1003 | SLING | 4.49 |
| 1003 | TNT1 | 2.50 |
| 1001 | ANV01 | 5.99 |
| 1001 | ANV02 | 9.99 |
| 1001 | ANV03 | 14.99 |
| 1002 | OL1 | 8.99 |
+---------+---------+------------+
--
UNION会从查询结果集中自动去除了重复的行 这是UNION的默认行为,但是如果需要,可以改变它。
-- 如果想返回所有匹配的行,可使用UNION ALL
SELECT vend_id,prod_id,prod_price FROM products WHERE prod_price <= 5
UNION ALL
SELECT vend_id,prod_id,prod_price FROM products WHERE vend_id in(1001,1002);
-- 结果
+---------+---------+------------+
| vend_id | prod_id | prod_price |
+---------+---------+------------+
| 1003 | FC | 2.50 |
| 1002 | FU1 | 3.42 | -- 重复数据
| 1003 | SLING | 4.49 |
| 1003 | TNT1 | 2.50 |
| 1001 | ANV01 | 5.99 |
| 1001 | ANV02 | 9.99 |
| 1001 | ANV03 | 14.99 |
| 1002 | FU1 | 3.42 | -- 重复数据
| 1002 | OL1 | 8.99 |
+---------+---------+------------+
对组合查询结果排序
SELECT语句的输出用ORDER BY子句排序。在⽤UNION组合查询时,只能使⽤一条ORDER BY子句,它必须出现在最后一条SELECT语句之后。ORDER BY子句是对整个结果集排序
SELECT vend_id,prod_id,prod_price FROM products WHERE prod_price <= 5
UNION
SELECT vend_id,prod_id,prod_price FROM products WHERE vend_id in(1001,1002)
ORDER BY prod_price;
-- 查询结果
+---------+---------+------------+
| vend_id | prod_id | prod_price |
+---------+---------+------------+
| 1003 | FC | 2.50 |
| 1003 | TNT1 | 2.50 |
| 1002 | FU1 | 3.42 |
| 1003 | SLING | 4.49 |
| 1001 | ANV01 | 5.99 |
| 1002 | OL1 | 8.99 |
| 1001 | ANV02 | 9.99 |
| 1001 | ANV03 | 14.99 |
+---------+---------+------------+
2. MySQL事务
事务(Transaction)是由⼀系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元。
事务的基本语法:
start transaction;或者 begin;
-- 事务操作的语句
commit; -- 使得当前的修改确认
rollback; -- 使得当前的修改被放弃
2.1 事务的ACID特性
- 原子性(Atomicity)
事务的原子性是指事务必须是一个原子的操作序列单元。事务中包含的各项操作在一次执行过程中,要么全部执行成功,要么全部执行失败。
事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,
会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体。 - 一致性(Consistency)
事务的一致性是指事务的执行不能破坏数据库数据的完整性和一致性,不能时数据发生错乱 - 隔离性(Isolation)
事务的隔离性是指在并发环境中,并发的事务是互相隔离的,各有各自的数据空间。 - 持久性(Duration)
事务的持久性是指事务一旦提交后,数据库中的数据必须被永久的保存下来。即使服务器系统崩溃或服务器宕机,只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态。
2.2 事务并发常见问题
- 脏读(Dirty Read):读取到了没有提交的数据。
例如:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据就是脏数据。 - 不可重复读(Unrepeatale Read):同⼀条命令返回不同的结果集
例如:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。 - 幻读(Phantom Read):重复查询的过程中,数据就发生了量的变化。
2.3 事务的隔离级别
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交(READ_UNCOMMITTED) | 允许 | 允许 | 允许 |
读已提交(READ_COMMITTED) | 禁止 | 允许 | 允许 |
可重复读(REPEATABLE_READ) | 禁止 | 禁止 | 可能会 |
顺序读(SERIALIZABLE) | 禁止 | 禁止 | 禁止 |
4种事务隔离级别从上往下,级别越高,并发性越差,安全性就越高。 一般数据默认级别是读已提交或可重复读。
查看和设置事务隔离级别的语句
-- 查看当前会话中事务的隔离级别 select @@tx_isolation; -- 设置当前会话中的事务隔离级别 set session transaction isolation level read uncommitted | read committed | repeatable read | SERIALIZABLE
- 读未提交(READ_UNCOMMITTED)
该隔离级别允许脏读取,其隔离级别是最低的。一个事务正在处理某⼀数据,并对其进行了更新,但同时尚未完成事务,因此还没有提交事务,而与此同时,允许另一个事务也能够访问该数据。 - 读已提交(READ_COMMITTED)
不同的事务执行的时候只能获取到已经提交的数据。 这样就不会出现上面的脏读的情况了。但是在同一个事务中执行同一个读取,结果不一致,即不可重复读问题 - 可重复读(REPEATABLE_READ)
保证在事务处理过程中,多次读取同一个数据时,该数据的值和事务开始时刻是一致的。因此该事务级别限制了不可重复读和脏读,但是有可能出现幻读的数据。 - 顺序读(SERIALIZABLE)
最严格的事务隔离级别,不允许事务并发,必须依次排序执行。
不同的隔离级别的锁的情况
- 读未提交(RU): 有行级的锁,没有间隙锁。它与RC的区别是能够查询到未提交的数据。
- 读已提交(RC):有行级的锁,没有间隙锁,读不到没有提交的数据。
- 可重复读(RR):有行级的锁,也有间隙锁,每次读取的数据都是一样的,并且没有幻读的情况。
- 序列列化(S):有行级锁,也有间隙锁,读表的时候,就已经上锁了。
3. MySQL存储过程
存储过程简单来说,就是为以后的使用而保存的一条或多条MySQL语句的集合。
存储过程就像脚本语⾔言中函数定义⼀样,是一组为了完成特定功能的SQL语句集,经过编译之后存储在数据库中,在需要时直接调用。
创建存储过程 :create procedure 存储过程名()
\d //
:修改MySQL默认的语句结尾符;
改为//
。
create procedure 存储过程名()
创建语句
BEGIN
和END
语句用来限定存储过程体
-- 定义存储过程 向users表中循环插入80条数据
\d //
create procedure p1()
begin
set @i=10;
while @i<90 do
insert into users values(null,concat('user:',@i),@i,0); --
set @i=@i+1;
end while;
end;
//
调用存储过程: call 存储过程名()
查看存储过程:show create procedure 存储过程名\G
删除存储过程:drop procedure 存储过程名
4. MySQL触发器
触发器是指MySQL响应写操作(增、删、改)而自动执行的一条或一组定义在BEGIN和END之间的MySQL语句。
或可理理解为:提前定义好一个或一组操作,在指定的SQL操作前或后来触发指定的SQL自动执行,触发器就像是JavaScript中的事件一样。
触发器语法:
-- 创建触发器
\d //
CREATE TRIGGER trigger_name trigger_time trigger_event
ON tbl_name FOR EACH ROW
BEGIN
trigger_stmt;
END;
//
\d ; -- 结束之后要把语句结束符改回来
-- 说明:
# trigger_name:触发器名称
# trigger_time:触发时间,可取值:BEFORE或AFTER
# trigger_event:触发事件,可取值:INSERT、UPDATE或DELETE。
# tb1_name:指定在哪个表上
# trigger_stmt:触发理SQL语句。
-- 查看所有的 触发器器
show triggers\G;
-- 删除触发器
drop trigger trigger_name;
在使用触发器时,可以建立虚拟表来方便某些操作
- 在INSERT触发器代码内,可引用⼀个名为NEW的虚拟表,访问被 插入的行;
- 在DELETE触发器代码内,可引用一个名为OLD的虚拟表,访问被删除的行;
- OLD中的值全都是只读的,不能更新。
- 在AFTER DELETE的触发器中无法获取OLD虚拟表
- 在UPDATE触发器代码中
- 可以引用一个名为OLD的虚拟表访问更新以前的值
- 可以引用一个名为NEW的虚拟表访问新更新的值;
5. MySQL的视图
视图的定义及特性
视图是虚拟的表。与包含数据的表不一样,视图只包含使用时动态检索数据的查询语句。
视图本身不不包含数据,它们返回的数据是从其他表中检索出来的。
视图的作用
- 重用SQL语句,简化复杂的SQL操作。在编写查询后,可以方便地重用它而不必知道它的基本查询细节。
- 保护数据。可以给用户授予表的特定部分的访问权限而不是整个表的访问权限。
- 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。
注意:视图不能索引,也不不能有关联的触发器或默认值。
视图的语法和使用
-- 创建视图
create view 视图名 as 要封装的select语句;
-- 例如:
create view v_users as select id,name,age from users where age >= 25 and age<= 35;
-- 查看当前库中所有的视图
show tables; -- 可以查看到所有的表和视图
show table status where comment='view'\G; -- 只查看当前库中的所有视图
-- 删除视图v_t1:
drop view 视图名;
6.MySQL索引与优化
6.1 索引概述
定义
MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。
索引类似图书的目录,是一种有序结构,可以提高数据检索的效率,降低数据库的IO成本。
分类
索引类型 | 说明 |
主键索引 | 根据主键建立的索引。不允许重复,不允许空值。 如果表中没有定义主键,InnoDB 会选择一个唯⼀的非空索引代替;如果没有这样的索引,InnoDB 会隐式定义一个主键来作为聚簇索引。 |
唯一索引 | 用来建立索引的列的值必须是唯一的,允许空值 |
普通索引 | 用表中普通列构建的索引,没有任何限制 |
全文索引 | 用大文本对象的列构建的索引(5.6之前MyISAM支持,InnoDB不支持;8.0之前不支持中文。) |
组合索引 | 用多个列组合构建的索引,这多个列的值不允许空值,需遵循“最左”原则 |
-- 创建索引和删除索引
-- 1.主键索引
-- 1.创建表时,直接创建主键索引 PRIMARY KEY
CREATE TABLE users(
id int NOT NULL AUTO_INCREMENT PRIMARY KEY
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2.修改时添加主键和自增
alter table users modify uid int primary key AUTO_INCREMENT;
-- 删除主键索引注意需要先取消自增,再删除主键
-- 先取消自增,修改字段
alter table users modify uid int;
-- 删除主键
alter table users drop primary key;
-- 2.唯一索引
-- 1.创建表时,直接创建唯一索引UNIQUE KEY
CREATE TABLE users(
name varchar(10) NOT NULL ,
UNIQUE KEY name(name), -- name为索引名
)ENGINE=InnoDB DEFAULT CHARSET-utf8mb4
-- 2.添加唯一索引UNIQUE当前列要求唯一,但允许为空
alter table users add untque u_name(name); -- u_name为索引名
-- 删除唯一索引
alter table users drop index u_name;
-- 3.普通索引
-- 1.创建表时,直接创建普通索引 KEY
CREATE TABLE users(
email varchar ( 10 ) NOT NULL ,
KEY index_email(email) -- index_email为索引名
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
-- 2.添加索引
alter table users add index in_email(email);
-- 删除普通索引
drop index in_email on users;
-- 4.添加全文索引
ALERT TABLE 表名 ADD FULLTEXT INDEX 索引名(要建立索引的字段);
-- 5.组合索引
-- 添加索引
alter table users add index in_x(email,phone,uname)
-- 删除索引
alter table users drop index in_x;
6.2 索引原理-索引与B+Tree
哈希索引
只有memory(内存)存储引擎支持哈希索引,哈希索引用索引列的值计算该值的hashCode,然后在hashCode相应的位置存执该值所在行数据的物理位置,因为使用散列算法,因此访问速度非常快,但是一个值只能对应一个hashCode,而且是散列的分布方式,因此哈希索引不支持范围查找和排序的功能。
正常情况下,如果不指定索引的类型,那么一般是指B+Tree索引。
存储引擎以不同的方式使用B+Tree索引。性能也各有不同,InnoDB时按照原数格式进行存储的。
首先,要了解B+树之前,我们先了解一下B树,B树是一种自平衡树状结构,一般多用于存储系统上,比如数据库或文件系统。
一个n阶的B树,每个节点可以存储n-1个值
可以看到B树每个叶子节点都带有数据,在效率上还是存在一些弊端,可以进行优化,提高查询效率,便演变出了B+树,如下图所示
使用B+树的优势
- 磁盘读写代价更低
B树的数据和索引都在同一个节点上,那么每个块中包含的索引是少量的,如果想要取出比较深层的数据,意味着要读取更多的块,才能得到想要的索引和数据,那么就增加了IO次数;
而B+树中每个块能存储的索引是B树的很多倍,那么获取比较深层的数据,也只需要读取少量的块
就可以,那么就减少了磁盘的IO次数 - 随机IO的次数更少
随机I/O是指读写操作时间连续,但访问地址不连续,时长约为10ms。
顺序I/O是指读取和写入操作基于逻辑块逐个连续访问来自相邻地址的数据,时长约为0.1ms
在相同情况下,B树要进行更多的随机IO,而B+树需要更多的顺序IO,因此B+树,效率也更快 - 查询速度更稳定
由于B+Tree非叶子节点不存储数据(data),因此所有的数据都要查询至叶子节点,而叶子节点的
高度都是相同的,因此所有数据的查询速度都是一样的。
6.3 聚簇索引和非聚簇索引
聚簇索引:索引即数据,找到索引便找到了数据
非聚簇索引:索引是一个文件,数据在另一个文件,根据索引文件找到数据的key值,然后到数据文件中根据key值找到数据
MyISAM的索引方案
InnoDB中索引即数据,也就是聚簇索引的那棵B+树的叶子节点中已经把所有完整的用户记录都包含了了,而MyISAM的索引方案虽然也使用树形结构,但是却将索引和数据分开存储,也就是把索引信息单独存到一个文件中,这个文件称为索引文件。
MyISAM会单独为表的主键创建⼀个索引,只不过在索引的叶子节点中存储的不是完整的数据记录,而是主键值 + 行号的组合。也就是先通过索引找到对应的行号,再通过行号去找对应的记录!其它非主键索引也是一样的,这种情况我们称为“回行”。所以在MyISAM中所有的索引都是非聚簇索引,也叫二级索引。
MyISAM和InnoDB的区别
- 数据存储方式:
- InnoDB由两种文件组成,表结构、数据和索引
- MyISAM由三种文件组成,表结构、数据、索引
- 索引的方式:
- 索引的底层都是基于B+Tree的数据结构建立
- InnoDB中主键索引为聚簇索引,辅助索引是非聚簇索引
- MyISAM中数据和索引存在不同的文件中,因此都是非聚簇索引
- 事务的支持:
- InnoDB支持事务
- MyISAM不支持事务
6.4 慢查询与SQL优化
6.4.1 慢查询与Explain执行计划
MySQL的慢查询,全名是慢查询日志,是MySQL提供的一种日志记录,用来记录在MySQL中响应时间超过阀值的语句。
默认情况下,MySQL数据库并不启动慢查询目思,需要手动来设置这个参数。如果不是调优需要的话,一般不建议启动该参数,开启慢查询日志会或多或少带来一定的性能影响。
Explain执行计划
一条查询语句在经过MySQL查询优化器的各种基于成本和规则的优化会后生成一个所谓的执行计划。这个执行计划展示了接下来具体执行查询的方式,比如多表连接的顺序是什么,对于每个表采用什么访问方法来具体执行查询等等。MySQL为我们提供了EXPLAIN语句来帮助我们查看某个语句的具体执行计划。
参数 | 参数解释 |
id | 在一个大的查询语句中每个 SELECT关键字都对应一个唯一的 id |
select_type | SELECT关键字对应的那个查询的类型 |
table | 表名 |
partitions | 匹配的分区信息 |
type | 针对单表的访问方法 |
possible_keys | 可能用到的索引 |
key | 实际上使用的索引 |
key_len | 实际使用到的索引长度 |
ref | 当使用索引列等值查询时,与索引列进行等值匹配的对象信息 |
rows | 预估的需要读取的记录条数 |
filtered | 某个表经过搜索条件过滤后剩余记录条数的百分比 |
Extra | 一些额外的信息 |
6.4.2 SQL优化
索引优化
- 适当建立索引
- 创建并使用自增数字来建立主键索引
- 为经常作为where条件的字段建立索引
- 添加索引的字段尽可能的保持唯一性
- 可考虑使用组合索引并进行索引覆盖(多个字段组合成一个联合索引,在查询时,所要的字段和查询条件中的索引是一致的)
- 索引绝不是加的越多越好,每建立一个索引都会建立一棵B+树,并且需要维护,很费性能和存储空间。
- 合理使用索引,查询时避免索引失效
- 不要在查询的索引列上使用函数
- 不要在查询的索引列上进行运算
- 避免查询条件左右类型不匹配发生隐式转换
- 使用like模糊查询时避免通配符%放在第一位
- 多个单列索引并不是最佳选择,可以使用组合索引
- 使用组合查询时谨记最左前缀原则(从最左字段开始使用索引:查询条件中使用了组合索引的第一个字段,索引才会被使用。因此,在组合索引中索引列的顺序至关重要。如果不是按照索引的最左列开始查找,则无法使用索引。)
SQL语句的优化
- 避免嵌套语句(子查询)
- 避免多表查询(复杂查询简单化)