MYSQL开发规范
日期 |
版本 |
说明 |
修订 |
|
|
|
|
2016.03.20 |
1.0 |
mysql开发规范v1.0 |
锤子 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
修订历史记录
目录
MYSQL开发规范.................................................... 1
1. 引言.......................................................... 3
1.1 背景及目的................................................... 3
1.2 适用范围..................................................... 3
2. 数据库对象命名规范............................................ 3
2.1 原则......................................................... 3
2.2 命名规范..................................................... 3
3. 数据库对象设计规范............................................ 6
3.1 存储引擎的选择............................................... 6
3.2 字符集的选择................................................. 6
3.3 数据库设计规范............................................... 7
3.4 表设计规范................................................... 7
3.5 字段设计规范................................................. 8
3.6 索引设计规范................................................. 9
3.7 约束设计规范................................................ 10
4. SQL编写规范.................................................. 10
4.1 数据类型转换规范............................................ 10
4.2 SELECT * 使用规范........................................... 10
4.3 字段上使用函数使用规范...................................... 11
4.4 表连接规范.................................................. 12
4.5 分页查询规范................................................ 12
4.6 从库多SQL线程复制规范...................................... 13
4.7 随机取数规范................................................ 14
4.8 其他规范.................................................... 14
5. 高效的设计模型............................................... 15
5.1 设计原则说明................................................ 15
5.2正则化与非正则化的选择....................................... 18
5.3表容量设计和数据切分......................................... 18
5.4索引设计..................................................... 21
6.SQL优化指导................................................... 33
6.1select子句优化方法........................................... 33
6.2join联接的优化方法........................................... 41
6.3数据更新语句优化............................................. 43
6.4子查询优化................................................... 44
6.5优化器相关explain以及常用hint介绍.......................... 48
7.常用函数...................................................... 54
7.1字符串函数................................................... 54
7.2日期函数..................................................... 54
7.3类型转换函数................................................. 55
第一条 1. 引言
1.1 背景及目的
随着合商云购电子商务有限公司业务的发展,使用MySQL数据库的系统和应用数量不断扩大,为了提高数据库效率,实现标准化开发及便于数据库的统一管理,制定本规范。
1.2 适用范围
本规范适用于合商云购电子商务有限公司所有与MySQL相关的开发人员、数据库管理员与运营人员。
第二条 2. 数据库对象命名规范
命名规范是指数据库对象如数据库(SCHEMA)、表(TABLE)、索引(INDEX)、约束(CONSTRAINTS)等的命名约定。
1
2.1 原则
n 命名使用具有意义的英文词汇,词汇中间以下划线分隔。
n 命名只能使用英文字母、数字、下划线。
n 避免用MySQL的保留字如:call、group等。
n 所有数据库对象使用小写字母。
2.2 命名规范
1
2
2.1
2.2
2.2.1 数据库命名规范
数据库命名规则如下:
项目简称+1位数据库类型代码+识别代码+序号
数据库类型代码:
1) T:业务型数据库
2) A:分析型数据库
3) H:历史数据库
识别代码:
1) DEV:开发数据库
2) TEST:测试数据库
如果一种类型的数据库一个数据库,则不加序号,否则末尾增加序号。
如果是生产库则不加识别代码,否则需要增加爱识别代码DEV或TEST
如果只作历史库,部分生产、开发或者测试,则只需要项目简称+H+序号
举例:
出入系统业务生产库:AOCT、AOCT1、AOCT2
出入系统业务开发库:AOCTDEV、AOCTDEV1、AOCTDEV2
出入系统业务测试库:AOCTTEST、AOCTTEST1、AOCTTEST2
n 数据库名不能超过30个字符。
n 数据库命名必须为项目英文名称或有意义的简写。
n 数据库创建时必须添加默认字符集和校对规则子句。默认字符集为UTF8。示例见设计规范。
n 命名应使用小写。
2.2.2 数据库命名原则
数据库对象的命名应该以最少的字母达到最容易理解的意义。如果没有特殊规定,数据库对象及其属性的命名应满足如下条件:
1) 命名不推荐使用保留字;
2) 数据库实体统一采用英文命名;
3) 对象命名长度最好不要超过30个字符,缩写要易于理解,符合通用的习惯,例如部门编码缩写:dept_code,组织机构编码缩写:org_code。
4) 前导字符为A至Z
5) 非前导字符可以为:
l A至Z
l 0至9
l _(下划线字符)
附: MySQL中Unicode字符集列表:
字符集名称 |
字节占用 |
字符集兼容性 |
Unicode字符支持 |
UCS2 |
每字符2字节 |
|
所有Unicode 3.0字符 |
UTF16 |
每字符2字节,或4字节。 |
与UCS2兼容 |
所有Unicode 5.0和Unicode 6.0字符,包括扩展字符。 |
UTF16LE |
与UTF16相同,只是字节顺序相反。 |
|
所有Unicode 5.0和Unicode 6.0字符,包括扩展字符。 |
UTF8 |
每字符1到3字节。 |
|
所有Unicode 3.0字符 |
UTF8MB4 |
每字符1到4字节。 |
与UTF8兼容 |
所有Unicode 5.0和Unicode 6.0字符,包括扩展字符。 |
UTF32 |
每字符4字节。 |
|
所有Unicode 5.0和Unicode 6.0字符,包括扩展字符。 |
2.2.2 表命名规范
n 同一个模块的表尽可能使用相同的前缀,表名称尽可能表达含义。
n 多个单词以下划线(_)分隔。
n 表名不能超过30个字符。
n 普通表名以t_开头,表示为table,命名规则为t_模块名(或有意义的简写)_+table_name。
n 临时表(运营、开发或数据库人员临时用作临时进行数据采集用的中间表)命名规则:加上tmp前缀和8位时间后缀(tmp_test_user_20130501)。
n 备份表(运营、开发或数据库人员备份用作保存历史数据的中间表)命名规则:加上bak前缀和8位时间后缀(bak_test_user_20130501)。
n 命名应使用小写。
2.2.3 字段命名规范
n 字段命名需要表示其实际含义的英文单词或简写,单词之间用下划线(_)进行连接。
n 各表之间相同意义的字段必须同名。
n 字段名不能超过30个字符。
n 常用约定:
序号列字段:以Id后缀,如:UserId表示用户编号。
编码字段:以Code后缀,如:CustCode表示客户编码。
时间字段:
1)精确到日的字段,以Date作为后缀。如:OpenDate表示开户日期。
2)精确到秒或毫秒的,以T_time作为后缀。如:RregisterTime表示注册时间。
布尔值字段:命名以“Is”前缀+字段描述。如Member表上表示为Enabled的会员的列命名为IsEnabled。
n 命名应使用帕斯卡(pascal)命名法。
2.2.4 视图命名规范
n 视图名以v_模块名开头,表示view。
n 视图名不能超过30个字符。如超过30个字符则取简写。
n 命名应使用小写。
2.2.5 存储过程命名规范
n 存储过程名以proc_开头,表示procedure。之后多个单词以下划线(_)进行连接。存储过程命名中应体现其功能。存储过程名不能超过30个字符。
n 存储过程中的输入参数以i开头,输出参数以o开头。
n 命名应使用小写。
2.2.6 函数命名规范
n 函数名以func_开头,表示function。之后多个单词以下划线(_)进行连接,函数命名中应体现其功能。函数名不能超过30个字符。
n 函数中输入参数以i开头,输出参数以o开头。
n 命名应使用小写。
2.2.7 触发器命名规范
n 触发器以tri_开头,表示trigger。
n 基本部分,描述触发器所加的表,触发器名不能超过30个字符。
n 后缀(_i,_u,_d),表示触发条件的触发方式(insert,update或delete)。
n 如无特殊需要,严禁开发人员使用触发器。
n 命名应使用小写。
2.2.8 索引命名规范
n 二级(辅助)索引以idx_开头,唯一索引以uidx_开头。后面紧跟索引所在的字段名。如要在id列上添加二级索引,则应为idx_id。
n 多单词组成的列名,取尽可能代表意义的缩写,如test_contact表member_id和friend_id上的组合索引:idx_mid_fid。
n 组合索引命名应注意字段顺序。如在字段member和字段userid上创建组合索引,则可以命名为idx_userid_member(‘userid’,‘member’)
n 命名应使用小写。
2.2.9 约束命名规范
n 唯一约束: uidx_表名称_字段名。
n 外键约束:fk_表名,后面紧跟该外键所在的表名和对应的主表名(不含t_).子表名和父表名用下划线(_)分隔。
n 非空约束:如无特殊需要,建议所有字段默认非空,不同数据类型必须给出默认值。
n 出于性能考虑,如无特殊需要,建议不使用外键。参照完整性由代码控制。
n 命名应使用小写。
2.2.10 脚本文件命名规范
说明:
执行用户:表示脚本在哪个用户下运行。例如脚本是在deployop用户下执行,则执行用户为deployop。
对象类型:表示是对数据库的什么对象类型(例如table)作的操作。不同对象类型的操作必须放在不同的文件中。
操作类型:包括DDL、DML。不同操作类型的sql脚本不能放在同一个文件中。
n DDL文件的命名:此次需求序列号(系统工单号即SR号)+执行顺序号+脚本执行用户+对象类型缩写+客户化的信息+mysql .sql。
例如:id5771_01_deployop_table_xiechuanjiang419_mysql.sql
n DML文件的命名:此次需求序列号(系统工单号即SR号)+执行顺序号+脚本执行用户+dml+客户化的信息+mysql.sql。
例如:id5771_02_deployop_dml_xiechuanjiang419_mysql.sql
n 不同数据库的DB脚本要分开编写。
n DML语句必须显示加commit语句。
2.2.11 脚本文件书写规范
因MYSQL是轻量级数据库,各(开发、测试、生产)环境的数据库名可能会不同,所以脚本文件中操作(创建、删除、访问)表时不能加数据库名。
移交的脚本中不能通过use database指定数据库名。
在实际执行脚本前,必须使用use database指定到当前实例对应的数据库上。
第三条 3. 数据库对象设计规范
2
3.1 存储引擎的选择
MySQL支持数个存储引擎作为对不同表的类型的处理器,MySQL中的插件式存储引擎架构是非常有特色的亮点。如无特殊要求,默认使用innodb存储引擎,该引擎为5.6版本中的默认存储引擎。
MySQL引擎 |
说明 |
InnoDB |
索引和数据都可以缓存到内存中; 支持事务; 支持行级锁,可实现更高的并发度; 支持故障恢复; 支持外键约束; 支持4种不同的事务隔离级别; |
3.2 字符集的选择
n 自开发系统的数据库Utf8作为字符集的惟一选择。
n 外购系统的字符集按照开发同事要求选择,需申请例外。
n 在开发环境中编写的建库建表脚本及使用工具导出的数据脚本文件本身,必须在导出工具中,显式选择utf8作为导出格式。
n 在开发环境中编写的建库建表脚本及使用工具导出的数据脚本文件,如在进库前需要编辑,必须使用纯文本方式打开,编辑和保存,防止隐含控制字符(如^M)添加进脚本。在Linux环境,可以通过 “cat -A脚本文件名”方式确认和检查是否携带了隐含控制字符。
3.3 数据库设计规范
n 控制单库表个数,建议单库不超过2048个表。
n 创建数据库的语句必须包含字符集子句和校对规则子句。如:
create database [if not exists]
default character set UTF8MB4 default collate utf8mb4_bin;
3.4 表设计规范
n 不同组件间所对应的数据库表之间的关联应尽可能减少,如果不同组件间的表需要外键关联也尽量不要创建外键关联,而只是记录关联表的一个主键,确保组件对应的表之间的独立性,为系统或表结构的重构提供可能性。
n 表设计的角度不应该针对整个系统进行数据库设计,而应该根据系统架构中组件划分,针对每个组件所处理的业务进行组件单元的数据库设计。
n 表必须要有PK。
n 一个字段只表示一个含义。
n 表不应该有重复列,比如,年月日用不同的字段设计是不允许的。
n 总是包含两个字段:created_date(创建日期),updated_date (修改日期),且这两个字段不应该包含有额外的业务逻辑,在创建或修改记录的时候,必须创建或修改这两个字段
示例:
created_time Datetime not null comment '创建日期' ,
updated_time Datetime not null comment '修改日期' ,
n 禁止使用复杂数据类型(数组,自定义等)。
n 需要join的字段(连接键),数据类型必须保持绝对一致,避免隐式转换。
n 设计应至少满足第三范式,尽量减少数据冗余。一些特殊场景允许反范式化设计,但在项目评审时需要对冗余字段的设计给出解释。
n TEXT字段必须放在独立的表中,用PK与主表关联。如无特殊需要,禁止使用TEXT、BLOB字段。
n 需要定期删除(或者转移)过期数据的表,通过分表解决。
n 单表字段数不要太多,建议最多不要大于50个。
n MySQL在处理大表时,性能就开始明显降低,所以建议单表物理大小限制在16GB,表中数据控制在2000W内。
n 如果数据量或数据增长在前期规划时就较大,那么在设计评审时就应加入分表策略。
n 无特殊需求,严禁使用分区表。
3.5 字段设计规范
n INT:如无特殊需要,存放整型数字使用UNSIGNED INT型。整型字段后的数字代表显示长度。整型类型如下表:
数据类型 |
最大存储长度(有符号) |
最大存储长度(无符号) |
tinyint(m) |
1个字节 范围(-128~127) |
1个字节 范围(0~256) |
smallint(m) |
2个字节 范围(-32768~32767) |
2个字节 范围(0~65535) |
mediumint(m) |
3个字节 范围(-8388608~8388607) |
3个字节 范围(0~16777215) |
int(m) |
4个字节 范围(-2147483648~2147483647) |
4个字节 范围(0~4294967294) |
bigint(m) |
8个字节 范围(+-9.22*10的18次方) |
8个字节 范围(0~1.84*10的20次方) |
n DECIMAL(M,D):定点小数使用此DECIMAL类型,且明确标识出为无符号型(UNSIGNED),除非确实会出现负数。
n DATE:所有只需要精确到天的字段全部使用DATE类型,而不应该使用TIMESTAMP或者DATETIME类型。
n DATETIME:所有需要精确到时间(时分秒)的字段均使用DATETIME,不要使用TIMESTAMP类型。
n VARCHAR:所有动态长度字符串 全部使用VARCHAR类型,类似于状态等有限类别的字段,也使用可以比较明显表示出实际意义的字符串,而不应该使用INT之类的数字来代替;VARCHAR(N),N表示的是字符数而不是字节数。比如VARCHAR(255),可以最大可存储255个字符(字符包括英文字母,汉字,特殊字符等)。但N应尽可能小,因为MySQL一个表中所有的VARCHAR字段最大长度是65535个字节,且存储字符个数由所选字符集决定。如UTF8存储一个字符最大要3个字节,那么varchar在存放占用3个字节长度的字符时不应超过21845个字符。同时,在进行排序和创建临时表一类的内存操作时,会使用N的长度申请内存。
如无特殊需要,原则上单个varchar型字段不允许超过255个字符。
n CHAR:仅仅只有单个字符的字段使用CHAR(1)类型,例如性别字段。如无特殊需要,建议INNODB引擎不使用CHAR型 。
n TEXT:仅仅当字符数量可能超过20000个的时候,才可以使用TEXT类型来存放字符类数据,因为所有MySQL数据库都会使用UTF8字符集。所有使用TEXT类型的字段必须和原表进行分拆,与原表主键单独组成另外一个表进行存放。如无特殊需要,严禁开发人员使用MEDIUMTEXT、TEXT、LONGTEXT类型。
n 使用INT UNSIGNED型存储IPV4。PHP程序推荐使用long型存储IPV4(非强制)。
n 对于精确浮点型数据存储,需要使用DECIMAL,严禁使用FLOAT和DOUBLE。
n 如无特殊需要,严禁开发人员使用BLOB类型。
n 如无特殊需要,字段必须使用NOT NULL属性,可用默认值代替NULL。MySQL NULL类型和Oracle的NULL有差异,会进入索引中。此外,NULL在索引中的处理也是特殊的,也会占用额外的存放空间。
n 不建议使用ENUM、SET类型,使用TINYINT来代替。
n 每个列定义的时候必须加上comments。
n 自增字段类型必须是整型且必须为UNSIGNED,推荐类型为INT或BIGINT,并且自增字段必须是主键或者主键的一部分。
n 日期类型的字段不能使用VARCHAR或者CHAR类型,只能使用DATE、DATETIME字段类型存放。
3.6 索引设计规范
n 索引必须创建在索引选择性选择性较高的列上。
索引选择性:索引列中不同值的数目与表中记录数的比值。如SEX字段共100条记录,只存放男、女两个值,则在SEX列上创建的索引idx_sex的索引选择性为2/100=0.02
MySQL没有位图索引。
n 组合索引的首字段,必须在where条件中。
n 对于确定需要组成组合索引的多个字段,建议将选择性高的字段靠前放。
n 禁止使用外键,容易产生死锁,应由程序保证参照完成性。
n Text类型字段必须使用前缀索引。
n 单张表的索引数量理论上应控制在5个以内。经常有大批量插入、更新操作表,应尽量少建索引。
n 组合索引中的字段数建议不超过5个。
n ORDER BY,GROUP BY,DISTINCT的字段需要添加在索引的后面,形成覆盖索引。
n 禁止对过长(MySQL的varchar索引只支持不超过768个字节,UTF8是三字节,即:768/3=256,所以字段最长为255)的VARCHAR类型字段建立索引,除前缀索引外超过32字节的varchar列加索引需要DBA评估。如果需要对超过32字节的varchar列整列进行完全匹配,需要新增一个字段,该字段是varchar列的md5值,使用md5来进行完全匹配;这种情况下,就无法进行范围查询。
3.7 约束设计规范
n PK应该是有序并且无意义的,尽量由开发人员自定义,且尽可能短,使用自增序列。
n 表中除PK以外,还存在唯一性约束的,可以在数据库中创建以“uidx_”作为前缀的唯一约束索引。
n PK字段不允许更新。
n 禁止创建外键约束,外键约束应有应用程序控制。
n 如无特殊需要,所有字段必须添加非空约束。
n 如无特殊需要,所有字段必须有默认值。
第四条 4. SQL编写规范
3
4.1 数据类型转换规范
- 1.
- 2.
- 3.
- 4.
4.1.
4.1.1 基本原则
在所有Query的Where条件中必须使用和过滤字段完全一致的数据类型,杜绝任何隐式类型转换,避免造成因为数据类型不匹配而导致Query执行计划的出错,造成性能问题。
4.1.2 详细说明
所有Where条件的字段上不允许使用函数做类型转换,如有需要转换类型,只能转换数值或变量代入值,而不是转换字段。
最为常见的隐式类型转换常见于时间类型与字符串类型之间,建议所有时间类型字段均以时间类型传入,或者以字符串传入然后通过字符串转换时间函数进行转换,如下:
select * from member
where member_create =str_to_date('20150701 01:02:03','%Y%m%d %H:%i:%s');
在表连接Query中,如果连接条件两端的数据类型不一致,必须保证将驱动表的连接条件数据类型转换为与被驱动表一致的数据类型。
select * from test_info a,test_customer b
where a.order_id =cast(b.order_id as unsigned int) and a.pay_type= 'cash ';
4.2 SELECT * 使用规范
4.2.
4.2.1 基本原则
严禁使用select *进行查询。
4.2.2 详细说明
n MySQL数据库进行Order By操作有两种算法:
² 第一种是直接取出所有需要返回字段(select后面的字段),存入内存中,然后排序(仅有需要排序的字段需要参与)。
² 第二种是先取出需要排序的字段,然后排序,再回表中取出其他的字段,就相当于所有数据都有两次磁盘IO。表现在MYSQL执行计划中为FILESORT。
如果select后面的字段长度总和超过1024字节(即参数max_length_for_sort_data的默认值)或者字段中包括BLOB、TEXT字段,都将会使用第二种算法。所以order by的查询不允许使用select *。
n join语句使用select *可能导致只需要访问索引即可完成的查询需要回表取数,所以禁止使用。
mysql> explain SELECTt1.a,t2.aFROM t1 LEFT JOIN t2 ON t1.a =t2.a;
+----+-------------+-------+-------+---------------+-----------+---------+--------------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+-----------+---------+--------------+------+-------------+
| 1 | SIMPLE | t1 | index | NUL | idx_t1_ac | 66 | NUL | 5 |Using index|
| 1 | SIMPLE | t2 | ref | idx_t2_a | idx_t2_a | 33 | test1.t1.a | 1 |Using index|
+----+-------------+-------+-------+---------------+-----------+---------+--------------+------+-------------+
mysql> explain SELECT*FROM t1 LEFT JOIN t2 ON t1.a =t2.a;
+----+-------------+-------+------+---------------+------+---------+------+------+----------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+----------------------------------+
| 1 | SIMPLE | t1 | ALL | NUL | NULL | NUL | NULL | 5 |NUL |
| 1 | SIMPLE | t2 | ALL | idx_t2_a | NULL | NUL | NULL | 5 |Using where; Using join buffer (Block Nested Loop)|
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------------+
n MySQL中的text类型字段和Oracle的clob一样,存储的时候不是和由其他普通字段类型的字段组成的记录存放在一起,而且读取效率本身也不如普通字段块。如果不需要取回text字段,又使用了select * ,会让完成相同功能的sql所消耗的io量大很多,而且增加部分的io效率也更低下。
4.3 字段函数使用规范
4.3.
4.3.1 基本原则:
在取出字段上可以使用相关函数(尽可能避免出现now(),rand(),sysdate(),current_user()等不确定结果的函数),但是在Where条件中的过滤条件字段上严禁使用任何函数,包括数据类型转换函数。
4.3.2 详细说明:
n 语句级(STATEMENT)复制场景下,引起主从数据不一致;不确定值的函数,产生的SQL语句无法利用QUERY CACHE。
n 错误的写法:
Select member_create
from .. .
Where date_format(member_create, '%Y%m%d %H:%i:%s')= '20150701 00:00:0'
n 正确的写法:
Select date_format(member_create, '%Y%m%d %H:%i:%s')
from .. .
Where member_create =str_to_date('20150701 00:00:00', '%Y%m%d %H:%i:s');
4.4 表连接规范
4.4.
4.4.1 基本原则:
所有连接的SQL必须使用Join ... On ...方式进行连接,而不允许直接通过普通的Where条件关联方式。外连接的SQL语句,可以使用Left Join ... On的Join方式,且所有外连接一律写成Left Join,而不要使用Right Join。
如无特殊需要,严禁开发人员使用STRAIGHT_JOIN。
4.4.2 详细说明:
n 错误的写法:
select a.id,b.id from a,b where a.id = b.a_id and ...
n 正确的写法:
select a.id,b.id from a inner join b on a.id = b.a_id where ...
4.5 分页查询规范
4.5.
4.5.1 基本原则:
分页查询语句全部都需要带有排序条件,除非业务方明确要求不要使用任何排序来随机展示数据。
4.5.2 详细说明:
常规分页语句写法(start:起始记录数,page_offset:每页记录数,步长):
select *
from table_a t
order by test_modified desc limit start, page_offset
多表Join的分页语句,如果过滤条件在单个表上,内查询语句必须走覆盖索引,先分页,再Join:
n 错误的写法:
select a.column_a, a.column_b .. . b.column_a, b.column_b .. .
from table_t a, table_b b
where a.xxx.. .
and a.column_c = b.column_d
order bya.yyy limit start, page_offset
n 正确的写法:
select a.column_a, a.column_b .. . b.column_a, b.column_b .. .
from (select t.column_a, t.column_b .. .
from table_t t
where t.xxx.. .
order by t.yyy limit
start, page_offerset) a,
table_b b
where a.column_c = b.column_d;
4.6 从库多SQL线程复制规范
4.6.
4.6.1 基本原则:
使用从库多SQL线程复制时必须保证一个事务只能包括单个数据库的相关对象。
4.6.2 详细说明:
n 错误的写法:
start transaction;
Update customer.customer_info set banlance= banlance-100 where customer_id=123;
Insert apply.apply_info values(apply_id,customer_id,apply_date,amount)
values(9900,123, str_to_date('20130801 00:00:00', '%Y%m%d %H:%i:s'),100) ;
commit;
n 正确的写法:
应该将customer_info表和apply_info表放在一个数据库里面,如果涉及到分库分表,也是按照customer_id将各个表的数据拆分到不同的数据库里面。
4.7 随机取数规范
4.7.
4.7.1 基本原则:
严禁在MYSQL数据库中使用RAND()函数生成随机数,严禁开发人员直接使用ORDER BY RAND()取随机数,而应在应用中使用其他方法替换。ORDER BY rand()会将数据从磁盘中读取,进行排序,会消耗大量的IO和CPU。
4.7.2 详细说明:
下面给出的是非必要写法。严禁直接在MYSQL中使用RAND()函数。
n 错误的写法:
Select id,name,age,sex
from member
order by rand() limit 10;
n 正确的写法:
SELECT id,name,age,sex
FROM `member`
WHERE id >= (SELECT floor(RAND() * (SELECT MAX(id) FROM `member`)))
ORDER BY id LIMIT 10;
4.8 其他规范
n 进行大批量操作时必须分批提交,每次数据量操作不能超过10W条。
n last_insert_id()函数只能返回当前session最近一次insert操作之后所使用到的auto_increment类型字段的值,且使用"select last_insert_id()",不要再跟一个"from table_name"。
n WHERE条件中严禁在索引列上进行数学运算或函数运算。
n 用in() /union替换or,并注意in的个数小于300。
n 严禁使用%前缀进行模糊前缀查询,可以使用%模糊后缀查询。如:select id,val from table where val like ‘name%’。
n 使用prepared statement,可以提高性能并且避免SQL注入。
n 严禁开发人员使用LOCK TABLE语句人为加锁;仅允许使用SELECT * .. FOR UPDATE语句。
n Where条件尽可能避免非等值条件,in,between,<,<=,>,>=会导致后面的条件使用不了索引。
n 使用union all 代替union。排序操作应当在union all前的子查询中执行。union all不需要对结果集再进行排序。
n Update,delete语句不要使用limit,否则可能会导致主从架构不一致,同时错误信息会记录到错误日志,占用大量空间。
n Insert 语句必须指明字段名称,避免后期因为字段扩展,影响原有应用。
n Insert语句使用bulk提交,values的个数不应该过多。bulk提交可以提高写效率,但个数过多,数据恢复需要时间过长。
n 拆分复杂的SQL为多个小SQL,避免大事务。简单的SQL容易使用到MYSQL的QUERY CACHE;减少锁表时间特别是MYISAM;可以使用多核CPU。
n 尽量采用批量SQL语句。
a)INSERT ... ON DUPLICATE KEY UPDATE
b)REPLACE INTO
c)INSERT IGNORE
d)INSERT INTO VALUES()
n 对同一表的多次alter操作必须合并为一次操作
严禁开发人员使用alter语法。
MYSQL对表的修改绝大部分操作都需要锁表并重建表,而锁表则会对线上业务造成影响。为减少这种影响,必须把对表的多次alter操作合并为一次操作。例如,要给表t增加一个字段b,同时给已有的字段aa建立索引, 通常的做法分为两步:
alter table t add column b varchar(10);
然后增加索引:
alter table t add index idx_aa(aa);
正确的做法是:
alter table t add column b varchar(10),add index idx_aa(aa);
第五条 5. 高效的设计模型
数据库设计是指对于一个给定的应用环境,构造合理的数据库模式,建立数据库及其应用系统,有效存储数据,满足用户信息要求和处理要求。
数据库设计在开发过程中处于一个非常重要的地位。一个高效的数据库模型是非常重要和必要的。
4
5.1 设计原则说明
- 5.
5.1.
5.1.1 完整性
数据库完整性是指数据库中数据的正确性和相容性。数据库完整性是由完整性约束来保证的。数据库完整性约束通过DBMS或者应用程序来实现的。
数据库完整性对于数据库应用系统非常关键,其作用主要体现在以下几个方面:
n 数据库完整性约束能够防止合法用户使用数据库时向数据库中添加不合语义的数据。
n 利用基于DBMS的完整性控制机制来实现业务规则,易于定义,容易理解,而且可以降低应用程序的复杂性,提高应用程序的运行效率。同时,基于DBMS的完整性控制机制是集中管理的,因此比应用程序更容易实现数据库的完整性。
n 合理的数据库完整性设计,能够同时兼顾数据库的完整性和系统的效能。比如装载大量数据时,只要在装载之前临时使基于DBMS的数据库完整性约束失效,此后再使其生效,就能保证既不影响数据装载的效率又能保证数据库的完整性。 如:在导入大量数据时,可以在创建表时不添加主键外的其他索引,可以得到明显的写入性能提升。
n 在应用软件的功能测试中,完善的数据库完整性有助于尽早发现应用软件的错误。
完整性主要包括实体完整性,参照完整性以及用户定义完整性。它们的实现机制如下:
n 实体完整性:主键
n 参照完整性:
² 父表中删除数据:级联删除;受限删除;置空值
² 表中插入数据:受限插入;递归插入
² 父表中更新数据:级联更新;受限更新;置空值
² DBMS对参照完整性可以有两种方法实现:外键实现机制(约束规则)和触发器实现机制
n 用户定义完整性:指针对某一具体关系数据库的约束条件,它反映某一具体应用所涉及的数据必须满足的语义要求,比如表中利用check关键字定义age的取值范围。
了解如上的基本知识外,在设计数据库完整性时,有一些原则需要注意:
n 根据数据库完整性约束的类型确定其实现的系统层次和方式,并提前考虑对系统性能的影响。一般情况下,静态约束应尽量包含在数据库模式中,而动态约束由应用程序实现。其中静态约束主要是指数据库确定状态时的数据对象所应满足的约束条件,它是反映数据库状态合理性的约束,这是最重要的一类完整性约束,比如列取值,列类型,空值等的约束。而动态约束主要是指数据库从一种状态转变为另一种状态时,新、旧值之间所应满足的约束条件,它是反映数据库状态变迁的约束,比如一些用户自定义类的完整性约束。
n 实体完整性约束、参照完整性约束是关系数据库最重要的完整性约束,在不影响系统关键性能的前提下可考虑使用。
n 要慎用目前主流DBMS都支持的触发器功能,一方面由于触发器的性能开销较大,另一方面,触发器的多级触发不好控制,容易发生错误。
n 要根据业务规则对数据库完整性进行细致的测试,以尽早排除隐含的完整性约束间的冲突和对性能的影响。
n 为了在数据库和应用程序代码之间提供另一层抽象,可以为应用程序建立专门的视图而不必非要应用程序直接访问数据表。这样做还等于在处理数据库变更时给你提供了更多的自由。
5.1.2 性能
性能是衡量一个系统的关键因素,在设计阶段就在性能方面就应该多关注,尽量减少后期的烦恼。
在数据库设计阶段,性能上的考虑时需要注意:不能以范式作为唯一标准或者指导,在设计过程中,需要从实际需求出发,以性能提升为根本目标来展开设计工作,一些时候为了提升性能,甚至会做反范式设计。
另外还有一些设计上的方法和技巧:
n 设置合理的字段类型和长度。字段类型在满足需求后应尽量短,比如,能用int就尽量不要用bigint。另外不同数据库在varchar和text类型在长度和性能上也是不同的,选择时要谨慎。
n 选择高效的主键和索引。由于对表记录的读取都是直接或者间接地通过主键或索引来获取,因此应该该根据具体应用特性来设计合理的主键或索引。同时索引长度的也应该关注,尽量减少索引长度。
n 适度冗余。适度的冗余可以避免关联查询,减少join查询。
n 精简表结构。表结构如果太过复杂,会引起业务上处理复杂,同时也可能会引起并发问题。如果根据业务特性拆分成多个表,可以避免高并发下的锁表现象。
5.1.3 扩展性
在大规模系统中,除了性能,可扩展性也是设计的关键点,而数据库表扩展性主要包含表逻辑结构、功能字段的增加、分表等。在扩展性上要把握的原则如下:
n 一表一实体。如果不同实体之间有关联时,可增加一个单独的表,不会影响以前的功能。
n 扩展字段。在表数据较小时增加一个字段可以很快完成,但是在表很大时,增加字段会比较困难。因此在设计时可考虑选择预留扩展字段。
n 分表设计。也就是水平切分。在设计阶段应该考虑数据的增长情况,并根据数据特性以及数据之间关系选择合适的切分策略。有关分表的更详细介绍具体可详见章节3.1.5的介绍
5.2正则化与非正则化的选择
5.2.
5.2.1 正则化和非正则化
数据库正则化是指消除冗余、有效组织数据、减少在数据操作期间潜在的不规则和提高数据一致性。在正则化数据库中,每个元素只会被存储一次,而在非正则化数据库中,信息是重复的或者保存在多个地方的。
比如在设计消费者与账号信息表时,如果消费者的信息比如姓名、联系方式等都存储在这张消费者与账号信息表中时,当“张三”有多个账号时,“张三”的基本信息就被多次存储,这样设计是不符合正则化的。
5.2.2 正则化的利弊
正则化包括根据设计规则创建表并在这些表间建立关系;通过取消冗余度与不一致相关性,该设计规则可以同时保护数据并提高数据的灵活性。
一般情况下正则化有如下优点:
n 正则化更新通常比非正则化更新快。
n 当数据被很好正则化后,数据量很少,重复数据小,更新量也会变少。
n 正则化表通常较小,更容易装入内存性能因此也会更好。
当然正则化也是存在一些缺点:在正则化结构上的非一般性查询,一般都需要至少一个联接。
5.2.3 非正则化的利弊
非正则化由于数据都在一个表中,避免了联接,同时也能运行更有效率的索引策略,因此性能不错。
当然非正则化在数据冗余,数据独立性,相关性以及提高数据一致性方面稍差。
5.2.4 正则化和非正则化的结合
正则化和非正则化各有利弊,在具体应用中通常是结合两种方案。
5.3表容量设计和数据切分
5.3.
5.3.1. 使用场景
随着数据库表中数据日积月累越来越多,数据库会越来越大,表记录数也会达到千万甚至亿级别,数据库表的访问效率下降明显,导致外层应用的访问效率非常差,访问时间急剧上升,用户体验下降。此时就必须使用数据切分来解决这个瓶颈了。
数据切分就是指通过某种特定的条件,将存放在同一个数据库中的数据分散存放到多个数据库上面,以达到分散单台设备负载的效果。数据切分根据切分规则的类型,可以分为两种模式。一种是按照不同的表切分到不同的数据库上或不同的表上,这种切分称为垂直切分;另外一种是根据表中数据的逻辑关系,将同一个表的数据按照某种规则切分到多台数据库上或不同的表上,这种称为水平切分。
5.3.2. 垂直切分
垂直切分就是要把表按模块划分到不同数据库或不同表中。这种切分在大型网站的演变过程中是很常见的。当一个网站还在很小的时候,只有少量的人来开发和维护,各模块和表都在一起,当网站不断丰富和壮大的时候,也会变成多个子系统来支撑,这时就有按模块和功能把表划分出来的需求。
一个架构设计较好的应用系统,其总体功能肯定是由多个功能模块所组成的,而每一个功能模块所需要的数据对应数据库中的就是一张表或者多个表。在架构设计中,各个功能模块互相之间的交互点越统一、越少,系统的耦合度就越低,系统各个模块的维护性及扩展性也就越好。这样的系统,实现数据的垂直切分也就越容易。比如公司的轩辕系统中,直销模块和工作流模块数据库原本在一起的,但随着推广工作进行,导致该数据库的压力较大,因此对直销模块和工作流模块进行了拆分工作。由于前期设计时这两个模块之间的耦合度较低,在拆分过程中也很顺利。
功能模块越清晰,耦合度越低,数据垂直切分的规则定义也就越容易。完全可以根据功能模块来进行数据切分,不同功能模块的数据存放在不同的数据库或不同表上,可以很容易就避免跨库的join存在,同时系统架构也是非常清晰的。
当然,很难有系统能够做到所有功能模块使用的表完全独立,根本不需要访问对方的表,或者不需要将两个模块的表进行join操作。这种情况下,就必须根据实际的应用场景来进行评估权衡。迁就应用程序就需要将待join的表的相关模块存放在同一个数据库表中,或者让应用程序做更多的事情——完全通过模块接口取得不同数据库表中的数据,然后在程序中完成join操作。
如果让多个模块集中共用数据源,实际上也是间接默认了各模块架构耦合度增大的发展,可能会恶化以后的架构。
所以,在数据库进行垂直切分的时候,如何切分、切分到什么样的程度,是一个比较考验人的难题,这只能在实际应用场景中通过平衡各方面的成本和收益,才能分析出一个真正合适自己的切分方案。
垂直切分的优缺点:
n 优点:
² 数据库表的切分简单明了,切分规则明确;
² 应用程序模块清晰明确,整合容易;
² 数据维护方便易行,容易定位;
n 缺点:
² 部分表关联无法在数据库级别完成,需要在程序中进行;
² 对于访问极其频繁且数据量超大的表仍然存在性能瓶颈,不一定能满足要求;
² 事务处理相对复杂;
² 切分达到一定程度之后,扩展性会受到限制;
² 过度切分可能会导致系统过于复杂而难以维护;
5.3.3. 水平切分
水平切分可以简单理解为按照数据行的切分,就是将表中的某些行切分到一个数据库或表,而另外的某些行又可以切分到其他的数据库或表中。为了能够比较容易判定各行数据被切分到哪个库或表中,切分需要按照某种特定的规则进行。
水平切分的切分标准可以按照数据范围分,比如1-100万一个表,100万-200万又是一个表;也可以按照时间顺序来切分,比如一年的数据归到一张表中等;也可以按照地域范围来分,比如按照地市来分,每个或多个地市一个库等;也可以按照某种计算公式来切分,比较简单的比如取模的方式,如根据用户id进行水平切分,可通过对ID被2取模,然后分别存放在不同的表中,这样关联时也非常方便。公司著名的凤巢拆库就是采用取模方式进行的拆分。
水平切分的优缺点:
n 优点:
² 表关联基本能够在数据库端全部完成;
² 不会存在某些超大型数据量和高负载的表遇到瓶颈的问题;
² 应用程序端整体架构改动相对较少;
² 事务处理相对简单;
² 只要切分规则能够定义好,扩展性一般不会受到限制;
n 缺点:
² 切分规则相对复杂,很难抽象出一个能满足整个数据库的切分规则;
² 后期的维护难度有所增加,人为手工定位数据较困难。
² 应用系统各模块耦合度非常高,可能会对后面数据的迁移切分造成一定的困难。
² 若切分不合理,会造成数据表的冷热不均现象。
5.3.4. 垂直与水平联合切分
由上面可知垂直切分能更清晰化模块划分,区分治理,水平切分能解决大数据量性能瓶颈问题。本节将结合垂直切分和水平切分的优缺点,进一步完善整体架构,并提高系统的扩展性。
在实际的应用场景中,除了那些负载并不是太大、业务逻辑相对简单的系统可以通过上面两种切分方法之一来解决扩展性问题,但是大部分的业务逻辑复杂、系统负载大的系统,都无法通过上面任何一种数据的切分方法来实现较好的扩展性。这就需要将上述两种方法结合使用,不同的场景使用不同的切分方法。
每一个应用系统的负载都是一步一步增长起来的,在开始遇到性能瓶颈时,大多架构师和DBA都会选择数据的垂直切分。然而随着业务的不断扩张,系统负载的持续增长,在系统稳定一段时间之后,经过垂直切分的数据库集群可能再次不堪重负,遇到性能瓶颈。
这时再进一步细分模块?随着模块的不断细化,应用系统的架构会越来越复杂,整个系统很可能会出现失控的局面。这时就必须利用数据水平切分的优势来解决问题。在垂直切分的基础上利用水平切分来避开垂直切分的弊端,解决系统不断扩大的问题。而水平切分的弊端也已经被之前的垂直切分解决了。
在大型的应用系统上,垂直和水平切分基本上是并存的,而且是不断的交替进行的,以增加系统的扩展能力。我们在应对不同的应用场景时就要充分考虑这两种切分方法的局限及优势,在不同的时期使用不同的方法。
联合切分的优缺点:
n 优点
² 可以充分利用垂直和水平切分各自的优势而避免各自的缺陷;
² 让系统扩展性得到最大化提升。
n 缺点
² 数据库系统架构会比较复杂,维护难度更大;
² 应用程序架构也更复杂。
5.4索引设计
5.4.
5.4.1. InnoDB B-Tree
存储引擎使用了不同的方式把B-Tree索引保存到磁盘上,它们会表现出不同的性能。例如MyISAM使用前缀压缩的方式以减小索引;而InnoDB不会压缩索引。同时MyISAM的B-Tree索引按照行存储的物理位置来引用被索引的行,但是InnoDB按照主键值引用行。这些不同有各自的优点和缺点。
1
2
3
4
5
5.1
5.2
5.3
5.4
5.4.1
5.4.1.1 InnoDB聚簇索引(cluster index)
聚簇索引不是一种单独的索引类型,而是一种存储数据的方式。当表有聚簇索引的时候,它的数据行实际保存在索引的叶子页。聚簇是指实际的数据行和相关的键值都保存在一起。每个表只能有一个聚簇索引。
由于是存储引擎负责实现索引,并不是所有的存储引擎都支持聚簇索引。当前只有SolidDB和InnoDB是唯一支持聚簇索引的存储引擎。
数据与索引在同一个B-Tree上,一般数据的存储顺序与索引的顺序一致。InnoDB cluster index每个叶子节点包含primary key和行数据,非叶子节点只包括被索引列的索引信息。
聚簇索引的优缺点:
n 优点:
² 相关的数据保存在一起,利于磁盘存取;
² 数据访问快,因为聚簇索引把索引和数据一起存放;
² 覆盖索引可以使用叶子节点的primary key的值使查询更快;
n 缺点:
² 如果访问模式与存储顺序无关,则聚簇索引没有太大的用处;
² 按主键顺序插入和读取最快,但是如果按主键随机插入(特别是字符串)则读写效率降低;
² 更新聚簇索引的代价较大,因为它强制InnoDB把每个更新的行移到新的位置;
² 建立在聚簇索引上的表在插入新行,或者在行的主键被更新,该行必须被移动的时候会进行分页。分页发生在行的键值要求行必须被放到一个已经放满了数据的页的时候,此时存储引擎必须分页才能容纳该行,分页会导致表占用更多的磁盘空间。
² 聚簇表可能会比全表扫描慢,尤其在表存储的比较稀疏或因为分页而没有顺序存储的时候。
² 非聚簇索引可能会比预想的大,因为它们的叶子节点包含了被引用行的主键列。
² 非聚簇索引访问需要两次索引查找,而不是一次。
其它需要说明点:
InnoDB的primary key为cluster index,除此之外,不能通过其他方式指定cluster index,如果InnoDB不指定primary key,InnoDB会找一个unique not null的field做cluster index,如果还没有这样的字段,则InnoDB会建一个非可见的系统默认的主键---row_id(6个字节长)作为cluster_index。
建议使用数字型auto_increment的字段作为cluster index。不推荐用字符串字段做cluster index (primary key) ,因为字符串往往都较长, 会导致secondary index过大(secondary index的叶子节点存储了primary key的值),而且字符串往往是乱序。cluster index乱序插入容易造成插入和查询的效率低下。
5.4.1.2 InnoDB辅助索引(secondary index)
InnoDB中非cluster index的所有索引都是secondary index。
secondary index的查询代价变大,需要两次B-Tree查询,一次secondary index,一次cluster index。所以在建立cluster index和secondary index的时候需要考虑到这点。
当secondary index满足covering index(参见3.5.1.4章节介绍)时,只需要一次B-Tree查询并且直接在secondary index便可获取所需数据,不需要再进行数据读取,提高了效率。我们在设计索引和写SQL语句的时候就可以考虑利用到covering index的优势。
建议尽量减少对primary key的更新,因为secondary index叶子节点包含primary key的value (这样避免当row被移动或page split时更新secondary index), primary key的变化会导致所有secondary index的更新。
5.4.1.3 InnoDB动态哈希(adaptive hash index)
动态哈希索引是InnoDB为了加速B-Tree上的节点查找而保存的hash表 。B-Tree上经常被访问的节点将会被放在动态哈希索引中。
注意点:
MySQL重启后的速度肯定会比重启前慢,因为InnoDB的innodb_buff_pool和adaptive hash index都是内存型的,重启后消失,需要预热(访问一段时间)后性能才能慢慢上来。
5.4.1.4 InnoDB覆盖索引(covering index)
索引通常是用于找到行的,但也可以用于找到某个字段的值而不需要读取整个行,因为索引中存储了被索引字段的值,只读索引不读数据,这种情况下的索引就叫做覆盖索引。
覆盖索引是很有力的工具,可以极大地提高性能。它主要的优势如下:
n 索引记录通常远小于全行大小,因此只读索引,MySQL就能极大的减少数据访问量。这对缓存的负载是非常重要的,它大部分的响应时间都花在拷贝数据上。对于I/O密集型的负载也有帮助。因为索引比数据小很多,能很好的装入内存。
n 索引是按照索引值来进行排序的,因此I/O密集型范围访问将会比随机地从磁盘提取每行数据要快的多。
n 覆盖索引对于InnoDB来说非常有用,因为InnoDB的聚集缓存。InnoDB的辅助索引在叶子节点保存了主键值,因此,覆盖了查询的第二索引在主键上避免了另外一次索引查找。
5.4.2. 前缀索引
5.4.2
5.4.2.1 前缀索引介绍
在MySQL中,索引只能从字段内容的最左端开始建, 查询的时候也只能从索引的最左端开始查, 对字段内容只建从左开始的部分字节的索引,而非全部做索引的这种index就叫做前缀索引(prefix index)。
前缀索引的优缺点:
n 优点
在索引满足一定的区分度的情况下,索引变得更小,更有利于放入或将更多的索引放入内存,减少I/O操作,提高效率。
n 缺点
前缀索引不支持covering index和order by。举例说明下:假如表account上有如下索引(balance,customer_email(50),account_number);其中字段customer_email的定义为varchar(100),那如下的两个SQL并不能完全使用该索引。
select account_number
from account
where balance=100.1 and customer_email=’1@1.com’;
select account_id
from account
where balance=100.1 and customer_email=’1@1.com’ order by account_number;
5.4.2.2 前缀索引适合的字段类型
前缀索引一般可以提供高性能所需的选择。如果索引blob和text列,或者很长的varchar列,就必须定义前缀索引,这样既能节约空间同时能得到好的性能。
int型的不建议使用prefix index,虽然可以提升效率,但是却不能使用order by, covering index等, 建议使用更小的数字类型如tinyint,bit等来满足。
5.4.2.3 前缀索引的合理长度选择
前缀索引涉及索引到底建多长的选择。短的索引可以节约空间。但是前缀又应该足够长,使他的选择性能够接近索引整个列,因此前缀的基数性应该接近于全列的基数性。
设计索引的时候结合记录数、字符集大小、字段长度、字段内容的重复程度、字符之间的相关性等考虑索引长度,索引长度不当将使索引过于庞大,内存资源利用不高, 造成IO较重,程序效率降低。合理的索引长度,可以在满足较好索引区分度的情况下减少索引所占空间,我们的目标就是找到索引空间大小与索引区分度的一个平衡点。
选择索引长度的方法:
1) 首先了解表中记录的总体情况,如果表中数据还不存在或者很少,应该通过了解业务去构造和模拟符合业务和产品特点的数据,使用这些数据来模拟上线后的真实数据。
2) show table status\G;能看到avg_row_length (每行的平均长度,不准确)、rows(不准确)、data所占空间、已有索引所占空间等信息。
3) select count(*) from table;查看准确的总体行数。
4) 查看欲建立索引的字段的总体情况
5) 通过select * from t procedure analyse()\G;能看到表中所有字段的min_value、max_value、min_length、max_length、是否为null、字段平均长度、字段类型优化建议等信息。其中字段长度的相关信息很重要,它给出了字段的大致信息,对索引长度的选择很有帮助,而字段类型优化则是在已有内容基础上给出的类型优化, 例如:如果你的表中有1000万行, 字段name为字符串, 但是却只有”a”,”b”,”c”三个值,则会建议优化字段类型为enum(“a”,”b”,”c”),这样查询和索引效率都会大大提高。
6) 查看欲建立字段的最佳索引区分度,select count(distinct city)/count(*) from city_demo;是该字段全部内容长度都做索引能达到的最理想的区分度,这个首先可以用来衡量该字段是否适合做索引。
7) 看不同索引长度的区分度,这个是个平均值例如:
Select count(distinct left(city, 3))/count(*) as sel3,
Count(distinct left(city, 4))/count(*) as sel4,
Count(distinct left(city, 5))/count(*) as sel5,
Count(distinct left(city, 6))/count(*) as sel6,
Count(distinct left(city, 7))/count(*) as sel7
From city_demo;
8) 查看到city字段做3个字节索引、4个字节索引、5个字节索引、6个字节索引、7个字节索引的区分度,可以一直增加索引长度来探测结果。
9) 如果随着索引长度的增加,索引区分度在很明显地增大, 那说明我们应该继续增加索引长度,使当我们增加索引长度时,索引区分度没有明显变化,我们仍然应该继续增加索引长度探测。
10) 那么探测到何时为止呢?当我们发现继续增加很多索引长度但是区分度却没有明显提升而现有区分度接近第3条中的最佳区分度时,这个时候的索引长度可能就比较合理了。
11) 截止上面的步骤,我们找的都是平均分布,有可能出现的是平均区分度很好而少量数据集中出现区分度极差的情况,所以我们还需要查看一下区分度分布是否均匀。
12) 查看区分度是否均匀:
select count(*) as cnt,city
from city_demo
group by city
order by cnt desc limit 100;
select count(*) as cnt,left(city,3) as pref
from city_demo
group by pref
order by cnt desc limit 100;
select count(*) as cnt,left(city,4) as pref
from city_demo
group by pref
order by cnt desc limit 100;
select count(*) as cnt,left(city,5) as pref
from city_demo
group by pref
order by cnt desc limit 100;
13) 索引选择的最终长度应该在平均区分度(前4条)与区分度是否均匀(第5条)之间长度做一个综合的选择。
建完索引后show table status查看索引大小。这是一个收尾且非常重要的工作,我们必须清楚的知道建立这个索引的代价。
5.4.3. 索引的合理设计和使用
5.4.3
5.4.3.1 索引的字段及长度
4.1.1
4.1.2
4.1.2.1
- 1.
- 2.
- 3.
- 4.
- 5.
5.1.
5.2.
5.3.
5.4.
5.4.1.
5.4.2.
5.4.3.
5.4.3.1.
5.4.3.1.1.
5.4.3.1.1 主键和候选键上的索引
在create table语句中,我们可以指定主键和候选键。主键和候选键都有unique约束,这些列不会包含重复值。MySQL自动为主键和每个候选键创建一个唯一索引,以便新值的唯一性可以很快检查,而不必扫描全表。同时加速对于这些列上确定值的查找。主键的索引名为prmariy,候选键的名为该键包含的第一列的列名。如果存在多个候选键的名字以同一个列名开头,就在该列明后放置一个顺序号码区别。
5.4.3.1.2 连接列上创建索引
一般会在表的连接列上建立索引,尤其是该表频繁参与连接操作。对于一个比较大的连接操作,如果被驱动表的连接列上没有索引的话,由于MySQL的连接算法是nested loop算法,会造成多次扫描被驱动表,对数据库造成的压力和开销是巨大的。
5.4.3.1.3 在高选择度的列上创建索引
属性列上的选择度是指该列所包含的不重复的值和数据表中总行数(T)的比值,它的比值在1到1/T之间。选择度越大,越适合建索引。因为对于要查找这个列上的一个值的行,通过索引可以过滤掉大部分数据行,剩下的符合要求的行数较少,可以快速在数据表中定位这些行。相反,如果列的选择度比较小,通过索引过滤后的行数依然很大,和全表扫描的开销没有明显的改善,甚至会更大(全表扫描带来的是顺序I/O,而通过索引过滤后的扫描可能是随机I/O)。因此,在选择索引列时的首要条件就是候选列的选择度。索引要建立在那些选择度高的索引上,在选择度低的列上尽量避免建索引。
5.4.3.1.4 创建联合索引的选择
在很多时候,where子句中的过滤条件并不是只针对某一个字段,经常会有多个字段一起作为查询过滤条件存在于where子句中。在这时候,就需要判断是该仅仅为过滤性最好的(选择度最大)的列建立索引,还是在所有过滤条件中所有列上建立组合索引。对于这个问题,需要衡量两种方案各自的优劣。当where子句中的这些字段组成的联合索引过滤性远大于其中过滤性最高的单列,就适合建联合索引。这样就意味着where子句中对应的每个列过滤性都不高,但是这些单列的过滤性乘在一起后过滤性就高了。
例如:要从存储着学籍信息的表中查找来自中国,大连的女性学生,使用的SQL的where子句如下:
where country =‘china’and city =‘dalian’and gender = female;
country和city的联合(country,city)的选择性会比country和city各自的选择性高,同时因为gender本身的选择性低,将其加入对于提高总体选择性贡献不大,所以在此情境下适合建立(country,city)的联合索引。
同时从性能角度讲,MySQL使用联合索引比使用index_merge算法来使用各个单列索引的效率要高,性能要好。因此对于经常一起出现在where子句中的过滤条件组合,优先考虑建立这些条件列的联合索引,而不是为每个单列建立索引。
5.4.3.1.5 通过索引列属性的前缀控制索引的长度
索引占用的空间越小,对于MySQL获得高性能越有益。不管是什么类型的索引,在查询中使用都是需要从磁盘中加载到内存中去的,无论是MyISAM对应的key cache,还是InnoDB对应的buffer pool。这些受到程序自身和硬件条件限制都是有大小限制的,如果索引大小比较大的话,会造成这些存放索引的内存区域无法存下整个索引数据,根据LRU算法频繁地淘汰索引,加载新的索引进去,这就造成比较大的I/O开销。
如果要建索引的列是很长的字符串的话,它会使索引变大。如果大小超过限制的话,可以考虑建前缀索引,即只索引数据列中存储的数据的前几个字符,而不是全部的值,这样可以有效地减小索引的大小。当然这样做的前提是保证索引的选择性。在选择列上要索引的字符长度时,考虑选择性不能只看平均值,还要考虑最坏情况下的选择性。因为使用前缀索引而索引的字符数不足的话,容易造成数据分布不均匀。如果这种情况比较极端,可能会造成索引的作用下降。
5.4.3.2 如何在操作中利用索引
3
4
5
5.1
5.2
5.3
5.4
5.4.1
5.4.2
5.4.3
5.4.3.1
5.4.3.2
5.4.3.2.1 索引与排序操作
MySQL有两种产生排序结果的方式:使用文件排序(filesort),或者使用扫描有序的索引。explain的输出中type列的值为“index”,这说明MySQL会扫描索引。
MySQL能为排序和查找行使用同样的索引。如果可能,按照这样一举两得的方式设计索引是个好主意。按照索引对结果进行排序,只有当order by子句中的顺序和索引最左前缀顺序完全一致,并且所有列排序的方向(升序或降序)一样才可以。order by无需定义索引的最左前缀的一种情况是索引中其它前导列在where子句中为常量。如果查询联接了多个表,只有在order by子句的所有列引用的是第一个表才可以。
5.4.3.2.2 索引与分组操作
group by实际上也同样需要进行排序操作,而且与order by相比,group by主要只是多了排序之后的分组操作。当然,如果在分组时还是用了其他一些聚合函数,还需要一些聚合函数的计算。所以在group by的实现过程中,与order by一样可以使用索引。同时使用索引带来的性能提升是巨大的。
在MySQL中group by使用索引的方式有两种:使用松散(loose)索引扫描;使用紧凑索引扫描。
松散索引扫描实现group by是指MySQL完全利用索引扫描来实现group by时,并不需要扫描所有满足条件的索引键即可完成操作,得出结果。如果MySQL使用了这种方式,则在explain的extra行会出现“using index for group-by”。要利用到松散索引扫描实现group by,需要至少满足以下几个条件:
a) group by条件列必须处在同一个索引的最左连续位置;
b) 在使用group by同时,只能使用max和min这两个聚合函数;
c) 如果引用到了该索引中group by条之外的列条件,它就必须以常量形式出现。
紧凑索引扫描实现group by是指MySQL需要在扫描索引时,读取所有满足条件的索引键值,然后再根据读取到的数据来完成group by操作,已得到相应的结果。这时的执行计划中就不会出现“using index for group-by”。使用这种索引扫描方式要满足的条件是:group by条件列必须是索引中的列,同时索引中位于该条件列左边的列必须以常数的形式出现在where子句中。
除了上述两种使用索引扫描来完成group by外,还可以使用临时表加filesort实现。但是这种方式带来的性能开销比较大,一般也比较费时。所以group by最好实现方式是松散索引扫描,其次是紧凑索引扫描,最后是使用临时表和filesort。
5.4.3.2.3 索引与求distinct查询
distinct实际上和group by操作非常相似,只是在group by之后的每组中只取其中一条记录而已。所以,distinct的实现方式和group by也基本相同。同样通过松散索引扫描或者紧凑索引扫描的方式实现要优于使用临时表实现。但在使用临时表时,MySQL仅是使用临时表缓存数据,而不需要进行排序,也就省了filesort操作。
5.4.3.2.4 索引与带有limit子句的查询
含有limit子句的查询往往同时含有order by子句(如果没有order by子句则优化方法和普通查询一样)。这样的查询最好在排序时使用索引扫描进行排序。否则即使limit子句中只取排序后起始部分很少的数据都会引起MySQL取出全部符合条件的数据进行排序。如果使用索引扫描的话,则不需要对所有数据排序,只需扫描索引取出满足limit限制的数据即可。
同时对于limit子句中的大偏移量的offset,比如limit 10000,20,它就会产生10020行数据,并且丢掉前10000行。这个操作的代价太大。一个提高效率的简单技巧是在覆盖索引上进行偏移,而不是对全行数据进行偏移。也可以将从覆盖索引上提取出来的数据和全表数据进行连接,然后取得需要的数据。
5.4.3.2.5 索引与连接操作
在MySQL中,只有一种连接join算法即nested loop join。该算法就是通过驱动表的结果集作为循环的基础数据,然后将该结果集中的数据作为过滤条件一条条地到下一个表中查询数据,最后合并结果。所以在通过结果集中的数据作为过滤条件到下一个表中地位数据时,最好是通过索引,而不是扫表。因为如果结果集中的数据比较多,要是每次都通过扫描来定位的话,造成的开销和对MySQL的压力是巨大的。因此,最好在被驱动表的连接列上建立索引,并且使MySQL在连接过程中使用索引。
5.4.3.2.6 索引与隔离列
如果在查询中没有隔离索引的列,MySQL通常不会使用索引。“隔离”列意味着它不是表达式的一部分,也没有位于函数中。
例如,下面的查询不能使用actor_id上的索引:
select account_id from account where account_id+1=5;
人们能轻易地看出where子句中的actor_id等于4,但是MySQL却不会帮你求解方程。应该主动去简化where子句,把被索引的列单独放在比较运算符的一边。
下面是另外一种常见的问题:
select expenditure from consume where to_days(current_date)-to_days(consume_time) <=10;
这个查询将会查找date_col值离今天不超过10的所有行,但是它不会使用索引,因为使用了to_days()函数。下面是一种比较好的方式:
select expenditure from consume where consume_time>= date_sub(current_date,interval 10 day) ;
这个查询就可以使用索引,但是还可以改进。使用current_date将会阻止查询缓存把结果缓存起来,可以使用常量替换掉current_date的值:
select expenditure from consume where consume_time>= date_sub('2010-12-12',interval 10 day);
5.4.3.3 索引创建的建议
5.4.3.3
5.4.3.3.1 对联合索引中包含属性列的选择
对于where子句中过滤条件涉及的属性列大致相同的一系列SQL建立共同的索引。如果共同涉及的属性列是多个的话,则应建立联合索引。在确定联合索引应该包含这些共同涉及的属性列中的哪些时,应该考察这些WHERE子句对于涉及这些列上的过滤条件的形式。对于那些是范围条件对应的列,由于B-Tree索引本身的限制,只能选取其中一个选择度比较高的列进入联合索引。而对于那些等值条件对应的列,原则上都可以进入联合索引,但是需要综合考虑联合索引最后的大小和进入索引的列的选择度。如果属性列的选择度非常低的话,把它放入索引对于联合索引的选择度贡献比较小,但是会增大索引大小,引起其它开销。所以不要把这样的列加入到索引中去。如3.2.1.4节中提到的例子,gender列的选择性较低,加入联合索引对于提高联合索引的选择性没有太大帮助,但却增加了联合索引的大小。
5.4.3.3.2 正确创建联合索引中各列的顺序
对于MySQL普遍使用的B-Tree索引,索引列的顺序对于SQL使用该索引至关重要。如果索引中列的顺序不合理,在使用过程中往往会使该索引无法被使用或者通过该索引得到的过滤能力大大减弱。
首先由于B-Tree索引的数据结构限制,只有当SQL的where子句使用索引的最左前缀的时候,索引才能被使用、发挥作用。所以在创建索引、决定索引的顺序时,应提取希望使用该索引SQL的where子句中的过滤条件,提炼出其中的最常出现的条件和其对应的属性列。按照这些列的选择度由高到低排列这些属性列,按照这个顺序创建这个索引。同时相关SQL的where子句中出现的过滤条件顺序,以尽量让这些SQL可以使用建立的索引的最左前缀。
对于联合索引中包含的属性列中,有一列对应在相关SQL的where子句的过滤条件是以范围条件出现,而索引中其他属性列是以等于条件出现,则应该把这些等值条件对应的列放在索引的前面,把范围条件对应的列放在索引的最后。
select account_id from consume where account_payee =72478814 andexpenditure>1.00;
为上述SQL创建对应的联合索引时:如果创建索引(expenditure,account_payee),由于expenditure列上是范围条件,所以索引(expenditure,account_payee)无法使用完全(只能使用索引中的expenditure部分);如创建索引(account_payee, expenditure),SQL则可以完全使用此索引。所以针对上述SQL应该创建联合索引(account_payee, expenditure)。
5.4.3.3.3 避免重复索引
MySQL允许你在同一列上创建多个索引,它不会注意到你的错误,也不会为错误提供保护。MySQL不得不单独维护每一个索引,并且查询优化器在优化查询的时候会逐个考虑它们,这会严重影响性能。重复索引是类型相同,以同样的顺序在同样的列上创建的索引。应该避免创建重复索引,并且在发现它时把它移除掉。
有时会在不经意间创建重复索引。例如下面的代码:
create table test(
id int not null primary key,
unique(id),
index(id)
);
对于id列,首先它是primary key,同时unique(id)使MySQL自动为id创建了名为id的索引,最后index(id),现在给id列创建了三个索引。这通常是不需要的,除非需要为同一列建立不同类型的索引,如B-Tree,fulltext等类型索引。
5.4.3.3.4 避免多余索引
多余索引和重复索引不同。例如列(A,B)上有索引,那么另外一个索引(A)就是多余的。也就是说(A,B)上的索引能被当成索引(A)(这种多余只适合B-Tree索引)。多余索引通常发生在向表添加索引的时候,例如,有人也许会在(A,B)上添加索引,而不是对索引(A)进行扩展。
对于B-Tree类型索引,有单列索引对应的属性列出现在了某个联合索引的第1位置上,那么这个单列索引可能是多余的。
如果某一索引是主键的超集,那么这个索引除非有特殊理由(如希望使用覆盖索引),否则也是多余索引。因为主键是唯一索引,过滤能力很强,和它建立联合索引意义不大。
在大部分情况下,多余索引都是不好的,为了避免它,应该扩展已有索引,而不是添加新的索引。但是,还有一些情况出于性能考虑需要多余索引。使用多余索引的主要原因是扩展已有索引的时候,它会变得很大。
5.4.3.3.5 使用覆盖索引
索引是找到行的高效方式,但是MySQL也能使用索引来接收数据,这样就可以不用读取行数据。包含所有满足查询需要的数据的索引叫覆盖索引。覆盖索引和任何一种索引都不一样。覆盖索引必须保存它包含的列的数据。MySQL只能使用B-Tree索引来覆盖查询。
在SQL执行中要使用覆盖索引的话,需要相应的索引包含SQL中where子句中涉及的列都在索引中且满足最左前缀,同时SQL的返回列也必须在索引中。同是where子句中的过滤条件中不能包含like等操作符和函数。MySQL使用了覆盖索引,会在explain输出中出现“using index”字样。
有关使用覆盖索引的案例请参见第4章4.2.3.6使用covering index优化select语句。
5.4.3.4 索引的维护
5.4.3.4
5.4.3.4.1 数据的optimize和analyze操作
MySQL查询优化器在决定如何使用索引的时候会调用两个API,以了解索引如何分布。第一个调用接受范围结束点并且返回该范围内记录的数量;第二个调用返回不同类型的数据,包括数据基数性(每个键值有多少记录)。当存储引擎没有向优化器提供查询检查的行的精确数量的时候,优化器会使用索引统计来估计行的数量,统计可以通过运行analyze table重新生成。MySQL的优化器基于开销,并且主要的开销指标是查询会访问多少数据。如果统计永远没有产生,或者过时了,优化器就会做出不好的决定。解决方案是运行analyze table。
每个存储引擎实现索引统计的方式不同,由于运行analyze table的开销不同,所以运行它的频率也不一样。
Memory存储引擎根本就不保存索引统计。
MyISAM把索引统计保存在磁盘上,并且analyze table执行完整的索引扫描以计算基数性。整个表都会在这个过程中被锁住。
InnoDB不会把统计信息保存到磁盘上,同时不会时时去统计更新它们,而是在第一次打开表的时候利用采样的方法进行估计。InnoDB上的analyze table命令就使用了采样方法,因此InnoDB统计不够精确,除非让服务器运行很长的时间,否则不要手动更新它们。同样,analyze table在InnoDB上不是阻塞性的,并且相对不那么昂贵,因此可以在不大影响服务器的情况下在线更新统计。
B-Tree索引能变成碎片,它降低了性能。碎片化的索引可能会以很差或非顺序的方式保存在磁盘上。同是表的数据存储也能变得碎片化。碎片化对于数据的读取,尤其是范围数据的读取,会使读取速度慢很多。为了消除碎片,可以运行optimize table解决。
5.4.3.5 索引的其他说明
5.4.3.5
5.4.3.5.1 索引对插入、更新的影响和避免
索引是独立于基础数据之外的一部分数据。假设在table t中的column c创建了索引idx_t_c,那么任何更新column c的操作包括插入insert,update,MySQL在更新表中column c的同时,都必须更新column c上的索引idx_t_c数据,调整因为更新带来键值变化的索引信息。而如果没有对column c建立索引,则仅仅是更新表中column c的信息就可以了。这样因调整索引带来的资源消耗是更新带来的I/O量和调整索引所致的计算量。
基于以上的分析,在更新非常频繁地字段不适合创建索引。很多时候是通过比较同一时间内被更新的次数和利用该列作为条件的查询次数来判断的,如果通过该列的查询并不多,可能几个小时或者更长时间才会执行一次,更新反而比查询更频繁,那么这样的字段肯定不适合创建索引。
第六条 6.SQL优化指导
- 1.
5
6
6.1 select子句优化方法
- 6.
6.1.
6.1.1. 去掉select子句中非必要的结果列
在select子句中使用select *会给MySQL数据库造成比较大的资源开销和性能影响。比如select *会造成覆盖索引(covering index)失效,因为索引极少甚至不可能包含表中所有的属性列;在MySQL使用filesort时,由于select *中包含blob,text列,而导致filesort使用双路排序算法,从而增加IO和SQL执行时间;select *本身返回了一些不需要的列,增加了无用的IO,网络传输,CPU等开销。
基于select *的缺点,在写select子句时,应该仔细分析需要返回的属性列,只把需要的属性列加入select子句。对于select *要保持警惕,除非有确实的需求,一般不写这样的select子句。
6.1.2. select子句中的聚合函数的优化
select子句中可以包含count(),min(),max()等聚合函数。而索引和列的可空性常常帮助MySQL优化掉这些聚合函数。比如,为了查找一个位于B树最左边的列的最小值,那么直接找索引的第一行就可以了。如果MySQL使用了这种优化,那么在explain中看到“select tables optimized away”。同样地,没有where子句的count(*)通常也会被一些存储引擎优化掉(比如MyISAM总是保留着表行数的精确值)。
但是对于没有索引直接可用的min()和max()时,一般做不到很好地优化。可以尝试优化掉min()和max(),利用数据的有序性配合limit 1将SQL等价转化。
应用场景:
select min(customer_id)from customer where customer_name= '张三';
因为在customer_name上没有索引,所以查询会扫描整个表。从理论上说,如果MySQL扫描主键,它应该在发现第一个匹配之后就立即停止,因为主键是按照升序排列的,这意味着后续的行会有较大的customer_id。但是在这个例子中,MySQL会扫描整个表。一个变通的方式就是去掉min(),并且使用limit来改写这个查询,如下:
select customer_id from customer use index(primary) where customer_name ='张三' limit 1;
这个通用策略在MySQL试图扫描超过需要的行时能很好地工作。
6.1.3. select子句中加入控制结构避免重复实现优化
访问同一张表的连续几个查询应该引起注意,即使它们嵌在if…then…else结构中,也是如此。看下面的应用场景:它包含了两个查询,但只引用到了一张表。
select count(*) from consume where account_id=6111and account_payee=51906734;
select count(*) from consume where account_id=6111and account_payee=95161305;
这两个SQL访问同一张表(consume),扫描同一个索引(index_accountid_expenditure_consumetime)两次。这些开销属于重复开销。可以使用汇总技术,不用依次检查不同的条件了,一遍就能收集到多个结果,然后再测试这些结果,而不需要在访问数据库。应该汇总技术改写合并上述两个SQL得到新的SQL:
select sum(case account_payee when '51906734'then 1 else 0 end) as p1_sum, sum(case account_payee when '95161305'then 1 else 0 end) as p2_sum from consume where account_id=6111;
SQL只需要扫描索引(index_accountid_expenditure_consumetime)一次及表(consume)就能获得结果,节省了很大的开销。
6.1.4. from子句优化方法
6
6.1
6.1.1
6.1.2
6.1.3
6.1.4
6.1.4.1 去掉from子句中未涉及的table及非必要的联接
在SQL时,应该检查一下在from子句中出现的那些表的作用。这其中主要会出现3种类型的表:
n 从中返回数据的表,其字段可能用于也可能没有用于where子句的过滤条件中;
n 该表中没有要返回的数据,但在where子句中的条件使用了该表的的列;
n 仅作为另两种表之间的“粘合剂”而出现的表,使SQL可以通过表连接将那些1),2)类型的表连在一起。
n 既没有从该表中返回数据,也没有过滤条件涉及该表,同时该表也没有参加联接。
对于第4)类型的表,属于from子句中整个SQL未涉及的表,可以从from子句中去掉。
对于第3)类型的表,举例分析:表A联接表B,联接列是ab,同时表B联接表C。其中表B是第3)类型的表。需要分析,A表中的ab列是否可以为空值:如果可以空值,则A和B的联接对最后的结果集是有贡献的,不能去掉表B及A与B的联接;反之,不可以为空,如果表A可以直接联接表C的话,可以去掉表B,表A直接联接表C。上述结论是基于分析:空值的值是未知的,不能说它等于任何东西,两个空值也不相等。对于表A中的列ab可以为空值的情况,如果去掉了表B及表A与它的联接,那么表A中那些其它属性列的值符合过滤条件,但ab列对应值为空值的记录有可能进入最后的结果集。因此在这种情况,去掉表B及相关联接,可能改变结果集。
如下场景:
select a.account_number from consume c, cust_acct b,account a where a.account_id=b.account_id and b.account_id=c.account_id and c.expenditure>1.00;
分析SQL中表c的account_id不可以为空值,所以可以去掉表b及其相关的联接,改写成:
select a.account_number from consume c,account a where a.account_id=c.account_id and c.expenditure>1.00;
6.1.5. where子句优化方法
6.1.5
6.1.5.1 index和full table scan的权衡
MySQL获取所需要的数据表中数据的主要方式有两种:1通过索引获得该数据所在行的位置后取得数据(index);2通过逐行扫描数据表中的数据行来过滤出所需要的数据(full table scan)。这两种数据获取方式各有利弊。
通过索引获取数据方式的利:
n 可以通过索引迅速找到需要的数据在数据表中存放的位置,迅速定位。这种方式在通过索引定位获得的数据行数不多的情况下,效率高;
n 通常情况下,索引数据和数据表相比较小,因此容易被缓存。
通过索引获取数据的弊:
如果通过索引获得的数据行数比较多,因为索引中的数据存放是有序的,而这种顺序如果和数据表中存放这些数据的顺序不一致,则在索引获得数据位置后从数据表中取数据会引起大量的随机读,这对MySQL的性能影响比较大。
通过扫描数据表获得所需数据方式的利:
n 扫描数据表时,是按照数据表中数据存放位置顺序扫描,这是顺序读操作。现在的硬件设备对顺序读性能还是不错的;
n 同时对于现在的硬件设备和文件系统,去读取某个block数据时,往往会将该block临近的其他几个block数据一起读到内存中,然后通过virtual I/O获取该block。这种模式对于顺序扫表操作比较有利。
通过扫描数据表获得所需数据方式的弊:
如果数据表中的数据行数比较多,则扫表一次,需要耗费比较多的CPU时间和多次的IO操作。
了解了通过索引获取数据(index)和扫表获取数据(full table scan)两种方式各自的利弊后,在实际情况中如何取舍,需要考虑以下因素:
n 数据表中的数据是否按照索引顺序存放,如InnoDB采用聚簇索引来存放数据(按照primary key顺序来存放数据)。这种情况下,通过索引获得的数据位置信息的顺序和数据存放顺序一致,因此不会引起大量的随机读。优先考虑使用这个索引;
n 考虑通过能利用索引过滤余下的数据行数,如果过滤后余下行数依然很大,那意味着还需要大量的IO从数据表中获得需要的数据。如果是随机IO,对MySQL的影响就更大了。而相比之下,顺序扫表,因为硬件和文件系统的特点(一次cache多个block)的特点,而变得有优势。
n 要考虑索引文件的大小。如果索引文件太大的话,无法在MySQL的cache中缓存下,则从索引中获得数据位置也会引起大量的IO。
select account_id from cust_acct where customer_id=1;
如果customer_id=1条件对应的account_id太多,从执行时间上看不使用索引index_customerid而使用顺序扫表,效果更好;相反对于customer_id=1,使用索引index_customerid效果更好。
6.1.5.2 where子句中限制条件的排列与index的命中
因为MySQL中的MyISAM,InnoDB等引擎使用的索引主要都是B-Tree数据类型,所以对where子句中的限制条件的排列顺序会有一些限制。这些限制包括:
n where子句中限制条件排列最好为对应索引的最左前缀;
n where子句中的range(范围)条件后的限制条件无法使用索引(即使这些条件对应的列在索引中)。
对于1)MySQL 5.0以上的MySQL优化器可以自己调整where子句中限制条件的顺序以使用对应的索引。但是这个过程本身会增加优化器的压力,尤其是where子句中的限制条件和对应索引包含列较多时。所以建议在写SQL的where子句中考虑打算使用的索引中列的顺序,相应地调整where子句中限制条件的顺序,以满足最左前缀的限制。
对于2)可以考虑同时调整索引中列的顺序及where子句中限制条件的顺序:将range(范围)限制条件调整到where子句的最后(最右)部分;将range条件对应的列调整到索引中列顺序的最后(最右)部。
select account_id from consume where expenditure>1.00 and account_payee =72478814;
如上语句,expenditure>1.00是一个范围限制条件,可以调整其至account_payee条件之后。同时设计索引index_accountpayee_expenditure时考虑到这个问题,索引中列顺序为(account_payee,expenditure)。
6.1.5.3 range限制条件的优化(in与>,<在index命中上的区别等)
在SQL中经常会出现多个range(范围)限制条件。这里指的范围限制条件包括>,<,between等。这些范围限制条件会对相应的索引使用造成影响。正常情况下,即使将这些范围条件调到where子句中的最后(最右)部,第一个范围限制条件后的范围条件对应的索引列也都无法在索引中同时被使用。
select account_id from consume where account_payee between51906734and51907000and expenditure>0.00;
因为account_payee between51906734and51907000这个条件,得到的执行计划中只能使用索引index_accountpayee_expenditure中的account_payee列,而无法使用expenditure列。
在SQL中出现范围限制条件后,可以考虑这个范围条件对应到数据时,包含的值是否是有限个(个数不是太多)。如果存在这种特性,可以将范围条件转换为多个等值条件in()。MySQL对于in()条件处理和等值条件一样,不会影响索引中其他列(右边列)的使用。针对上述应用场景,可以改为in():
select account_id from consume where account_payee in (51907000,51906734,51906740) and expenditure>0.00;
经过改写的SQL就可以使用索引index_accountpayee_expenditure中包含的全部列信息。
6.1.5.4 in减轻optimizer MySQL优化器的压力
一条SQL在MySQL中执行的过程中,会经过SQL解析,优化器制定执行计划,存储引擎查找过滤数据,返回客户端等步骤。其中优化器制定计划阶段有可能成为MySQL的性能瓶颈。优化器为一条SQL制定执行计划的过程相对比较复杂,其中包括语法树优化,相关开销信息获取,计算和比较等。如果并发比较大,在优化器处堆积了大量的SQL需要优化的话,优化器可能成为性能瓶颈。此时在MySQL中通过查看各个线程的状态(show processlist),可以看到很多线程处在statistics。
对于这种情况,可以检查一下向MySQL发起的SQL是否是形式一致,只是变量不同。如果是这种情况,可以由应用层在一个时间段(应用可以接受的)内收集这样的请求,使用in()将不同变量的SQL合并成一个SQL发给MySQL。这样MySQL原本需要制定数个执行计划的工作,变成只需要制定一个,减轻了优化器的压力。
应用场景:
select expenditure from consume where account_id =1;
可以将数个上述的SQL合并成下面的SQL ,从而减轻优化器的压力,并且不会影响到索引的使用。
select expenditure from consume where account_id in(1,2,3);
6.1.5.5 is null限制条件的优化
对于is null限制条件,MySQL会首先检查is null限制条件涉及的列,如果该列声明时是not null的,则优化器会直接忽略掉该is null限制条件。
对于可以为null的列上的is null限制条件,MySQL同样可以使用该列上的索引。使用索引的条件和等值,range限制使用索引的条件一样。
同时需要注意的是MySQL对于一条SQL只能使用索引来处理一个is null限制条件。比如:
select * from consume where account_payee is null and expenditure is null;
上述SQL只能使用index_accountpayee_expenditure中account_payee列,索引中expenditure列数据无法使用,这和等值限制不同。
6.1.5.6 index merge优化方法
index merge(索引合并)方法适用于通过多个等值条件index定位或range
扫描搜索数据行并将结果合并成一个的SQL语句。合并会产生并集,交集等。
对于下面的SQL,MySQL优化器会考虑使用index merge算法。
select id from cust_acct where customer_id=1 or account_id=1;
通过explain观察该SQL的执行计划,得到MySQL同时使用了customer_id列和account_id列上的两个索引。在这种情况下使用index merge会大大加速SQL的执行,可以将上面的SQL与下面的SQL进行比较。
select id from cust_acct ignore index(idx_accountid) where customer_id=1 or account_id=1;
同时需要指出如果or条件是在同一属性列上的,MySQL则会考虑使用该属性列上的索引,这和上面所讲的or条件在不同属性列上是不同的。(此时相当于IN)
select id from cust_acct where customer_id=1 or customer_id=2;
index merge优化对于这种or条件的并集操作比较有效。对于and条件的交集操作,其效果不如联合索引。如:
select id from cust_acct where customer_id=1 and account_id=1;
建(customer_id,accout_id)的联合索引,MySQL使用该联合索引的效果要优于使用index merge的效果。
对于or条件和and条件同时存在的where子句,MySQL优化器会优先使用and子句中的索引,而放弃使用or条件的index merge。如果能确定or条件的index merge优于and子句中的索引,可以使用ignore index(and子句的索引)的hint技术干预MySQL优化器制定的执行计划。
6.1.6. order by子句优化方法
在MySQL中,order by的实现有如下两种类型:
n 通过有序索引直接获得有序的数据,这样不用任何排序操作即可得到满足要求的有序数据;
n 须通过MySQL的排序算法将存储引擎返回的数据进行排序后得到有序的数据。
order by子句场景:
select consume_time from consume where account_payee=48370945order by expenditure;
select account_id from consume where consume_time between '2009-09-07'and '2009-09-10'order by expenditure;
第一个SQL使用索引index_accountpayee_expenditure。Order by使用索引需要满足一些条件:where子句中涉及的列加order by子句中涉及的列要满足要使用的索引的最左前缀;同时where子句中的条件必须是等值条件。
第二个SQL无法借助索引实现order by,需要通过MySQL本身的排序算法完成order by操作。
利用索引实现数据排序是MySQL中实现结果集排序的最佳方法,可以完全避免因为排序计算带来的资源消耗。所以,在优化SQL中的order by时,尽可能利用已有的索引来避免实际的排序计算,甚至可以增加索引字段,这可以很大幅度地提升order by操作的性能。当然这需要从整体上权衡,不能因此影响其他SQL的性能。
6.1.7. limit子句优化方法
在分页等系统中使用limit和offset是很常见的,它们通常也会和order by一起使用。一个比较常见的问题是偏移量很大,比如查询使用了limit 10000,20,它就会产生10020行数据,并且丢掉前面10000行。这个操作代价比较高。
一个提高效率的简单技巧就是在覆盖索引上进行偏移,而不是对全行数据进行偏移。可以将从覆盖索引上提取的数据和全行数据进行联接,然后取得所需的列。这会更有效率。
应用场景:
select * from consume where account_payee=72478814order by expenditure limit 50,5;
select * from consume inner join (select account_id from consume where account_payee=72478814order by expenditure limit 50,5) as lim using(account_id);
下面的SQL是上面SQL的优化版本,它使用覆盖索引,避免了整个结果集上进行offset操作。
6.1.8. 使用covering index优化select语句
在前面章节已经介绍了覆盖索引,覆盖索引可以使MySQL从索引中获得所需的数据,而不需要再到数据表中去取所需要的数据。通常索引要比数据表小,同时有序。因此使用覆盖索引对提高SQL性能会有很大的帮助。
在MySQL中,覆盖索引的使用要求select子句中涉及的列必须在相应的同一索引中,同时where子句中的限制条件应该符合使用这个索引的要求。比如限制条件要满足索引的最左前缀要求。如果有range条件,order by子句等,都要满足索引对这些条件的限制。对于那些不满足这些条件的SQL,可以通过适当的改造,以使它能使用覆盖索引达到优化的效果。
应用场景:
select account_id from account where account_bank= 'icbc.beijing.fengtai'and customer_email='email8002';
select * from account where account_bank= 'icbc.beijing.fengtai'and customer_email like '%@baidu.com';
因为account表使用的是InnoDB引擎,所以索引index_accountbank_customeremail存储有account_id的信息(它是primary key)。第一个SQL可以使用覆盖索引;但是第二个SQL却不能,因为select子句要求返回所有列,超出了索引index_accountid_expenditure_consumetime的范围。可以将SQL修改一下:
select * from account join (select account_id from account where account_bank='icbc.beijing.fengtai' and customer_email like '%@baidu.com') t on t.account_id=account.account_id;
通过上述改写,from子句中的select account_id from account where account_bank='icbc.beijing.fengtai' and customer_email like '%@baidu.com'可以使用覆盖索引。这样在account_bank='icbc.beijing.fengtai'过滤出来的数据行比较多,而加上customer_email like '%@baidu.com'条件后过滤出来的数据不多的情况下,这种改写会有比较大的优化效果。因为未改写前对customer_email条件过滤需要去数据表取数据,而改写后直接在索引中进行。同时由于改写后最终到数据表中取的数据较少,所以优化效果明显。
6.1.9. 由返回结果推动反向优化技巧
在写完SQL后,一定将其在数据库上执行一遍,观察一下输出结果。如果输出结果呈一定的规律性,则可考察一下数据库中相应的数据是否都符合这种规律。如果是的话,则可以重新审视先前的SQL,其中的限制条件等是否可以修改,来避免其中使用函数等复杂的限制条件。这样有助于MySQL指定执行计划时充分使用索引,指定最优的执行计划。同样有助于提升SQL的执行速度。
应用场景:
select account_id from account where substr(account_bank,6,7)= 'beijing';
上述SQL由于使用函数substr,则不能使用索引index_accountbank_customeremail。但是通过观察SQL的输出,发现account_bank列的数据都是以icbc.开头,进而可以SQL改写如下:
select account_id from account where account_bank like 'icbc.beijing%';
因为限制条件改写成了like 'icbc.beijing%',符合MySQL索引使用规范,该SQL就可以使用索引index_accountbank_customeremail了。
6.2 join联接的优化方法
6.2.
6.2.1. MySQL join的相关算法
在介绍join语句的优化思路之前,首先要理解在MySQL中是如何实现join的。只要理解了其实现原理,优化就比较简单了。
在MySQL中,只有一种join算法,就是nested loop join,它没有很多其他数据库提供的hash Join,也没有sort merge join。nested loop join实际上就是通过驱动表的结果集作为循环基础数据,然后将该结果集中的数据(联接键对应的值)作为过滤条件一条条地到下一个表中查询数据,最后合并结果。如果还有第三个表参与join,则把前两个表的join结果集作为循环基础数据,再一次通过循环查询条件到第三个表中查询数据,如此往复。
比如一个表t1,t2,t3的联接,通过explain观察到的执行计划中join的类型(explain中type行的值)是:
table join type
t1 range
t2 ref
t3 All
则相应的这个联接的算法伪代码如下:
for each row in t1 matching range {
for each row in t2 matching reference key {
for each row in t3 {
if row satisfies join conditions
send to client
}
}
}
6.2.2. 小结果集驱动大结果集
针对MySQL这种比较简单join算法,只能通过嵌套循环来实现。如果驱动结果集越大,所需循环也就越多,那么被驱动表的访问次数自然也就越多,而且每次访问被驱动表,即使所需的IO很少,循环次数多了,总量也不可能小,而且每次循环都不能避免消耗CPU,所以CPU运算量也会跟着增加。但是不能仅以表的大小来作为驱动表的判断依据,假如小表过滤后所剩下的结果集比大表过滤得到的结果集还大,结果就会在嵌套循环中带来更多的循环次数。反之,所需要的循环次数就会更小,总体IO量和CPU运算量也会更少。所以,在优化join联接时,最基本的原则是“小结果集驱动大结果集”,通过这个原则来减少循环次数。
如下场景:
通过explain观察MySQL为该SQL制定的执行计划:
select a.expenditure, b.balance from consume a, account b where a.account_id = b.account_id and a.account_payee=13301087 and b.account_bank='icbc.beijing.fengtai';
join中选取表consume为驱动表。
如果通过explain观察,发现MySQL在join过程中选取的驱动表不是很合适的话,建议最先通过索引,再通过hint技术straight_join来干预MySQL,使其按照最优的方式选取驱动表,制定最优的执行计划。
6.2.3. 被驱动表的join column最好能命中index
对于nested loop join算法,内层循环是整个join执行过程中执行次数最多的,如果每次内层循环中执行的操作能节省很少的资源,就能在整个join循环中节约很多的资源。
而被驱动表的join column能使用索引正是基于上述考虑的。只有让被驱动表的join条件字段被索引了,才能保证循环中每次查询都能通过索引迅速定位到所需的行,这样可以减少内层一次循环所消耗的资源;否则只能通过扫表等操作来找到所需要的行。
例如
select a.expenditure, b.balance from consume a, account b where a.account_id = b.account_id and a.account_payee=13301087 and b.account_bank='icbc.beijing.fengtai';
join过程中被驱动表join条件列account_id是primary key,通过主键索引每次内层循环定位所需的行非常快且开销非常小。
6.3 数据更新语句优化
6.3.
6.3.1. insert优化
insert插入一条记录花费的时间由以下几个因素决定:连接、发送查询给服务器、解析查询、插入记录、插入索引和关闭连接。
此处没有考虑初始化时打开数据表的开销,因为每次运行查询只会做一次。如果是B-tree索引,随着索引数量的增加,插入记录的速度以logN的比例下降。
可以用以下几种方法来提高插入速度:
n 如果要在同一个客户端在同一时间内插入很多记录,可以使用insert语句附带有多个values值。这种做法比使用单一值的insert语句快多了(在一些情况下比较快)。如果是往一个非空数据表增加记录,可以调整变量bulk_insert_buffer_size的值使其更快。
n 对实时性要求不高情况下,如要从不用的客户端插入大量记录,使用insert delayed语句也可以提高速度。
n 想要将一个文本文件加载到数据表中,可以使用load data infile。速度上通常是使用大量insert语句的20倍。
n 若插入上百万,建议分批进行,批量插入3000~5000后,sleep数秒钟之后进行下一次插入,可以避免同步延迟的累积。
6.3.2. delete优化
如果编写的delete语句中没有where子句,则所有的行都被删除。当不想知道被删除的行的数目时,有一个更快的方法,即使用truncate table。
如果删除的行中包括用于auto_increment列的最大值,对于MyISAM表或InnoDB表不会被重新用。如果在autocommit模式下使用delete from tbl_name(不含where子句)删除表中的所有行,则对于所有的表类型(除InnoDB和MyISAM外),序列重新编排。对于InnoDB表,此项操作有一些例外。
对于MyISAM和BDB表,您可以把auto_increment次级列指定到一个多列关键字中。在这种情况下,从序列的顶端被删除的值被再次使用,甚至对于MyISAM表也如此。delete语句支持以下修饰符:
如果您指定low_priority,则delete的执行被延迟,直到没有其它客户端读取本表时再执行。
在删除行的过程中,ignore关键词会使MySQL忽略所有的错误。(在分析阶段遇到的错误会以常规方式处理。)由于使用本选项而被忽略的错误会作为警告返回。
6.3.3. update优化
update更新查询的优化同select查询一样,但需要额外的写开销。写的速度依赖更新的数据大小和更新的索引的数量。所以,锁定表,同时做多个更新比一次做一个快得多。(需注意UPDATE的规模)
另一个提高更新速度的办法是推迟更新并且把很多次更新放在后面一起做。如果锁表了,那么同时做很多次更新比分别做更新来得快多了。
6.3.4. replace优化
由于replace是先delete再insert的操作,有可能会导致系统的空洞无法得到使用。所以采用先select判断是否存在记录,然后再考虑是否进行insert还是update的方式进行数据的更新可能更好。可以考虑采用insert on duplicate key,replace和insert on duplicate key差别在于前者是delete-insert,后者是update。
6.3.5. truncate优化
truncate和不带where子句的delete,以及drop都会删除表内的数据;
truncate和delete只删除数据不删除表的结构(定义);drop语句将删除表的结构被依赖的约束(constrain),触发器(trigger),索引(index);依赖于该表的存储过程/函数将保留,但是变为invalid状态;
delete语句是DML语句,这个操作会放到rollback segement中,事务提交之后才生效,如果有相应的trigger,执行的时候将被触发;truncate,drop是DDL语句,操作立即生效,原数据不放到rollback segment中,不能回滚.操作不触发trigger;
在执行速度方面,一般来说: drop> truncate > delete;
但是在安全性上,小心使用drop和truncate,尤其没有备份的时候;
使用上,想删除部分数据行用delete,注意带上where子句,否则回滚段要足够大;想删除表,当然用drop;想保留表而将所有数据删除,如果和事务无关,用truncate即可;如果和事务有关,或者想触发trigger,还是用delete.
6.4 子查询优化
当一个查询语句嵌套在另一个查询的查询条件之中时,称为子查询。子查询总是写在圆括号中,可以用在使用表达式的任何地方。子查询也称为内部查询或者内部选择,而包含子查询的语句也称为外层查询或外层选择。MySQL是从4.1支持子查询功能的,之前可考虑使用联表查询进行替代。
根据子查询出现的位置:位于select子句中,位于from子句中,位于where条件中的子查询来逐个介绍。
6.4.
6.4.1. 位于select子句中的子查询
位于select子句的子查询是指子查询在外层查询的select项。此时子查询返回的值是n行一列的表集合(n>=1)。
应用场景:账号名为'a-001'的客户信息。
select a.account_id,a.balance,
(select c.customer_name
from customer as c
where c. customer_id=ac.customer_id )as cust_name
from account as a, cust_acct as ac
where a. account_id=ac.account_id and a. account_id= 'a-001';
子查询通过与ac.customer_id相等的条件引用了主查询的当前记录。而且该子查询只能返回一条记录,如果没有符合条件的,则结果中的cust_name值为空。
优化关注点:对于子查询来说,外层查询返回的每条记录都会需要执行这个子查询。如果返回几百条或者更少记录来说,上面语句的性能应该还不错,但是对于返回几百万,上千万行数据的查询来说,这样做就非常致命了。可以替换为外连接,如下:
select a.account_id,a.balance,c.customer_id
from account as a, cust_acct as ac left join customer as c
on c.customer_id = ac.customer_id
where a.account_id=ac.account_id and a.account_id= 'a-001';
注意,连接需要是个外连接,这样才能在customer表中无对应记录时得到空值。
6.4.2. 6.4.2位于from中的子查询
位于from中的子查询是指子查询落在外层查询的from项。此时子查询返回的值是n行n列的表集合(n>=1)。
应用场景:一次消费超过100元的同时消费是在最近一个月进行的账号信息。
Select consume_gt100.account_id
from
(
select c2. account_id,c2. consume_time
from consumeas c2
where c2.expenditure>100
) asconsume_gt100
Where consume_gt100. consume_time>'2010-11-17';
其中子查询和外层查询没有任何关联。一般情况下,from中的子查询都是可以独立执行的。
6.4.3. 位于where中的子查询
位于whrere中的子查询是指子查询位于外层查询的where条件中。这类查询通常有三种基本的子查询。
n 通过使用in引起的范围查询。
n 通过由any,some或all修改的比较运算符引入的列表上操作。
n 通过exists引入的存在测试。
如上三种子查询与外层查询极可能有关联,也可能无关联。第一种和第二种属于无关联的,可以独立执行;exists一般是和外层查询都是有关联的。
位于where中的子查询暂时有个限制,在带in或者由any,some,all的嵌套子查询中,不能使用limit关键字。但是此限制可通过其他方式来变相实现。
关联子查询和非关联子查询的两个较大的区别如下:
n 关联子查询在来自外层查询的值每次发生变化就会被触发一次;而非关联子查询却永远只需要触发一次。
n 关联子查询的话,是外层查询在驱动执行过程。如果是无关联查询,那么子查询就有可能驱动外层查询。
6.4.4. 带in的嵌套查询
应用场景:和张三有在同一银行开户的账号的客户信息,并且按照customer_id递减排序,取前10个客户信息。
select c. customer_name,c.customer_id
from customer as c, cust_acct as ac, account as a
where c.customer_id=ac.customer_id and a.account_id=ac.account_id and a. account_bank in(
select a2.account_bank
from account as a2,cust_acct as ac2,customer as c2
where c2.customer_name='张三' and c2.customer_id=ac2.customer_id and ac2.account_id=a2.account_id
)
order by c.customer_id desc limit 0,10;
该SQL中的子查询为非关联子查询,该子查询也可以移到from中。但是由于in()隐含了distinct,因此在from子查询中需要显式写distinct。如上可以改写为:
select c.customer_name,c.customer_id
from customer as c,cust_acct as ac,account as a,
(
select distinct a2.account_bank
from account as a2,cust_acct as ac2,customer as c2
where c2.customer_name='张三' and c2.customer_id=ac2.customer_id and ac2.account_id=a2.account_id
) as tr
where tr. account_bank=a. account_bank
and c.customer_id=ac.customer_id and a.account_id=ac.account_id
order by c.customer_id desc limit 0,10;
这两种写法的对比:
n from子句支持limit子句,在索引合适的情况下,添加order by和limit在子查询中可减少文件排序。
n 带in的子查询和联表比起来,MySQL更喜欢联表。尤其是in()中子查询为空时,外层查询会逐行扫描对比,而此时联表查询会有较大优势。
6.4.5. 带any,some或者all的嵌套查询
some,any和all可对子查询中返回的多行结果进行处理,下面简单介绍下这几个关键字的含义:
some:表示满足其中一个的含义,是由or连起来的比较从句。
any:也表示满足其中一个的意义,也是用or串起来的比较从句,区别是any一般用在非“=”的比较关系中,这也很好理解,英文中的否定句中使用any,肯定句中使用some,这一点是一样的。
all:表示满足其中所有的查询结果的含义,使用and串起来的比较从句。
应用场景:本月内消费额度超过'a-001'账户的所有账户信息。
select c.customer_name,c.customer_id
from customer as c ,cust_acct as ac, consume as c2
where c.customer_id=ac.customer_id and ac.account_id=c2.account_id
and c2.expenditure > any (select c3.expenditure
from consume as c3
where c3.account_id='a-001');
这里改为
>some(select c3. expenditure from consume c3 where c3.account_id='a-001')
也是符合语法的,查询结果也是一致的。
6.4.6. 带exists的嵌套查询
应用场景:账户开户以来一直没有消费记录的账户信息。
select c.customer_name,c.customer_id
from customer as c,cust_acct as ac
where c.customer_id=ac.customer_id and not exists
(
select c2. account_id
fromconsumeas c2
where c2.account_id=ac.account_id
);
该子查询是通过account_id进行关联,该查询也可以改写为无关联查询,如下:
select c.customer_name,c.customer_id
from customer as c,cust_acct as ac
where c.customer_id=ac.customer_id and ac.account_id not in
(
select c2. account_id
fromconsumeas c2
);
在关联子查询和非关联子查询之间做选择并不是很困难,有些优化器会帮你做选择,在使用关联子查询和非关联子查询时,对于索引情况的假设不同,因为它不再是其他部分的查询的那个部分了。优化器会尽可能使用可用的索引,但不会创建索引。
6.5 优化器相关explain以及常用hint介绍
6.5.
6.5.1. 查看select语句执行计划的语法explain
6.2
6.3
6.4
6.5
6.5.1
6.5.1.1 MySQL explain输出信息介绍
MySQL Query Optimizer通过执行explain命令告诉我们它将使用一个怎么样的执行计划来优化query。因此explain是在优化query中最直接有效的验证我们想法的工具。
要使用explain,只需把explain放在查询语句的关键字select前面就可以了。MySQL会在查询里设置一个标记,当它执行查询时,这个标记会促使MySQL返回执行计划里每一步的信息。用不着真正执行。它会返回一行或多行。每行都会显示执行计划的每一个组成部分,以及执行的次序。
MySQL只能解释select查询,无法解释存储过程的调用、insert、update、delete和其它语句。这时只有通过重写这些非select语句为select,使能够被explain。
下面来详细解释下explain功能中展示的各种信息的解释。
项 |
子类型 |
说明 |
id |
|
MySQL Query Optimizer选定的执行计划中查询的序列号。表示查询中执行select子句或操作表的顺序,id值越大优先级越高,越先被执行。id相同,执行顺序由上至下。 |
select_type(所使用的查询类型) |
dependent subquery |
子查询内层的第一个查询,依赖于外部查询的结果集。 |
dependent unoin |
子查询中的unoin,且为union中从第二个select开始的后面所有select,同样依赖于外部查询的结果集。 |
|
primary |
子查询中的最外层查询,注意并不是主键查询。 |
|
simple |
除子查询或union外的其他查询。 |
|
derived |
用于from子句里有子查询的情况。MySQL会递归执行这些子查询,把结果放在临时表里。 |
|
subquery |
子查询内层查询的第一个查询,结果不依赖于外部查询结果集。 |
|
uncacheable subquery |
结果集无法缓存的子查询。 |
|
union |
union语句中第二个select开始后面的所有select,第一个select为primary。 |
|
union result |
union中的合并结果集。 |
|
table |
|
显示这一步中锁访问的数据库中的表名称。 |
type(表示表的连接类型) |
all |
通常意味着必须扫描整张表,从头到尾,去找匹配的行。(这里也有例外,例如查询中使用了limit,或者在extra列里显示使用了distinct或not exists等限定词)。 |
index |
全索引扫描。与全表扫描一样,只是扫描表的时候按照索引次序进行而不是行。主要优点就是避免了排序;最大的缺点就是要承担索引次序读取整张表的开销。这一般意味着若是随机次序访问行,开销将会非常大。 |
|
range |
索引范围扫描。就是有限制的索引扫描,返回匹配某个值域的行。这比全索引扫描好些,因为不用便利遍历全部索引。常见于between、<、>等的查询。 |
|
ref |
一种索引访问方式,它返回匹配某个单独值的所有行。但是它可能查找到多个符合条件的行,因此,它是查找和扫描的混合体。此类索引访问只有当使用非唯一索引或者唯一性索引的非唯一性前缀才会发生。叫做ref是因为索引要跟某个参考值相比较。这个参考值或者是一个常数,或者是来自一个表里的多表查询的结果值。ref_or_null是ref之上的一个变体,它意味着MySQL必须进行二次查找,在初次查找的结果里找出null条目。 |
|
eq_ref |
唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描。 |
|
const,system |
当MySQL对查询某部分进行优化,并转换为一个常量时,使用这些类型访问。如将主键置于where列表中,MySQL就能将该查询转换为一个常量。system是const类型的特例,当查询的表只有一行的情况下, 使用system。 |
|
null |
MySQL在优化过程中分解语句,执行时甚至不用访问表或索引。 |
|
possible_key |
|
这一列显示查询可以使用的索引,如果没有索引可以使用,就会显示为null。 |
key |
|
这一列显示的是MySQL Query Optimizer从possible_key选择使用的索引。需要注意的是查询中若使用了覆盖索引,则该索引仅出现在key列表中,不会出现在possible_keys列。possible_keys说明哪一个索引能有助于查询,而key显示的是优化器采用哪一个索引可以最小化查询成本。 |
key_len |
|
该列显示了使用的索引的索引键长度。由于索引的最左策略,因此通过该值可以计算查询中使用的索引情况。 |
ref |
|
这一列显示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值。 |
rows |
|
这一列显示的表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数。 |
filtered |
|
这一列是5.1加进去的。当使用explain extended时才会出现。它显示的是针对表里符合某个条件的记录数的百分比所作的一个悲观估算。rows列和这个百分比相乘,就能看到MySQL估算的它将和查询计划里前一个表联接的行数 |
extra(这一列包含的是不适合在其他列显示的额外信息) |
using index |
该值表示MySQL将使用覆盖索引,以避免访问表。 |
using where |
表示MySQL服务器在存储引擎收到记录后进行“后过滤”(post-filter),如果查询未能使用索引,using where的作用只是提醒我们MySQL将用where子句来过滤结果集。 |
|
using temporary |
表示MySQL在对查询结果排序时使用临时表。常见于排序和分组查询。 |
|
using filesort |
表示MySQL会对结果使用一个外部索引排序,而不是从表里按索引次序读到相关内容。可能在内存或者磁盘上进行排序。MySQL中无法利用索引完成的排序操作称为“文件排序” |
|
range checked for each record(index map:n) |
这表示没有好的索引可用,新的索引将在联接的每一行上被重新评估。n是显示在possible_keys列索引的位图。这是一个冗余。 |
6.5.1.2 extend explain输出信息
explain extended和普通的explain很相似,但是它会告知服务器把执行计划“反编译”成select语句,然后立即执行show warnings就能看到这些生成的语句。这些语句是直接来自执行计划,而不是原始的SQL语句。
6.5.1.3 explain的局限
explain只是一个近似,没有更多的细节。有时它是一个很好的近似,但是有时它会远离真实情况,以下就是它的几个局限性:
n explain不会告诉你关于触发器、存储过程的信息或用户自定义函数对查询的影响情况。
n explain不考虑各种cache。
n explain不能显示MySQL在执行查询时所作的优化工作。
n 一些显示出来的统计信息是估算的,不是很精确。
n expalin只能解释select操作,其他操作要重写为select后查看执行计划。
6.5.1.4 MySQL制定的执行计划不一定最优
尽管优化器的作用是将很糟糕的语句转化成高效的处理方式,但即便是在索引建立的很合理,并且优化器得到正确的相关信息后,优化器还是可能会制定出错误方向的执行计划。一般会有两个方面的原因:
n 优化器的缺陷。
n 查询太复杂,优化器只能在给它的有限时间内尝试大量可能的组合中很少一部分优化方案,因此可能无法找到最佳执行计划。
针对这种情况,最有效的解决办法就是正确编写查询。以更简单更直接的方式写SQL通常能节省优化器很多工作,因为它能很快命中一个很好而且很高效的执行计划。
6.5.2. MySQL干预执行计划的方法(hint)
如果不满意MySQL优化器选择的优化方案,可以使用一些优化提示来控制优化器的行为。下面简单介绍下在MySQL中常用的提示,以及使用它们的时机。
6.5.2
6.5.3
1
2
3
3.1
3.2
3.3
3.4
3.5
3.5.1
3.5.2
3.5.2.1 force index、use index、ignore index
这些提示告诉优化器在该表中查询时使用或者忽略该索引。
force index和using index是一样的。但是它告诉优化器,表扫描比起索引来说代价要高很多,即使索引不是非常有效。
在MySQL5.0及以前版本中,它们不会影响排序和分组使用的索引。
3.5.2.2 straight_join
这个提示用于select语句中select关键字的后面,也可以用于联接语句。因此它的第一个用途是强制MySQL按照查询中表出现的顺序来联接表,第二个用途是当它出现在两个联表的表中间时,强制这两个表按照顺序联接。
straight_join在MySQL没有选择好的连接顺序,或者当优化器花费很长时间确定连接顺序的时候很有用。在后一种的情况下,线程将会在“统计”状态停留很长时间,添加这个提示将会减少优化器的搜索空间。
可以使用explain查看优化器选择的联接顺序,然后按照顺序重写连接,并且加上straight_join提示。
3.5.2.3 sql_no_cache、sql_cache
sql_cache表明查询结果需要进行缓存,结果进行缓存和变量query_cache_type,have_query_cache以及query_cache_limit的设置有关。
而sql_no_cache表明查询结果不需要进行缓存。
3.5.2.4 6.5.2.4 high_priority、low_priority
这两个提示决定了访问同一个表的SQL语句相对其它语句的优先级。
high_priority提示用于select和insert。是将一个查询语句放在队列的前面,而不是在队列中等待。
low_priority提示用于select、insert、update、replace、delete、load data。和high_priority相反,如果有其他语句访问数据,它就把当前语句放在队列的最后。
这两个提示不是指在查询上分配较多或者较少资源。它们只是影响服务器对访问表的队列的处理。
3.5.2.5 delayed
这个提示用于insert和update。使用了该提示的语句会立即返回并且将插入的列放在缓冲区中,在表空闲的时候在执行插入。它对于记录日志很有用,对于某些需要插入大量数据,对每一个语句都引发I/O操作但是又不希望客户等待的应用程序很有用。但是它有很多限制,比如:延迟插入不能运行于所有的存储引擎上(仅适用于MyISAM, Memory和Archive表),并且它也无法使用last_insert_id()。
3.5.2.6 sql_small_result、sql_big_result
这两个提示用于select语句。它们会告诉MySQL在group by或distinct查询中如何并且何时使用临时表。sql_small_result告诉优化器结果集会比较小,可以放在索引过的临时表中,以避免对分组后的数据排序。sql_big_result的意思是结果集比较大,最好使用磁盘上的临时表进行排序。
3.5.2.7 6.5.2.7 sql_buffer_result
这个提示告诉优化器将结果存放在临时表中,并且尽快释放掉表锁。
7. 常用函数
7
7.1 字符串函数
字符串连接方法,使用CONCAT()或CONCAT_WS()函数,语法如下:
CONCAT(string1,string2,...)
CONCAT_WS(separator,string1,string2,..)
字符串长度统计:
LENGTH(string) #返回string所占的字节数
CHAR_LENGTH(string) #返回string中的字符个数
统计字符个数,就不区分是汉字还是字母或数字,也跟字符集没有关系,若统计的是字节数,则由字符是汉字、字母或数字类型,以及字符集共同决定。
7.2 日期函数
- 7.
7.1.
7.2.
7.2.1. 获取当前时间:
n NOW()函数精确到秒,格式:YYYY-MM-DD HH:MM:SS
n CURDATE函数精确到天,格式:YYYY-MM-DD
n CURTIME函数精确到秒,格式:HH:MM:SS
7.2.2. 日期数值的加减函数:
n DATE_ADD(date,INTERVAL expr type)
n DATE_SUB(date,INTERVAL expr type)
常用的几种type类型:YEAR、MONTH、DAY、HOUR、MINUTE,其中expr可以为正数或负数,我们在开过程中,一般使用DATE_ADD()函数,若要作日期减去一个数字的方式,就使用负数。
n DATEDIFF(expr1,expr2),是返回开始日期expr1与结束日期expr2之间,相差的天数 ,返回值为正数或负数。
7.2.3. 返回日期某部分信息的函数:
n YEAR(expr1) 返回日期expr1部分的年份;
n MONTH(expr1)返回日期expr1部分的月份;
n DAY(expr1)返回expr1部分的天数;
n WEEKDAY(expr1)返回expr1对应的星期数字;
7.3 类型转换函数
7.3.
7.3.1. 字符串转换成日期方式:
n DATE_FORMAT(expr1,format)
n STR_TO_DATE(expr1, format)
常用的日期格式YYYY-MM-DD HH:MM:SS对应的format为%Y-%m-%d %H:%i:%S
7.3.2. 通用的类型转换函数:
n CAST(expr AS type)
n CONVERT(expr,type)
n CONVERT(expr USING transcoding_name)
8. 附录:
MYSQL5.6保留字列表:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|