mysql优化-数据库优化、SQL优化

时间:2022-09-21 00:09:14

我有一张表w1000,里面有1000万条数据,这张表结构如下:
CREATE TABLE `w1000` (
`id` varchar(36) NOT NULL,
`name` varchar(10) DEFAULT NULL,
`age` int(3) DEFAULT NULL,
`money` double(8,2) DEFAULT NULL,
`address` varchar(100) DEFAULT NULL,
`create_date` datetime(3) DEFAULT NULL,
`modify_date` datetime(3) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_modify_date` (`modify_date`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

表字段没什么好说的,无非就是选择了各种不同的字段类型。
亮点在 ENGINE=InnoDB,所以表也是可以选择哪种引擎的,并非只能创建数据库时才可选择。那么问题来了:
一、表为什么要选择InnoDB引擎
请参考:http://blog.csdn.net/com360/article/details/7692117
1 ISAM
ISAM是一个定义明确且历经时间考验的数据表格管理方法,它在设计之时就考虑到数据库被查询的次数要远大于更新的次数。因此,ISAM执行读取操作的速度很快,而且不占用大量的内存和存储资源。ISAM的两个主要不足之处在于,它不支持事务处理,也不能够容错:如果你的硬盘崩溃了,那么数据文件就无法恢复了。如果你正在把ISAM用在关键任务应用程序里,那就必须经常备份你所有的实时数据,通过其复制特性,MySQL能够支持这样的备份应用程序。
2 InnoDB
它提供了事务控制能力功能,它确保一组命令全部执行成功,或者当任何一个命令出现错误时所有命令的结果都被回退,可以想像在电子银行中事务控制能力是非常重要的。支持COMMIT、ROLLBACK和其他事务特性。最新版本的Mysql已经计划移除对BDB的支持,转而全力发展InnoDB。
PS:也就是说当你选用了ISAM作为mysql的数据引擎时,你在应用中的spring数据库事物管理配置将彻底失效,因为它根本就不支持事物!
查看哪些引擎支持事物
show engines; 在MySQL5.7当中,支持很多engines,可是支持事物的engines只有一个:

mysql优化-数据库优化、SQL优化

show variables like '%storage_engine%'; 查看MySQL默认存储引擎。

事物的重要性,事物的ACID属性
事务具有ACID属性(参考http://www.cnblogs.com/jerryxing/archive/2012/04/24/2468986.html):
o 原子性(Atomic):事务由一个或多个行为绑在一起组成,好像是一个单独的工作单元。原子性确保在事务中的所有操作要么都发生,要么都不发生。
o 一致性(Consistent):一旦一个事务结束了(不管成功与否),系统所处的状态和它的业务规则是一致的。即数据应当不会被破坏。
o 隔离性(Isolated):事务应该允许多个用户操作同一个数据,一个用户的操作不会和其他用户的操作相混淆。
o 持久性(Durable):一旦事务完成,事务的结果应该持久化。
事务的ACID特性是由关系数据库管理系统(RDBMS)来实现的。
o 数据库管理系统采用日志来保证事务的原子性、一致性和持久性。日志记录了事务对数据库所做的更新,如果某个事务在执行过程中发生错误,就可以根据日志,撤销事务对数据库已做的更新,使数据库退回到执行事务前的初始状态。
o 数据库管理系统采用锁机制来实现事务的隔离性。当多个事务同时更新数据库相同的数据时,只允许持有锁的事务能更新该数据,其他事务必须等待,直到前一个事务释放了锁,其他事务才有机会更新该数据。
二、修改my.ini,优化mysql数据库配置
我本地的安装目录 C:\ProgramData\MySQL\MySQL Server 5.7
找到该目录下的my.ini,对里面的参数进行优化,以下=后边的参数为我已经修改后的参数,即本地优化后的。
datadir= 修改实际mysql数据目录存储地址,以免出现默认存到C盘去的现象
max_connections=1500 加大数据库连接数。从原来的151修改为1500
query_cache_size=2M 加大查询缓存。如果你查询相同sql的次数多,那么再次执行sql会直接从缓存取,如果你的表经常改变或者sql经常改变,则可能会拖慢查询速度,慎用。
tmp_table_size=200M 这个值的最大值为内存值,如果一个表数据大于这个值,那么它自动转为为基于磁盘表。这个参数的限制是针对于表。
key_buffer_size=0M 这个值只针对表引擎为MyISAM时。一般情况下我们建表和数据库都是为了事物支持,所以默认都是InnoDB。
innodb_flush_log_at_trx_commit=1 有三个值0,1,2。
  当值为0时,大约每一秒就会把日志写入到文件,并把文件刷新到磁盘,也就是保存日志文件。
  当值为1时,InnoDB每次commit操作都会使事物日志刷新到磁盘,也就是日志持久化,比如日志文件。
  当值为2时,表示每次commit时都写入日志文件,但是大约每一秒会执行一次日志刷新到磁盘操作。
innodb_log_buffer_size=5M InnoDB的log数据缓冲区大小,如果这个InnoDB设置为每一秒刷新到磁盘的话,那这个值没太大意义,即使是长事物。
待续……

三、优化SQL前
先来看下未经优化的sql,--后面跟的是这句sql执行的时间
SELECT * from w1000 limit 0,1550;                   -- 0.015s
SELECT * from w1000 where name='东方颜实';   -- 35.355s 第一次查询
SELECT * from w1000 where name='东方颜实';   -- 0.001s 第二次查询时,查询缓存(query_cache_size)生效了
SELECT * from w1000 where id = '0000c51e-0ede-4dc2-871a-78c1eebd0041';       -- 0.003s 查询条件为主键字段
SELECT * from w1000 where age = 19 or age = 20;                                             -- 32.234s
SELECT * from w1000 where age != 20;                               -- 75.222s access violation at address 00007FFE6FCC0BD0 in module 'scilexer.dll'.
SELECT * from w1000 where modify_date is not null;             -- 73.387ss access violation at address 00007FFE6FCC0BD0 in module 'scilexer.dll'.
SELECT * from w1000 where name like '张%';                        -- 25.519s
SELECT * from w1000 where age in (20,21);                         -- 34.47s
SELECT * from w1000 where age/2 = 50;                             -- 30.352s
SELECT * from w1000 where SUBSTRING(name FROM 1 FOR 1) = '张';   -- 26.091s
红色是执行完毕后navicat报错,因为查询结果集数据太大,已经超出了navicat的内存使用。
在以上SQL中,我没有定义任何表索引,也没有更改数据库隔离级别,仅仅定义了表引擎为InnoDB。其它全都是默认。

注意我们现在需要优化的是什么?是查询速度,不是插入速度,也不是更新速度。
所以以下所有解决方案只是为了如何让查询更快。
四、选择合适的数据库隔离级别
最近面试的时候,经常被问到数据库的隔离级别是什么?
答案参考:http://blog.csdn.net/gaopu12345/article/details/50868501 以下为转载该文章:
(零)没有并发控制
数据库当然支持没有隔离级别,这个查询速度最快。
可是存在的问题:更新遗失。
解决办法就是下面的“可读取未确认”。
(一)可读取未确认(Read uncommitted)
写事务阻止其他写事务,避免了更新遗失。但是没有阻止其他读事务。
存在的问题:脏读。即读取到不正确的数据,因为另一个事务可能还没提交最终数据,这个读事务就读取了中途的数据,这个数据可能是不正确的。
解决办法就是下面的“可读取确认”。
(二)可读取确认(Read committed)
写事务会阻止其他读写事务。读事务不会阻止其他任何事务。
存在的问题:不可重复读。即在一次事务之间,进行了两次读取,但是结果不一样,可能第一次id为1的人叫“李三”,第二次读id为1的人就叫了“李四”。因为读取操作不会阻止其他事务。
解决办法就是下面的“可重复读”。
(三)可重复读(Repeatable read)
读事务会阻止其他写事务,但是不会阻止其他读事务。
存在的问题:幻读。可重复读阻止的写事务包括update和delete(只给存在的表加上了锁),但是不包括insert(新行不存在,所以没有办法加锁),所以一个事务第一次读取可能读取到了10条记录,但是第二次可能读取到11条,这就是幻读。
解决办法就是下面的“串行化”。
(四)可串行化(Serializable)
读加共享锁,写加排他锁。这样读取事务可以并发,但是读写,写写事务之间都是互斥的,基本上就是一个个执行事务,所以叫串行化。
总结:我们需要数据库支持事物,又需要数据库隔离级别切换到最快查询模式,怎么办?
看上去可以选择“可读取未确认(Read uncommitted)”,但是不建议。
一般保持数据库的默认隔离级别(Repeatable read)即可。

查看和设置数据库隔离级别
MySQL5.7版本中,默认数据库隔离级别是REPEATABLE-READ,这个一般在应用开发中不用改变。
select @@tx_isolation; -- 查看当前会话数据库隔离级别
select @@global.tx_isolation; -- 查看全局数据库隔离级别
-- 设置当前会话事物隔离级别
set session transaction isolation level READ UNCOMMITTED;
set session transaction isolation level READ COMMITTED;
set session transaction isolation level REPEATABLE READ;
set session transaction isolation level SERIALIZABLE;
-- 设置全局事物隔离级别
set global transaction isolation level READ UNCOMMITTED;
set global transaction isolation level READ COMMITTED;
set global transaction isolation level REPEATABLE READ;
set global transaction isolation level SERIALIZABLE;

五、表里面的行锁-数据库的悲观锁:for update和AUTOCOMMIT的关系

行锁-悲观锁的使用 for update
使用for update来做到符合某条件的行锁,而不是表锁,这样可以让查询更快。
行锁,比如这样:select * from w1000 where name = 'zhangsan' for update; -- 你希望查询出name = 'zhangsan'的所有行数据,并且给这些行加上行锁。
但是当你执行下一个update语句: update w1000 set age = 50 where name ='zhangsan' 却发现没有执行成功。
为什么?因为你的autocommit默认为开启!所以一旦select * from w1000 where name = 'zhangsan' for update;这句话执行完毕,等不到执行下面的update,它就已经事物完毕了!
那怎么办?更改autocommit为关闭。下面是autocommit的设置方法 (0为关闭状态,1为开启状态)
设置autocommit
SHOW VARIABLES LIKE '%AUTOCOMMIT%'; -- 查看当前会话中的autocommit状态
set @@autocommit=0; -- 设置当前会话中auttocommit关闭状态 OFF
set @@autocommit=1; -- 设置当前会话中auttocommit开启状态 ON
SHOW GLOBAL VARIABLES LIKE '%AUTOCOMMIT%'; -- 查看全局autocommit状态
SET GLOBAL init_connect='SET autocommit=0'; -- 设置全局auttocommit关闭状态 OFF
SET GLOBAL init_connect='SET autocommit=1'; -- 设置全局auttocommit开启状态 ON
完整的行锁示例(参考http://blog.csdn.net/qq_36617521/article/details/53379846)
set autocommit=0; -- 关闭事物自动提交,必须显式的commit才提交事物
begin; / begin work; / start transaction; -- 开始事务(三者选一就可以)
select status from w1000 where id='XXX' for update; -- 符合查询条件的行都会被行锁,如果是整表都符合,那么就是表锁,所以select * from table_name for update是没有任何意义的。
update w1000 set age = 22 where id = 'XXX' ; -- 对锁住行更新,或者拿锁住行的ID去做别的insert都可以
commit;/commit work; -- 因为我们关闭了事物,所以需要手动显式提交事物
set autocommit=1; -- 开启事物自动提交
PS:这一段sql一般可以写在存储过程里,如果频率很高的话,动不动就通过java执行这么一串命令,也是很费时间的

六、添加索引
添加某列的索引可以让以该列为查询条件的SQL查询速度提升,这已经是共识。
对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
MySQL建立索引需要指定四项:
1,索引名称(自定义索引名字)
2,索引列(指定哪一个列上要建立索引)
3,索引类型(Normal普通索引, Unique值不允许重复索引, FullText 全文索引,用于文本较长的列,在mysql5.7版本中,支持InnoDB和MyISAM,官方参考https://dev.mysql.com/doc/refman/5.7/en/fulltext-search.html)
4,索引方法(分为BTREE和HASH两种,BTREE适用于用在像=,>,>=,<,<=和BETWEEN这些比较操作符上和不以%开头的模糊匹配查询上,HASH可以用在字符列上)
MySQL自动建立主键索引
我为w1000手动建立了index_modify_date,可是在show index from w1000; 查看某张表的全部索引时,却发现主键已经被自动创建索引。

mysql优化-数据库优化、SQL优化

索引信息中的列的信息说明(参考http://blog.51yip.com/mysql/1222.html)
Table :表的名称。
Non_unique:如果索引不能包括重复词,则为0。如果可以,则为1。
Key_name:索引的名称。
Seq_in_index:索引中的列序列号,从1开始。
Column_name:列名称。
Collation:列以什么方式存储在索引中。在MySQLSHOW INDEX语法中,有值’A’(升序)或NULL(无分类)。
Cardinality:索引中唯一值的数目的估计值。通过运行ANALYZE TABLE或myisamchk -a可以更新。基数根据被存储为整数的统计数据来计数,所以即使对于小型表,该值也没有必要是精确的。基数越大,当进行联合时,MySQL使用该索引的机会就越大。
Sub_part:如果列只是被部分地编入索引,则为被编入索引的字符的数目。如果整列被编入索引,则为NULL。
Packed:指示关键字如何被压缩。如果没有被压缩,则为NULL。
Null:如果列含有NULL,则含有YES。如果没有,则为空。
Index_type:存储索引数据结构方法(BTREE, HASH)
optimize table用法
如果数据发生大量改动,而表并没有最优化,可以使用optimize table table_name; 来优化表的索引排序。
默认不支持InnoDB,需要修改,本人未验证修改后是否支持InnoDB.
参考http://blog.csdn.net/e421083458/article/details/39522513
索引建立示例(参考http://www.cnblogs.com/bk7788/p/6050184.html)
1.添加PRIMARY KEY(主键索引)
mysql>ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` )
2.添加UNIQUE(唯一索引)
mysql>ALTER TABLE `table_name` ADD UNIQUE (`column`)
3.添加INDEX(普通索引)
mysql>ALTER TABLE `table_name` ADD INDEX index_name ( `column` )
4.添加FULLTEXT(全文索引)
mysql>ALTER TABLE `table_name` ADD FULLTEXT (`column`)
5.添加多列索引,即聚合索引
mysql>ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`); 建议只有在频繁使用这些列同时作为查询条件时,比如日期区间,才这样建立索引。

七、SQL中索引的使用
我们添加了索引,但不能以为只要在where条件中用到了索引列,就一定使用了索引。以下情况即使定义了where后面的查询条件字段为索引字段,它也没有用到索引,让我们来回顾下之前那些优化前的SQL:

mysql优化-数据库优化、SQL优化

1)应尽量避免查询冗余字段,只要查询用的着的字段即可
    SELECT name,age from w1000 where id = '0000c51e-0ede-4dc2-871a-78c1eebd0041'; -- 0.003s 查询条件为主键字段
2)应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描
    SELECT * from w1000 where age = 19 or age = 20; -- 32.234s
    or语句可以改为
    select id from w1000 where age=19
    union all
      select id from w1000 where age=20
3)应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描
    SELECT * from w1000 where age != 20; -- 75.222s
4)应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,可以设置默认值。
    SELECT * from w1000 where modify_date is not null; -- 73.387ss
5)应尽量避免在like条件中以通配符开头匹配,以通配符结束是可以经过索引的。
    SELECT * from w1000 where name like '%张%'; -- 25.519s
6)in 和 not in 也要慎用,否则会导致全表扫描
    SELECT * from w1000 where age in (20,21); -- 34.47s
    很多时候用 exists 代替 in 是一个好的选择:
    select age from w1000 where exists(...)
7)应尽量避免在=号左边索引列做运算或者使用函数,否则索引列上的索引失效
    SELECT * from w1000 where age/2 = 50; -- 30.352s
    SELECT * from w1000 where SUBSTRING(name FROM 1 FOR 1) = '张'; -- 26.091s
8)在参数为变量时,可以强制使用索引
    select id from t where num=@num
    可以改为强制查询使用索引:
    select id from t with(index(索引名)) where num=@num
注意,如果一个索引列中大量的数据出现重复,那么即使在该列上建立了索引也对查询效率没多大作用。
以上论点参考《mysql sql 百万级数据库优化方案》( http://www.cnblogs.com/huangye-dream/archive/2013/05/21/3091906.html)

八、explain查看SQL中的索引使用情况
既然索引很重要,可是SQL执行过程中到底有没有使用了索引,这个可以使用explain查看,使用方法:explain select * from table_name; 就是在select语句前加上explain。
我本地测试EXPLAIN SELECT * from w1000 where id = '0000c51e-0ede-4dc2-871a-78c1eebd0041' ;

我们可以看到type值为const,查询已经达到最优,并且key列实际使用了索引PRIMARY。
参考《mysql查询语句分析 explain用法》http://www.cnblogs.com/lijiasnong/p/5486111.html
explain列的解释
table:显示这一行的数据是关于哪张表的
type:这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为const、eq_reg、ref、range、indexhe和all
possible_keys:显示可能应用在这张表中的索引。如果为空,没有可能的索引。可以为相关的域从where语句中选择一个合适的语句
key: 实际使用的索引。如果为null,则没有使用索引。很少的情况下,mysql会选择优化不足的索引。这种情况下,可以在select语句中使用use index(indexname)来强制使用一个索引或者用ignore index(indexname)来强制mysql忽略索引
key_len:使用的索引的长度。在不损失精确性的情况下,长度越短越好
ref:显示索引的哪一列被使用了,如果可能的话,是一个常数
rows:mysql认为必须检查的用来返回请求数据的行数
extra:关于mysql如何解析查询的额外信息。将在表4.3中讨论,但这里可以看到的坏的例子是using temporary和using filesort,意思mysql根本不能使用索引,结果是检索会很慢
extra列返回的描述的意义
distinct:一旦mysql找到了与行相联合匹配的行,就不再搜索了
not exists: mysql优化了left join,一旦它找到了匹配left join标准的行,就不再搜索了
range checked for each record(index map:#):没有找到理想的索引,因此对于从前面表中来的每一个行组合,mysql检查使用哪个索引,并用它来从表中返回行。这是使用索引的最慢的连接之一
using filesort: 看到这个的时候,查询就需要优化了。mysql需要进行额外的步骤来发现如何对返回的行排序。它根据连接类型以及存储排序键值和匹配条件的全部行的行指针来排序全部行
using index: 列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候
using temporary 看到这个的时候,查询需要优化了。这里,mysql需要创建一个临时表来存储结果,这通常发生在对不同的列集进行order by上,而不是group by上
where used 使用了where从句来限制哪些行将与下一张表匹配或者是返回给用户。如果不想返回表中的全部行,并且连接类型all或index,这就会发生,或者是查询有问题不同连接类型的解释(按照效率高低的顺序排序)
system 表只有一行:system表。这是const连接类型的特殊情况
const:表中的一个记录的最大值能够匹配这个查询(索引可以是主键或惟一索引)。因为只有一行,这个值实际就是常数,因为mysql先读这个值然后把它当做常数来对待
eq_ref:在连接中,mysql在查询时,从前面的表中,对每一个记录的联合都从表中读取一个记录,它在查询使用了索引为主键或惟一键的全部时使用
ref:这个连接类型只有在查询使用了不是惟一或主键的键或者是这些类型的部分(比如,利用最左边前缀)时发生。对于之前的表的每一个行联合,全部记录都将从表中读出。这个类型严重依赖于根据索引匹配的记录多少—越少越好
range:这个连接类型使用索引返回一个范围中的行,比如使用>或<查找东西时发生的情况
index: 这个连接类型对前面的表中的每一个记录联合进行完全扫描(比all更好,因为索引一般小于表数据)
all:这个连接类型对于前面的每一个记录联合进行完全扫描,这一般比较糟糕,应该尽量避免

九、建立索引后查询对比
在第三章SQL优化前,我们看到sql的执行时间很长。这些SQL查询的where字段是age和name,现在我们建立在这两个列上建立索引:
ALTER TABLE `w1000` ADD Normal(`age`); -- 204.562s 在1000万级的表上执行这种影响全表的更改表语句很慢
ALTER TABLE `w1000` ADD Normal(`name`); -- 222.041s
之前查询慢的SQL,不仅是因为没有建立索引,而且即使建立了索引,这些where子句里的查询方法也会使索引失效。
建立完索引后,那些sql再拉出来溜一遍,当然了,查询条件的值要改一下,否则,SQL一模一样会使mysql直接返回查询缓存里的数据。
注意,只select我们实际用到的字段,不要select *
对比结果:
-- 未优化之前
SELECT id,name from w1000 where age = 40; -- 9.893s
SELECT id,name from w1000 where age = 80 limit 0,100; -- 0.008s
SELECT id,name from w1000 where name like '古%'; -- 11.182s
SELECT id,name from w1000 where name='宋安安'; -- 13.384s
SELECT id,name from w1000 where age=25 limit 8000000,1550; -- 10.168s
SELECT id,name from w1000 where age =11 limit 0,100; -- 0.027s
-- 优化之后
SELECT id,name from w1000 where age = 18 ; -- 14.802s
SELECT id,name from w1000 where age = 12 limit 0,100; -- 0.011s
SELECT id,name from w1000 where name like '史%'; -- 0.070s
SELECT id,name from w1000 where name='齐豆荀'; -- 0.001s
SELECT id,name from w1000 limit 8000000,1550; -- 8.643s
SELECT id,name from w1000 where age =14 limit 0,100; -- 0.013s
总结
从上面结果看出,建立了索引并且正确使用了索引之后,varchar类型的列确实能很大程度上提高查询效率,但是int类型的好像更慢了,没有搞明白为什么。
不过在都不建立索引的情况下,某些比如性别字段,只有男和女的值,应该用int类型的(0或1)更快一点。

十、查看当前连接信息
mysql> show processlist\G
或者直接在navicat工具里执行
show processlist(或者show full processlist)

mysql优化-数据库优化、SQL优化

这个命令可以让你看到当前连接到数据库的信息,方便观察是否死锁等情况。如果State为空或者NULL,则代表已经执行完毕,否则会显示Updating、Sending data等状态
比如数据库连接挂了,就可以查看是哪一条SQL被阻塞。