千万级别的数据库优化
Posted daofaziran
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了千万级别的数据库优化相关的知识,希望对你有一定的参考价值。
前言
平时在写一些小web系统时,我们总会对mysql不以为然。然而真正的系统易用应该讲数据量展望拓展到千万级别来考虑。因此,今天下午实在是无聊的慌,自己随手搭建一个千万级的数据库,然后对数据库进行一些简单的CRUD来看看大数据情况下的CRUD效率。
结果发现,曾经简单的操作,在数据量大的时候还是会造成操作效率低下的。因此先写下这篇文章,日后不断更新纪录一下自己工作学习到的Mysql优化技巧。
搭建千万级数据库
首先,需要一个测试环境。一开始想到的是写一个SImple JDBC程序然进行简单的数据INSERT。结果发现单线程情况下,每次INSERT了一百多万条的时候效率就变得非常的低下。但是程序也没报OUT MEMORY之类的异常。初步判断应该是单一线程不断的疯狂创建PrepareStatement对象CG没来得及清理造成内存逐渐被吃紧的原因。
后来改进了一下,用多线程的机制。创建十个进程,每个负责一万条数据的插入。这效率一下子提升了好几倍。然而好景不长,很快的Java程序报错:OUT MEMORY。内存溢出了,CG没来得及清理。
这可把我给急的了。插入的太快内存CPU吃紧,插入的太慢又失去了创建测试环境“快”的初衷。后来想了下,既然是要批量插入数据,那么不是可以简单的写一段数据库存储过程吗?
于是,先建立一张测试表,就叫goods表吧。先写sql语句创建表:
-
CREATE TABLE goods (
-
id serial,
-
NAME VARCHAR (10),
-
price DOUBLE
-
) ENGINE = MYISAM DEFAULT CHARACTER
-
SET = utf8 COLLATE = utf8_general_ci AUTO_INCREMENT = 1 ROW_FORMAT = COMPACT;
接下来根据表结构写一段存储过程让数据库自行重复插入数据:
-
begin
-
declare i int default 0 ;
-
dd:loop
-
insert into goods values
-
(null,‘商品1‘,20),
-
(null,‘商品2‘,18),
-
(null,‘商品3‘,16),
-
(null,‘商品4‘,4),
-
(null,‘商品5‘,13),
-
(null,‘商品6‘,1),
-
(null,‘商品7‘,11),
-
(null,‘商品8‘,12),
-
(null,‘商品9‘,13),
-
(null,‘商品0‘,12);
-
commit;
-
set i = i+10 ;
-
if i = 10000000 then leave dd;
-
end if;
-
end loop dd ;
-
end
写完后运行一下,ok千万级别的数据库马上就插入进去了。
再谈数据库优化
既然有了数据现在开始进入数据库优化环节。
一、分页查询的优化
首先我们常常涉及到的CRUD操作莫过于分页操作。 对于普通的分页操作我们常常是这样子
select * from goods limit 100,1000;
这样当然没有任何的问题,但是当我们的数据量非常大,假如我要查看的是第八百万条数据呢?对应的sql语句为:
select * from goods limit 8000000,1000;
Mysql执行时间为 1.5秒左右。那么我们可以做一些什么优化吗?
上述的sql语句造成的效率低下原因不外乎:
大的分页偏移量会增加使用的数据,MySQL会将大量最终不会使用的数据加载到内存中。就算我们假设大部分网站的用户只访问前几页数据,但少量的大的分页偏移量的请求也会对整个系统造成危害
那么我要怎样来优化呢?如果我们的id为自增的。也就是说每一条记录的id为上一条id + 1那么,分页查找我们可以使用id进行范围查找替代传统的limit。
例如上述的sql语句可以代替为:
select * from goods where id > 8000000 limit 1000;
上述sql的到同样的执行结果,执行时间却只有0.04秒。提升了40倍左右。
结论:对应于自增id的表,如果数据量非常大的分页查找,可以观察id的分布规律计算出其id的范围通过范围查找来实现分页效果。
二、索引优化
谈到数据库效率,大部分人的第一想法应该就是建立索引。没错,正确的建立索引可以很好的提升效率。
关于索引,这是一个很大的话题我就不打算在这篇文章概括起来了。推荐一篇美团技术博客关于索引的文章。这篇文章很好的概述了索引的使用场景。主要要注意最左前缀匹配原则,并且将索引建立在区分度高的列。区分度的计算公式为:
count(distinct col)/count(*)
因此像我的模拟数据中即使建立了索引效率也提升不了多少,因为区分度非常的低。
总结一些索引会失效的情况,我们在实际的开发中应该尽量避免:
- like查询是以%开头,不会使用索引
- WHERE条件中有or,即使其中有条件带索引也不会使用
- !=,not in ,not exist不会使用索引
- WHERE字句的查询条件里使用了函数或计算
- 复合索引如果单独使用,只有复合索引里第一个字段有效
结论:索引很重要,也是一个大话题。推荐看看那篇美团技术博客的文章可以学习到很多。有些看似简单的函数操作如果放在SQL语句中却会导致索引失效,严重的影响效率,因此推荐将一些操作放到客户端中进行计算而不是SQL语句中。索引的使用情况可以使用EXPLAIN进行查看。
三、谈谈COUNT(*)
查询一张表有多少条记录常用语句为:
SELECT COUNT(*) FROM `goods`;
有些人认为这里使用了星号可能效率不如直接使用COUNT(COL)来的高,所以他们认为对于goods表(存在逻辑主键)更高效的语句应该是这样的:
SELECT COUNT(id) FROM `goods`;
但是其实两条执行的时间是一样的。因为COUNT(*)默认走最短的索引。由于id是这里最短的索引所以COUNT(*)等价于COUNT(id)。
结论:如果表中的最短索引很长,而且需要COUNT(*)操作,不放添加一个冗余的索引在一个比较短的列上,这样可以大大加大索引的速度。并且记住:COUNT(*)走的永远是最优的。
四、varchar 不是越大越好
有人认为varchar在的大小是按数据实际大小存储的,所以为了防止长度溢出就一开始就将长度定义的很长。但是事实是:
- VARCHAR在硬盘占用上确实是按实际大小占用
- 但如果涉及到临时表,是按后面的数字分配内存的
- 在VARCHAR列建立索引,ken_len也是按照后面数字分配的
结论:varchar按需取长,防止临时表占满内存溢出至磁盘导致速度下降。
五、联合查询与单表查询的选择
Mysql有很多联合查询的方式,诸如left join、inner join等等。
但是这些联合查询其实效率是很低的,现在考虑两张表一张为job表 数据量大约10万 + ,另外一张是job的分析表数据量较少,想通过job表中的job_name查询所有工作的工作分析情况。其中在两张表的列 job_name 均建立了索引。现在如果用联合查询:
-
SELECT * FROM `job` a LEFT JOIN `job_analysis` b ON a.job_name = b.job_name;
-
-- 运行时间: 0.93s
-
-- EXPLAIN 结果:
-
1 SIMPLE a ALL 169497
-
1 SIMPLE b ref job_name job_name 212 jobs.a.job_name 1
可以看到使用left join的查询只有一张表使用了索引,而另外一张表却要ALL去遍历。这对数据不是很大的时候还好,对数据量上百万 千万简直是噩梦。
诚然这种情况下使用单表多次效率并不能更高(至少一次ALL + 一次走索引)但数据量大还是要选择单表多次可能更优,因为单表多次查询有利于后面对数据的分库分表,且多次查询可以支持部分的缓存操作以及分为多次减少数据库锁的竞争。
摘自《高性能MYSQL》
事实上,用分解关联查询的方式重构查询有如下优势:
- 让缓存的效率更高。许多应用程序可以很方便的缓存单表查询对应的结果对象。另外对于MYSQL的查询缓存来说,如果关联中的某个表发生了变化,那么就无法使用查询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。
- 将查询分解后,执行单个查询就可以减少锁的竞争。
- 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。
- 查询本身效率也可能会有提升。
- 可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需查询一次,而在数据库中做关联查询,则可能需要重复地访问一部分数据(冗余数据引起)。从这点看,这样的重构还可能会减少网络和内存的消耗。
- 更进一步,这样做相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。某些场景哈希关联的效率要高的多。
结论:恰当的时候选择恰当的方法,遵循以下原则:
- 数据量小时,联合查询比较简便,10万记录以上不建议
- 数据量大时,单表多次查询好处多多。
以上是关于千万级别的数据库优化的主要内容,如果未能解决你的问题,请参考以下文章