MySQL的索引和查询缓存

MySQL的索引:

  • 索引:定义在查找时作为查找条件的字段上,构建出的一个独特的数据结构;
    • 基本法则:应该构建在经常被用作查询条件的字段上
    • 作用:加速查询操作
    • 副作用:占用额外空间,降低写操作性能
    • 索引实现在存储引擎
      • 表中数据子集:把表中某个或某些字段的数据提取出来另存为一个特定数据结构组织的数据
      • 某个字段或某些字段:WHERE子集中用到的字段
  • 索引的类型:B+ TREE,HASH
    • B+ TREE:
      • 顺序存储:
        常见的数据存储格式有两种:一种是线性存储也即顺序的,第二种叫堆存储,杂乱无章的存储;
        B树索引指定就是平衡树索引,它存在的目的:是能够加快数据访问速度;如果没有索引,一次数据查询过程就要全表扫描实现;但有了B树索引,首先查询过程就变成了,先去查询全表扫描根节点,注意是从索引的根节点进行开始,再找出二级节点,从根节点找出一个指针,而后找出下一节点即二级分支节点找出后,再找三级分支节点,最终找到叶子节点;而叶子节点则有可能是指向最终数据所存放的数据块的指针,从而找到数据;
      • 每个叶子节点到根节点的距离相同:
        对于B树索引,每一个分支节点都两部分组成,一部分存储的是自己的数据是什么、下一级节点的指针指向的位置,另一部分是一个指针,指向了同一级节点的下一个节点的位置;这些都是在内存空间中存储的内容;
        所以在每一个层级上,它们都是独立的由左而右按顺序存储完成以后,一个链表结构的数据;
        根节点和二级、三级节点都是稀疏格式的索引,叶子节点是稠密格式的;在字段上构建了索引,字段中的每一行的值,都有叶子节点,所以找到某一叶子节点以后,这个叶子节点就对应行实际存储的位置,从而找到数据的;
      • 左前缀索引,适合于范围类型的数据查询
    • 适用于B+ TREE 索引的查询类型:全键值、键值范围或键前缀查找
      • 全值匹配:精确查找某个值;WHERE COLUMN = 'value';
      • 匹配最左前缀:只精确匹配起头部分;WHERE COLUMN LIKE 'PREFIX%';
      • 匹配范围值: 精确酦醅某一列,范围匹配另外一列;
      • 只用访问索引的查询叫做覆盖索引;index(name) SELECT Name FROM students WHERE Name LIKE 'L%';
    • 不适用B+ TREE索引:
      • 如果查询条件不是最左侧列开始,索引无效;
      • 不能跳过索引中的某列;
      • 如果查询中的某列是为范围查询,那么其右侧的列都无法再使用索引优化查询
    • Hash索引:基于哈希表实现,特别使用与值得精确匹配查询;
      • 适用场景:只支持等值比较查询
      • 不适用场景:所有非精确值查询;MySQL仅对memory存储引擎支持显示的hash索引;
  • 索引有点:
    • 降低需要扫描的数据量,减少IO次数
    • 可以帮助避免排序操作,避免使用临时表
    • 帮助将随机IO转为顺序IO
  • 高性能索引策略:
    • (1) 在WHERE 中独立使用列,尽量避免其参与运算;
    • (2) 左前缀索引:索引构建与字段的最左侧的多少个字符,要通过索引选择性来评估;
      • 索引选择性:不重复的索引和数据表的记录总数的比值;
    • (3)多列索引:AND 连接的多个查询条件更适合使用多列索引,而非多个单建索引;
    • (4) 选择合适的索引列次序,选择性最高的放左侧;
EXPLAIN来分析索引有效性

用法:EXPLAIN [explain_type] SELECT select_options

    explain_type:
        EXTEBDED | PARTITIONS

示例:MariaDB [hellodb]> EXPLAIN SELECT * FROM students\G;
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: students
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 25
            Extra: 
    1 row in set (0.00 sec)
  • id:当前查询语句中,第几个SELECT语句的编号
    复杂的查询的类型主要三种:
    • 简单子查询
    • 用于FROM中的子查询
    • 联合查询
    • 注意: 联合查询的分析结果会出现一个额外的匿名临时表
  • select_type:查询类型
    • 简单查询:SIMPLE
    • 复杂查询:
      • 简单子查询:SUBQUERY
      • 用于FROM中的子查询:DERIVED
      • 联合查询中的第一个查询:PRIMARY
      • 联合查询中的第一个查询之后的其它查询:UNION
      • 联合查询生成的临时表:UNION RESULT
  • table:查询针对的表
  • type:关系类型,或称为访问类型,即MySQL如何与查询表中的行
    • ALL:权标扫描
    • index:根据索引的顺序进行的权标扫描;但同时如果Extra列出现了“Using index”表示使用了覆盖索引
    • range:有范围限制地根据索引实现范围扫描;扫描位置始于索引中的某一项,结束于另一项;
    • ref:根据索引返回的表中匹配到某单个值的所有行(匹配给定值的行不止一个);
    • eq_ref:根据索引返回的表中匹配到某单个值的单一行,仅返回一个行,但需要与某个额外的参考值比较,而不是常数;
    • const,system:与某个常数比较,且只返回一行;
  • possiable_keys:查询中可能会用到的索引;
  • key:查询中使用的索引;
  • key_len:查询中用到的索引长度;
  • ref:在利用key字段所显示的索引完成查询操作时所引用的列或常量值;
  • rows:MySQL估计出的为找到所有的目标项而需要读取的行数;
  • Extra:额外信息
    • Using index:使用了覆盖索引进行的查询;
    • Using where:拿到数据后还要再次进行过滤;
    • Using temporary:使用了临时表以完成查询;
    • Using filesort:对结果使用了一个外部索引排序;

注意:判断索引是否有效:
type类型中,all最差-->index-->range-->ref-->eq_ref-->const,system最好
一般保持在range上,避免冗余索引,移除无用索引;

示例:
    MariaDB [hellodb]> CREATE INDEX age ON students(Age);
    Query OK, 25 rows affected (0.02 sec)
    Records: 25  Duplicates: 0  Warnings: 0
    
    MariaDB [hellodb]> EXPLAIN SELECT Name FROM students WHERE Age>100;
    +------+-------------+----------+-------+---------------+------+---------+------+------+-----------------------+
    | id   | select_type | table    | type  | possible_keys | key  | key_len | ref  | rows | Extra                 |
    +------+-------------+----------+-------+---------------+------+---------+------+------+-----------------------+
    |    1 | SIMPLE      | students | range | age           | age  | 1       | NULL |    1 | Using index condition |
    +------+-------------+----------+-------+---------------+------+---------+------+------+-----------------------+
    1 row in set (0.00 sec)
:
        MariaDB [hellodb]> EXPLAIN SELECT Name FROM students WHERE Age=100;
+------+-------------+----------+------+---------------+------+---------+-------+------+-------+
| id   | select_type | table    | type | possible_keys | key  | key_len | ref   | rows | Extra |
+------+-------------+----------+------+---------------+------+---------+-------+------+-------+
|    1 | SIMPLE      | students | ref  | age           | age  | 1       | const |    1 |       |
+------+-------------+----------+------+---------------+------+---------+-------+------+-------+
1 row in set (0.00 sec)

MariaDB [hellodb]> EXPLAIN SELECT Name FROM students WHERE StuID=3;
+------+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
| id   | select_type | table    | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+------+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
|    1 | SIMPLE      | students | const | PRIMARY       | PRIMARY | 4       | const |    1 |       |
+------+-------------+----------+-------+---------------+---------+---------+-------+------+-------+
1 row in set (0.00 sec)

查询缓存

mysql把整个查询缓存维持在内存中,内存空间的频繁的创建和回收,会导致内存碎片,类似于memcached一样,必须有个高效的内存分配回收算法,以尽可能降低内存碎片;
查询缓存是完全存储在内存中的,对整个内存空间的分配回收等,也会额外的产生系统资源消耗,此外,为了能够使用内存空间当做缓存,mysql服务器的查询缓存,还必须把一些内存空间拿来做缓存空间的元数据,例如整个空间中哪些分配了、哪些没分配等等,内置需要维护一个追踪表以始终得知哪些空间仍在在使用,哪些空间没被使用,一旦要有可能产生碎片时,怎么能降低碎片的产生等等;
不是所有场景中缓存查询都能提高性能的;缓存和失效都会带来额外开销,所以只有当带来的收益大于开销时,才使用查询缓存;
可通过缓存命中率来判断缓存是否足够有效;而命中率除了查询语句的命中率之外,还有结果集的命中率;虽然查询语句10个里,只有2个命中,但是这2个里结果集都很大处理起来都很慢,这个命中仍然认为收益是较大的;所以,也不能简单的以为语句命中率少就一定是命中率低;
内存空间不足,数据修改等都会造成缓存失效,如果配置了足够的缓存空间,而且对应的参数设置合理,缓存失效就只应该是数据修改导致的;所以要配置好查询缓存,避免缓存失效是因为空间过少或产生过多空间碎片导致缓存失效;

  • 缓存:k/v;
    任何缓存都是键值存放的,而缓存键放在哈希表中,当再次查询是,没一个查询语句会被同样做哈希计算,并查找表中是否有与之对应的条目,如果有把对应的键所指向的值直接返回给客户端,否则就表示缓存未命中

    • key:查询语句的hash值
    • value:查询语句的执行结果
  • 如何判断缓存是否命中:

    • 通过查询语句的哈希值判断;

      • 哈希值考虑的因素:查询本身、要查询数据库、客户端使用的协议版本、...

      例如:SELECT Name FROM students WHERE StuID=3;
      select Name From students where StuID=3;两个查询语句的hash值是不一样的

  • 哪些查询可能不会被缓存:

    • 查询语句中包含UDF(UDF:用户自定义函数:user define function)
    • 存储函数
    • 用户自定义变量
    • 临时表
    • mysql系统表或者是包含列级别权限的查询
    • 有着不确定结果值得函数(now())
  • 查询缓存相关的服务器变量:>SHOW GLOBAL VARIABLES LIKE '%query%';

    • query_cache_min_res_unit:查询缓存中内存块的最小分配单位;缓存过小的查询结果集会浪费内存空间;单位字节

      • 较小的值会减少空间浪费,但会导致更频繁地内存分配及回收操作;
      • 较大值的会带来空间浪费;
    • query_cache_limit:mysql能够缓存的最大查询结果;单语句结果集大小上限); 有着较大结果集的语句,显式使用SQL_NO_CACHE,以避免先缓存再移出;

    • query_cache_size:查询缓存空间的总共可用的大小;单位十字街,必须是1024的整数倍

    • query_cache_type:缓存功能启用与否;

      • ON:启用
      • OFF:禁用
      • DEMAND:表示只有在查询时明确写明SQL_CACHE才缓存,其他都不缓存
    • query_cache_wlock_invalidate:如果某表被其它的连接锁定,是否仍然可以从缓存中返回查询结果;默认为OFF,表示可以;ON则表示不可以;

      MariaDB [hellodb]> SHOW GLOBAL VARIABLES LIKE '%query%';
      +------------------------------+--------------------+
      | Variable_name                | Value              |
      +------------------------------+--------------------+
      | expensive_subquery_limit     | 100                |
      | ft_query_expansion_limit     | 20                 |
      | have_query_cache             | YES                |
      | long_query_time              | 10.000000          |
      | query_alloc_block_size       | 8192               |
      | query_cache_limit            | 1048576            |
      | query_cache_min_res_unit     | 4096               |
      | query_cache_size             | 4096000            |
      | query_cache_strip_comments   | OFF                |
      | query_cache_type             | ON                 |
      | query_cache_wlock_invalidate | OFF                |
      | query_prealloc_size          | 8192               |
      | slow_query_log               | OFF                |
      | slow_query_log_file          | localhost-slow.log |
      +------------------------------+--------------------+
      14 rows in set (0.00 sec)
      
  • 优化缓存


    mysql优化缓存查询.png

判断是否因为大部分查询都是不可缓存的导致不能命中;如果是这个原因导致的,则判断是否是因为query_cache_limit(查询结果最大上限)设置不够大导致不能缓存,如果不能缓存的原因是因为查询缓存的结果集超出了缓存所设定的上限导致,则增加query_cache_limit的值即可;如果不是因为query_cache_limit设定的值不够大导致的,则查询不能被缓存;

如果大部分查询结果都是可缓存的但是没有被缓存(缓存命中率不能被接受),判断是否发生了很多验证工作(验证缓存是否有效),如果是则判断缓存是否碎片化了,如果是碎片化了则要降低query_cache_min_res_unit参数值以减少碎片或使用FLUSH QUERY CACHE命令整理缓存;
如果缓存不是因为碎片过多导致的,则判断是否因为缓存空间过低而发生修正,就是缓存空间小,导致缓存被频繁清空,所导致缓存不能命中,如果是这个原因则增加query_cache_size参数值即可;
如果不是因为缓存空间太小导致缓存无法命中,则判断是否频繁更新语句,如果是表示为负载并不适合缓存则关闭缓存;如果不是频繁更新语句导致缓存失效,则可能是配置错误;

各种原因都不是,没有碎片、又不是不可缓存的、又没有发生验证工作、又没有被碎片化、sql语句又没有频繁更新则可认为是其它配置错误导致;

如果不是发送很多验证工作,则判断缓存是否启动,如果缓存启动,则从没见过该查询;如果缓存没启动则启动缓存即可;

  • 查询相关的状态变量:SHOW GLOBAL STATUS LIKE 'Qcache%';
    • Qcache_free_blocks:空闲内存块,缓存失效了还没被回收,可被下一次存储缓存查询结果继续使用;free_blocks这是内存空间被分隔成block以后仍然处于空闲的块;
      注意:真正查询缓存所能够得到的内存空间未必是连续的内存空间;
    • Qcache_free_memory:空闲的内存空间;
    • Qcache_hits:缓存命中
    • Qcache_inserts:向缓存中插入查询结果的次数,就是把可缓存查询语句的结果被放入缓存的次数;
    • Qcache_lowmem_prunes: 表示有多少次因为查询缓存空间太少,而不得不利用LRU算法清理缓存空间的次数;此值过大通常表示由query_cache_size太小导致的;
    • Qcache_not_cached:可缓存却没能被缓存的结果;
    • Qcache_queries_in_cache: 当前查询缓冲空间中被缓存下来查询语句的个数;例如16M的查询缓存空间能缓存1000个查询结果个数;
    • Qcache_total_blocks:整个查询缓存一共有多少个内存块;注意:个个内存块不是分割以后能够缓存内存查询结果的内存块,而指的是内存区段;

注意:query_cache_size调整的话,整个缓存空间必须重新进行分配,所以整个缓存都会被失效的;因此,最好不要轻易调整此值;
如果要使用memcached来缓存数据,mysql的缓存就显得不那么重要了;可自行决定继续开启还是关闭;

  • 缓存命中率的评估:Qcache_hits/(Qcache_hists+Com_select)
    • Com_select:记录的并不是所有的查询语句,仅记录了由mysql执行的查询语句,如果从缓存命中了此值是不会增加的;

推荐阅读更多精彩内容