本文将介绍MySQL数据库SQL的一些实用优化知识和数据库建表规范,主要在于介绍EPLAIN关键字的使用、SQL语句优化的基本思路以及不同SQL语句的优化技巧等等。本文主要参考了《阿里巴巴Java编程规范》数据库篇以及《MySQL技术精粹:架构、高级特性、性能优化与集群实战》两书,如果对此有兴趣的童鞋可以详读书中内容。
对于SQL语句进行优化有一些常用的切入点,一般来说可以从以下几个角度依次进行分析:
对数据库表的设计其实不应该出现在这里,但是这一点又相当的重要,因为对于表的设计会影响到SQL语句的执行效率(譬如对含有blog字段的表进行查询相对于varchar字段的表会耗时更多)。首先,对于表的设计应该符合表设计的基本范式,在字段数量考虑上应该尽量地减少冗余,在字段的类型确定上应该尽量地符合业务需求(比如对于存储手机号的字段应该设置为bigint型而不是int型),对于不常用的字段应该通过id分表存储,对于多对多关系的两张表间应该新建一张关系映射表等。
每条SQL的背后一般都有相应的业务作为支撑(除了一些数据库设置的SQL),而优化SQL的第一步应该是考虑是否在这个业务情景下一定需要这么写SQL语句,是不是会有其他的SQL语句可以被选择,以及能否通过更简洁的语句去获得所需要的结果。从业务的角度确定了SQL语句后,即开始进入真正的SQL优化环节。
在确定了所需的SQL语句后,开始进行进一步的优化。这里会涉及到explain工具的使用和对结果的分析,以及对索引的设计,这也是本文的重点。
这是SQL优化要考虑的最后一步,即对数据库自身的设置,例如是否开启查询缓存,是否需要在执行语句前锁表或者取消主键校验等。
对于数据库表的设计,通常需要根据具体的业务流程画出E-R图,确定每个表中基本的字段,然后确定表与表之间的关系,以下只是简单地小结基本的设计要求:
首先需要设计出一张表最基本的字段,一般来说,数据库表名以及字段名不能使用驼峰命名法而是使用first_second这种形式命名,且必须包括以下几种字段:
假设一张学生表tb_student已经确定需要11个字段:
学生号,姓名,年龄,性别,所在年级,所在班级,兴趣爱好,档案简历,创建时间,修改时间
这些数据中,经常需要被查询出来的数据有学生号,姓名,年龄,性别所在年级与班级,而其他字段例如兴趣爱好,档案简历等都属于不常用字段,并且这两个字段可能使用的是大文本类型(blog),如果每次查询都需要去获取这些数据的话会给查询效率带来影响,这时可以考虑将这几个不常用字段分开为另一张表tb_student_details用以存储,并且以学生号作为主键,如果在查询中需要完整的学生信息,则可以使用连接查询:
select *
from
tb_student s1 left outer join tb_student_details s2
on
s1.id = s2.id
当表与表之间的字段存在多对多的关系时需要创建第三方表作为它们之间信息连接的桥梁,例如有老师表和课程表,一个老师可以上多门课程,一门课程也可以由多个老师来上,那么它们之间的第三方表可以简单地表示为 老师ID-课程ID 这样的形式(根据实际业务需求来确定,至少包括三个字段,即表记录唯一主键、老师id、课程id)。
值得一提的是,在阿里巴巴Java开发规范中已经强制要求开发者停止使用外键和存储过程,表之间所有的关系都不由数据库外键维护,而是在程序的业务层去进行维护。
EPLAIN是MySQL内置的一个检查查询SQL语句性能的工具,它的使用十分地简单,只需在查询语句前加上这个关键字即可:
EXPLAIN
SELECT
*
FROM
tb_item
id
id是该查询SQL执行的次序,id越大则说明该段SQL越早执行,当id相同时,则由下到上的顺序执行。
select_type
表示的是该段SQL语句的类别是什么,一共有以下几种常见的状态:
SIMPLE
:表示是简单查询,不包括子查询和连接查询。
PRIMARY
:表示是最外层的查询或者是主查询语句。
ONION
:表示的是连接查询。
SUBQUERY
:表示的是子查询。
DERIVERD
:表示的是from子句中还有子查询的情况,MySQL会递归地执行这些查询。
table
表明这些数据来自哪张表,如果是来自一张临时表,则会显示为derivedXXX。
key
表示这个查询调用了哪个索引。
rows
表示这个查询遍历了多少行记录才获得结果。
Extra
这是MySQL处理该查询时的详细信息,主要包括以下几种:
using index
:说明在查询中使用了索引,这是最好的情况。
using where
:说明在收到请求后对行进行了过滤。
using temporary
:说明对查询结果进行排序时使用了一张临时表,此时需要进行SQL优化。
using filesort
:说明对查询结果使用了外部索引进行排序,此时需要进行SQL优化。
对SQL的优化方法有很多种,一般来说,对于大量数据插入的优化可以是将单次插入改为批量插入,对于大量数据的更新可以通过批量更新来解决,这样避免了多次打开连接而消耗连接资源;对于查询的各种情况例如使用了like模糊查询、order by排序、group by分组、or和and等都可以通过建立相应的索引策略来解决。
对于INSERT语句的执行过程大致如下:
那么从这个执行过程就可以找到对应的优化方案:
原生SQL语句
:
INSERT INTO
tb_emp
VALUES
(NULL,'arong1',23),
(NULL,'arong2',24),
(NULL,'arong3',25),
(NULL,'arong4',26),
(NULL,'arong5',24),
(NULL,'arong6',11),
(NULL,'arong7',13),
(NULL,'arong8',18),
(NULL,'arong9',14);
MyBatis XML
:
<insert id="insertUserBatch" parameterType="java.util.List">
INSERT INTO
tb_emp
VALUES
<foreach collection="list" item="emp" index="index" separator=",">
(null,
#{emp.empName},
#{emp.empAge}
)
foreach>
insert>
改为延迟插入
如果MySQL使用的存储引擎为MyISAM的情况下,可以使用 INSERT DELAYED INTO 代替 INSERT INTO,这能够起到延迟插入数据的作用,虽然说执行完这条语句后会给客户端返回插入成功的提示,但是在MySQL内部是将这些插入的数据放到队列中,等待空闲时才插入的,这将减小数据库的写入压力。但是也会有弊端,即如果在数据库发生故障时仍有未执行的写入任务在队列中,那么这些任务都会被遗失掉。
在写入数据时锁表
在写入数据时锁住表防止被读取,可以提高数据的插入效率。
//写时锁表
LOCK TABLE tb_test WRITE;
//执行INSERT语句
UNLOCK TABLE;
对于更新语句的优化,通常有两种:
对于WHERE语句来说,只需要在其条件上加索引即可:
EXPLAIN
SELECT
*
FROM
tb_emp emp
WHERE
emp.emp_weight > 1
通过EXPLAIN的rows里可以看出,这次查询把所有行都遍历了一遍去筛选出结果,所以我们应该在WHERE之后的值加索引。
CREATE INDEX index_weight ON tb_emp(emp_weight);
加了索引之后可以rows里边显示只遍历了两百多行,效率得到大幅的提高:
对于一些特定的ORDER BY语句,我们可以通过添加索引和联合索引来达到优化效果:
EXPLAIN
SELECT
*
FROM
tb_emp emp
ORDER BY
emp.emp_weight
DESC
LIMIT 0,20
使用EXPLAIN关键字进行分析的结果显示,这条SQL语句使用了外部的索引进行排序,这时应该优化:
添加单个索引
CREATE INDEX index_weight ON tb_emp(emp_weight);
这时可以看到,这20条数据已经不依赖外部排序了,它们直接通过索引进行排序。
EXPLAIN
SELECT
*
FROM
tb_emp emp
WHERE
emp.emp_active = 0
ORDER BY
emp.emp_weight
DESC
LIMIT 0,30
可以建立联合索引:
CREATE INDEX index_active_weight ON tb_emp(emp_active,emp_weight);
TIP:除此之外的ORDER BY语句都无法被优化,如不限制LIMIT时,只能靠外部索引去排序。
在使用GROUP BY语句时,MySQL会将其结果创建一张临时表将数据存储并且排序,此时可以在语句的末尾加上ORDER BY NULL,禁止创建虚拟表排序。
优化嵌套查询时如果出现了子查询,优化的方法是将子查询改为用连接查询。因为在使用子查询的过程中,MySQL会创建出一张临时表来存储子查询的数据,数据量大的时候会影响效率,这时改成连接查询的话就可以解决这个问题。但是如果子查询需要的结果改为连接查询后不好获取,那就不要强改了。
MySQL数据库含有一些高级特性,例如查询缓存的使用和视图的使用。
查询缓存一般在数据变更不频繁的时候开启,当一个数据被查询出来后,下一次执行该SQL语句时会首先通过缓存读取到数据;当该数据所涉及的表数据发生改变,再次查询时会再次刷新数据并存入缓存中。但是查询缓存只适合在数据变化频率低的情况下使用,因为当数据变化频率高时,每次几乎都需要判断缓存是否变化,然后再去请求数据库,这样就多此一举了。
SELECT @@QUERY_CACHE_TYPE;
SET @@QUERY_CACHE_TYPE = ON;
SHOW STATUS LIKE 'Qcache_hits';
视图(View)类似于一张数据表,但是它又和表不同,一般被用作虚拟的表用于存储一些经常需要被读取的数据集。例如我们需要查询出员工表中所有的老板(权重大于1的员工),我们可以新建一个视图vw_emp_boss_message
去存储查询到的结果集:
CREATE VIEW vw_emp_boss_message AS
SELECT *
FROM
tb_emp emp
WHERE
emp.emp_weight > 1;
当我们需要这些数据时,可以直接从视图中获取,这和从表中获取数据没什么区别:
SELECT
*
FROM
vw_emp_boss_message;
这就是本次要写的内容了。如果光说数据库优化的话,一两本书或许都不能总结完,小弟也没有这个能力(哭);所以只总结了一些平时常用的SQL优化技巧,虽然写得很浅显,许多高深的知识都没有涉及到(哭),如有遗漏和错误,欢迎各位指正♪( ^ ∇ ^ )~