存储数据格式优化
时间相关的数据存放
存放时间戳:可以用字符串或者int类型存放,若使用int类型,效率更高,并且可以使用相关的函数转换格式
存放日期:效率低一些,但是看起来数据更加直观
存储IP
常规方式:通过varchar存放
优化方式:通过如mysql内置的inet_aton()
(address to number)将ip地址转成数字,然后用int unsigned方式(仅占4个字节)存放,取出时通过inet_ntoa()
将数字转回ip地址读取
枚举和集合
区别:前者只能选一个,后者能多选,但都很少用(维护成本较高,一旦要修改枚举内容时,需要锁整张表检查值的合法性),所以一般会使用关联表的方式替代,比如新建一张表,里面的记录就是所有枚举的情况
浮点数存储
对于金额这种精度要求较高的浮点数据,为了保证结果的准确,有时候会使用如精确到小数点后几位的定点数,或者使用大整数来替代,如100.12元,就以分为单位,变成:10012分,这时候使用整数来计算就没有精度的问题了(只有小数的计算和存储有精度问题,因为计算机里不能把小数完全地转换为二进制)
定长数据和变长数据选择
定长数据:存储空间固定,包括:int、float、double、char、date、time、datetime、year、timestamp
变长数据:存储空间可变,如:varchar、decimal、text
如定点数(精确到小数点后几位)和浮点数:定点数是变长数据,占用的空间会随着数字的增加而增加,不会导致精度的丢失;浮点数是定长数据,因为占用控件固定,所以会导致精度的丢失
尽量不要允许null
理由:
- 获取数据时无须判断是否为空
- 不允许为空的字段处理效率更高
- null的存储需要占用额外的空间,运算也要特殊的运算符
- null能够参数常规运算,结果就是null
优化查询
慢查询日志
慢查询日志记录了查询过程当中影响查询速度的因素,可通过下面的命令查看是否开启:
show variables like '%slow%';
输入命令后可以看到slow_query_log
参数,如果值为ON
,则说明开启;如果没开启,可以通过下面命令开启:
set global slow_query_log=on;
然后需要设置慢的标准,此时输入命令:
show variables like 'long%';
可以看到long_query_time
参数,对应的值即为慢的标准,此时可以通过下面命令来修改慢的时间标准:
set long_query_time=时间;
此时如果有语句执行时间超过该标准,就会被记录下来,从而知道哪些语句效率低下,需要优化了
执行计划
当知道是哪个语句需要优化时,可以在这个语句前面加上:explain
,查看执行计划,举例:
mysql> explain select password from peoples where id=1\G
mysql> explain select password from peoples where username='aaa'\G
可以看到两个的查询过程情况如下:
#语句1情况
id: 1
select_type: SIMPLE
table: peoples
partitions: NULL
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: const
rows: 1
filtered: 100.00
Extra: NULL id: 1
########################
#语句2情况
select_type: SIMPLE
table: peoples
partitions: NULL
type: ALL
possible_keys: username
key: NULL
key_len: NULL
ref: NULL
rows: 16
filtered: 10.00
Extra: Using where
这里重要查看的数据就是possible_keys
(查询的类型) 、key
(索引)、key_len
(索引长度)、rows
(查询行数),可以看到左边的数据查询是主键索引,且查询行数只有1;而右边的没有索引,并且查询行数为16,可以看出这里性能就有很大的差异了。
注:
查询计划并不会真正执行语句,只是告诉我们语句将如何执行
性能查询
可以通过profile信息查看语句性能,其记录了前面执行过的语句以及对应执行时间,可以通过输入下面命令来查看是否开启:
show variables like '%profiling%';
值为ON
说明为打开;如果没打开,可以通过下面命令打开:
set profiling=on;
此时执行sql语句后,输入命令:
show profiles;
会显示前面输入的所有sql语句的执行时间和对应语句id,然后可以输入下面命令来查看某个语句内部执行过程的每个部分所花的时间:
show profile for query 语句ID
此时就可以找到耗时间的部分,然后去寻找优化的方法
优化数据表
建立索引
数据库查询优化最常见的方法就是建立索引,有无索引直接关系到查询的行数等,比如上面的语句查询中遇到的情况就是没有索引而导致的性能低下,那么上面那个情况的解决方案就是给username
这列加个索引,举例:
alter table peoples add index(username);
此时用explain查看如下(关键部分):
possible_keys: username_2,username
key: username_2
key_len: 137
rows: 6
可以发现查询的行数少了很多,通过命令:show profiles
,也可以发现在耗时上也有所减少。
确定类型
数据类型如果设置不当,或者语句的类型没有对应,也会影响到性能,比如设置一个password
的类型为varchar
,那么查询语句中:
select * from peoples where password=333
select * from peoples where password='333'
这两句的效率也是有所区别,因为数据类型并非int型,所以第一条的333还需要转成varchar型,这中间就会耗费部分时间,所以数据类型的设置也很重要,从时间上也能看出两个之间的区别:
Query_ID | Duration| Query
16 | 0.00893175 | select * from peoples where password=333
17 | 0.00027225 | select * from peoples where password='333'
优化语句
批量插入语句优化
对于大批量的数据插入,如果一条一条的执行,势必消耗不少时间,因此一种建议是合并在一条内执行,例如:
insert into xxx values(xx, xx, xx);
...
insert into xxx values(yy, yy, yy);
可以改成如下的一条语句:
insert into xxx values(xx, xx, xx), ..., (yy, yy, yy);
或者基于事务来进行插入操作:
start transaction;
insert into xxx values(xx, xx, xx);
...
insert into xxx values(yy, yy, yy);
commit;
更多大批量插入数据优化参考:https://www.cnblogs.com/myseries/p/11191134.html
限制内容
查询语句中一般有很大一部分时间在传输数据,所以尽量少使用select * ...
,还有就是可以用limit
关键字来截取需要的数据,同样也能够提高性能。
避免运算
尽量不要在sql语句中做运算,这样也会降低性能,比如下面的两个语句:
| 1 | 0.00067375 | select * from peoples where id=1
| 2 | 0.00884850 | select * from peoples where id+1=2
可以看出第二条语句明显耗时高于第一条,所以当需要运算时,先将结果运算好,然后在传入sql语句执行。
还有就是尽量不要用>
/<
/!=
这类的运算符,也会降低效率
少用关键字
像count()
函数、like
关键字(特别是左边加%
时)这些在sql语句中也会影响性能,所以尽量少用,可以寻找能替代的方式,比如要计算行数有多少可能会用:count(*)
,来实现,但是如果在一个id递增的表里,且数据完整的情况下,就可以使用:
select id from ... group by id desc limit 1;
#根据id排序,倒序查询出数据的最后一条
此时就能够使效率大大提高
缓存优化
开启缓存后能够将查询的内容进行缓存,从而在下次执行相同内容时大大提高效率,但其本身也会消耗部分效率,尤其是不同语句第一次执行的时候,所以可以根据需求开启/关闭缓存
缓存设置
输入下面命令查看缓存开启状态和缓存大小:
show variables like '%cache%';
此时可以看到两个参数:query_cache_size
、query_cache_type
分别代表缓存大小和状态,如果缓存关闭了,需要配置my.ini
文件:将query_cache_type
的值设置为1
(0代表关闭;1代表开启,且默认对所有缓存,如果想要不缓存,需要增加sql-no-cache
提示;2代表开启,且默认都不缓存,如果要缓存,需要增加sql-cache
提示),然后重启mysql服务即可。设置缓存大小可通过下面命令设置:
set global query_cache_size=缓存大小;
此时,连续执行两次同样的语句可以发现效率上的差距:
| Query_ID | Duration | Query |
| 1 | 0.01313100 | select * from food |
| 2 | 0.00014100 | select * from food |
因为开启缓存只是对于重复的操作能够相对提高效率,其本身也有部分消耗,所以当不需要开启时可通过以下命令关闭缓存:
set query_cache_type=0;
注:
由于缓存开启模式当中设置1和2开启的机制不同,此时使用sql-no-cache
/sql-cache
的示例如下:
select sql-no-cache * from xxx ...
select sql-cache * from xxx ...
缓存命中率
缓存并非每次都能刚好用到,其存在命中率,查看缓存命中率:
show status like '%qcache%';
其中qcache_hits
参数对应的值就是命中率
重置缓存
使用下面命令可以重置缓存:
reset query cache;
缓存使用注意
- 当数据表结构修改时,基于该表的缓存会全部删除
- 动态的数据无法缓存
- 缓存的语句必须一样,一旦大小写变化、多或少一个空格都会导致缓存不匹配(在mysql缓存机制里通过语句作为key存放,因此语句只要有一点改变,就会导致缓存没起到作用)
分区优化
指将一张表中的数据和索引分散到不同文件中进行存储
通常一个表对应一组数据和索引文件,表的数据和索引几种存在这组文件当中。当一个表中出现了大量记录时,可以将其分到不同的数据和索引文件当中进行存储,从而使得每个文件包含的记录数量减少,并保证单个文件的执行效率
innodb中一个表对应多个.ibd
文件;myisam中一个表对应多个.myi
和.myd
文件
设置分区
在设计表时,可以通过partition
配置分区,并提供分区算法、参数等,举例:
create table xxx (
...
)
partition by hash(id) partitions 10;
# 基于id字段,使用hash算法将数据分到10个分区里
分区后对于客户端来说还是一张表,但在服务器端实际上是将这张表中的数据分散到不同分区内进行存储(分区后可以在表文件夹下看到表被分成了多个文件,其中.ibd
存储数据和索引、.frm
存储结构、.par
存储分区结构),此时可以进行插入数据测试,会发现数据根据插入的id最终被存入不同的分区文件当中
四种分区算法
哈希算法
采用整数求余运算来获取hash值,是最常用的分区算法,逻辑上将记录均匀分布到不同的分区的,适合数据量大且没有明显逻辑区分时使用
key算法
相比于hash算法,允许使用非整型字段求hash值,适合字符型字段,要注意的是分区字段必须为主键的一部分,举例:
create table xxx (
...
primary key (id, name) # 分区字段必须为主键一部分
)
partition by key(name) partitions 10;
range算法
范围条件算法,基于<
/less than
运算符来进行条件分区,举例:
create table xxx (
...
)
partition by range(cost) (
# 根据消费额度进行分区
partition part1 values less than (100),
partition part2 values less than (1000),
partition part3 values less than (10000)
);
list算法
也是一种条件算法,基于in
来进行条件分区,举例:
create table xxx (
...
)
partition by list(sex) (
# 根据性别进行分区
partition man values in (0),
partition woman values in (1)
);
分区管理
- list、range算法可以进行添加、删除分区操作(删除分区后,对应分区的数据也会被删除),举例:
alter table xxx add partition (
partition part4 values ...,
...
);
# 添加分区
alter table xxx drop partition part1;
# 删除分区
- hash、key算法可以进行修改分区数量操作:
alter table xxx add partition partitions 2;
# 添加2个分区
alter table xxx coalesce partition 2;
# 删除2个分区
- 由于hash、key算法的分区管理时需要重新给数据分配分区,因此效率较低,建议一开始就设计好分区数量,不要随意改动
分区总结
- 当客户端程序不变时,可以在服务端进行分区优化
- 只有检索字段为分区字段时,效果才比较明显,因此分区字段的选择很重要
- 通过分区将数据文件分到不同的磁盘上,从而充分利用磁盘的性能
分表优化
分表常指在应用层上将数据划分到不同表中进行存储(相比之下,分区是在服务器层完成的),通过分表,可能会使得客户端出现明显改变,而服务器端出现结构一样的多张表
水平分表
指分出多个结构相同但表名不同的表,例如交易记录存储时创建多个结构相同的表,而表名基于时间命名:
create table trade_2001_01 (
...
);
create table trade_2001_02 (
...
);
...
其中为了避免多个表中的id冲突(虽然存在多个表,但每个交易记录的id还是应该唯一),可以设计一个表专门存储id,每次先往id表存数据,然后再到指定表进行操作
垂直分表
每个表的记录数量一致,但是字段不一致,例如用户信息数据拆分到常用信息表和隐私信息表当中进行存储
分表优势
- 数据库减压:同类数据根据时间等不同进行分布式存储
- 分区算法有一定的局限性,需要分表算法来弥补
- 分区算法在5.1以后才支持,这之前可以用分表代替
压力测试
可以使用内置的工具mysqlslap,在Mysql的bin目录下,可以直接通过下面命令测试:
mysqlslap --auto-generate-sql -u用户 -p密码
# 自动生成sql语句执行
mysqlslap --auto-generate-sql --concurrency=200 -u用户 -p密码
# 设置并发数量为200
mysqlslap --auto-generate-sql --concurrency=200 --iterations=10 -u用户 -p密码
# 设置多轮测试
mysqlslap --auto-generate-sql --concurrency=200 --iterations=10 --engine=innodb -u用户 -p密码
# 测试innodb引擎
mysqlslap --auto-generate-sql --concurrency=200 --iterations=10 --engine=myisam -u用户 -p密码
# 测试myisam引擎
然后查看执行语句的耗时,以及一些警告或者是否有请求失败等情况
sql语句注意
更新表操作注意
更新表结构时会导致全表独占锁定,此时表处于不可操作的状态,在线上服务器上执行时容易导致服务阻塞,因此建议的流程方式有以下几种:
1.创建新表(满足新的结构的表),并将旧表数据逐条导入,若导入过程中原表里存在更新操作,则以日志形式记录,当导入完成后,再更新日志,在新表重新执行一遍,这样就能保证修改表结构的同时能够执行其他任务
2.直接新表替换旧表,或者替换成修改结构后的数据库
导入数据时注意
为了提高导入数据时的效率,有以下优化方式:
- 在导入时先禁用索引和约束,当导入完毕后再开启索引和约束,命令如下:
alter table xxx disable keys;
alter table xxx enable keys;
- 如果使用支持事务的引擎,可以将多条导入放在事务当中完成
- innodb是基于主键排序的,此时如果导入的数据本身按主键排序,那么速度也会相对较快
- 可以使用prepare预编译导入操作,从而减少相同结构的sql编译次数
大页码查询注意
对于大页码查询时,尽量使用条件过滤,而不是通过offset
跳过已查询到的数据,例如下面的示例就是不好的:
select * from xxx limit 100000, 10;
此时优化可以参考:https://www.jb51.net/article/127728.htm
select * 少用
尽量只获取需要的字段
不要用随机排序
前面不要使用order by rand()
这样的随机排序
尽量使用单表查询,避免多表查询
对于多表查询,如join
、子查询也都是去执行一个查询,然后再进行结果合并,并且多表查询会增加表的锁定时间,导致程序的并发性能降低
limit 1问题
如果确定检索的结果只要一个,建议加上limit 1
总结
- 减少不必要数据的提取
- 应用层缓存(如redis缓存配置)
- 添加索引、查询缓存、分表、分区操作
- 多台服务器进行读写分离、负载均衡