一、索引原理
什么是索引
索引是存储引擎用于快速找到记录的一种数据结构。可以联想到字典中的目录。
索引的分类
(1) Hash 索引
Hash 索引是比较常见的一种索引,他的单条记录查询的效率很高,时间复杂度为1。但是,Hash索引并不是最常用的数据库索引类型,尤其是我们常用的Mysql Innodb引擎就是不支持hash索引的。主要有以下原因:
Hash索引适合精确查找,但是范围查找不适合,因为存储引擎都会为每一行计算一个hash码,hash码都是比较小的,并且不同键值行的hash码通常是不一样的,hash索引中存储的就是Hash码,hash 码彼此之间是没有规律的;
且 Hash 操作并不能保证顺序性,所以值相近的两个数据,Hash值相差很远,被分到不同的桶中。这就是为什么hash索引只能进行全职匹配的查询,因为只有这样,hash码才能够匹配到数据。
(2) 二叉树
先来介绍下最经典的二叉树的特点:
- 二叉树的时间复杂度为 O(n)
- 一个节点只能有两个子节点。即度不超过2
- 左子节点 小于 本节点,右子节点 大于 本节点
但是在极端情况下会出现链化的情况,即节点一直在某一边增加。
平衡二叉树(Balanced Binary Tree,简称 ABT)是一种特殊的二叉树,其中每个节点的左右子树的高度之差的绝对值不超过1,并且它的左子树和右子树都是平衡二叉树。平衡二叉树的特点:
- 根节点会随着数据的改变而变更
- 数据量越多,遍历次数越多,IO次数就越多,就越慢(磁盘的IO由树高决定)
(3)B-树
B-树是一棵多路平衡查找树,对于一棵M阶的B-树有以下的性质:
- 根节点至少有两个子女.
- 每个节点包含k-1个元素和k个孩子,其中m/2 <= k <= m.
- 每一个叶子节点都包含k-1个元素,其中m/2 <= k <= m.
- 所有的叶子节点位于同一层.
- 每个节点中的元素从小到大排列,那么k-1个元素正好是k个孩子包含的值域的划分.
可以将B-树理解为一棵更加矮胖的二叉搜索树.
二叉搜索树(Binary Search Tree,简称 BST),是一种特殊的二叉树,其中每个节点的左子树的值都小于该节点的值,而每个节点的右子树的值都大于该节点的值。
(4)B+树
MySQL 中最常用的索引的数据结构是 B+ 树。B+树是B-树的进阶版本,在B-树的基础上又做了如下的限制:
- 每个中间节点不保存数据,只用来索引,也就意味着所有非叶子节点的值都被保存了一份在叶子节点中.
- 叶子节点之间根据自身的顺序进行了链接.
这样可以带来什么好处呢?
- 中间节点不保存数据,那么就可以保存更多的索引,树的层级更少,减少数据库磁盘IO的次数.
- 因为中间节点不保存数据,所以每一次的查找都会命中到叶子节点,而叶子节点是处在同一层的,因此查询的性能更加的稳定.
- 所有的叶子节点按顺序链接成了链表,因此可以方便的话进行范围查询.
聚簇索引
聚簇索引不是一种索引类型,而是一种存储数据的方式。Innodb的聚簇索引是在同一个数据结构中同时保存了索引和数据.
mysql中,主键索引页+数据页组成的B+树就是聚簇索引。聚簇索引中数据页记录的是一条记录的完整的记录。
MySQL 在存储数据的时候是以数据页为最小单位的,且数据在数据页中的存储是连续的,数据页中的数据是按照主键排序的(没有主键是由 MySQL自己维护的 ROW_ID 来排序的),数据页和数据页之间是通过双向链表来关联的,数据与数据时间是通过单向链表来关联的。
也就是说有一个在每个数据页中,他必然就有一个最小的主键,然后每个数据页的页号和最小的主键会组成一个主键目录,假设现在要查找主键为 2 的数据,通过二分查找法最后确定下主键为 2 的记录在数据页 1 中,此时就会定位到数据页 1 接着再去定位主键为 2 的记录。
假设上面的主键目录中的记录是非常非常多的,此时MySQL 会将索引里面的记录拆分到不同的索引页中,最终演变成这个样子:
来自:https://zhuanlan.zhihu.com/p/394429932
刚刚上面是说的其实可以理解为是主键索引,主键索引也是最简单的最基础的索引。所以说建立了主键查询就能变快了。
如何查看索引的一些相关信息?
(1)索引信息
在mysql中,可以使用show create table table_name
来查看建表语句,其中包含创建索引的语句:
可以使用show index from table_name
来查看某个表上的索引,它将会有如下的输出:
可以看到,第一行默认使用了id 作为主索引。B+树在新增数据时,会根据主索引进行重整,影响性能,因此InnoDB推荐以自增id作为主索引:自增且连续,在插入的时候只需要不断的在数据后面追加即可。
后两行使用了联合索引,使用联合索引可以在多个列上进行快速的筛选和排序,特别适用于需要同时查询多个列的情况。
在创建联合索引时,需要注意以下几点:
- 列的顺序很重要:联合索引的效果与列的顺序有关。通常,将最常用于查询的列放在前面可以提高索引的效率。
- 列的选择要合理:不是所有的列都适合创建联合索引,只有在经常用于查询和筛选的列上创建索引才能发挥最大的作用。
- 索引的大小要控制:联合索引的大小会影响插入和更新的性能,因此需要根据实际情况控制索引的大小。
如果需要查询的字段不在联合索引中,MySQL 就需要进行回表查询了,如:
SELECT user_id,product_name,price FROM orders WHERE user_id='2' and product_name='Apple watch';
此时 MySQL 会再次根据主键从聚簇索引的根节点开始查找,这个过程就叫回表。
另外,每建立1个索引,MySQL 就会多维护1个B+树,所以不能建立太多索引,因为索引也会占用空间。
(2)索引大小
在5.0以后的版本中,我们可以通过查看information_schema.TABLES
表中的数据来获取更加详细的数据.
该表各字段的含义如下表:
我们可以通过一些查询语句来获取详细的信息,比如:
// 查看当前MySQL服务器所有索引的大小(以MB为单位,默认是字节)
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024), 2), ' MB') AS 'Total Index Size' FROM TABLES
// 查看某一个库的所有大小
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024), 2), ' MB') AS 'Total Index Size' FROM TABLES WHERE table_schema = 'XXX';
// 查看某一个表的索引大小
SELECT CONCAT(ROUND(SUM(index_length)/(1024*1024), 2), ' MB') AS 'Total Index Size' FROM TABLES WHERE table_schema = 'yyyy' and table_name = "xxxxx";
// 汇总查看一个库中的数据大小及索引大小
SELECT CONCAT(table_schema,'.',table_name) AS 'Table Name', CONCAT(ROUND(table_rows/1000000,4),'M') AS 'Number of Rows', CONCAT(ROUND(data_length/(1024*1024*1024),4),'G') AS 'Data Size', CONCAT(ROUND(index_length/(1024*1024*1024),4),'G') AS 'Index Size', CONCAT(ROUND((data_length+index_length)/(1024*1024*1024),4),'G') AS'Total'FROM information_schema.TABLES WHERE table_schema LIKE 'xxxxx';
注意:上面的表格是有缓存的,当更新数据库索引之后,最好执行analyze table xxxx
,然后再进行查看.MySQL会在表格数据发生较大的变化时才更新此表(大小变化超过1/16或者插入20亿行).
(3) 索引碎片
在索引的创建删除过程中,不可避免的会产品索引碎片,当然还有数据碎片,我们可以通过执行optimize table xxx
来重新整理索引及数据,对于不支持此命令的存储引擎来说,可以通过一条无意义的alter语句来触发整理,比如:将表的存储引擎更换为当前的引擎,alter table xxxx engine=innodb
.
二、优化方式
1,系统优化:
-
硬件:使用好的硬件,更快的硬盘、大内存、多核CPU,专业的存储服务器(NAS、SAN)
-
架构:设计合理架构,如果 MySQL 访问频繁,考虑 Master/Slave 读写分离;数据库分表、数据库切片(分布式),也考虑使用相应缓存服务帮助 MySQL 缓解访问压力
2, 服务优化
(1)配置合理的MySQL服务器,尽量在应用本身达到一个MySQL最合理的使用
(2)针对 InnoDB 等不同引擎进行不同定制性配置
(3)针对不同的应用情况进行合理配置
公共选项:
Innodb选项:
华为云 RDS for MySQL参数调优建议:
https://support.huaweicloud.com/usermanual-rds/rds_08_00001.html
3, 应用优化
(1) 设计合理的数据表结构:适当的数据冗余
(2)对数据表建立合适有效的数据库索引
(3)数据查询:编写简洁高效的SQL语句
表结构设计原则
- 选择合适的数据类型:如果能够定长尽量定长
- 使用 ENUM 而不是 VARCHAR,ENUM类型是非常快和紧凑的,在实际上,其保存的是 TINYINT,但其外表上显示为字符串。这样一来,用这个字段来做一些选项列表变得相当的完美 。
- 不要使用无法加索引的类型作为关键字段,比如 text类型
- 为了避免联表查询,有时候可以适当的数据冗余,比如 邮箱、姓名这些不容易更改的数据
- 选择合适的表引擎,有时候 MyISAM 适合,有时候 InnoDB适合
- 为保证查询性能,最好每个表都建立有 auto_increment 字段, 建立合适的数据库索引
- 最好给每个字段都设定 default 值
索引建立原则
- 一般针对数据分散的关键字进行建立索引,比如ID、QQ,像性别、状态值等等建立索引没有意义
- 字段唯一,最少,不可为null
- 对大数据量表建立聚集索引,避免更新操作带来的碎片。
- 尽量使用短索引,一般对int、char/varchar、date/time 等类型的字段建立索引
- 需要的时候建立联合索引,但是要注意查询SQL语句的编写
- 谨慎建立 unique 类型的索引(唯一索引)
- 大文本字段不建立为索引,如果要对大文本字段进行检索,可以考虑全文索引
- 频繁更新的列不适合建立索引
- order by 字句中的字段,where 子句中字段,最常用的sql语句中字段,应建立索引。
- 唯一性约束,系统将默认为改字段建立索引。
- 对于只是做查询用的数据库索引越多越好,但对于在线实时系统建议控制在5个以内。
- 索引不仅能提高查询SQL性能,同时也可以提高带where字句的update,Delete SQL性能。
- Decimal 类型字段不要单独建立为索引,但覆盖索引可以包含这些字段。
- 只有建立索引以后,表内的行才按照特地的顺序存储,按照需要可以是asc或desc方式。
- 如果索引由多个字段组成将最常用来查询过滤的字段放在前面可能会有更好的性能。
编写高效的 SQL
- 能够快速缩小结果集的 WHERE 条件写在前面,如果有恒量条件,也尽量放在前面
- 尽量避免使用 GROUP BY、DISTINCT 、OR、IN 等语句的使用,避免使用联表查询和子查询,因为将使执行效率大大下降
- 能够使用索引的字段尽量进行有效的合理排列,如果使用了联合索引,请注意提取字段的前后顺序
- 针对索引字段使用 >, >=, =, <, <=, IF NULL和BETWEEN 将会使用索引, 如果对某个索引字段进行 LIKE 查询,使用 LIKE ‘%abc%’ 不能使用索引,使用 LIKE ‘abc%’ 将能够使用索引
- 如果在SQL里使用了MySQL部分自带函数,索引将失效,同时将无法使用 MySQL 的 Query Cache,比如 LEFT(), SUBSTR(), TO_DAYS() DATE_FORMAT(), 等,如果使用了 OR 或 IN,索引也将失效
- 使用 Explain 语句来帮助改进我们的SQL语句
- 不要在where 子句中的“=”左边进行算术或表达式运算,否则系统将可能无法正确使用索引
- 尽量不要在where条件中使用函数,否则将不能使用索引
- 避免使用 select *, 只取需要的字段
- 对于大数据量的查询,尽量避免在SQL语句中使用order by 字句,避免额为的开销,替代为使用ADO.NET 来实现。
- 如果插入的数据量很大,用select into 替代 insert into 能带来更好的性能
- 采用连接操作,避免过多的子查询,产生的CPU和IO开销
- 只关心需要的表和满足条件的数据
- 适当使用临时表或表变量
- 对于连续的数值,使用between代替in
- where 字句中尽量不要使用CASE条件
- 尽量不用触发器,特别是在大数据表上
- 更新触发器如果不是所有情况下都需要触发,应根据业务需要加上必要判断条件
- 使用union all 操作代替OR操作,注意此时需要注意一点查询条件可以使用聚集索引,如果是非聚集索引将起到相反的结果
- 当只要一行数据时使用 LIMIT 1
- 尽可能的使用 NOT NULL填充数据库
- 拆分大的 DELETE 或 INSERT 语句
- 批量提交SQL语句
常用技巧
- 使用 Explain/ DESC 来分析SQL的执行情况
- 使用 SHOW PROCESSLIST 来查看当前MySQL服务器线程执行情况,是否锁表,查看相应的SQL语句
- 设置 my.cnf 中的 long-query-time 和 log-slow-queries 能够记录服务器那些SQL执行速度比较慢
- 另外有用的几个查询:SHOW VARIABLES、SHOW STATUS、SHOW ENGINES
- 使用 DESC TABLE xxx 来查看表结构,使用 SHOW INDEX FROM xxx 来查看表索引
- 使用 LOAD DATA 导入数据比 INSERT INTO 快多了
- SELECT COUNT(*) FROM Tbl 在 InnoDB 中将会扫描全表
- Explain 使用。 语法:EXPLAIN SELECT select_options
Type: 类型,是否使用了索引还是全表扫描, const,eg_reg,ref,range,index,ALL
Key: 实际使用上的索引是哪个字段
Ken_len: 真正使用了哪些索引,不为 NULL 的就是真实使用的索引
Ref: 显示了哪些字段或者常量被用来和 key 配合从表中查询记录出来
Rows: 显示了MySQL认为在查询中应该检索的记录数
Extra: 显示了查询中MySQL的附加信息,关心Using filesort 和 Using temporary,性能杀手
参考:
https://zhuanlan.zhihu.com/p/394429932
https://zhuanlan.zhihu.com/p/76355753