开门见山
本文核心内容思维导图

概述
为什么要优化
系统的吞吐量瓶颈往往出现在数据库的访问速度上 随着应用程序的运行,数据库的中的数据会越来越多,处理时间会相应变慢 数据是存放在磁盘上的,读写速度无法和内存相比
如何优化
设计数据库时:数据库表、字段的设计,存储引擎 利用好MySQL自身提供的功能,如索引等 横向扩展:MySQL集群、负载均衡、读写分离 SQL语句的优化(收效甚微)
字段设计
字段类型的选择,设计规范,范式,常见设计案例
原则:尽量使用整型表示字符串
存储IP
INET_ATON(str),address to numberINET_NTOA(number),number to address
MySQL内部的枚举类型(单选)和集合(多选)类型
原则:定长和非定长数据类型的选择
decimal不会损失精度,存储空间会随数据的增大而增大。double占用固定空间,较大数的存储会损失精度。非定长的还有varchar、text
金额
对数据的精度要求较高,小数的运算和存储存在精度问题(不能将所有小数转换成二进制)
定点数decimal
小单位大数额避免出现小数
字符串存储
原则:尽可能选择小的数据类型和指定短的长度
原则:尽可能使用 not null
原则:单表字段不宜过多,二三十个就极限了
原则:可以预留字段
在使用以上原则之前首先要满足业务需求
关联表的设计
外键foreign key只能实现一对一或一对多的映射
一对多
多对多
一对一
范式 Normal Format
数据表的设计规范,一套越来越严格的规范体系(如果需要满足N范式,首先要满足N-1范式)。N
第一范式1NF:字段原子性
关系型数据库,默认满足第一范式
第二范式:消除对主键的部分依赖
即在表中加上一个与业务逻辑无关的字段作为主键
第三范式:消除对主键的传递依赖
存储引擎选择
早期问题:如何选择MyISAM和Innodb? 现在不存在这个问题了,Innodb不断完善,从各个方面赶超MyISAM,也是MySQL默认使用的。
存储引擎Storage engine:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。
功能差异
存储差异
锁扩展 表级锁(table-level lock):
lock tables <table_name1>,<table_name2>... read/write,unlock tables <table_name1>,<table_name2>...。
其中read是共享锁,一旦锁定任何客户端都不可读;write是独占/写锁,只有加锁的客户端可读可写,其他客户端既不可读也不可写。锁定的是一张表或几张表。行级锁(row-level lock):锁定的是一行或几行记录。
共享锁:select * from <table_name> where <条件> LOCK IN SHARE MODE;,
对查询的记录增加共享锁;select * from <table_name> where <条件> FOR UPDATE;,对查询的记录增加排他锁。
这里值得注意的是:innodb的行锁,其实是一个子范围锁,依据条件锁定部分范围,而不是就映射到具体的行上,因此还有一个学名:间隙锁。
比如select * from stu where id < 20 LOCK IN SHARE MODE会锁定id在20左右以下的范围,你可能无法插入id为18或22的一条新纪录。
选择依据
索引
关键字与数据的映射关系称为索引(==包含关键字和对应的记录在磁盘中的地址==)。关键字是从数据当中提取的用于标识、检索数据的特定内容。
索引检索为什么快?
关键字相对于数据本身,==数据量小== 关键字是==有序==的,二分查找可快速确定位置
MySQL中索引类型
普通索引(key),唯一索引(unique key),主键索引(primary key),全文索引(fulltext key)
普通索引:对关键字没有限制 唯一索引:要求记录提供的关键字不能重复 主键索引:要求关键字唯一且不为null
alter table user_index drop KEY name;alter table user_index drop KEY id_card;alter table user_index drop KEY information;
alter table user_index-- 重新定义字段MODIFY id int,drop PRIMARY KEY
但通常不会删除主键,因为设计主键一定与业务逻辑无关。
执行计划explain
CREATE TABLE innodb1 ( id INT auto_increment PRIMARY KEY, first_name VARCHAR (16), last_name VARCHAR (16), id_card VARCHAR (18), information text, KEY name (first_name, last_name), FULLTEXT KEY (information), UNIQUE KEY (id_card));insert into innodb1 (first_name,last_name,id_card,information) values ('x','x','1001','xxx');

索引使用场景(重点)
where
上图中,根据id查询记录,因为id字段仅建立了主键索引,因此SQL执行可选的索引只有主键索引,如果有多个,最终会选一个较优的作为检索的依据。
-- 增加一个没有建立索引的字段alter table innodb1 add sex char(1);-- 按sex检索时可选的索引为nullEXPLAIN SELECT * from innodb1 where sex='男'
可以尝试在一个字段未建立索引时,根据该字段查询的效率,然后对该字段建立索引(alter table 表名 add index(字段名)),同样的SQL执行的效率,你会发现查询效率会有明显的提升(数据量越大越明显)。
order by
如果该字段没有建立索引,那么执行计划会将查询出的所有数据使用外部排序(将数据从硬盘分批读取到内存使用内部排序,最后合并排序结果).
这个操作是很影响性能的,因为需要将查询涉及到的所有数据从磁盘中读到内存(如果单条数据过大或者数据量过多都会降低效率),更无论读到内存之后的排序了。
而且如果分页的,那么只用取出索引表某个范围内的索引对应的数据,而不用像上述那取出所有数据进行排序再返回某个范围内的数据。
join
对join语句匹配关系(on)涉及的字段建立索引能够提高效率
索引覆盖
语法细节(要点)
在满足索引使用的场景下(where/order by/join on或索引覆盖),索引也不一定被使用
字段要独立出现
select * from user where id = 20-1;select * from user where id+1 = 20;
like查询,不能以通配符开头
select * from article where title like '%mysql%';
这种SQL的执行计划用不了索引(like语句匹配表达式以通配符开头),因此只能做全表扫描,效率极低,在实际工程中几乎不被采用。而一般会使用第三方提供的支持中文的全文索引来做。
复合索引只对第一个字段有效
alter table person add index(first_name,last_name);
select * from person where first_name = ?是可以利用索引的,而select * from person where last_name = ?无法利用索引。
那么该复合索引的应用场景是什么?==组合查询==
复合索引就比对first_name和last_name单独建立索引要高效些。
很好理解,复合索引首先二分查找与first_name = ?匹配的记录,再在这些记录中二分查找与last_name匹配的记录,只涉及到一张索引表。
而分别单独建立索引则是在first_name索引表中二分找出与first_name = ?匹配的记录,再在last_name索引表中二分找出与last_name = ?的记录,两者取交集。
or,两边条件都有索引可用
一但有一边无索引可用就会导致整个SQL语句的全表扫描
状态值,不容易使用到索引
索引是随机访问磁盘,而全表扫描是顺序访问磁盘,这就好比有一栋20层楼的写字楼,楼底下的索引牌上写着某个公司对应不相邻的几层楼,你去公司找人,与其按照索引牌的提示去其中一层楼没找到再下来看索引牌再上楼,不如从1楼挨个往上找到顶楼。
如何创建索引
建立基础索引:在where、order by、join字段上建立索引。 优化,组合索引:基于业务逻辑 如果条件经常性出现在一起,那么可以考虑将多字段索引升级为==复合索引== 如果通过增加个别字段的索引,就可以出现==索引覆盖==,那么可以考虑为该字段建立索引 查询时,不常用到的索引,应该删除掉
索引的存储结构
BTree
B+Tree聚簇结构
哈希索引
查询缓存
缓存select语句的查询结果
在配置文件中开启缓存
0:不开启 1:开启,默认缓存所有,需要在SQL语句中增加select sql-no-***提示来放弃缓存 2:开启,默认都不缓存,需要在SQL语句中增加select sql-***来主动缓存(==常用==)
重启后可通过show variables like ‘query_***_type’;来查看:
将查询结果缓存
select sql_*** * from user;
重置缓存
reset query ***;
缓存失效问题(大问题)
注意事项
应用程序,不应该关心query ***的使用情况。可以尝试使用,但不能由query ***决定业务逻辑,因为query ***由DBA来管理。 缓存是以SQL语句为key存储的,因此即使SQL语句功能相同,但如果多了一个空格或者大小写有差异都会导致匹配不到缓存。
分区
create table article(id int auto_increment PRIMARY KEY,title varchar(64),content text)PARTITION by HASH(id) PARTITIONS 10
==服务端的表分区对于客户端是透明的==,客户端还是照常插入数据,但服务端会按照分区算法分散存储数据。
MySQL提供的分区算法
==分区依据的字段必须是主键的一部分==,分区是为了快速定位数据,因此该字段的搜索频次较高应作为强检索字段,否则依照该字段分区毫无意义
hash(field)
key(field)
create table article_key(id int auto_increment,title varchar(64),content text,PRIMARY KEY (id,title) -- 要求分区依据字段必须是主键的一部分)PARTITION by KEY(title) PARTITIONS 10
range算法
create table article_range(id int auto_increment,title varchar(64),content text,created_time int, -- 发布时间到1970-1-1的毫秒数PRIMARY KEY (id,created_time) -- 要求分区依据字段必须是主键的一部分)charset=utf8PARTITION BY RANGE(created_time)(PARTITION p201808 VALUES less than (1535731199), -- select UNIX_TIMESTAMP('2018-8-31 23:59:59')PARTITION p201809 VALUES less than (1538323199), -- 2018-9-30 23:59:59PARTITION p201810 VALUES less than (1541001599) -- 2018-10-31 23:59:59);
注意:条件运算符只能使用==less than==,这以为着较小的范围要放在前面,比如上述p201808,p201819,p201810分区的定义顺序依照created_time数值范围从小到大,不能颠倒。
insert into article_range values(null,'MySQL优化','内容示例',1535731180);flush tables; -- 使操作立即刷新到磁盘文
由于插入的文章的发布时间1535731180小于1535731199(2018-8-31 23:59:59),因此被存储到p201808分区中,这种算法的存储到哪个分区取决于数据状况。
list算法
create table article_list(id int auto_increment,title varchar(64),content text,status TINYINT(1), -- 文章状态:0-草稿,1-完成但未发布,2-已发布PRIMARY KEY (id,status) -- 要求分区依据字段必须是主键的一部分)charset=utf8PARTITION BY list(status)(PARTITION writing values in(0,1), -- 未发布的放在一个分区PARTITION published values in (2) -- 已发布的放在一个分区);
分区管理语法
range/list
增加分区
alter table article_range add partition(partition p201811 values less than (1543593599) -- select UNIX_TIMESTAMP('2018-11-30 23:59:59')-- more);
删除分区
alter table article_range drop PARTITION p201808
key/hash
新增分区
alter table article_key add partition partitions 4
key/hash分区的管理不会删除数据,但是每一次调整(新增或销毁分区)都会将所有的数据重写分配到新的分区上。==效率极低==,最好在设计阶段就考虑好分区策略。
分区的使用
水平分割和垂直分割
水平分割:通过建立结构相同的几张表分别存储数据 垂直分割:将经常一起使用的字段放在一个单独的表中,分割后的表记录之间是一一对应关系。
分表原因
为数据库减压 分区算法局限 数据库支持不完善(5.1之后mysql才支持分区操作)
id重复的解决方案
借用第三方应用如mem***、redis的id自增器 单独建一张只包含id一个字段的表,每次自增该字段作为数据记录的id
集群
横向扩展:从根本上(单机的硬件处理能力有限)提升数据库性能 。由此而生的相关技术:==读写分离、负载均衡==
安装和配置主从复制
环境
centos6.5(虚拟机) mysql5.7(下载地址)
安装和配置可以参考我的其他文章
配置主从节点可以参考我的其他文章
读写分离
测试读写分离
如何测试读是从slave中读的呢?可以将写后复制到slave中的数据更改,再读该数据就知道是从slave中读了。==注意==,一但对slave做了写操作就要重新手动将slave与master同步一下,否则主从复制就会失效。
负载均衡
负载均衡算法
轮询 加权轮询:按照处理能力来加权 负载分配:依据当前的空闲状态(但是测试每个节点的内存使用率、CPU利用率等,再做比较选出最闲的那个,效率太低)
高可用
SQL
线上DDL
在线上执行DDL,在低于MySQL5.6版本时会导致全表被独占锁定,此时表处于维护、不可操作状态,这会导致该期间对该表的所有访问无法响应。但是在MySQL5.6之后,支持Online DDL,大大缩短了锁定时间。
思路:创建一个满足新结构的新表,将旧表数据==逐条==导入(复制)到新表中,以保证==一次性锁定的内容少==(锁定的是正在导入的数据),同时旧表上可以执行其他任务。
导入的过程中,将对旧表的所有操作以日志的形式记录下来,导入完毕后,将更新日志在新表上再执行一遍(确保一致性)。
最后,新表替换旧表(在应用程序中完成,或者是数据库的rename,视图完成)。
数据库导入语句
导入时==先禁用索引和约束==:
alter table table-name disable keys
alter table table-name enable keys
数据库如果使用的引擎是Innodb,那么它==默认会给每条写指令加上事务==(这也会消耗一定的时间),因此建议先手动开启事务,再执行一定量的批量导入,最后手动提交事务。 如果批量导入的SQL指令格式相同只是数据不同,那么你应该先prepare==预编译==一下,这样也能节省很多重复编译的时间。
limit offset,rows
这是一个==offset做无用功==的问题。对应实际工程中,要避免出现大页码的情况,尽量引导用户做条件过滤。
select * 要少用
只是我们在设计表的时候注意将大数据量的字段分离,比如商品详情可以单独抽离出一张商品详情表,这样在查看商品简略页面时的加载速度就不会有影响了。
order by rand()不要用
如select * from student order by rand() limit 5的执行效率就很低,因为它为表中的每条数据都生成随机数并进行排序,而我们只要前5条。
单表和多表查询
因此可以说单表查询将计算压力放在了应用程序上,而多表查询将计算压力放在了数据库上。
count(*)
limit 1
慢查询日志
用于记录执行时间超过某个临界值的SQL日志,用于快速定位慢查询,为我们的优化做参考。
开启慢查询日志
配置项:slow_query_log可以使用show variables like ‘slov_query_log’查看是否开启,如果状态值为OFF,可以使用set GLOBAL slow_query_log = on来开启,它会在datadir下产生一个xxx-slow.log的文件。设置临界时间配置项:long_query_time查看:show VARIABLES like 'long_query_time',单位秒设置:set long_query_time=0.
查看日志
profile信息
开启profile
mysql> show variables like 'profiling';+---------------+-------+| Variable_name | Value |+---------------+-------+| profiling | OFF |+---------------+-------+1 row in set, 1 warning (0.00 sec)mysql> set profiling=on;Query OK, 0 rows affected, 1 warning (0.00 sec)
查看profile信息
mysql> show variables like 'profiling';+---------------+-------+| Variable_name | Value |+---------------+-------+| profiling | ON |+---------------+-------+1 row in set, 1 warning (0.00 sec)mysql> insert into article values (null,'test profile',':)');Query OK, 1 row affected (0.15 sec)mysql> show profiles;+----------+------------+-------------------------------------------------------+| Query_ID | Duration | Query |+----------+------------+-------------------------------------------------------+| 1 | 0.00086150 | show variables like 'profiling' || 2 | 0.15027550 | insert into article values (null,'test profile',':)') |+----------+------------+-------------------------------------------------------+
通过Query_ID查看某条SQL所有详细步骤的时间
服务器配置
以下的配置全都取决于实际的运行环境
max_connections,最大客户端连接数 table_open_***,表文件句柄缓存(表数据是存储在磁盘上的,缓存磁盘文件的句柄方便打开文件读取数据) key_buffer_size,索引缓存大小(将从磁盘上读取的索引缓存到内存,可以设置大一些,有利于快速检索) innodb_buffer_pool_size,Innodb存储引擎缓存池大小(对于Innodb来说最重要的一个配置,如果所有的表用的都是Innodb,那么甚至建议将该值设置到物理内存的80%,Innodb的很多性能提升如索引都是依靠这个) innodb_file_per_table(innodb中,表数据存放在.ibd文件中,如果将该配置项设置为ON,那么一个表对应一个ibd文件,否则所有innodb共享表空间)
压测工具mysqlslap
自动生成sql测试
C:\Users\zaw>mysqlslap --auto-generate-sql -uroot -prootmysqlslap: [Warning] Using a password on the command line interface can be insecure.BenchmarkAverage number of seconds to run all queries: 1.219 secondsMinimum number of seconds to run all queries: 1.219 secondsMaximum number of seconds to run all queries: 1.219 secondsNumber of clients running queries: 1Average number of queries per client: 0
并发测试
C:\Users\zaw>mysqlslap --auto-generate-sql --concurrency=100 -uroot -prootmysqlslap: [Warning] Using a password on the command line interface can be insecure.BenchmarkAverage number of seconds to run all queries: 3.578 secondsMinimum number of seconds to run all queries: 3.578 secondsMaximum number of seconds to run all queries: 3.578 secondsNumber of clients running queries: 100Average number of queries per client: 0C:\Users\zaw>mysqlslap --auto-generate-sql --concurrency=150 -uroot -prootmysqlslap: [Warning] Using a password on the command line interface can be insecure.BenchmarkAverage number of seconds to run all queries: 5.718 secondsMinimum number of seconds to run all queries: 5.718 secondsMaximum number of seconds to run all queries: 5.718 secondsNumber of clients running queries: 150Average number of queries per client: 0
存储引擎测试
C:\Users\zaw>mysqlslap --auto-generate-sql --concurrency=150 --iterations=3 --engine=innodb -uroot -prootmysqlslap: [Warning] Using a password on the command line interface can be insecure.BenchmarkRunning for engine innodbAverage number of seconds to run all queries: 5.911 secondsMinimum number of seconds to run all queries: 5.485 secondsMaximum number of seconds to run all queries: 6.703 secondsNumber of clients running queries: 150Average number of queries per client: 0
C:\Users\zaw>mysqlslap --auto-generate-sql --concurrency=150 --iterations=3 --engine=myisam -uroot -prootmysqlslap: [Warning] Using a password on the command line interface can be insecure.BenchmarkRunning for engine myisamAverage number of seconds to run all queries: 53.104 secondsMinimum number of seconds to run all queries: 46.843 secondsMaximum number of seconds to run all queries: 60.781 secondsNumber of clients running queries: 150Average number of queries per client: 0
文章转载自数据与人,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




