作者: pbdatacn

  • 慢SQL排查方法  

    慢 SQL 的一般排查步骤为:
    1.定位慢 SQL;
    2.定位性能损耗节点;
    3.定位性能损耗原因并处理。
    说明:排查过程中,建议通过 MySQL 命令行进行连接:mysql -hIP -PPORT -uUSER -pPASSWORD -c 。请务必加上 “-c”,防止 MySQL 客户端过滤掉注释(默认)从而影响 HINT 的执行。
    定位慢 SQL
    定位慢 SQL 一般有两种场景:历史信息可从慢 SQL 记录中查询;实时慢 SQL 执行信息可使用SHOW PROCESSLIST 指令展示。
    查看慢 SQL 记录
    执行以下指令查询慢 SQL Top 10: mysql> SHOW SLOW limit 10;
    查看当前实时 SQL 执行信息: mysql>SHOW PROCESSLIST WHERE COMMAND != ‘Sleep’;
    0004.png

    定位性能损耗节点

    从慢 SQL 记录或者实时 SQL 执行信息中定位到慢 SQL 后,可以执行 TRACE 指令跟踪该 SQL 的运行时间,以便定位瓶颈。TRACE 命令会实际执行 SQL,在执行过程中记录所有节点消耗的时间,并返回执行结果。
    针对定位的慢 SQL,可以执行以下指令: mysql> trace select detail_url, sum(distinct price) from t_item group by detail_url;
    TRACE 指令执行完毕后,可以执行 SHOW TRACE 命令查看结果,根据每个组件的时间消耗来判断慢 SQL 的瓶颈: mysql> SHOW TRACE;
    SHOW TRACE 返回的结果中,根据 TIME_COST (单位毫秒)列可以判断哪个节点上的执行时间消耗大。同时可以看到对应的 GROUP_NAME (即 DRDS/RDS 节点),以及 STATEMENT 列信息(即正在执行的 SQL)。
    将组装好的 HINT 及带 EXPLAIN 前缀的 STATEMENT 拼装成新的 SQL 并执行。EXPLAIN 指令不会真正执行,而只是显示该 SQL 的执行计划信息。出现了 Using temporary; Using filesort 现象,说明没有正确的使用索引从而导致执行缓慢。此时可以修正索引问题后重新执行。

  • 创建分表

    通过分区字段(shardkey)把一个大表水平拆分到多个数据库,下面给大家介绍下分表的方法:

    如何选择分区字段

    一旦定好分区字段,就不能轻易修改分区字段,因此开发人员需要提前评估。选择分区字段的时候主要考虑两个维度:

    • 通过该字段能否对数据进行均衡的存储和访问
    • 多个相关联的表能否使用同一个字段。(相同分区字段数据会存储在同一个物理分片中,大多数的业务逻辑需要进行join时,可无需走分布式事务逻辑而直接在单节点内执行,效率大大提示)

    举个例子,如果业务有两张表,一个用于记录用户的基本信息,一个用于用户的订单信息,此时如果选取用户ID作为shardkey,则理论数据分布和访问都会比较均衡,同时单个用户对应的基本信息和订单信息都会在一个后端数据库中,方便后续的join等操作。

    shardkey选择的限制

    普通的分表创建时在最后面指定shardkey的值,该值为表中的一个字段名字,会用于后续sql的路由选择:

    shardkey字段有限定要求如下:

    1.如存在主键或者唯一索引,则shardkey字段必须是主键以及所有唯一索引的一部分
    2.shardkey字段的类型必须是int,bigint,smallint/char/varchar
    3.shardkey字段的值尽量使用ascii码,网关不会转换字符集,所以不同字符集可能会路由到不同的分区(且尽量不要有中文)
    4.不要update shardkey字段的值,如必须则先delete,再insert5.`shardkey=` 放在create语句的最后面,如下示例
    6.访问数据尽量都能带上shardkey字段
    
    
    

    创建一张分表

        mysql> create table test.right ( a int not null,b int not null, c char(20) not null,primary key(a,b) ,unique key(a,c)) shardkey=a;
        Query OK, 0 rows affected (0.12 sec)
    

    shardkey'是系统标记分片字段的关键字,不可占用。
    shardkey=noshardkey_allset定义该表为广播表的关键字,广播表表示该表不分表,但会在每个物理分片中都存储一份。

    常见DML操作

    使用分表时,对DML有一定的要求,具体如下(下面的例子中a为shardkey):

    SELECT最好带上shardkey

    select最好带上shardkey字段,由于分布式路由默认采用hash方式。

    • 若是=或者in,路由将自动跳转到对应分片,此时效率最高。
    • 若无=或者in,分布式系统会自动全表扫描,然后在网关进行结果集聚合,此时效率较低:

    例如:下面两条sql根据shardkey的值直接可以发送到对应的数据库,通常5ms内可以处理完成。

        mysql> select a,b,c from test.test1 where a=2 order by b;
        mysql> select a,b,c from test.test1 where a in (2) order by b;
    

    例如:如下的sql都会发到所有的后端数据库,然后需要对数据进行额外的汇总排序,通常要5~20ms才能处理完成。

        mysql> select a,b,c from test.test1 where a>2 order by b;
        mysql> select a,b,c from test.test1 where c=2 order by b;
    

    insert/replace字段必须包含shardkey

    insert/replace字段必须包含shardkey,否则路由不知道应该将数据插入到哪个物理分片,会拒绝执行该sql;

        mysql> insert into test.test1 (b,c) values(4,"record3");
        ERROR 1105 (07000): Proxy Warning - sql have no shardkey
    
        mysql> insert into test.test1 (a,c) values(4,"record3");
        Query OK, 1 row affected (0.01 sec)
    

    使用广播表或单表时除外。

    delete/update字段必须包含shardkey

    delete/update时为了安全考虑,执行该类sql的时候必须带有where条件,系统拒绝执行该sql命令,where条件最好也和select一样带上shardkey:

        mysql> delete from test.test1;
        ERROR 1005 (07000): Proxy Warning - sql is not legal,tokenizer_gram went wrong
        mysql> delete from test.test1 where a=1;
        Query OK, 1 row affected (0.01 sec)
    

    使用广播表或单表时除外。

    修改shardkey字段值

    同时update不能修改shardkey的字段的值,需要的话先insert再delete

        mysql> update test.test1 set a=10 where d=1;
        ERROR 7013 (HY000): Proxy ERROR:combine_sql_key return null,something went wrong
        mysql> update test.test1 set d=1 where a=1;
        Query OK, 0 rows affected (0.00 sec)
    

    不能更换shardkey字段类型、修改字段名称、删除shardkey字段、或更换shardkey字段,除非您新建一个表。

    常见问题:

    表没有主键:

        mysql> create table test.e1 ( a int ,b int) shardkey=a;
        ERROR 1105 (HY000): This table type requires a primary key
    

    主键或者唯一键没有包含shardkey:

        mysql> create table test.e2 ( a int not null,b int not null, c char(20) not null,primary key(a,b) ) shardkey=c;
        ERROR 1105 (HY000): A PRIMARY KEY must include all columns in the table's partitioning function
    

    shardkey的拼写错误或者列名错误:

        mysql> create table test.e3 ( a int key,b int,c char(20)) shardkey1=d;
        ERROR 1911 (HY000): Unknown option 'shardkey1'
        mysql> create table test.e4 ( a int key,b int,c char(20)) shardkey=d;
        ERROR 7008 (HY000): Proxy ERROR:shardkey must be one of the column
  • SQL 优化方法(二)

    Mysql 数据库作为数据持久化的存储系统,在实际业务中应用广泛。在应用也经常会因为 SQL 遇到各种各样的瓶颈。最常用的 Mysql 引擎是 innodb,索引类型是 B-Tree 索引,增删改查等操作最经常遇到的问题是“查”,查询又以索引为重点。

    接下来的内容,安排如下:

    1. 介绍索引的工作原理
    2. 引用实例具体介绍索引
    3. 如何使用 explain 排查线上问题
    4. 实际碰到的问题汇总

    索引如何工作

    当查询时,Mysql 的查询优化器会使用统计数据预估使用各个索引的代价(COST),与不使用索引的代价(COST)比较。Mysql 会选择代价最低的方式执行查询。Mysql 如何使用索引,可以用下面的伪代码来说明:

    min_cost = INIT_VALUE
    
    min_cost_index = NONE
    
    for(index in all_indexs):
    
        if (index match WHERE_CLAUSE):
    
            cur_cost = COST(index)
    
            if(cur_cost < min_cost):
    
                min_cost = cur_cost
    
                min_cost_index = index
    

    INIT_VALUE:不使用索引时的代价

    all_indexs:查询表上所有的索引 COST:基本是由“估计需要扫描的行数”(rows)来确定

    WHERE_CLAUSE:查询 SQL 中的 WHERE 子句

    大致的意思:Mysql 会遍历该查询相关的表(table)的每一条索引,然后判断该索引能否被本次查询使用(possible_keys)。当索引可以使用时,Mysql 预估使用该索引进行查询的 cost ,然后选择预估代价最低的代价的方式(key)执行查询。

    索引匹配(match)

    怎样判断索引是否匹配(match)SQL查询?

    1、索引的左前缀规则;索引中的列由左向右逐一匹配,如果中间某一列不能使用索引则后序列不在查询中不再被使用。

    例如,如果有一个3列索引(str_col1,col2,col3),其中str_col1为字符串,则对(str_col1)(str_col1,col2)(str_col1,col2,col3)上的查询进行了索引。

    如果列不构成索引最左面的前缀,MySQL 不能使用索引。假定有下面显示的 SELECT 语句。

    SELECT * FROM tbl_name WHERE str_col1=val1;
    
    SELECT * FROM tbl_name WHERE str_col1=val1 AND col2=val2;
    
    SELECT * FROM tbl_name WHERE col2=val2;
    
    SELECT * FROM tbl_name WHERE col2=val2 AND col3=val3;
    

    如果(str_col1,col2,col3)有一个索引,只有前2个查询使用索引。第3个和第4个查询确实包括索引的列,但(col2)(col2,col3)不是(col1,col2,col3)的最左边的前缀。

    2、where 语句中列的表达式为 = 、 > 、 >= 、< 、<= 、BETWEEN 、ISNULL 或者 LIKE ’ pattern ’(其中’ pattern ’不以通配符开始)

    3、每个 AND 组作为表达式匹配索引。

    SELECT * FROM tbl_name WHERE (str_col1=val1 OR col4 =val4) AND col2=val2;

    因为str_col1=val1ORcol4 =val4作为一组,col4不匹配索引中的列,所以查询不匹配索引。

    4、如果表达式中存在类型转换或者列上有复杂函数则与该列不匹配索引中的列。

    SELECT * FROM tbl_name WHERE str_col1=1;
    
    SELECT * FROM tbl_name WHERE SUBSTRING(str_col1,1,8) = ‘title’;
    

    第1个查询,因为1是整数、str_col1是字符串,所以不匹配索引;第2个查询str_col1有复杂函数,同样不匹配索引。

    索引的COST

    Mysql 如何计算索引的 COST?

    索引的 cost 基本是由“估计需要扫描的行数”(rows)来确定。数据来源于information_schema,在 Mysql 启动的时候读入内存,运行时只使用内存值,存储引擎会动态更新这些值。

    我们可以通过 explain 看下“估计需要扫描的函数”,可以通过optimizer_trace查询适用每一条 SQL 的具体的 cost 值。explain 也是线上排查问题的利器,后面会重点介绍。

    索引实例分析

    索引的字段究竟是怎么从 where 语句中提取,并被 Mysql 使用呢,下面将以一个实例分析这个过程。内容全文为摘取何登成的文章《 SQL 中的 where 条件,在数据库中提取与应用浅析》,并做了部分删改。

    我们创建一张测试表,一个索引索引,然后插入几条记录。(注意:下面的实例,使用的表的结构不是 InnoDB 引擎所采用的聚簇索引表。图例仅为说明,原理适用 innodb )

    create table t1 (a int primary key, b int, c int, d int, e varchar(20));
    
    create index idx_t1_bcd on t1(b, c, d);
    
    insert into t1 values (4,3,1,1,’d’);
    insert into t1 values (1,1,1,1,’a’);
    insert into t1 values (8,8,8,8,’h’):
    insert into t1 values (2,2,2,2,’b’);
    insert into t1 values (5,2,3,5,’e’);
    insert into t1 values (3,3,2,2,’c’);
    insert into t1 values (7,4,5,5,’g’);
    insert into t1 values (6,6,4,4,’f’);
    

    t1表的存储结构如下图所示(只画出了idx_t1_bcd索引与 t1 表结构,没有包括 t1 表的主键索引):

    简单说明上图,idx_t1_bcd索引上有[b,c,d]三个字段,不包括[a,e]字段。idx_t1_bcd索引,首先按照b字段排序,b字段相同,则按照c字段排序,以此类推。

    考虑以下 SQL :

    select * from t1 where b >= 2 and b < 8 and c > 1 and d != 4 and e != ‘a’;

    可以发现where条件使用到了[b,c,d,e]四个字段,而 t1 表的idx_t1_bcd索引,恰好使用了[b,c,d]这三个字段,那么走idx_t1_bcd索引进行条件过滤,应该是一个不错的选择。

    所有SQL的where条件,均可归纳为3大类:Index Key (First Key & Last Key),Index Filter,Table Filter。

    接下来,让我们来详细分析者3大类分别是如何定义,以及如何提取的。

    1、Index Key

    用于确定 SQL 查询在索引中的连续范围(起始范围+结束范围)的查询条件,被称之为 Index Key。由于一个范围,至少包含一个起始与一个终止,Index Key 也被拆分为 Index First Key 和 Index Last Key ,分别用于定位索引查找的起始,以及索引查询的终止条件。

    • Index First Key

    提取规则:从索引的第一个键值开始,检查其在where条件中是否存在,若存在并且条件是= 、>= ,则将对应的条件加入Index First Key 之中,继续读取索引的下一个键值,使用同样的提取规则;若存在并且条件是>,则将对应的条件加入Index First Key 中,同时终止Index First Key的提取;若不存在,同样终止 Index First Key 的提取。

    针对上面的SQL,应用这个提取规则,提取出来的 Index First Key 为(b >= 2, c > 1)。由于 c 的条件为 >,提取结束,不包括d。

    • Index Last Key

    提取规则:从索引的第一个键值开始,检查其在 where 条件中是否存在,若存在并且条件是=、<=,则将对应条件加入到Index Last Key中,继续提取索引的下一个键值,使用同样的提取规则;若存在并且条件是 < ,则将条件加入到Index Last Key中,同时终止提取;若不存在,同样终止 Index Last Key 的提取。

    针对上面的SQL,应用这个提取规则,提取出来的 Index Last Key 为(b < 8),由于是 < 符号,因此提取b之后结束。

    2、Index Filter

    在完成 Index Key 的提取之后,我们根据 where 条件固定了索引的查询范围,但是此范围中的项,并不都是满足查询条件的项。在上面的 SQL 用例中,(3,1,1),(6,4,4)均属于范围中,但是又均不满足 SQL 的查询条件。

    Index Filter 的提取规则:同样从索引列的第一列开始,检查其在 where 条件中是否存在:若存在并且 where 条件仅为 =,则跳过第一列继续检查索引下一列,下一索引列采取与索引第一列同样的提取规则;若 where 条件为 >=、>、<、<= 其中的几种,则跳过索引第一列,将其余 where 条件中索引相关列全部加入到Index Filter之中;若索引第一列的where条件包含 =、>=、>、<、<= 之外的条件,则将此条件以及其余 where 条件中索引相关列全部加入到 Index Filter 之中;若第一列不包含查询条件,则将所有索引相关条件均加入到 Index Filter 之中。

    针对上面的用例 SQL,索引第一列只包含 >=、< 两个条件,因此第一列可跳过,将余下的c、d两列加入到 Index Filter 中。因此获得的 Index Filter 为 c > 1 and d != 4 。

    3、Table Filter

    Table Filter 是最简单,最易懂,也是提取最为方便的。提取规则:所有不属于索引列的查询条件,均归为 Table Filter 之中。

    同样,针对上面的用例 SQL,Table Filter 就为 e != ‘a’。

    根据以上实例其实可以总结出一些规律,WHERE 语句究竟怎样(是否)匹配索引,不用迷信出自他人之口的规则。只需要简单的按照索引自左向右的每一列,从 WHERE 语句提取条件,能否从索引树的根节点出发,到达索引树的叶节点,成功匹配出一个或几个范围区间,即能自己自行判断是否能使用索引。反过来,最左前缀匹配、Like 不能以通配符开始、AND 分组,也都是由 B-Tree 本身特性决定的。

    索引问题排查

    前面我们谈使用索引的 cost 的值提到过explain。下面介绍 explain 的值,并以一个实际遇到的问题说明如何排查问题。

    Explain详解

    使用一个示例 SQL 来解释 explain :

    select id from r_ibeacon_biz_device_d where ftime >= 20151126 and ftime <= 20151126 and biz_id = 11602 limit 50;

    IDX_BID_FTIME<biz_id, ftime>是表r_ibeacon_biz_device_d的其中一条索引。
    Biz_id,ftime均为bigint类型。

    我们着重关注几个重点字段的重点值:

    - type:索引的使用方式

      eq_ref      …  索引,关联匹配若干行
       ref          …  索引(前缀)匹配   
        range        …  索引范围扫(BETWEEN、IN、>=、LIKE)得到数据
       index        …  索引全扫描
        all           …  表全扫描
    

    示例中使用的索引是使用全索引范围扫描,所以type为range

    - possible_keys:适用查询的索引列表。示例中有三条索引适用本次查询。

    - key: 查询实际执行使用的索引。示例使用的为IDX_BID_FTIME

    - key_len:查询使用索引的长度。

     null    1字节
       tinyint  1字节
       int    4字节
       bigint  8字节
       double  8字节
       datetime 8字节
       timestamp 4字节
       varchr(10)变长字段且允许NULL: 10*(Character Set:utf8=3,gbk=2,latin1=1)+1(NULL)+2(变长字段)
       char(10)固定字段且允许NULL: 10*(Character Set:utf8=3,gbk=2,latin1=1)+1(NULL)
    

    以上是常用类型的长度,示例中key_len为18,即:8字节( biz_id bigint )+1字节( biz_id 允许为 null )+8字节( ftimebigint )+1字节( ftime 允许为 null )。所以本次查询是使用了索引的所有字段加速查询

    – rows:查询预估扫描的行数

    Explain跟进问题

    摇一摇周边后台的数据统计接口尔会有小尖峰,涉及了一条 SQL:

    一条SQL搞定卡方检验计算select d.id from r_ibeacon_biz_page_d d where d.ftime >= 20151126 and d.ftime <= 20151126 and d.biz_id = 11023 and d.page_id = 778495 limit 0,20;
    r_ibeacon_biz_page_d 的主要字段信息如下:

    ftime  bigint(20)
    biz_id  bigint(20) 
    page_id varchar(200)
    

    索引为:IDX_BID_PID_FTIME<biz_id,page_id,ftime>

    Explain结果如下

    观察以上explain结果可以看到一切正常,SQL“符合预期”的走了索引。但是rows稍微多了点,但是看起来也“好像”ok。但是问题就是出现尖峰。

    问题排查:

    首先,注意到的一点就是 explain 中的 type 异常,是 ref 。按照上面的解释,如果走了索引那应该是 range 类型才对啊。

    其次,观察key_len,9,发现确实有些不对,怎么会这么小。按照类型所占字节,9刚好为biz_id的长度,确定这条SQL 虽然走了索引,但是只使用了 biz_id 字段。原因呢?

    然后执行“desc r_ibeacon_biz_page_d”,查看表结构的索引字段,突然发现page_id的类型怎么是 varchar,再看SQL中page_id=11023。突然意识到了什么,此时刚好违反索引匹配的第四条规则。更改SQL“page_id=11023”为“page_id=‘11023’”验证,如下

    可以看到type=rangekey_len=621,符合预期。接下来要做的就是更改表中page_id的类型为 bigint。隔天再看接口的尖峰果然削平。

    Explain 是一个很好的工具,可以用来验证 SQL 是否使用了索引,更重要的是验证 SQL 是否如预期的使用索引上。排查线上问题还有 profile 和 optimizer_trace,由于实际没有太多用到暂且不表。

    常见问题汇总

    – Range怎么使用索引?

    详见上文

    - Order by使用索引吗?

    该问题可以由以下资料解释:

    SQL queries with an order by clause don’t need to sort the result explicitly if the relevant index already delivers the rows in the required order. That means the same index that is used for the where clause must also cover the order by clause.
    总之一句话:索引本身并不能避免排序,当根据索引取出的数据已经满足order by子句的要求就可以避免排序操作。

    - order by太慢?

    避免数据排序,采用索引排序(分页查询文艺写法)

    `- limit offset太慢?

    避免大offset,使用where语句过滤更多的行。更多参考的实践《 Efficient Pagination Using MySQL 》

    – 为什么不走索引(索引也走了,还是慢)?

    类型是否一致: int vs char(varchar)、varchar(32)vs varchar(64)
    字符集是否一致:涉及表关联时,两表字符集是否一致。

  • SQL 优化方法

    SQL 优化的基本原则

    在MySQL 执行的 SQL 计算称为可下推计算。可下推计算能够减少数据传输,减少网络层的开销,提升 SQL 语句的执行效率。

    因此,SQL 语句优化的基本原则为:尽量让更多的计算可下推到 MySQL 上执行。

    可下推计算主要包括:

    • JOIN 连接;
    • 过滤条件,如 WHERE 或 HAVING 中的条件;
    • 聚合计算,如 COUNT,GROUP BY 等;
    • 排序,如 ORDER BY;
    • 去重,如 DISTINCT;
    • 函数计算,如 NOW() 函数等;
    • 子查询。

    注意:上述列表只是列出可下推计算的各种可能形式,并不代表所有的子句/条件或者子句/条件的组合一定是可下推计算。

    不同类型和条件的 SQL 优化有不同的侧重点和方法,下面将针对以下几种情况介绍 SQL 优化的具体方法:

    • 单表 SQL 优化
      • 过滤条件优化
      • 查询返回行数优化
      • 分组及排序优化
    • JOIN 优化
      • 可下推的 JOIN 优化
      • 分布式 JOIN 优化
    • 子查询优化

    单表 SQL 优化

    单表 SQL 优化有以下几个原则:

    • SQL 语句尽可能带有拆分键;
    • 拆分键的条件尽可能是等值条件;
    • 如果拆分键的条件是 IN 条件,则 IN 后面的值的数目应尽可能少(需要远少于分片数,并且数目不会随业务的增长而增多);
    • 如果 SQL 语句不带有拆分键,那么 DISTINCT、GROUP BY 和 ORDER BY 在同一个 SQL 语句中尽量只出现一种。

    过滤条件优化

    DRDS 的数据是按拆分键水平切分的,过滤条件中应尽量包含带有拆分键的条件,可以让 DRDS 根据拆分键对应的值将查询直接下推到特定的分库,避免 DRDS 做全表扫描。

    例如,表 test 的拆分键是 c1,过滤条件中若不带有拆分键,会做全表扫描:

    1. mysql> SELECT * FROM test WHERE c2 = 2;
    2. +----+----+
    3. | c1 | c2 |
    4. +----+----+
    5. | 2 | 2 |
    6. +----+----+
    7. 1 row in set (0.05 sec)

    对应的执行计划为:

    1. mysql> EXPLAIN SELECT * FROM test WHERE c2 = 2;
    2. +------------------------------------------------+--------------------------------------------------------------------+--------+
    3. | GROUP_NAME | SQL | PARAMS |
    4. +------------------------------------------------+--------------------------------------------------------------------+--------+
    5. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0004_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    6. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0007_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    7. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0005_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    8. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0002_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    9. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0003_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    10. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0006_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    11. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0000_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    12. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0001_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c2` = 2) | {} |
    13. +------------------------------------------------+--------------------------------------------------------------------+--------+
    14. 8 rows in set (0.00 sec)

    含拆分键的过滤条件的取值范围越小,越有助于提高 DRDS 的查询速度。

    例如,对表 test 查询时包含带有拆分键 c1 的范围过滤条件:

    1. mysql> SELECT * FROM test WHERE c1 > 1 AND c1 < 4;
    2. +----+----+
    3. | c1 | c2 |
    4. +----+----+
    5. | 2 | 2 |
    6. | 3 | 3 |
    7. +----+----+
    8. 2 rows in set (0.04 sec)

    对应的执行计划为:

    1. mysql> EXPLAIN SELECT * FROM test WHERE c1 > 1 AND c1 < 4;
    2. +------------------------------------------------+--------------------------------------------------------------------------------------------+--------+
    3. | GROUP_NAME | SQL | PARAMS |
    4. +------------------------------------------------+--------------------------------------------------------------------------------------------+--------+
    5. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0002_RDS | select `test`.`c1`,`test`.`c2` from `test` where ((`test`.`c1` > 1) AND (`test`.`c1` < 4)) | {} |
    6. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0003_RDS | select `test`.`c1`,`test`.`c2` from `test` where ((`test`.`c1` > 1) AND (`test`.`c1` < 4)) | {} |
    7. +------------------------------------------------+--------------------------------------------------------------------------------------------+--------+
    8. 2 rows in set (0.00 sec)

    等值条件会比范围条件执行得更快。例如:

    1. mysql> SELECT * FROM test WHERE c1 = 2;
    2. +----+----+
    3. | c1 | c2 |
    4. +----+----+
    5. | 2 | 2 |
    6. +----+----+
    7. 1 row in set (0.03 sec)

    对应的执行计划为:

    1. mysql> EXPLAIN SELECT * FROM test WHERE c1 = 2;
    2. +------------------------------------------------+--------------------------------------------------------------------+--------+
    3. | GROUP_NAME | SQL | PARAMS |
    4. +------------------------------------------------+--------------------------------------------------------------------+--------+
    5. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0002_RDS | select `test`.`c1`,`test`.`c2` from `test` where (`test`.`c1` = 2) | {} |
    6. +------------------------------------------------+--------------------------------------------------------------------+--------+
    7. 1 row in set (0.00 sec)

    此外,在向拆分表中插入数据时,插入字段中必须带有拆分键。

    例如,向表 test 中插入数据时带有拆分键 c1:

    1. mysql> INSERT INTO test(c1,c2) VALUES(8,8);
    2. Query OK, 1 row affected (0.07 sec)

    查询返回行数优化

    DRDS 在执行带有 LIMIT [ offset, ] row_count 的查询时,实际上是依次将 offset 之前的记录读取出来并直接丢弃,这样当 offset 非常大的时候,即使 row_count 很小,也会导致查询非常缓慢。例如以下的 SQL:

    1. SELECT * 
    2. FROM sample_order
    3. ORDER BY sample_order.id
    4. LIMIT 10000, 2

    它虽然只返回第10000与10001两条记录,可它的执行时间为12秒左右,这是因为 DRDS 实际读取的记录数为10002条:

    1. mysql> SELECT * FROM sample_order ORDER BY sample_order.id LIMIT 10000,2;
    2. +--------------+------------+--------------+--------------+------------+
    3. | id | sellerId | trade_id | buyer_id | buyer_nick |
    4. +--------------+------------+--------------+--------------+------------+
    5. | 242012755468 | 1711939506 | 242012755467 | 244148116334 | zhangsan |
    6. | 242012759093 | 1711939506 | 242012759092 | 244148138304 | wangwu |
    7. +--------------+------------+--------------+--------------+------------+
    8. 2 rows in set (11.93 sec)

    对应的执行计划为:

    1. mysql> EXPLAIN SELECT * FROM sample_order ORDER BY sample_order.id LIMIT 10000,2;
    2. +------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+
    3. | GROUP_NAME | SQL | PARAMS |
    4. +------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+
    5. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0004_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    6. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0007_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    7. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0005_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    8. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0002_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    9. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0003_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    10. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0006_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    11. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0000_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    12. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0001_RDS | select `sample_order`.`id`,`sample_order`.`sellerId`,`sample_order`.`trade_id`,`sample_order`.`buyer_id`,`sample_order`.`buyer_nick` from `sample_order`order by `sample_order`.`id` asc limit 0,10002 | {} |
    13. +------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------+
    14. 8 rows in set (0.01 sec)

    针对上述情况,SQL 优化方向是先查出 id 集合,再通过 IN 匹配真正的记录内容,改写后的 SQL 查询如下:

    1. SELECT * 
    2. FROM sample_order o
    3. WHERE o.id IN ( 
    4. SELECT id
    5. FROM sample_order
    6. ORDER BY id
    7. LIMIT 10000, 2 )

    这样改写的目的是先用内存缓存 id(前提是 id 数目不多),如果 sample_order 表的拆分键是 id,那么 DRDS 还可以将这样的 IN 查询通过规则计算下推到不同的分库来查询,避免全表扫描和不必要的网络 IO。观察改写后的 SQL 查询效果:

    1. mysql> SELECT *
    2.  -> FROM sample_order o
    3.  -> WHERE o.id IN ( SELECT id FROM sample_order ORDER BY id LIMIT 10000,2 );
    4. +--------------+------------+--------------+--------------+------------+
    5. | id | sellerId | trade_id | buyer_id | buyer_nick |
    6. +--------------+------------+--------------+--------------+------------+
    7. | 242012755468 | 1711939506 | 242012755467 | 244148116334 | zhangsan |
    8. | 242012759093 | 1711939506 | 242012759092 | 244148138304 | wangwu |
    9. +--------------+------------+--------------+--------------+------------+
    10. 2 rows in set (1.08 sec)

    执行时间由原来的12秒减少到1.08秒,缩减了一个数量级。

    对应的执行计划为:

    1. mysql> EXPLAIN SELECT *
    2.  -> FROM sample_order o
    3.  -> WHERE o.id IN ( SELECT id FROM sample_order ORDER BY id LIMIT 10000,2 );
    4. +------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------+--------+
    5. | GROUP_NAME | SQL | PARAMS |
    6. +------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------+--------+
    7. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0002_RDS | select `o`.`id`,`o`.`sellerId`,`o`.`trade_id`,`o`.`buyer_id`,`o`.`buyer_nick` from `sample_order` `o` where (`o`.`id` IN (10002)) | {} |
    8. | SEQPERF_1478746391548CDTCSEQPERF_OXGJ_0001_RDS | select `o`.`id`,`o`.`sellerId`,`o`.`trade_id`,`o`.`buyer_id`,`o`.`buyer_nick` from `sample_order` `o` where (`o`.`id` IN (10001)) | {} |
    9. +------------------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------+--------+
    10. 2 rows in set (0.03 sec)

    分组及排序优化

    在 DRDS 中,如果在一条 SQL 查询中必须同时使用 DISTINCT、GROUP BY 与 ORDER BY,应尽可能保证 DISTINCT、GROUP BY 与 ORDER BY 语句后所带的字段相同,且尽量为拆分键,使最终的 SQL 查询只返回少量数据。这样能够让分布式查询中消耗的网络带宽最小,并且不需要取出大量数据在临时表内进行排序,系统的性能能够达到最优状态。

    JOIN 优化

    DRDS 的 JOIN 查询分为可下推的 JOIN 和不可下推的 JOIN(即分布式 JOIN)两类,其优化策略各不相同。

    可下推的 JOIN 优化

    可下推的 JOIN 主要分为以下几类:

    • 单表(即非拆分表)之间的 JOIN;
    • 参与 JOIN 的表在过滤条件中均带有拆分键作为条件,并且拆分算法相同(即通过拆分算法计算的数据分布在相同分片上);
    • 参与 JOIN 的表均按照拆分键作为 JOIN 条件,并且拆分算法相同;
    • 广播表(也称为小表广播)与拆分表之间的 JOIN。

    使用 DRDS 时,应尽可能将 JOIN 查询优化成能够在分库上执行的可下推的 JOIN 形式。

    以广播表与拆分表之间的 JOIN 为例,应将广播表作为 JOIN 驱动表(将 JOIN 中的左表称为驱动表)。DRDS 的广播表在各个分库都会存放一份同样的数据,当作为 JOIN 驱动表时,该表与分表的 JOIN 可以转化为单库的 JOIN 并进行合并计算,提高查询性能。

    例如,有以下的三个表做 JOIN 查询(其中表 sample_area 是广播表,sample_item 和 sample_buyer 是拆分表),查询执行时间约15秒:

    1. mysql> SELECT sample_area.name
    2.  -> FROM sample_item i JOIN sample_buyer b ON i.sellerId = b.sellerId JOIN sample_area a ON b.province = a.id
    3.  -> WHERE a.id < 110107 
    4.  -> LIMIT 0, 10;
    5. +------+
    6. | name |
    7. +------+
    8. | BJ |
    9. | BJ |
    10. | BJ |
    11. | BJ |
    12. | BJ |
    13. | BJ |
    14. | BJ |
    15. | BJ |
    16. | BJ |
    17. | BJ |
    18. +------+
    19. 10 rows in set (14.88 sec)

    如果调整一下 JOIN 的顺序,将广播表放在最左边作为 JOIN 驱动表,则整个 JOIN 查询在 DRDS 中会被下推为单库 JOIN 查询:

    1. mysql> SELECT sample_area.name
    2.  -> FROM sample_area a JOIN sample_buyer b ON b.province = a.id JOIN sample_item i ON i.sellerId = b.sellerId
    3.  -> WHERE a.id < 110107 
    4.  -> LIMIT 0, 10;
    5. +------+
    6. | name |
    7. +------+
    8. | BJ |
    9. | BJ |
    10. | BJ |
    11. | BJ |
    12. | BJ |
    13. | BJ |
    14. | BJ |
    15. | BJ |
    16. | BJ |
    17. | BJ |
    18. +------+ 
    19. 10 rows in set (0.04 sec)

    查询执行时间从15秒减少到0.04秒,性能提升非常明显。

    注意:广播表在分库上通过同步机制实现数据一致,有秒级延迟。

    分布式 JOIN 优化

    如果一个 JOIN 查询不可下推(即 JOIN 条件和过滤条件中均不带有拆分键),则需要由 DRDS 完成查询中的部分计算,即分布式 JOIN。

    通常将分布式 JOIN 中的表按照数据量大小分为两类:

    • 小表:经过条件过滤后,参与 JOIN 计算的中间结果的数据量比较少(一般少于 100 条,或者相较于其它表数据更少)的表;
    • 大表:经过条件过滤后,参与 JOIN 计算的中间结果的数据量比较大(一般多于 100 条,或者相较于其它表数据更多)的表。

    在 DRDS 层的 JOIN 计算中,大多数情况下采用的 JOIN 算法都是 Nested Loop 及其派生算法(若 JOIN 有排序要求,则使用 Sort Merge 算法)。采用 Nested Loop 算法时,如果 JOIN 中左表的数据量越少,那么 DRDS 对右表做查询的次数就越少,如果右表上建有索引或者表中的数据量也很少,则 JOIN 的速度会更快。因此,在 DRDS 中,分布式 JOIN 的左表被称为驱动表,对分布式 JOIN 的优化应将小表作为驱动表,且让驱动表带有尽可能多的过滤条件。

    以下面的分布式 JOIN 为例,查询约需要24秒:

    1. mysql> SELECT t.title, t.price
    2.  -> FROM sample_order o,
    3.  -> ( SELECT * FROM sample_item i WHERE i.id = 242002396687 ) t
    4.  -> WHERE t.source_id = o.source_item_id AND o.sellerId < 1733635660;
    5. +----------------------------------+--------+
    6. | title | price |
    7. +----------------------------------|--------+
    8. | Sample Item for Distributed JOIN | 239.00 |
    9. | Sample Item for Distributed JOIN | 239.00 |
    10. | Sample Item for Distributed JOIN | 239.00 |
    11. | Sample Item for Distributed JOIN | 239.00 |
    12. | Sample Item for Distributed JOIN | 239.00 |
    13. | Sample Item for Distributed JOIN | 239.00 |
    14. | Sample Item for Distributed JOIN | 239.00 |
    15. | Sample Item for Distributed JOIN | 239.00 |
    16. | Sample Item for Distributed JOIN | 239.00 |
    17. | Sample Item for Distributed JOIN | 239.00 |
    18. +----------------------------------+--------+
    19. 10 rows in set (23.79 sec)

    通过初步分析,上述 JOIN 查询是一个 INNER JOIN,并不知道参与 JOIN 计算的中间结果的实际数据量,可以对 o 表与 t 表分别做 COUNT() 查询得到实际数据。

    对于 o 表,观察到 WHERE 条件中的 o.sellerId < 1733635660 只与 o 表相关,可以将其提取出来,附加到 o 表的 COUNT() 查询条件中,得到如下的查询结果:

    1. mysql> SELECT COUNT(*) FROM sample_order o WHERE o.sellerId < 1733635660;
    2. +----------+
    3. | count(*) |
    4. +----------+
    5. | 504018 |
    6. +----------+
    7. 1 row in set (0.10 sec)

    o 表的中间结果约有50万条记录。类似地,t 表是一个子查询,直接将其抽取出来进行 COUNT() 查询:

    1. mysql> SELECT COUNT(*) FROM sample_item i WHERE i.id = 242002396687;
    2. +----------+
    3. | count(*) |
    4. +----------+
    5. | 1 |
    6. +----------+
    7. 1 row in set (0.01 sec)

    t 表的中间结果只有1条记录,所以可确定 o 表为大表,t 表为小表。根据尽量将小表作为分布式 JOIN 驱动表的原则,将 JOIN 查询调整后的查询结果为:

    1. mysql> SELECT t.title, t.price
    2.  -> FROM ( SELECT * FROM sample_item i WHERE i.id = 242002396687 ) t,
    3.  -> sample_order o
    4.  -> WHERE t.source_id = o.source_item_id AND o.sellerId < 1733635660;
    5. +----------------------------------+--------+
    6. | title | price |
    7. +----------------------------------|--------+
    8. | Sample Item for Distributed JOIN | 239.00 |
    9. | Sample Item for Distributed JOIN | 239.00 |
    10. | Sample Item for Distributed JOIN | 239.00 |
    11. | Sample Item for Distributed JOIN | 239.00 |
    12. | Sample Item for Distributed JOIN | 239.00 |
    13. | Sample Item for Distributed JOIN | 239.00 |
    14. | Sample Item for Distributed JOIN | 239.00 |
    15. | Sample Item for Distributed JOIN | 239.00 |
    16. | Sample Item for Distributed JOIN | 239.00 |
    17. | Sample Item for Distributed JOIN | 239.00 |
    18. +----------------------------------+--------+
    19. 10 rows in set (0.15 sec)

    查询时间从约24秒减少到0.15秒,性能提升非常明显。

    子查询优化

    在包含子查询的 SQL 优化中,应尽可能将查询下推到具体的分库上执行,并减少 DRDS 层的计算量。要达到这一目标,可以尝试两个方面的优化:

    • 将子查询的形式改写为多表 JOIN 形式,并参照 JOIN 优化方法进一步优化;
    • 尽量在 JOIN 条件或过滤条件中带上拆分键,有利于 DRDS 将查询下推到特定的分库,避免全表扫描。

    以下面的子查询为例:

    1. SELECT o.*
    2. FROM sample_order o
    3. WHERE NOT EXISTS
    4.  (SELECT sellerId FROM sample_seller s WHERE o.sellerId = s.id)

    可将其改写为 JOIN 的形式:

    1. SELECT o.*
    2. FROM sample_order o LEFT JOIN sample_seller s ON o.sellerId = s.id
    3. WHERE s.id IS NULL
  • MySQL 索引及查询优化总结

    一个简单的对比测试

    前面的案例中,c2c_zwdb.t_file_count表只有一个自增id,FFileName字段未加索引的sql执行情况如下:

    在上图中,type=all,key=null,rows=33777。该sql未使用索引,是一个效率非常低的全表扫描。如果加上联合查询和其他一些约束条件,数据库会疯狂的消耗内存,并且会影响前端程序的执行。

    这时给FFileName字段添加一个索引:

    alter table c2c_zwdb.t_file_count add index index_title(FFileName);

    再次执行上述查询语句,其对比很明显:

    在该图中,type=ref,key=索引名(index_title),rows=1。该sql使用了索引index_title,且是一个常数扫描,根据索引只扫描了一行。

    比起未加索引的情况,加了索引后,查询效率对比非常明显。

    MySQL索引

    通过上面的对比测试可以看出,索引是快速搜索的关键。MySQL索引的建立对于MySQL的高效运行是很重要的。对于少量的数据,没有合适的索引影响不是很大,但是,当随着数据量的增加,性能会急剧下降。如果对多列进行索引(组合索引),列的顺序非常重要,MySQL仅能对索引最左边的前缀进行有效的查找。

    下面介绍几种常见的MySQL索引类型。

    索引分单列索引和组合索引。单列索引,即一个索引只包含单个列,一个表可以有多个单列索引,但这不是组合索引。组合索引,即一个索引包含多个列。

    1、MySQL索引类型

    (1) 主键索引 PRIMARY KEY

    它是一种特殊的唯一索引,不允许有空值。一般是在建表的时候同时创建主键索引。

    当然也可以用 ALTER 命令。记住:一个表只能有一个主键。

    (2) 唯一索引 UNIQUE

    唯一索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。可以在创建表的时候指定,也可以修改表结构,如:

    ALTER TABLE table_name ADD UNIQUE (column)

    (3) 普通索引 INDEX

    这是最基本的索引,它没有任何限制。可以在创建表的时候指定,也可以修改表结构,如:

    ALTER TABLE table_name ADD INDEX index_name (column)

    (4) 组合索引 INDEX

    组合索引,即一个索引包含多个列。可以在创建表的时候指定,也可以修改表结构,如:

    ALTER TABLE table_name ADD INDEX index_name(column1column2column3)

    (5) 全文索引 FULLTEXT

    全文索引(也称全文检索)是目前搜索引擎使用的一种关键技术。它能够利用分词技术等多种算法智能分析出文本文字中关键字词的频率及重要性,然后按照一定的算法规则智能地筛选出我们想要的搜索结果。

    可以在创建表的时候指定,也可以修改表结构,如:

    ALTER TABLE table_name ADD FULLTEXT (column)

    2、索引结构及原理

    mysql中普遍使用B+Tree做索引,但在实现上又根据聚簇索引和非聚簇索引而不同,本文暂不讨论这点。

    b+树介绍

    下面这张b+树的图片在很多地方可以看到,之所以在这里也选取这张,是因为觉得这张图片可以很好的诠释索引的查找过程。

    如上图,是一颗b+树。浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。

    真实的数据存在于叶子节点,即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。

    查找过程

    在上图中,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。

    性质

    (1) 索引字段要尽量的小。

    通过上面b+树的查找过程,或者通过真实的数据存在于叶子节点这个事实可知,IO次数取决于b+数的高度h。

    假设当前数据表的数据量为N,每个磁盘块的数据项的数量是m,则树高h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;

    而m = 磁盘块的大小/数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的;如果数据项占的空间越小,数据项的数量m越多,树的高度h越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。

    (2) 索引的最左匹配特性。

    当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。

    建索引的几大原则

    (1) 最左前缀匹配原则

    对于多列索引,总是从索引的最前面字段开始,接着往后,中间不能跳过。比如创建了多列索引(name,age,sex),会先匹配name字段,再匹配age字段,再匹配sex字段的,中间不能跳过。mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配。

    一般,在创建多列索引时,where子句中使用最频繁的一列放在最左边。

    看一个补符合最左前缀匹配原则和符合该原则的对比例子。

    实例:表c2c_db.t_credit_detail建有索引(Flistid,Fbank_listid)

    不符合最左前缀匹配原则的sql语句:

    select * from t_credit_detail where Fbank_listid=’201108010000199’\G

    该sql直接用了第二个索引字段Fbank_listid,跳过了第一个索引字段Flistid,不符合最左前缀匹配原则。用explain命令查看sql语句的执行计划,如下图:

    从上图可以看出,该sql未使用索引,是一个低效的全表扫描。

    符合最左前缀匹配原则的sql语句:

    select * from t_credit_detail where Flistid=’2000000608201108010831508721′ and Fbank_listid=’201108010000199’\G

    该sql先使用了索引的第一个字段Flistid,再使用索引的第二个字段Fbank_listid,中间没有跳过,符合最左前缀匹配原则。用explain命令查看sql语句的执行计划,如下图:

    从上图可以看出,该sql使用了索引,仅扫描了一行。

    对比可知,符合最左前缀匹配原则的sql语句比不符合该原则的sql语句效率有极大提高,从全表扫描上升到了常数扫描。

    (2) 尽量选择区分度高的列作为索引。

    比如,我们会选择学号做索引,而不会选择性别来做索引。

    (3) =和in可以乱序

    比如a = 1 and b = 2 and c = 3,建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。

    (4) 索引列不能参与计算,保持列“干净”

    比如:Flistid+1>‘2000000608201108010831508721‘。原因很简单,假如索引列参与计算的话,那每次检索时,都会先将索引计算一次,再做比较,显然成本太大。

    (5) 尽量的扩展索引,不要新建索引。

    比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

    索引的不足

    虽然索引可以提高查询效率,但索引也有自己的不足之处。

    索引的额外开销:

    (1) 空间:索引需要占用空间;

    (2) 时间:查询索引需要时间;

    (3) 维护:索引须要维护(数据变更时);

    不建议使用索引的情况:

    (1) 数据量很小的表

    (2) 空间紧张

    常用优化总结

    优化语句很多,需要注意的也很多,针对平时的情况总结一下几点:

    1、有索引但未被用到的情况(不建议)

    (1) Like的参数以通配符开头时

    尽量避免Like的参数以通配符开头,否则数据库引擎会放弃使用索引而进行全表扫描。

    以通配符开头的sql语句,例如:select * from t_credit_detail where Flistid like ‘%0’\G

    这是全表扫描,没有使用到索引,不建议使用。

    不以通配符开头的sql语句,例如:select * from t_credit_detail where Flistid like ‘2%’\G

    很明显,这使用到了索引,是有范围的查找了,比以通配符开头的sql语句效率提高不少。

    (2) where条件不符合最左前缀原则时

    例子已在最左前缀匹配原则的内容中有举例。

    (3) 使用!= 或 <> 操作符时

    尽量避免使用!= 或 <>操作符,否则数据库引擎会放弃使用索引而进行全表扫描。使用>或<会比较高效。

    select * from t_credit_detail where Flistid != ‘2000000608201108010831508721’\G

    (4) 索引列参与计算

    应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。

    select * from t_credit_detail where Flistid +1 > ‘2000000608201108010831508722’\G

    (5) 对字段进行null值判断

    应尽量避免在where子句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
    低效:select * from t_credit_detail where Flistid is null ;

    可以在Flistid上设置默认值0,确保表中Flistid列没有null值,然后这样查询:
    高效:select * from t_credit_detail where Flistid =0;

    (6) 使用or来连接条件

    应尽量避免在where子句中使用or来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
    低效:select * from t_credit_detail where Flistid = ‘2000000608201108010831508721’ or Flistid = ‘10000200001’;

    可以用下面这样的查询代替上面的 or 查询:
    高效:select from t_credit_detail where Flistid = ‘2000000608201108010831508721’ union all select from t_credit_detail where Flistid = ‘10000200001’;

    2、避免select *

    在解析的过程中,会将’*’ 依次转换成所有的列名,这个工作是通过查询数据字典完成的,这意味着将耗费更多的时间。

    所以,应该养成一个需要什么就取什么的好习惯。

    3、order by 语句优化

    任何在Order by语句的非索引项或者有计算表达式都将降低查询速度。

    方法:1.重写order by语句以使用索引;

      2.为所使用的列建立另外一个索引
    
      3.绝对避免在order by子句中使用表达式。
    

    4、GROUP BY语句优化

    提高GROUP BY 语句的效率, 可以通过将不需要的记录在GROUP BY 之前过滤掉

    低效:

    SELECT JOB , AVG(SAL)

    FROM EMP

    GROUP by JOB

    HAVING JOB = ‘PRESIDENT’

    OR JOB = ‘MANAGER’

    高效:

    SELECT JOB , AVG(SAL)

    FROM EMP

    WHERE JOB = ‘PRESIDENT’

    OR JOB = ‘MANAGER’

    GROUP by JOB

    5、用 exists 代替 in

    很多时候用 exists 代替 in 是一个好的选择:
    select num from a where num in(select num from b)
    用下面的语句替换:
    select num from a where exists(select 1 from b where num=a.num)

    6、使用 varchar/nvarchar 代替 char/nchar

    尽可能的使用 varchar/nvarchar 代替 char/nchar ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

    7、能用DISTINCT的就不用GROUP BY

    SELECT OrderID FROM Details WHERE UnitPrice > 10 GROUP BY OrderID

    可改为:

    SELECT DISTINCT OrderID FROM Details WHERE UnitPrice > 10

    8、能用UNION ALL就不要用UNION

    UNION ALL不执行SELECT DISTINCT函数,这样就会减少很多不必要的资源。

    9、在Join表的时候使用相当类型的例,并将其索引

    如果应用程序有很多JOIN 查询,你应该确认两个表中Join的字段是被建过索引的。这样,MySQL内部会启动为你优化Join的SQL语句的机制。

    而且,这些被用来Join的字段,应该是相同的类型的。例如:如果你要把 DECIMAL 字段和一个 INT 字段Join在一起,MySQL就无法使用它们的索引。对于那些STRING类型,还需要有相同的字符集才行。(两个表的字符集有可能不一样)

  • MySQL 数据库设计总结

    规则1:一般情况可以选择MyISAM存储引擎,如果需要事务支持必须使用InnoDB存储引擎。

    注意:MyISAM存储引擎 B-tree索引有一个很大的限制:参与一个索引的所有字段的长度之和不能超过1000字节。另外MyISAM数据和索引是分开,而InnoDB的数据存储是按聚簇(cluster)索引有序排列的,主键是默认的聚簇(cluster)索引,因此MyISAM虽然在一般情况下,查询性能比InnoDB高,但InnoDB的以主键为条件的查询性能是非常高的。

    规则2:命名规则。

    1. 数据库和表名应尽可能和所服务的业务模块名一致
    2. 服务与同一个子模块的一类表应尽量以子模块名(或部分单词)为前缀或后缀
    3. 表名应尽量包含与所存放数据对应的单词
    4. 字段名称也应尽量保持和实际数据相对应
    5. 联合索引名称应尽量包含所有索引键字段名或缩写,且各字段名在索引名中的顺序应与索引键在索引中的索引顺序一致,并尽量包含一个类似idx的前缀或后缀,以表明期对象类型是索引。
    6. 约束等其他对象也应该尽可能包含所属表或其他对象的名称,以表明各自的关系

    规则3:数据库字段类型定义

    1. 经常需要计算和排序等消耗CPU的字段,应该尽量选择更为迅速的字段,如用TIMESTAMP(4个字节,最小值1970-01-01 00:00:00)代替Datetime(8个字节,最小值1001-01-01 00:00:00),通过整型替代浮点型和字符型
    2. 变长字段使用varchar,不要使用char
    3. 对于二进制多媒体数据,流水队列数据(如日志),超大文本数据不要放在数据库字段中

    规则4:业务逻辑执行过程必须读到的表中必须要有初始的值。避免业务读出为负或无穷大的值导致程序失败

    规则5:并不需要一定遵守范式理论,适度的冗余,让Query尽量减少Join

    规则6:访问频率较低的大字段拆分出数据表。有些大字段占用空间多,访问频率较其他字段明显要少很多,这种情况进行拆分,频繁的查询中就不需要读取大字段,造成IO资源的浪费。

    规则7:大表可以考虑水平拆分。大表影响查询效率,根据业务特性有很多拆分方式,像根据时间递增的数据,可以根据时间来分。以id划分的数据,可根据id%数据库个数的方式来拆分。

    一.数据库索引

    规则8:业务需要的相关索引是根据实际的设计所构造sql语句的where条件来确定的,业务不需要的不要建索引,不允许在联合索引(或主键)中存在多于的字段。特别是该字段根本不会在条件语句中出现。

    规则9:唯一确定一条记录的一个字段或多个字段要建立主键或者唯一索引,不能唯一确定一条记录,为了提高查询效率建普通索引

    规则10:业务使用的表,有些记录数很少,甚至只有一条记录,为了约束的需要,也要建立索引或者设置主键。

    规则11:对于取值不能重复,经常作为查询条件的字段,应该建唯一索引(主键默认唯一索引),并且将查询条件中该字段的条件置于第一个位置。没有必要再建立与该字段有关的联合索引。

    规则12:对于经常查询的字段,其值不唯一,也应该考虑建立普通索引,查询语句中该字段条件置于第一个位置,对联合索引处理的方法同样。

    规则13:业务通过不唯一索引访问数据时,需要考虑通过该索引值返回的记录稠密度,原则上可能的稠密度最大不能高于0.2,如果稠密度太大,则不合适建立索引了。

    当通过这个索引查找得到的数据量占到表内所有数据的20%以上时,则需要考虑建立该索引的代价,同时由于索引扫描产生的都是随机I/O,生其效率比全表顺序扫描的顺序I/O低很多。数据库系统优化query的时候有可能不会用到这个索引。

    规则14:需要联合索引(或联合主键)的数据库要注意索引的顺序。SQL语句中的匹配条件也要跟索引的顺序保持一致。

    注意:索引的顺势不正确也可能导致严重的后果。

    规则15:表中的多个字段查询作为查询条件,不含有其他索引,并且字段联合值不重复,可以在这多个字段上建唯一的联合索引,假设索引字段为 (a1,a2,…an),则查询条件(a1 op val1,a2 op val2,...am op valm)m<=n,可以用到索引,查询条件中字段的位置与索引中的字段位置是一致的。

    规则16:联合索引的建立原则(以下均假设在数据库表的字段a,b,c上建立联合索引(a,b,c))

    1. 联合索引中的字段应尽量满足过滤数据从多到少的顺序,也就是说差异最大的字段应该房子第一个字段
    2. 建立索引尽量与SQL语句的条件顺序一致,使SQL语句尽量以整个索引为条件,尽量避免以索引的一部分(特别是首个条件与索引的首个字段不一致时)作为查询的条件
    3. Where a=1,where a>=12 and a<15,where a=1 and b<5 ,where a=1 and b=7 and c>=40为条件可以用到此联合索引;而这些语句where b=10,where c=221,where b>=12 and c=2则无法用到这个联合索引。
    4. 当需要查询的数据库字段全部在索引中体现时,数据库可以直接查询索引得到查询信息无须对整个表进行扫描(这就是所谓的key-only),能大大的提高查询效率。
      当a,ab,abc与其他表字段关联查询时可以用到索引
    5. 当a,ab,abc顺序而不是b,c,bc,ac为顺序执行Order by或者group不要时可以用到索引
    6. 以下情况时,进行表扫描然后排序可能比使用联合索引更加有效
      a.表已经按照索引组织好了
      b.被查询的数据站所有数据的很多比例。

    规则17:重要业务访问数据表时。但不能通过索引访问数据时,应该确保顺序访问的记录数目是有限的,原则上不得多于10.

    二.Query语句与应用系统优化

    规则18:合理构造Query语句

    1. Insert语句中,根据测试,批量一次插入1000条时效率最高,多于1000条时,要拆分,多次进行同样的插入,应该合并批量进行。注意query语句的长度要小于mysqld的参数 max_allowed_packet
    2. 查询条件中各种逻辑操作符性能顺序是and,or,in,因此在查询条件中应该尽量避免使用在大集合中使用in
    3. 永远用小结果集驱动大记录集,因为在mysql中,只有Nested Join一种Join方式,就是说mysql的join是通过嵌套循环来实现的。通过小结果集驱动大记录集这个原则来减少嵌套循环的循环次数,以减少IO总量及CPU运算次数
    4. 尽量优化Nested Join内层循环。
    5. 只取需要的columns,尽量不要使用select *
    6. 仅仅使用最有效的过滤字段,where 字句中的过滤条件少为好
    7. 尽量避免复杂的Join和子查询

      Mysql在并发这块做得并不是太好,当并发量太高的时候,整体性能会急剧下降,这主要与Mysql内部资源的争用锁定控制有关,MyIsam用表锁,InnoDB好一些用行锁。

    规则19:应用系统的优化

    1. 合理使用cache,对于变化较少的部分活跃数据通过应用层的cache缓存到内存中,对性能的提升是成数量级的。
    2. 对重复执行相同的query进行合并,减少IO次数。
    3. 事务相关性最小原则
  • MYSQL 实例CPU超过100%的分析

    关于云数据库实例cpu 超过100%,通常这种情况都是由于sql 性能问题导致的,下面我用一则案例来分析:

    用户实例xxx反馈cpu 超过100%,实例偶尔出现卡住的现象

    1.原理:cpu 消耗过大通常情况下都是有慢sql 造成的,这里的慢sql 包括全表扫描,扫描数据量过大,内存排序,磁盘排序,锁争用等待等;

    2.表现现象:sql 执行状态为:sending data,Copying to tmp table,Copying to tmp table on disk,Sorting result,locked;

    3.解决方法:用户可以登录到云数据,通过show processlist查看当前正在执行的sql,当执行完show processlist后出现大量的语句,通常其状态出现sending data,Copying to tmp table,Copying to tmp table on disk,Sorting result, Using filesort 都是sql有性能问题;

    A.sending data表示:sql正在从表中查询数据,如果查询条件没有适当的索引,则会导致sql执行时间过长;

    B.Copying to tmp table on disk:出现这种状态,通常情况下是由于临时结果集太大,超过了数据库规定的临时内存大小,需要拷贝临时结果集到磁盘上,这个时候需要用户对sql进行优化;

    C.Sorting result, Using filesort:出现这种状态,表示sql正在执行排序操作,排序操作都会引起较多的cpu消耗,通常的优化方法会添加适当的索引来消除排序,或者缩小排序的结果集;

    通过show processlist发现如下sql:

    Sql A.

    | 2815961 | sanwenba | 10.241.142.197:55190 | sanwenba |

    Query | 0 | Sorting result | select z.aid,z.subject from

    www_zuowen z right join www_zuowenaddviews za on za.aid=z.aid order by

    za.viewnum desc limit 10;

    性能sql:

    select z.aid,z.subject from www_zuowen z right join www_zuowenaddviews za

    on za.aid=z.aid order by za.viewnum desc limit 10;

     

    用explain 查看执行计划:

    sanwenba@3018 10:00:54>explain select z.aid,z.subject from www_zuowen z

    right join www_zuowenaddviews za on za.aid=z.aid order by za.viewnum desc

    limit 10;

    +—-+————-+——-+——–+—————+———+———+—————–+——

    | id | select_type | table | type | possible_keys | key | key_len | ref |

    rows | Extra |

    +—-+————-+——-+——–+—————+———+———+—————–+——

    | 1 | SIMPLE | za | index | NULL | viewnum | 6 |

    NULL | 537029 | Using index; Using filesort |

    | 1 | SIMPLE | z | eq_ref | PRIMARY | PRIMARY | 3 |

    sanwenba.za.aid | 1 | |

     

    添加适当索引消除排序:

    sanwenba@3018 10:02:33>alter table www_zuowenaddviews add index

    ind_www_zuowenaddviews_viewnum(viewnum);

    sanwenba@3018 10:03:27>explain select z.aid,z.subject from www_zuowen z

    right join www_zuowenaddviews za on za.aid=z.aid order by za.viewnum desc

    limit 10;

    +—-+————-+——-+——–+—————+——————————–+———+-

    | id | select_type | table | type | possible_keys | key |

    key_len | ref | rows | Extra |

    +—-+————-+——-+——–+—————+——————————–+———+-|

    1 | SIMPLE | za | index | NULL |

    ind_www_zuowenaddviews_viewnum | 3 | NULL | 10 | Using index |

    | 1 | SIMPLE | z | eq_ref | PRIMARY PRIMARY | 3 | sanwenba.za.aid

    | 1 | |

    +—-+————-+——-+——–+—————+——————————–+———+-

    Sql B:

    | 2825321 | netzuowen | 10.200.120.41:44172 | netzuowen |

    Query | 2 | Copying to tmp table on disk |

    SELECT * FROM `www_article` WHERE 1=1 ORDER BY rand() LIMIT 0,30

     

    这种sql order by rand()同样也会出现排序;

    netzuowen@3018 10:23:55>explain SELECT * FROM `www_zuowensearch`

    WHERE checked = 1 ORDER BY rand() LIMIT 0,10 ;

    +—-+————-+——————+——+—————+——–+———+——-+——+

    | id | select_type | table | type | possible_keys | key | key_len | ref |

    rows | Extra |

    +—-+————-+——————+——+—————+——–+———+——-+——+

    | 1 | SIMPLE | www_zuowensearch | ref | newest | newest | 1 |

    const | 1443 | Using temporary; Using filesort |

    +—-+————-+——————+——+—————+——–+———+——-+——+

    这种随机抽取一批记录的做法性能是很差的,表中的数据量越大,性能就越差。

    第一种方案,即原始的Order By Rand() 方法:

     

    $sql=”SELECT * FROM content ORDER BY rand() LIMIT 12″;

    $result=mysql_query($sql,$conn);

    $n=1;

    $rnds=”;

    while($row=mysql_fetch_array($result)){

    $rnds=$rnds.$n.”.

    href=’show”.$row[‘id’].”-“.strtolower(trim($row[‘title’])).”‘>”.$row[‘title’].”

    />\n”;

    $n++;

    }

    3万条数据查12条随机记录,需要0.125秒,随着数据量的增大,效率越来越低。

     

    第二种方案,改进后的JOIN 方法:

     

    for($n=1;$n<=12;$n++){

    $sql=”SELECT * FROM `content` AS t1

    JOIN (SELECT ROUND(RAND() * (SELECT MAX(id) FROM `content`)) AS id) AS t2

    WHERE t1.id >= t2.id ORDER BY t1.id ASC LIMIT 1″;

    $result=mysql_query($sql,$conn);

    $yi=mysql_fetch_array($result);

    $rnds = $rnds.$n.”.

    href=’show”.$yi[‘id’].”-“.strtolower(trim($yi[‘title’])).”‘>”.$yi[‘title’].”
    \n”;

    }

    3万条数据查12条随机记录,需要0.004秒,效率大幅提升,比第一种方案提升

    了约30倍。缺点:多次select查询,IO开销大。

     

    第三种方案,SQL语句先随机好ID序列,用IN 查询(飘易推荐这个用法,IO

    开销小,速度最快):

     

    $sql=”SELECT MAX(id),MIN(id) FROM content”;

    $result=mysql_query($sql,$conn);

    $yi=mysql_fetch_array($result);

    $idmax=$yi[0];

    $idmin=$yi[1];

    $idlist=”;

    for($i=1;$i<=20;$i++){

    if($i==1){ $idlist=mt_rand($idmin,$idmax); }

    else{ $idlist=$idlist.’,’.mt_rand($idmin,$idmax); }

    }

    $idlist2=”id,”.$idlist;

    $sql=”select * from content where id in ($idlist) order by field($idlist2) LIMIT

    0,12″;

    $result=mysql_query($sql,$conn);

    $n=1;

    $rnds=”;

    while($row=mysql_fetch_array($result)){

    $rnds=$rnds.$n.”.

    href=’show”.$row[‘id’].”-“.strtolower(trim($row[‘title’])).”‘>”.$row[‘title’].”

    />\n”;

    $n++;

    }

    3万条数据查12条随机记录,需要0.001秒,效率比第二种方法又提升了4倍左右,比第一种方法提升120倍。注,这里使用了order by field($idlist2) 是为了不排序,否则IN 是自动会排序的。缺点:有可能遇到ID被删除的情况,所以需要多选几个ID。

     

    C.出现sending data的情况:

    | 2833185 | sanwenba | 10.241.91.81:45964 | sanwenba | Query

    | 1 | Sending data | SELECT * FROM `www_article` WHERE

    CONCAT(subject,description) like ‘%??%’ ORDER BY aid desc LIMIT 75,15

    性能sql:

    SELECT * FROM `www_article` WHERE CONCAT(subject,description) like

    ‘%??%’ ORDER BY aid desc LIMIT 75,15

    这种sql是典型的sql分页写法不规范的情况,需要将sql进行改写:

    select * from www_article t1,(select aid from www_article where

    CONCAT(subject,description) like ‘%??%’ ORDER BY aid desc LIMIT 75,15)t2 where t1.aid=t2.aid;

    注意这里的索引需要改用覆盖索引:aid+ subject+description

    总结:

     

     

     

    Sql优化是性能优化的最后一步,虽然位于塔顶,他最直影响用户的使用,但也是最容易优化的步骤,往往效果最直接。RDS-mysql由于有资源的隔离,不同的实例规格拥有的iops能力不同,比如新1型提供的iops为150个,也就是每秒能够提供150次的随机磁盘io操作,所以如果用户的数据量很大,内存很小,由于iops的限制,一条慢sql就很有可能消耗掉所有的io资源,而影响其他的sql查询,对于数据库来说就是所有的sql需要执行很长的时间才能返回结果,对于应用来说就会造成整体响应的变慢。

  • MySQL实际内存分配情况介绍

    内存是重要的性能参数,常常出现由于异常的sql请求以及待优化的数据库导致内存利用率升高,更有甚者由于OOM导致实例发生HA切换。

    MySQL的内存大体可以分为两部分:共享内存和session私有内存,下面详细介绍下各部分的构成。

    1. 共享内存

    以下为240M内存规格RDS实例的共享内存分配示意:

    mysql>show variables where variable_name in (
    'innodb_buffer_pool_size','innodb_log_buffer_size','innodb_additional_mem_pool_size','key_buffer_size','query_cache_size'
    );
    +---------------------------------+-----------------+
    | Variable_name                   | Value           |
    +---------------------------------+-----------------+
    | innodb_additional_mem_pool_size | 2097152         |
    | innodb_buffer_pool_size         | 67108864        |
    | innodb_log_buffer_size          | 1048576         |
    | key_buffer_size                 | 16777216        |
    | query_cache_size                | 0               |
    +---------------------------------+-----------------+
    共返回 5 行记录,花费 342.74 ms.
    • innodb_buffer_pool
      该部分缓存是innodb引擎最重要的缓存区域,是通过内存来弥补物理数据文件的重要手段。其中主要包含有数据页、索引页、undo页、insert buffer、自适应哈希索引、锁信息以及数据字典等信息。在进行sql的读和写的操作首先并不是对物理数据文件操作,而是先对buffer_pool进行操作,然后再通过checkpoint等机制写回数据文件。该空间大的优点就是可以提升数据库的性能、加快sql运行速度,缺点是故障恢复速度较慢。在RDS上会采用实例规格配置的80%作为该部分大小(上图即是240M*0.8=192M)。
    • innodb_log_buffer
      该部分主要存放redo log的信息。InnoDB会首先将redo log写在这里,然后按照一定频率将其刷新回重做日志文件中。该空间不需要太大,因为一般情况下该部分缓存会以较快频率刷新至redo log(Master Thread会每秒刷新、事务提交时会刷新、其空间少于1/2同样会刷新)。在RDS上会设置1M的大小。
    • innodb_additional_mem_pool
      该部分主要存放InnoDB内的一些数据结构。经常是在buffer_pool中申请内存的时候还需要在额外内存中申请空间存储该对象的结构信息。该大小主要与表数量有关,表数量越大需要更大的空间。在RDS中统一设置为2M。
    • key_buffer
      该部分是MyISAM表的重要缓存区域。该部分主要存放MyISAM表的键。MyISAM表不同于InnoDB表,其缓存的索引缓存是放在key_buffer中的,而数据缓存则存储于操作系统的内存中。RDS的系统是MyISAM引擎的,因此在RDS中是给予该部分一定量的空间的。所有的实例统一为16M。
    • query_cache
      该部分是对查询结果做缓存以减少解析sql和执行sql的花销。主要适合于读多写少的应用场景,因为它是按照sql语句的hash值进行缓存的,当表数据发生变化后即失效。在RDS上关闭了该部分的缓存。

    2. Session私有内存

    上面这些内存空间是实例创建的时候即分配的内存空间,并且是所有连接共享的。而出现OOM异常的实例都是由于下面各个连接私有的内存造成的。

    主要包括以下部分(以下为测试实例配置):

    mysql>show variables where variable_name in (
    'read_buffer_size','read_rnd_buffer_size','sort_buffer_size','join_buffer_size','binlog_cache_size','tmp_table_size'
    );
    +-------------------------+-----------------+
    | Variable_name           | Value           |
    +-------------------------+-----------------+
    | binlog_cache_size       | 262144          |
    | join_buffer_size        | 262144          |
    | read_buffer_size        | 262144          |
    | read_rnd_buffer_size    | 262144          |
    | sort_buffer_size        | 262144          |
    | tmp_table_size          | 262144          |
    +-------------------------+-----------------+
    共返回 6 行记录,花费 356.54 ms.
    • read_buffer&read_rnd_buffer
      分别存放了对顺序和随机扫描(例如按照排序的顺序访问)的缓存。当thread进行顺序或随机扫描数据时会首先扫描该buffer空间以避免更多的物理读。每个sessionRDS给予256K的大小。
    • sort_buffer
      需要执行order by和group by的sql都会分配sort_buffer用来存储排序的中间结果,当排序的过程中如果存储带下大于sort_buffer_size的话会在磁盘生成临时表以完成操作。根据MySQL的文档可知在linux系统中,当分配空间大于2M时会使用mmap() 而不是 malloc() 来进行内存分配,导致效率降低。在RDS上给予256K。
    • join_buffer
      MySQL仅支持nest loop的join算法,处理逻辑是驱动表的一行和非驱动表联合查找,这时就可以将非驱动表放入join_buffer,不需要访问拥有并发保护机制的buffer_pool。RDS给予256K大小。
    • binlog_cache
      该区域用来缓存该thread的binlog日志。在一个事务还没有commit之前会先将其日志存储于binlog_cache中,等到事务commit后会将其binlog刷回磁盘上的binlog文件以持久化。同样该大小为256K。
    • tmp_table
      不同于上面的各个session层次的buffer,这个参数是可以在控制台上修改。是指用户内存临时表的大小,如果该thread创建的临时表超过它设置的大小会把临时表转换为磁盘上的一张MyISAM临时表。如果用户在执行事务的时候遇到类似“”这样的错误的时候可以考虑将其修改更大一些。
    • [Err] 1114 - The table '/home/mysql/data3081/tmp/#sql_6197_2' is full
  • MySQL 慢日志 介绍

    什么是慢日志?

    MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10S以上的语句。默认情况下,MySQL数据库并不启动慢查询日志,需要我们手动来设置这个参数,当然,如果不是调优需要的话,一般不建议启动该参数,因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件,也支持将日志记录写入数据库表。

    参考文档:

    什么情况下产生慢日志?

    看图说话,有很多开关影响着慢日志的生成,相关的参数后面会挨个说明。从上图可以看出慢日志输出的内容有两个,第一执行时间过长(大于设置的long_query_time阈值);第二未使用索引,或者未使用最优的索引。这两种日志默认情况下都没有打开,特别是未使用索引的日志,因为这一类的日志可能会有很多,所以还有个特别的开关log_throttle_queries_not_using_indexes用于限制每分钟输出未使用索引的日志数量。

    慢日志相关参数


    以上应该是最完整的和慢日志相关的所有参数,大多数参数都有前置条件,所以在使用的时候可以参照上面的流程图。5.6官方文档:

    https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html

    https://dev.mysql.com/doc/refman/5.6/en/server-options.html

    慢日志输出内容

    第一行:标记日志产生的时间,准确说是SQL执行完成的时间点,改行记录每一秒只打印一条;

    第二行:客户端的账户信息,两个用户名(第一个是授权账户,第二个为登录账户),客户端IP地址,还有mysqld的线程ID;

    第三行:查询执行的信息,包括查询时长,锁持有时长,返回客户端的行数,扫描行数。通常我需要优化的就是最后一个内容,尽量减少SQL语句扫描的数据行数。

    第四行:通过代码看,貌似和第一行的时间没有区别。

    第五话:最后就是产生慢查询的SQL语句;

    --log-short-format=true

    如果mysqld启动时指定了--log-short-format参数,则不会输出第一、第二行。

    log-queries-not-using-indexes=on   
    
    log_throttle_queries_not_using_indexes > 0 :
    

    如果启用了以上两个参数,每分钟超过log_throttle_queries_not_using_indexes配置的未使用索引的慢日志将会被抑制,被抑制的信息会被汇总,每分钟输出一次。格式如下:

    
    # Time: 170526 11:26:10
    # User@Host: [] @ [] Id: 38
    # Query_time: 0.021872 Lock_time: 0.008620 Rows_sent: 0 Rows_examined: 0
    SET timestamp=1495769170;
    throttle:         14 'index not used' warning(s) suppressed.;
    

    慢日志分析工具

    1. 官方自带工具: mysqldumpslow
    2. 开源工具:mysqlsla
    3. percona-toolkit:工具包中的pt-query-digest工具可以分析汇总慢查询信息,具体逻辑可以看SlowLogParser这个函数;

     

    慢日志的清理与备份

    删除:直接删除慢日志文件,执行flush logs(必须的);

    备份:先用mv重命名文件(不要跨分区),然后执行flush logs(必须的);

    另外修改系统变量slow_query_log_file也可以立即生效;

    执行flush logs,系统会先close当前的句柄,然后重新open;mv , rm日志文件系统并不会报错,具体的原因可以Google下linux i_count i_nlink ;

  • MySQL 开发实践

    1.MySQL读写性能是多少,有哪些性能相关的重要参数?

    这里做了几个简单压测实验

    机器:8核CPU,8G内存
    表结构(尽量模拟业务):12个字段(1个bigint(20)为自增primary key,5个int(11),5个varchar(512),1个timestamp),InnoDB存储引擎。
    实验1(写):insert => 6000/s
    前提:连接数100,每次insert单条记录
    分析:CPU跑了50%,这时磁盘为顺序写,故性能较高

    实验2(写):update(where条件命中索引) => 200/s
    前提:连接数100,10w条记录,每次update单条记录的4个字段(2个int(11),2个varchar(512))
    分析:CPU跑2%,瓶颈明显在IO的随机写

    实验3(读):select(where条件命中索引) => 5000/s
    前提:连接数100,10w条记录,每次select单条记录的4个字段(2个int(11),2个varchar(512))
    分析:CPU跑6%,瓶颈在IO,和db的cache大小相关

    实验4(读):select(where条件没命中索引) => 60/s
    前提:连接数100,10w条记录,每次select单条记录的4个字段(2个int(11),2个varchar(512))
    分析:CPU跑到80%,每次select都需遍历所有记录,看来索引的效果非常明显!

    几个重要的配置参数,可根据实际的机器和业务特点调整

    max_connecttions:最大连接数

    table_cache:缓存打开表的数量

    key_buffer_size:索引缓存大小

    query_cache_size:查询缓存大小

    sort_buffer_size:排序缓存大小(会将排序完的数据缓存起来)

    read_buffer_size:顺序读缓存大小

    read_rnd_buffer_size:某种特定顺序读缓存大小(如order by子句的查询)

    PS:查看配置方法:show variables like '%max_connecttions%';

    2.MySQL负载高时,如何找到是由哪些SQL引起的?

    方法:慢查询日志分析(MySQLdumpslow)

    慢查询日志例子,可看到每个慢查询SQL的耗时:

    # User@Host: edu_online[edu_online] @ [10.139.10.167]
    # Query_time: 1.958000 Lock_time: 0.000021 Rows_sent: 254786 Rows_examined: 254786
    SET timestamp=1410883292;
    select * from t_online_group_records;
    

    日志显示该查询用了1.958秒,返回254786行记录,一共遍历了254786行记录。及具体的时间戳和SQL语句。

    使用MySQLdumpslow进行慢查询日志分析

    MySQLdumpslow -s t -t 5 slow_log_20140819.txt

    输出查询耗时最多的Top5条SQL语句

    -s:排序方法,t表示按时间 (此外,c为按次数,r为按返回记录数等)
    -t:去Top多少条,-t 5表示取前5条

    执行完分析结果如下:

    Count: 1076100  Time=0.09s (99065s)  Lock=0.00s (76s)  Rows=408.9 (440058825), edu_online[edu_online]@28hosts
      select * from t_online_group_records where UNIX_TIMESTAMP(gre_updatetime) > N
    Count: 1076099  Time=0.05s (52340s)  Lock=0.00s (91s)  Rows=62.6 (67324907), edu_online[edu_online]@28hosts
      select * from t_online_course where UNIX_TIMESTAMP(c_updatetime) > N
    Count: 63889  Time=0.78s (49607s)  Lock=0.00s (3s)  Rows=0.0 (18), edu_online[edu_online]@[10x.213.1xx.1xx]
      select f_uin from t_online_student_contact where f_modify_time > N
    Count: 1076097  Time=0.02s (16903s)  Lock=0.00s (72s)  Rows=52.2 (56187090), edu_online[edu_online]@28hosts
      select * from t_online_video_info where UNIX_TIMESTAMP(v_update_time) > N
    Count: 330046  Time=0.02s (6822s)  Lock=0.00s (45s)  Rows=0.0 (2302), edu_online[edu_online]@4hosts
      select uin,cid,is_canceled,unix_timestamp(end_time) as endtime,unix_timestamp(update_time) as updatetime 
      from t_kick_log where unix_timestamp(update_time) > N
    

    以第1条为例,表示这类SQL(N可以取很多值,这里MySQLdumpslow会归并起来)在8月19号的慢查询日志内出现了1076100次,总耗时99065秒,总返回440058825行记录,有28个客户端IP用到。

    通过慢查询日志分析,就可以找到最耗时的SQL,然后进行具体的SQL分析

    慢查询相关的配置参数

    log_slow_queries:是否打开慢查询日志,得先确保=ON后面才有得分析

    long_query_time:查询时间大于多少秒的SQL被当做是慢查询,一般设为1S

    log_queries_not_using_indexes:是否将没有使用索引的记录写入慢查询日志

    slow_query_log_file:慢查询日志存放路径

    3.如何针对具体的SQL做优化?

    使用Explain分析SQL语句执行计划

    MySQL> explain select * from t_online_group_records where UNIX_TIMESTAMP(gre_updatetime) > 123456789;
    +----+-------------+------------------------+------+---------------+------+---------+------+------+-------------+
    | id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       | +----+-------------+------------------------+------+---------------+------+---------+------+------+-------------+ |  1 | SIMPLE | t_online_group_records | ALL | NULL          | NULL | NULL    | NULL |   47 | Using where |
    +----+-------------+------------------------+------+---------------+------+---------+------+------+-------------+
    1 row in set (0.00 sec)
    

    如上面例子所示,重点关注下type,rows和Extra:

    type:使用类别,有无使用到索引。结果值从好到坏:… > range(使用到索引) > index > ALL(全表扫描),一般查询应达到range级别

    rows:SQL执行检查的记录数

    Extra:SQL执行的附加信息,如”Using index”表示查询只用到索引列,不需要去读表等

    使用Profiles分析SQL语句执行时间和消耗资源

    MySQL> set profiling=1; (启动profiles,默认是没开启的)
    MySQL> select count(1) from t_online_group_records where UNIX_TIMESTAMP(gre_updatetime) > 123456789; (执行要分析的SQL语句)
    MySQL> show profiles;
    +----------+------------+----------------------------------------------------------------------------------------------+
    | Query_ID | Duration   | Query |
    +----------+------------+----------------------------------------------------------------------------------------------+
    | 1 | 0.00043250 | select count(1) from t_online_group_records where UNIX_TIMESTAMP(gre_updatetime) > 123456789 |
    +----------+------------+----------------------------------------------------------------------------------------------+
    1 row in set (0.00 sec)
    MySQL> show profile cpu,block io for query 1; (可看出SQL在各个环节的耗时和资源消耗)
    +----------------------+----------+----------+------------+--------------+---------------+
    | Status | Duration | CPU_user | CPU_system | Block_ops_in | Block_ops_out | +----------------------+----------+----------+------------+--------------+---------------+ ... | optimizing           | 0.000016 | 0.000000 | 0.000000 |            0 | 0 |
    | statistics | 0.000020 | 0.000000 |   0.000000 | 0 |             0 | | preparing            | 0.000017 | 0.000000 | 0.000000 |            0 | 0 |
    | executing | 0.000011 | 0.000000 |   0.000000 | 0 |             0 | | Sending data         | 0.000076 | 0.000000 | 0.000000 |            0 | 0 |
    ...
    

    SQL优化的技巧 (只提一些业务常遇到的问题)

    1. 最关键:索引,避免全表扫描。

    对接触的项目进行慢查询分析,发现TOP10的基本都是忘了加索引或者索引使用不当,如索引字段上加函数导致索引失效等(如where UNIX_TIMESTAMP(gre_updatetime)>123456789)

    +----------+------------+---------------------------------------+
    | Query_ID | Duration   | Query                                 |
    +----------+------------+---------------------------------------+
    |        1 | 0.00024700 | select * from mytable where id=100    |
    |        2 | 0.27912900 | select * from mytable where id+1=101  |
    +----------+------------+---------------------------------------+
    

    另外很多同学在拉取全表数据时,喜欢用select xx from xx limit 5000,1000这种形式批量拉取,其实这个SQL每次都是全表扫描,建议添加1个自增id做索引,将SQL改为select xx from xx where id>5000 and id<6000;

    +----------+------------+-----------------------------------------------------+
    | Query_ID | Duration   | Query                                               |
    +----------+------------+-----------------------------------------------------+
    |        1 | 0.00415400 | select * from mytable where id>=90000 and id<=91000 |
    |        2 | 0.10078100 | select * from mytable limit 90000,1000              |
    +----------+------------+-----------------------------------------------------+
    

    合理用好索引,应该可解决大部分SQL问题。当然索引也非越多越好,过多的索引会影响写操作性能

    1. 只select出需要的字段,避免select
      +----------+------------+-----------------------------------------------------+
      | Query_ID | Duration   | Query                                               |
      +----------+------------+-----------------------------------------------------+
      |        1 | 0.02948800 | select count(1) from ( select id from mytable ) a   |
      |        2 | 1.34369100 | select count(1) from ( select * from mytable ) a    |
      +----------+------------+-----------------------------------------------------+
      
    2. 尽量早做过滤,使Join或者Union等后续操作的数据量尽量小
    3. 把能在逻辑层算的提到逻辑层来处理,如一些数据排序、时间函数计算等
    4. …….

    PS:关于SQL优化,已经有足够多文章了,所以就不讲太全面了,只重点说自己1个感受:索引!基本都是因为索引!

    4.SQL层面已难以优化,请求量继续增大时的应对策略?

    • 分库分表
    • 使用集群(master-slave),读写分离
    • 增加业务的cache层
    • 使用连接池
Copyright © 2014-2025 奋奋的愤愤 | 京ICP备14029030号-1