MySQL数据库优化设计与高级应用
Posted lijianming180
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL数据库优化设计与高级应用相关的知识,希望对你有一定的参考价值。
mysql数据库优化主要涉及两个方面,一方面是对SQL语句优化,另一方面是对数据库服务器和数据库配置的优化。
数据库优化
SQL语句优化
为了更好的看到SQL语句执行效率的差异,建议创建几个结构复杂的数据表,多导入一些数据进行测试,看到的效果比较直观。
尽量避免在列上进行运算,这样会导致索引失效。
优化前
SELECT * FROM t WHERE YEAR(d)>=2011;
优化后
SELECT * FROM t WHERE d>=‘2011-01-01‘;
使用JOIN时,应该用小结果集驱动大结果集。同时把复杂的JOIN查询拆分成多个Query。因为JOIN多个表时,可能导致更多的锁定和堵塞。
不建议:
SELECT * FROM a JOIN b ON a.id=b.id LEFT JOIN c ON c.time=a.date LEFT JOIN d ON c.pid=b.aid LEFT JOIN e ON e.cid=a.did;
注意LIKE模糊查询的使用,避免%%。
优化前
SELECT * FROM t WHERE name LIKE ‘%de%‘;
优化后
SELECT * FROM t WHERE name>=‘de‘ AND name<=‘df‘;
仅列出需要查询的字段,这对速度不会有明显的影响,主要考虑节省内存。
优化前
SELECT * FROM member;
优化后
SELECT id,name,pwd FROM member;
使用批量插入语句节省交互。
优化前:
1
2
3INSERT INTO t(id,name) VALUES(1,‘a‘);
INSERT INTO t(id,name) VALUES(2,‘b‘);
INSERT INTO t(id,name) VALUES(3,‘c‘);优化后
INSERT INTO t(id,name) VALUES(1,‘a‘),(2,‘b‘),(3,‘c‘);
limit的基数比较大时,使用between。
优化前
SELECT * FROM article ORDER BY id LIMIT 1000000,10;
优化后
SELECT * FROM article WHERE id BETWEEN 1000000 AND 1000010 ORDER BY id;
between限定比limit快,所以在海量数据访问时,建议用between或where替换掉limit。但是between也有缺陷,如果id中间有断行或是中间部分id不读取的情况,总读取的数量会少于预计数量。
在取比较后面的数据时,通过desc方式把数据反向查找,以减少对前段数据的扫描,让limit的基数越小越好。
不要使用rand函数获取多条随机记录。
优化前
SELECT * FROM table ORDER BY rand() LIMIT 20;
优化后
SELECT * FROM
tableAS t1 JOIN (SELECT ROUND(ROUND()*((SELECT MAX(id) FROM
table) - (SELECT MIN(id) FROM
table)) + (SELECT MIN(id) FROM
table)) AS id ) AS t2 WHERE t1.id >= t2.id ORDER BY t1.id LIMIT 1 ;
这是获取一条随机记录,这样即使执行20次,也比优化前的sql语句高效。或者先用php产生随机数,把这个字符串传给MySQL,MySQL里用in查询。
- 避免使用NULL。
不要使用COUNT(id),而应该使用COUNT(*)。
注意:个别情况,结果有变。
- 不要做无谓的排序操作,而应该尽可能在索引中完成排序。
索引
查看SQL语句执行效率
1 | mysql> SET @@profiling=1; |
对于同一条sql语句,第二次查询明显比第一次查询快,因为MySQL缓存做了查询。
查看第四条sql语句执行细节:
mysql> SHOW PROFILE FOR QUERY 4;
EXPLAIN
MySQL执行计划就是在一条SELECT语句前放上关键字EXPLAIN,MySQL解释它将如何处理SELECT,提供有关表如何联合和以什么次序联合的信息。
借助EXPLAIN可以知道:
- 什么时候必须为表添加索引,以得到一个使用索引找到记录的更快的SELECT方法。
优化器是否以一个最佳次序联合表。
注意:为了强制优化器对一个SELECT语句使用一个特定联合次序,需增加一个STRAIGHT_JOIN子句,方法如下:
EXPLAIN [EXTENDED] SELECT select_options
对比较复杂的查询进行计划分析时,可能会得到多条执行计划,如下:
1 | mysql> EXPLAIN SELECT * FROM user; |
各属性含义如下:
- id:查询的序列号
- select_type:查询的类型,主要包括普通查询、联合查询和子查询。
- table:所访问的数据库中表的名称。
- type:联合查询使用的类型。
- possible_keys:指出MySQL能使用哪个索引在该表中找到该行。如果这个值是空的,则表示没有相关的索引。这时要提高性能,可通过检索WHERE子句,看是否引用了某些字段,或者检查字段是否适合索引。
- key:显示MySQL实际决定使用的键。如果没有索引被选择,键是NULL。
- key_len:显示MySQL决定使用的键长度。如果键是NULL,长度就是NULL。注意,这个值可以反映出一个多重主键里MySQL实际使用了哪部分。
- ref:显示哪个字段或常数与key一起被使用。
- rows:这个值表示MySQL要遍历多少数据才能找到所需的结果集,其在InnoDB上是不准确的。
- Extra:如果是Only index,意味着信息只能用索引树中的信息检索,这比扫描整个表要快;如果是where used,则表示使用了where限制,但是用索引还不够;如果是impossible where,则表示通过收集到的统计信息判断出不可能存在的结果。
除此之外,Extra还有下面一些可能的值:- Using filesort:表示包含order by且无法使用索引进行派讯操作时,不得不使用相应的派讯算法实现。
- using temporary:使用临时表,常见于order by和group by。
- select tables optimized way:使用聚合函数,并且MySQL进行了快速定位。通常是MAX,MIN,COUNT(*)等函数。
需要说明的是,type显示的访问类型是较为重要的指标,结果值从好到坏依次是:system(系统表)、const(读常量)、eq_ref(最多一条匹配结果,通常是通过主键访问)、ref(被驱动表索引引用)、fulltext(全文索引检索)、ref_or_null(带空值的索引查询)、index_merge(合并索引结果集)、unique_subquery(子查询中返回的字段是唯一组合或索引)、index_subquery(子查询返回的索引,但非主键)、range(索引范围扫描)、index(全索引扫描)、ALL(全表扫描)。
一般来说,保证查询至少达到range级,最好能达到ref级。ALL为全表扫描,是最坏的情况,这种情况往往是没用上索引。
MySQL索引建立和使用的基本原则如下:
- 合理设计和使用索引。
- 在关键字段的索引上,建与不建索引,查询速度相差近100倍。
- 差的索引和没有索引效果一样。
- 索引并非越多越好,因为维护索引需要成本。
- 每个表的索引应该在5个以下,应合理利用部分索引和联合索引。
- 不在结果集中的结果单一的列上建索引。比如性别字段只有0和1两种结果集,在这个字段上建索引并不会有太多帮助。
- 建索引的字段结果集最好分布均匀,或者符合正态分布。
关于MySQL索引的更多介绍,请参考MySQL索引
配置
MySQL存储引擎
想要好的性能,第一步是选择合适的存储引擎。MySQL中常见的三种存储引擎是MyISAM、InnoDB和Memmory。
通常的观点是MyISAM注重性能,InnoDB注重事务,所以一般使用MyISAM的表做非事务型的业务。
这种观点产生于早期InnoDB还不成熟的时候,其实事实上并不是这样。MySQL在高并发下的性能瓶颈很明显,主要原因就是锁定机制导致的阻塞。而InnoDB在锁定机制上采用行级锁,不同于MyISAM的表级锁,行级锁在锁定上带来的消耗大于表级锁,但是在系统并发访问量较高时,InnoDB整体性能远高于MyISAM。同时,InnoDB的索引不仅缓存索引本身,也缓存数据,所以InnoDB需要更大的内存,现在内存是很廉价的。
存储引擎的选择方法
在介绍选择存储引擎之前,先来了解一下什么是读写比(R/W)。通过在数据库中执行SHOW GLOBAL STATUS
得到系统当前状态。在这些变量中,以Com_为前缀的变量表示某语句的执行次数,比如Com_select表示SELECT语句的执行次数,以此类推。通过计算读类型和写类型语句的比例,即可以确定一个大概的读写比例。理想的读写比例为100:1,当读写比达到10:1的时候,就认为是以写为主的数据库。一般来说,读写比在30:1左右。
选择存储引擎的原则如下:
- 采用MyISAM存储引擎
R/W > 100:1并且UPDATE相对较少。
并发不高,不需要事务。
表数据库小。
硬件资源有限。 - 采用InnoDB存储引擎
R/W比较小,频繁更新大字段。
表数据量超过1000万,并发高。
安全性和可用性要求高。 - 采用Memory引擎
有足够的内存。
对数据一致性要求不高,如在线人数和Session等应用。
需要定期归档数据。
MySQL服务器调整优化措施
关闭不必要的二进制日志和慢查询日志,仅在内存足够或开发调试时打开它们。
使用下面的语句查看查询是否打开:SHOW VARIABLES LIKE ‘%slow%‘;
还可以使用下面的语句查看慢查询的条数,定期打开方便优化。SHOW GLOBAL STATUS LIKE ‘%slow%‘;
但是慢查询也会带来一些CPU损耗。建议间断性打开慢查询日志来定位性能瓶颈。- 适度使用Query Cache。
增加MySQL允许的最大连接数。可用下面的语句查看MySQL允许的最大连接数。
SHOW VARIABLES LIKE ‘max_connections‘;
对于MyISAM表适当增加key_buffer_size。当前这需要根据key_cache的命中率进行计算。例如:
SHOW GLOBAL STATUS LIKE ‘key_read%‘;
key_cache_miss_rate = Key_reads / Key_read_requests * 100%
当key_cache_miss_rate值大于1%时就需要适当增加key_buffer_size了。
对于MyISAM,还需要注意table_cache的设置。当table_cache不够用的时候,MySQL会采用LRU算法踢掉最长时间没有使用的表;如果table_cache设置过小,MySQL就会反复打开、关闭FRM文档,造成一定性能的损失;如果table_cache设置过大,MySQL将会消耗很多CPU资源去处理table_cache算法。因此table_cache值一定要设置合理,可参考opened_tables参数的值,如果这个值一直增长,就需要适当增加table_cache值。
对于InnoDB,需要重点注意innodb_buffer_pool_size参数。
- 从表中删除大量行后,可运行
OPTIMIZE TABLE TableName
进行碎片整理。
瓶颈
当MySQL单表数据量达到千万级以上时,无论如何对MySQL进行优化,查询如何简单,MySQL的性能都会明显降低,这时候可以采取以下措施:
- 性能优化中,效果最明显、成本最低的方式就是硬件和服务器优化,可以增加服务器CPU数量和内存大小,增加MySQL配置中buffer和Cache的数值。
- 使用第三方引擎或衍生版本,如MariaDB。
- 迁移至其它数据库。
如PostgreSQL和Oracle。与MySQL线程模式相比,进程模式能更充分应用CPU资源,在复杂业务查询上更有优势。 - 对数据库进行分区、分表操作,降低单表数据量。
- 使用NoSQL等辅助解决方案,如Memcached、Redis。
- 使用中间件做数据拆分和分布式部署。如阿里开源的数据库中间件Cobar。
- 使用数据库连接池。
MySQL是线程模式,可以支持更多的并发连接。为什么还要使用数据库连接池?因为MySQL的锁机制不够完善,还有进程中的一些问题都有可能导致MySQL数据库连接阻塞,在大并发情况下,就会浪费很多连接资源和反复连接的消耗。使用数据库连接池,让连接进行排队和复用,一定程度上可以缓解高并发下的连接压力。
数据库设计
反范式
为什么要反范式?
首先,要了解一下范式理论的背景。数据库范式理论是在20世纪70年代提出的,并且在20世纪80年代基本完善定型。那时候存储资源和网络资源都还不成熟。因此,当时数据库范式理论强调减少依赖、降低冗余。而现在存储资源和网络资源已不是问题,同时面临着高并发、极度复杂的业务逻辑,低延迟要求等情况下,还一味固执遵循范式设计理论是不当的。适当降低范式,增加冗余,用空间换时间是值得的。
通常在设计数据库是要遵循如下原则:
- 核心业务使用范式。
- 弱一致性需求(反ACID)。
- 空间换时间,冗余换效率。
- 避免不必要的冗余。
分区
MySQL5.1及以上版本支持分区。数据库分区就是把一个数据表的文档和索引分散存储在不同的物理文档中。
查看数据库是否支持分区
5.1-5.5版本:SHOW VARIABLES LIKE ‘%partition%‘
5.6及以上版本:SHOW PLUGINS
查询结果中包含如下内容,表示支持分区。
1 | | partition | ACTIVE | STORAGE ENGINE | NULL | GPL | |
MySQL支持的分区类型包括Range、List、Hash、Key,其中Range最常用。
创建Range类型分区表
1 | mysql> CREATE TABLE member( |
添加分区
1 | mysql> ALTER TABLE member ADD PARTITION ( |
删除分区
1 | mysql> ALTER TABLE member DROP PARTITION member_3; |
检索information_schema数据库能看到刚刚创建的分区信息,检索方法如下:
1 | USE information_schema; |
此时,打开MySQL数据目录(SHOW VARIABLES LIKE ‘datadir‘
),如果MySQL配置文档设置innodb file per table 为ON(由于前面创建分区表时定义的是INNODB),则会发现:
1 | [root@localhost ~]# ll /usr/local/mysql/var/test/ |
由此可见,MySQL通过分区把数据保存到不同的数据文档里,同时索引也是分区的。相对于未分区的表来说,分区后单独的数据文档和索引文档的大小都明显降低,执行效率则明显提升。
验证执行效率
1 | # 插入几条数据 |
注意:使用分区功能后,相关的查询最好都用EXPLAIN PARTITIONS
过一遍,确认分区是否生效。
不过有些情况,比如说在主从结构中,因为主服务器由于很少使用SELECT查询,所以在主服务器上使用Range类型的分区通常并没有太大意义,此时使用Hash类型的分区相对更好。假设使用PARTITION BY HASH (id) PARTITIONS 10
,当插入新数据时,会根据id把数据平均分散到各个分区上(这里是10个分区),由于文档小,效率高,更新操作会变得更快。
创建HASH类型分区表
1 | mysql> CREATE TABLE blog ( |
验证
1 | # 插入几条数据 |
如何选择分区字段?
一般情况按照时间字段分区,不过具体情况还是应该按照需求而定。比如需求是按照用户和时间查询,哪种用的多就选哪种分区。如果使用主从结构,可以设计成不同的从服务器使用不同的分区字段,然后写个代理脚本,当执行查询时,通过代理脚本来决定选择正确的从服务器查询。
分区的缺点有哪些?
- 主键或者唯一索引必须包含分区字段,例如上面创建的Range类型分区表,PRIMARY KEY (id,birthday),不过对INNODB来说,大主键(复合主键)性能不好。很多时候,使用分区就不要使用主键,否则可能影响性能。
- 只能通过INT类型的字段或者返回INT类型的表达式来分区,通常使用YEAR或TO_DAYS等函数。不过MySQL5.6对这个限制开始开放。
- 每个表最多1024个分区,不可能无限制的扩展分区。过度使用分区会消耗大量的内存。
- 使用分区的表不支持外键,相关的约束逻辑必须通过进程来实现。
- 分区后,可能会造成索引失效,需要验证分区可行性。
分库分表
分库解决单台数据库并发访问压力,分表解决单表大数据量查询性能低。
垂直切分
水平切分
- 取模分表(不要使用自增,建议使用序列)
- 日期分表
- 增量分表
- 哈希分表
- 使用merge存储引擎实现分表
MySQL的高级应用
序列
视图
存储过程、事件调度
消息队列
SQL防注入
MySQL主从复制
主从复制功能(读写分离)通过在主服务器和从服务器之间切分处理客户查询的负荷,可以得到更好的客户响应时间SELECT查询可以发送到从服务器,以降低主服务器的查询处理负荷。修改数据库的语句仍然发送到主服务器,以使主、从服务器保持同步。如果非更新查询为主(如SELECT查询),该负载均衡策略很有效。
MySQL主从复制的优点:
- 增加健壮性。主服务器出现问题是,切换到从服务器作为备份。
- 优化响应时间。不要同时在主从服务器上进行更新,这样可能引起冲突。
- 在从服务器备份过程中,主服务器继续处理更新。
主从复制工作原理
主从复制过程
- BinLog Dump线程
BinLog Dump线程运行在主服务器上,主要工作是把Binary Log二进制日志文档的数据发送给从服务器。
使用SHOW PROCESSLIST语句查看该线程是否正在运行。 - I/O线程
从服务器执行START SLAVE语句后,创建一个I/O线程。此线程运行在从服务器上,与主服务器建立连接,然后向主服务器发出更新请求。之后,I/O线程将主服务器发送的更新操作复制到本地Relay Log日志文档中。
使用SHOW SLAVE STATUS语句查看I/O线程状态。 - SQL线程
SQL线程运行在从服务器上,主要工作是读取Relay Log日志文档中的更新操作,并将这些操作依次执行,从而使主从服务器数据得到同步。
主从复制配置
- 确认主从服务器的MySQL版本。
MySQL不同版本的BinLog格式可能不一样,最好采用相同版本。如果达不到要求,必须保证主服务器版本不高于从服务器版本。使用mysql -V
命令查看版本号。 - 在主服务器上为从服务器设置一个连接帐号,授予
REPLICATION SLAVE
权限。
每个从服务器使用标准MySQL用户名和密码连接主服务器。假定域为mydomain.com,要创建y用户名为“repl”的帐号,从服务器使用该帐号从域内任何主机使用密码“123”访问主服务器。
创建该帐号使用GRANT语句:
mysql> GRANT REPLICATION SLAVE ON *.* TO ‘repl‘ @ ‘%.mydomain.com‘ IDENTIFIED BY ‘123‘;
配置主服务器。
打开二进制日志,指定唯一Server ID。例如,在my.cnf配置文档中加入如下值:1
2[mysqld]
log-bin=mysql-bin server-id=1重启主服务器。
运行SHOW MASTER STATUS
语句,如图所示:File表示主服务器正在使用binlog文档;Position的值与binlog文档的大小相同,表示下一个被记录事件的位置;Binlog_Do_DB和Binlog_Ignore_DB是主服务器控制写入binlog文档内容的过滤选项,默认为空,表示不做任何过滤。
配置从服务器。
从服务器的配置与主服务器类似,必须提供唯一的Server ID(不能和主服务器Server ID相同),配置完毕后同样需要重启MySQL从服务器,配置如下:1
2[mysqld]
log-bin=mysql-bin server-id=2启动从服务器。
接下来让从服务器连接主服务器,并开始重做主服务器binlog文档中的事件。指定主服务器信息。
使用CHANGE MASTER TO
语句指定主服务器的信息,不要在配置文档中设置。下面语句可以替代在配置文档中提供主服务器信息,而且不需要停止服务器便可以为从服务器指定不同主服务器。1
2
3
4
5mysql> CHANGE MASTER TO MASTER_HOST=‘192.168.1.100‘,
-> MASTER_USER=‘repl‘,
-> MASTER_PASSWORD=‘123‘,
-> MASTER_LOG_FILE=‘mysql-bin.000001‘,
-> MASTER_LOG_POS=0;此处指定MASTER_LOG_POS的值为0,因为要从日志的开始位置开始读。
查看从服务器的设置是否正确。
使用SHOW SLAVE STATUS
语句查看从服务器的设置是否正确:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16mysql> SHOW SLAVE STATUSG;
*************************** 1. row ***************************
Slave_IO_State:
Master_Host:192.168.1.100
Master_User:repl
Master_Port:3306
Connect_Retry:60
Master_Log_File:mysql-bin.000001
Read_Master_Log_Pos:4
Relay_Log_File:mysql-relay-bin.000001
Relay_Log_Pos:4
Relay_Master_Log_File:mysql-bin.000001
Slave_IO_Running:No
Slave_SQL_Running:No
...
Seconds_Behind_Master:NULLSlave_IO_State、Slave_IO_Running和Slave_SQL_Running表明从服务器还没有开始复制过程。
注意:日志位置为4而不是0,因为0只是日志文档的开始位置,并不是日志记录事件的开始位置。实际上,MySQL知道的第一个事件的位置是4.
连接主从服务器
- 执行
START SLAVE
语句开始复制:
msyql> START SLAVE;
运行
SHOW SLAVE STATUS
查看输出结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16mysql> SHOW SLAVE STATUSG;
*************************** 1. row ***************************
Slave_IO_State:Wating for master to send event
Master_Host:192.168.1.100
Master_User:repl
Master_Port:3306
Connect_Retry:60
Master_Log_File:mysql-bin.000001
Read_Master_Log_Pos:164
Relay_Log_File:mysql-relay-bin.000001
Relay_Log_Pos:164
Relay_Master_Log_File:mysql-bin.000001
Slave_IO_Running:Yes
Slave_SQL_Running:Yes
...
Seconds_Behind_Master:0从结果看出,从服务器的I/O线程和SQL线程都已经开始运行,而且Seconds_Behind_Master不再是Null。日志位置增加意味着一些事件被获取并执行。如果在主服务器上进行修改,可以在从服务器上看到各种日志文档位置变化,以及数据库中数据的变化。
使用SHOW PROCESSLIST
语句查看主、从服务器的线程状态。在主服务器上,可以看到从服务器的I/O线程创建的l连接:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20mysql> SHOW PROCESSLISTG;
*************************** 1. row ***************************
Id:1
User:root
Host:localhost:2096
db:test
Command:Query
Time:0
State:NULL
Info:show processlist
*************************** 2. row ***************************
Id:2
User:repl
Host:localhost:2144
db:NULL
Command:Binlog Dump
Time:1838
State:Has sent all binlog to slave;wating for binlog to be updated
Info:NULL
2 rows in set(0.00 sec)第2行就是处理从服务器的I/O线程的连接。
在从服务器上运行
SHOW PROCESSLIST
语句。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29mysql> SHOW PROCESSLISTG;
*************************** 1. row ***************************
Id:1
User:system user
Host:
db:NULL
Command:Connect
Time:2291
State:Waiting for master to send event
Info:NULL
*************************** 2. row ***************************
Id:2
User:system user
Host:
db:NULL
Command:Connect
Time:1852
State:Has read all relay log;waiting for the salve I/O thread to update it
Info:NULL
*************************** 3. row ***************************
Id:5
User:root
Host:localhost:2152
db:test
Command:Query
Time:0
State:NULL
Info:show processlist
3 rows in set(0.00 sec)第一行为I/O线程状态,第二行为SQL线程状态。
注意:从服务器t通过读主服务器的二进制日志实现自我更新,对数据库进行修改的操作都要放在主服务器上执行,而从服务器只用来查询(只读不写的数据库操作)。
以上是关于MySQL数据库优化设计与高级应用的主要内容,如果未能解决你的问题,请参考以下文章