聊聊数据库优化

5,048 阅读11分钟

写在前面的话

数据库优化涉及到方方面面的知识,每种数据库的架构,优化方式也都有着很大的差异,如果想做好数据库优化要了解数据库的技术架构、存储结构、存储方式、缓存结构、SQL语句执行过程等有很深刻的了解。本文只是针对开发人员日常用到的通用的优化方法进行介绍,至于数据库参数调整等数据库相关内容也不再本文的讨论范围之内。目的是让开发人员对常用数据库优化有所了解。

数据库优化的二八法则

二八法则认为,在任何一组东西中,最重要的只占其中一小部分,约20%,其余80%尽管是多数,却是次要的,如:20%的富人拥有80%的财富。20%的权贵消耗了80%的资源,等等。数据库的优化也符合二八法则:

  • 80%的性能问题是由20%的应用导致的。如少量大表的全表扫描导致的性能瓶颈。并不是应用一有问题,都需要对系统进行重构,只需优化少部分存在性能问题的功能便可以使系统的性能大幅度提升。
  • 80%的性能问题可以由20%的优化技术所解决。如增加索引,执行计划分析等,能解决绝大部分性能问题

所以我们如果能够了解常见的数据库优化方法,可以解决开发中遇到的80%问题。

数据库优化该怎么做

在小品里面问大象放冰箱分几步?把冰箱门打开,把大象放进去,把冰箱门关上。

数据库优化分几步?找到具体影响数据库性能的原因,把问题解决了。

发现问题

误区: 我的SQL语句执行的很快,问题肯定不会出在我这里。

正解:SQL语句单次执行的快慢并不一定代表语句的好坏。有的语句虽然单次执行效率还比较高,但是随着数据量的增加,并发量的增加很可能成为性能瓶颈。

该怎么做?SQL写好了之后看一下这条SQL的执行计划,用事实说话。

那么什么是执行计划呢?

SQL是一种傻瓜式语言,每一个条件就是一个需求,访问的顺序不同就形成了不同的执行计划。数据库必须做出选择,一次只能有一种访问路径。执行计划是一条查询语句在数据库中的执行过程或访问路径的描述。

每种数据库都有工具可以查看SQL的执行计划。只有执行计划没问题才代表SQL没问题。如果你能看懂了执行计划。那么恭喜你,优化问题你已经解决了80%。因为发现问题往往比解决问题更困难。

解决问题

上面说到了如何发现问题,其实除了看执行计划还有其它手段,如查看单个SQL执行次数等等,但是,执行计划是开发人员最常用最直接的方式,能够帮助我们解决大部分问题。那么问题发现了之后如何解决呢?

简单的说优化有两个方向:能少做事尽量少做事,如果不能少做事尽量利用起服务器的性能。

如何提高服务器性能,可以利用并发的方式让服务器的资源尽量发挥到极致。有些数据库本身就提供了一些并发机制,如oracle可以通过增加hint来设置SQL的并发数,也可以通过应用程序的并发来尽量压榨出服务器的性能。当然还可以通过增加服务器,对数据库进行分库分表来提升性能。

另外一种方式是尽量少做事(敲黑板,划重点,这很关键)。

如何让数据库少做事呢?方式也有很多,最常见的是通过索引。曾经有一位在ORACLE从业20余年的专家说了这样一句话,“其实我只懂一点IT知识,IT知识里我只懂一点ORACLE,而ORACLE我也只懂一点数据库,数据库里面只懂点SQL,SQL里面只懂点索引”。可见索引对于数据库优化有多么重要。

聊聊索引

前面也提到索引对于数据库的优化特别重要,接下来聊聊创建和使用索引时的一些注意事项。索引创建的前缀性和可选择性,索引创建的几条建议,常见的索引被抑制情况。

索引的前缀性

先看以下例子假设在员工表(emp)的(ENAME, JOB, MGR)三个字段上建了一个索引,例如索引名叫IDX_1。三个字段分别为员工姓名、工作和所属经理号。然后,写如下一个查询语句,并不断进行查询条件和次序的排列组合,例如:

Select * from emp where ENAME=’a’ and JOB=’b’ and MGR=3;
Select * from emp where JOB=’b’ and MGR=3 and ENAME=’a’;
Select * from emp where JOB=’b’ and ENAME=’a’ and MGR=3;
Select * from emp where JOB=’b’ and MGR=3;
Select * from emp where ENAME=’a’ and MGR=3;
Select * from emp where ENAME=’a’;
Select * from emp where JOB=’b’;
Select * from emp where MGR=3;

在各种条件组合情况下,刚才建的索引(IDX_1)是用还是不用?也就是说对emp表的访问是全表扫描和还是按索引(IDX_1)访问?

答案是只要有ENAME=’a’条件,就能用上索引(IDX_1),而不是全表扫描。创建复合索引时一定要考虑到索引的前缀性否则会由于没有前缀列在检索条件中导致的全表扫描。

索引的前缀性指的是必须用到索引的第一个字段。

索引的可选择性

索引的可选择性,指的是不重复的索引值(基数)和表记录数的比值。可选择性是索引筛选能力的一个指标。当可选择性越大,索引价值也就越大。

如一张订单表order记录为10万条,表中user_id列的不重复值为10000,order_date列不重复值为1000,则创建在user_id上创建索引的查询效率要比在order_date上创建索引的查询效率高。这是因为,字段值越多,可选性越强,按照索引查询后需要定位的记录越少,查询效率越高。

几条创建索引的建议

数据库最常用的索引为B树索引(不同的数据库实现稍有不同,例如:oralce创建的是B*树,mysql是B+树),不同数据库可能还有自己特有的索引如:oracle的位图索引,mysql的hash索引等等。这里我们只讨论常用的B树索引。下面给出几条创建B树索引时设计单字段索引和复合索引的建议:

  1. 分析SQL语句中的约束条件字段,如果约束条件字段比较固定,则优先考虑创建针对多字段复合索引。例如同时涉及到多个字段的条件,则可以考虑建立一个复合索引。
  2. 如果单个字段是主键或唯一字段,或者可选性非常高的字段,尽管约束条件字段比较固定,也不一定要建成复合索引,可建成单字段索引,降低复合索引开销。
  3. 在复合索引设计中,需首先考虑复合索引第一个设计原则:复合索引的前缀性。即SQL语句中,只有复合索引的第一个字段作为约束条件,该复合索引才会启用。
  4. 在复合索引设计中,其次应考虑复合索引的可选性。即按可选性高低,进行复合索引字段的排序。
  5. 如果条件涉及的字段不固定,组合比较灵活,则分别为不同的列建立单字段索引。
  6. 如果是多表连接SQL语句,注意是否可以在被驱动表(drived table)的连接字段与该表的其它约束条件字段上,创建复合索引。
  7. 通过多种SQL分析工具,分析执行计划并以量化形式评估效果。

常见的索引被抑制情况

在了解了索引创建的规则后根据业务需要创建好了索引,但是通过看语句的执行计划发现仍然走的全表扫描,创建的索引没有被使用。那么索引不被使用都有可能因为什么原因导致的呢。

1.索引列上有表达式或者函数操作。则索引是失效的。

如有如下语句:

select user_name from user where age -30 = 0

select user_name from user where age  = 30

虽然age列上创建了索引,但是第一条语句依然是会按照全表扫描来执行的

2.存在隐式数据类型转换

如有如下语句:

select user_name from user where age  = ‘30’

但是user表定义的age列为number类型,在执行查询时发生了隐式类型转换。则索引被抑制。这种情况在开发过程中没有上一种情况明显。很容易被大家忽视。

3.数据可选性不高

例如user表有10万条数据,但是性别列只有男女两种,这种情况下,即使创建了索引,执行语句时也不会走索引。原因是根据索引查找出的结果集依然很大,查询效率还不如全表扫描的效率高,这是数据库就会执行全表扫描。

4.忽略的索引的前缀性

如上文所述,执行语句时,忽略了索引的前置性,则执行语句时是不会走索引的。

使用is null或者is not null,null值并没有被定义,所以索引会被抑制。

再聊一点

关于执行计划

前面也提到SQL的优化很大程度上要依赖执行计划,那么执行计划是如何生成出来的呢。简单的说,数据库会定期的收集数据库中每个表的数据量等基础信息。当一条SQL发送到数据库要执行之前,在完成合法性检查之后数据库会根据每张表的数据量,索引等信息通过计算给出一个SQL执行的最优计划。

关于绑定变量

上文提到SQL的执行计划需要通过各种信息计算得到,那么如果我把SQL的执行计划缓存起来。那么每次当同一个SQL执行多次的时候不就免去了每次计算的代价吗。实际上数据库也正是这样做的。数据库会根据每条SQL的hash值将执行计划缓存到内存。

为了提高缓存SQL的命中率,我们写SQL的时候更多的使用占位符。而不是直接传值。

如有如下SQL:

select user_name from user where user_id = 1
select user_name from user where user_id = 2

这时数据库会当做两条SQL来处理。因为缓存并没有命中。做如下修改:

select user_name from user where user_id = ?

每次通过绑定不同的参数则会命中缓存,减少了SQL的解析代价

那么是不是所有的变量都要以绑定变量的方式来传入呢?当然也不是。看下面的例子

如有订单表t_order(user_id, order_date,amount),其中user_id为用户ID,order_date为订单日期

分别创建索引 :

idx_order_1(user_id)
idx_order_2(order_date)

如果我希望查询的SQL为:

select user_id, order_date,amount
from t_order
where user_id >= :num1
and user_id <= :num2
and order_date >= :date1
and order_date <= :date2

这时当传入的参数num1和num2之间范围较小极限情况下num1=num2,也就是说我要查询某一个人一段时间的订单,则使用idx_order_2的效率会更高。而当传入的参数num1和num2之间范围较大但date1和date2的范围较小时。如查询某一天所有人的订单,则使用idx_order_1的效率会更高。这时如果我们使用绑定变量的方式。由于数据库分析执行计划时并不能知道参数的范围是什么。也就不能够给出最优的计划,这时就不建议使用绑定变量方式来传值。

本文也只是对数据库常见的优化方式 进行了讨论。要做好数据库的优化还要在日常开发中多多尝试。

作者介绍

李光明,民生科技有限公司,用户体验技术部Firefly移动金融开发平台Java开发工程师。