特别说明: 1、 本文只是面对数据库应用开发的程序员,不适合专业DBA,DBA在数据库性能优化方面需要了解更多的知识; 2、 本文许多示例及概念是基于Oracle数据库描述,对于其它关系型数据库也可以参考,但许多观点不适合于KV数据库或内存数据库或者是基于SSD技术的数据库; 3、 本文未深入数据库优化中最核心的执行计划分析技术。 读者对像: 开发人员:如果你是做数据库开发,那本文的内容非常适合,因为本文是从程序员的角度来谈数据库性能优化。 架构师:如果你已经是数据库应用的架构师,那本文的知识你应该清楚90%,否则你可能是一个喜欢折腾的架构师。 DBA(数据库管理员):大型数据库优化的知识非常复杂,本文只是从程序员的角度来谈性能优化,DBA除了需要了解这些知识外,还需要深入数据库的内部体系架构来解决问题。 引言 在网上有很多文章介绍数据库优化知识,但是大部份文章只是对某个一个方面进行说明,而对于我们程序员来说这种介绍并不能很好的掌握优化知识,因为很多介绍只是对一些特定的场景优化的,所以反而有时会产生误导或让程序员感觉不明白其中的奥妙而对数据库优化感觉很神秘。 很多程序员总是问如何学习数据库优化,有没有好的教材之类的问题。在书店也看到了许多数据库优化的专业书籍,但是感觉更多是面向DBA或者是PL/SQL开发方面的知识,个人感觉不太适合普通程序员。而要想做到数据库优化的高手,不是花几周,几个月就能达到的,这并不是因为数据库优化有多高深,而是因为要做好优化一方面需要有非常好的技术功底,对操作系统、存储硬件网络、数据库原理等方面有比较扎实的基础知识,另一方面是需要花大量时间对特定的数据库进行实践测试与总结。 作为一个程序员,我们也许不清楚线上正式的服务器硬件配置,我们不可能像DBA那样专业的对数据库进行各种实践测试与总结,但我们都应该非常了解我们SQL的业务逻辑,我们清楚SQL中访问表及字段的数据情况,我们其实只关心我们的SQL是否能尽快返回结果。那程序员如何利用已知的知识进行数据库优化?如何能快速定位SQL性能问题并找到正确的优化方向? 面对这些问题,笔者总结了一些面向程序员的基本优化法则,本文将结合实例来坦述数据库开发的优化知识。 一、数据库访问优化法则简介要正确的优化SQL,我们需要快速定位能性的瓶颈点,也就是说快速找到我们SQL主要的开销在哪里?而大多数情况性能最慢的设备会是瓶颈点,如下载时网络速度可能会是瓶颈点,本地复制文件时硬盘可能会是瓶颈点,为什么这些一般的工作我们能快速确认瓶颈点呢,因为我们对这些慢速设备的性能数据有一些基本的认识,如网络带宽是2Mbps,硬盘是每分钟7200转等等。因此,为了快速找到SQL的性能瓶颈点,我们也需要了解我们计算机系统的硬件基本性能指标,下图展示的当前主流计算机性能指标数据。 从图上可以看到基本上每种设备都有两个指标: 延时(响应时间):表示硬件的突发处理能力; 带宽(吞吐量):代表硬件持续处理能力。 从上图可以看出,计算机系统硬件性能从高到代依次为: CPU——Cache(L1-L2-L3)——内存——SSD硬盘——网络——硬盘 由于SSD硬盘还处于快速发展阶段,所以本文的内容不涉及SSD相关应用系统。 根据数据库知识,我们可以列出每种硬件主要的工作内容: CPU及内存:缓存数据访问、比较、排序、事务检测、SQL解析、函数或逻辑运算; 网络:结果数据传输、SQL请求、远程数据库访问(dblink); 硬盘:数据访问、数据写入、日志记录、大数据量排序、大表连接。 根据当前计算机硬件的基本性能指标及其在数据库中主要操作内容,可以整理出如下图所示的性能基本优化法则: 这个优化法则归纳为5个层次: 1、 减少数据访问(减少磁盘访问) 2、 返回更少数据(减少网络传输或磁盘访问) 3、 减少交互次数(减少网络传输) 4、 减少服务器CPU开销(减少CPU及内存开销) 5、 利用更多资源(增加资源) 由于每一层优化法则都是解决其对应硬件的性能问题,所以带来的性能提升比例也不一样。传统数据库系统设计是也是尽可能对低速设备提供优化方法,因此针对低速设备问题的可优化手段也更多,优化成本也更低。我们任何一个SQL的性能优化都应该按这个规则由上到下来诊断问题并提出解决方案,而不应该首先想到的是增加资源解决问题。 以下是每个优化法则层级对应优化效果及成本经验参考: 优化法则 性能提升效果 优化成本 减少数据访问 1~1000 低 返回更少数据 1~100 低 减少交互次数 1~20 低 减少服务器CPU开销 1~5 低 利用更多资源 @~10 高 接下来,我们针对5种优化法则列举常用的优化手段并结合实例分析。 二、Oracle数据库两个基本概念数据块(Block) 数据块是数据库中数据在磁盘中存储的最小单位,也是一次IO访问的最小单位,一个数据块通常可以存储多条记录,数据块大小是DBA在创建数据库或表空间时指定,可指定为2K、4K、8K、16K或32K字节。下图是一个Oracle数据库典型的物理结构,一个数据库可以包括多个数据文件,一个数据文件内又包含多个数据块; ROWID ROWID是每条记录在数据库中的唯一标识,通过ROWID可以直接定位记录到对应的文件号及数据块位置。ROWID内容包括文件号、对像号、数据块号、记录槽号,如下图所示: 三、数据库访问优化法则详解 1、减少数据访问 1.1、创建并使用正确的索引数据库索引的原理非常简单,但在复杂的表中真正能正确使用索引的人很少,即使是专业的DBA也不一定能完全做到最优。 索引会大大增加表记录的DML(INSERT,UPDATE,DELETE)开销,正确的索引可以让性能提升100,1000倍以上,不合理的索引也可能会让性能下降100倍,因此在一个表中创建什么样的索引需要平衡各种业务需求。 索引常见问题: 索引有哪些种类? 常见的索引有B-TREE索引、位图索引、全文索引,位图索引一般用于数据仓库应用,全文索引由于使用较少,这里不深入介绍。B-TREE索引包括很多扩展类型,如组合索引、反向索引、函数索引等等,以下是B-TREE索引的简单介绍: B-TREE索引也称为平衡树索引(Balance Tree),它是一种按字段排好序的树形目录结构,主要用于提升查询性能和唯一约束支持。B-TREE索引的内容包括根节点、分支节点、叶子节点。 叶子节点内容:索引字段内容+表记录ROWID 根节点,分支节点内容:当一个数据块中不能放下所有索引字段数据时,就会形成树形的根节点或分支节点,根节点与分支节点保存了索引树的顺序及各层级间的引用关系。 一个普通的BTREE索引结构示意图如下所示: 如果我们把一个表的内容认为是一本字典,那索引就相当于字典的目录,如下图所示: 图中是一个字典按部首+笔划数的目录,相当于给字典建了一个按部首+笔划的组合索引。 一个表中可以建多个索引,就如一本字典可以建多个目录一样(按拼音、笔划、部首等等)。 一个索引也可以由多个字段组成,称为组合索引,如上图就是一个按部首+笔划的组合目录。 SQL什么条件会使用索引? 当字段上建有索引时,通常以下情况会使用索引: INDEX_COLUMN = ? INDEX_COLUMN > ? INDEX_COLUMN >= ? INDEX_COLUMN < ? INDEX_COLUMN <= ? INDEX_COLUMN between ? and ? INDEX_COLUMN in (?,?,...,?) INDEX_COLUMN like ?||'%'(后导模糊查询) T1. INDEX_COLUMN=T2. COLUMN1(两个表通过索引字段关联) SQL什么条件不会使用索引? 查询条件 不能使用索引原因 INDEX_COLUMN <> ? INDEX_COLUMN not in (?,?,...,?) 不等于操作不能使用索引 function(INDEX_COLUMN) = ? INDEX_COLUMN + 1 = ? INDEX_COLUMN || 'a' = ? 经过普通运算或函数运算后的索引字段不能使用索引 INDEX_COLUMN like '%'||? INDEX_COLUMN like '%'||?||'%' 含前导模糊查询的Like语法不能使用索引 INDEX_COLUMN is null B-TREE索引里不保存字段为NULL值记录,因此IS NULL不能使用索引 NUMBER_INDEX_COLUMN='12345' CHAR_INDEX_COLUMN=12345 Oracle在做数值比较时需要将两边的数据转换成同一种数据类型,如果两边数据类型不同时会对字段值隐式转换,相当于加了一层函数处理,所以不能使用索引。 a.INDEX_COLUMN=a.COLUMN_1 给索引查询的值应是已知数据,不能是未知字段值。 注: 经过函数运算字段的字段要使用可以使用函数索引,这种需求建议与DBA沟通。 有时候我们会使用多个字段的组合索引,如果查询条件中第一个字段不能使用索引,那整个查询也不能使用索引 如:我们company表建了一个id+name的组合索引,以下SQL是不能使用索引的 Select * from company where name=? Oracle9i后引入了一种index skip scan的索引方式来解决类似的问题,但是通过index skip scan提高性能的条件比较特殊,使用不好反而性能会更差。 我们一般在什么字段上建索引? 这是一个非常复杂的话题,需要对业务及数据充分分析后再能得出结果。主键及外键通常都要有索引,其它需要建索引的字段应满足以下条件: 1、字段出现在查询条件中,并且查询条件可以使用索引; 2、语句执行频率高,一天会有几千次以上; 3、通过字段条件可筛选的记录集很小,那数据筛选比例是多少才适合? 这个没有固定值,需要根据表数据量来评估,以下是经验公式,可用于快速评估: 小表(记录数小于10000行的表):筛选比例<10%; 大表:(筛选返回记录数)<(表总记录数*单条记录长度)/10000/16 单条记录长度≈字段平均内容长度之和+字段数*2 以下是一些字段是否需要建B-TREE索引的经验分类: 字段类型 常见字段名 需要建索引的字段 主键 ID,PK 外键 PRODUCT_ID,COMPANY_ID,MEMBER_ID,ORDER_ID,TRADE_ID,PAY_ID 有对像或身份标识意义字段 HASH_CODE,USERNAME,IDCARD_NO,EMAIL,TEL_NO,IM_NO 索引慎用字段,需要进行数据分布及使用场景详细评估 日期 GMT_CREATE,GMT_MODIFIED 年月 YEAR,MONTH 状态标志 PRODUCT_STATUS,ORDER_STATUS,IS_DELETE,VIP_FLAG 类型 ORDER_TYPE,IMAGE_TYPE,GENDER,CURRENCY_TYPE 区域 COUNTRY,PROVINCE,CITY 操作人员 CREATOR,AUDITOR 数值 LEVEL,AMOUNT,SCORE 长字符 ADDRESS,COMPANY_NAME,SUMMARY,SUBJECT 不适合建索引的字段 描述备注 DESCRIPTION,REMARK,MEMO,DETAIL 大字段 FILE_CONTENT,EMAIL_CONTENT 如何知道SQL是否使用了正确的索引? 简单SQL可以根据索引使用语法规则判断,复杂的SQL不好办,判断SQL的响应时间是一种策略,但是这会受到数据量、主机负载及缓存等因素的影响,有时数据全在缓存里,可能全表访问的时间比索引访问时间还少。要准确知道索引是否正确使用,需要到数据库中查看SQL真实的执行计划,这个话题比较复杂,详见SQL执行计划专题介绍。 索引对DML(INSERT,UPDATE,DELETE)附加的开销有多少? 这个没有固定的比例,与每个表记录的大小及索引字段大小密切相关,以下是一个普通表测试数据,仅供参考: 索引对于Insert性能降低56% 索引对于Update性能降低47% 索引对于Delete性能降低29% 因此对于写IO压力比较大的系统,表的索引需要仔细评估必要性,另外索引也会占用一定的存储空间。 1.2、只通过索引访问数据有些时候,我们只是访问表中的几个字段,并且字段内容较少,我们可以为这几个字段单独建立一个组合索引,这样就可以直接只通过访问索引就能得到数据,一般索引占用的磁盘空间比表小很多,所以这种方式可以大大减少磁盘IO开销。 如:select id,name from company where type='2'; 如果这个SQL经常使用,我们可以在type,id,name上创建组合索引 create index my_comb_index on company(type,id,name); 有了这个组合索引后,SQL就可以直接通过my_comb_index索引返回数据,不需要访问company表。 还是拿字典举例:有一个需求,需要查询一本汉语字典中所有汉字的个数,如果我们的字典没有目录索引,那我们只能从字典内容里一个一个字计数,最后返回结果。如果我们有一个拼音目录,那就可以只访问拼音目录的汉字进行计数。如果一本字典有1000页,拼音目录有20页,那我们的数据访问成本相当于全表访问的50分之一。 切记,性能优化是无止境的,当性能可以满足需求时即可,不要过度优化。在实际数据库中我们不可能把每个SQL请求的字段都建在索引里,所以这种只通过索引访问数据的方法一般只用于核心应用,也就是那种对核心表访问量最高且查询字段数据量很少的查询。 1.3、优化SQL执行计划 SQL执行计划是关系型数据库最核心的技术之一,它表示SQL执行时的数据访问算法。由于业务需求越来越复杂,表数据量也越来越大,程序员越来越懒惰,SQL也需要支持非常复杂的业务逻辑,但SQL的性能还需要提高,因此,优秀的关系型数据库除了需要支持复杂的SQL语法及更多函数外,还需要有一套优秀的算法库来提高SQL性能。 目前ORACLE有SQL执行计划的算法约300种,而且一直在增加,所以SQL执行计划是一个非常复杂的课题,一个普通DBA能掌握50种就很不错了,就算是资深DBA也不可能把每个执行计划的算法描述清楚。虽然有这么多种算法,但并不表示我们无法优化执行计划,因为我们常用的SQL执行计划算法也就十几个,如果一个程序员能把这十几个算法搞清楚,那就掌握了80%的SQL执行计划调优知识。 由于篇幅的原因,SQL执行计划需要专题介绍,在这里就不多说了。 2、返回更少的数据 2.1、数据分页处理一般数据分页方式有: 2.1.1、客户端(应用程序或浏览器)分页将数据从应用服务器全部下载到本地应用程序或浏览器,在应用程序或浏览器内部通过本地代码进行分页处理 优点:编码简单,减少客户端与应用服务器网络交互次数 缺点:首次交互时间长,占用客户端内存 适应场景:客户端与应用服务器网络延时较大,但要求后续操作流畅,如手机GPRS,超远程访问(跨国)等等。 2.1.2、应用服务器分页将数据从数据库服务器全部下载到应用服务器,在应用服务器内部再进行数据筛选。以下是一个应用服务器端Java程序分页的示例: List list=executeQuery(“select * from employee order by id”); Int count= list.size(); List subList= list.subList(10, 20); 优点:编码简单,只需要一次SQL交互,总数据与分页数据差不多时性能较好。 缺点:总数据量较多时性能较差。 适应场景:数据库系统不支持分页处理,数据量较小并且可控。 2.1.3、数据库SQL分页采用数据库SQL分页需要两次SQL完成 一个SQL计算总数量 一个SQL返回分页后的数据 优点:性能好 缺点:编码复杂,各种数据库语法不同,需要两次SQL交互。 oracle数据库一般采用rownum来进行分页,常用分页语法有如下两种: 直接通过rownum分页: select * from ( select a.*,rownum rn from (select * from product a where company_id=? order by status) a where rownum<=20) where rn>10; 数据访问开销=索引IO+索引全部记录结果对应的表数据IO 采用rowid分页语法 优化原理是通过纯索引找出分页记录的ROWID,再通过ROWID回表返回数据,要求内层查询和排序字段全在索引里。 create index myindex on product(company_id,status); select b.* from ( select * from ( select a.*,rownum rn from (select rowid rid,status from product a where company_id=? order by status) a where rownum<=20) where rn>10) a, product b where a.rid=b.rowid; 数据访问开销=索引IO+索引分页结果对应的表数据IO 实例: 一个公司产品有1000条记录,要分页取其中20个产品,假设访问公司索引需要50个IO,2条记录需要1个表数据IO。 那么按第一种ROWNUM分页写法,需要550(50+1000/2)个IO,按第二种ROWID分页写法,只需要60个IO(50+20/2); 2.2、只返回需要的字段通过去除不必要的返回字段可以提高性能,例: 调整前:select * from product where company_id=?; 调整后:select id,name from product where company_id=?; 优点: 1、减少数据在网络上传输开销 2、减少服务器数据处理开销 3、减少客户端内存占用 4、字段变更时提前发现问题,减少程序BUG 5、如果访问的所有字段刚好在一个索引里面,则可以使用纯索引访问提高性能。 缺点:增加编码工作量 由于会增加一些编码工作量,所以一般需求通过开发规范来要求程序员这么做,否则等项目上线后再整改工作量更大。 如果你的查询表中有大字段或内容较多的字段,如备注信息、文件内容等等,那在查询表时一定要注意这方面的问题,否则可能会带来严重的性能问题。如果表经常要查询并且请求大内容字段的概率很低,我们可以采用分表处理,将一个大表分拆成两个一对一的关系表,将不常用的大内容字段放在一张单独的表中。如一张存储上传文件的表: T_FILE(ID,FILE_NAME,FILE_SIZE,FILE_TYPE,FILE_CONTENT) 我们可以分拆成两张一对一的关系表: T_FILE(ID,FILE_NAME,FILE_SIZE,FILE_TYPE) T_FILECONTENT(ID, FILE_CONTENT) 通过这种分拆,可以大大提少T_FILE表的单条记录及总大小,这样在查询T_FILE时性能会更好,当需要查询FILE_CONTENT字段内容时再访问T_FILECONTENT表。 3、减少交互次数 3.1、batch DML 数据库访问框架一般都提供了批量提交的接口,jdbc支持batch的提交处理方法,当你一次性要往一个表中插入1000万条数据时,如果采用普通的executeUpdate处理,那么和服务器交互次数为1000万次,按每秒钟可以向数据库服务器提交10000次估算,要完成所有工作需要1000秒。如果采用批量提交模式,1000条提交一次,那么和服务器交互次数为1万次,交互次数大大减少。采用batch操作一般不会减少很多数据库服务器的物理IO,但是会大大减少客户端与服务端的交互次数,从而减少了多次发起的网络延时开销,同时也会降低数据库的CPU开销。 假设要向一个普通表插入1000万数据,每条记录大小为1K字节,表上没有任何索引,客户端与数据库服务器网络是100Mbps,以下是根据现在一般计算机能力估算的各种batch大小性能对比值: 单位:ms No batch Batch=10 Batch=100 Batch=1000 Batch=10000 服务器事务处理时间 0.1 0.1 0.1 0.1 0.1 服务器IO处理时间 0.02 0.2 2 20 200 网络交互发起时间 0.1 0.1 0.1 0.1 0.1 网络数据传输时间 0.01 0.1 1 10 100 小计 0.23 0.5 3.2 30.2 300.2 平均每条记录处理时间 0.23 0.05 0.032 0.0302 0.03002 从上可以看出,Insert操作加大Batch可以对性能提高近8倍性能,一般根据主键的Update或Delete操作也可能提高2-3倍性能,但不如Insert明显,因为Update及Delete操作可能有比较大的开销在物理IO访问。以上仅是理论计算值,实际情况需要根据具体环境测量。 3.2、In List 很多时候我们需要按一些ID查询数据库记录,我们可以采用一个ID一个请求发给数据库,如下所示: for :var in ids[] do begin select * from mytable where id=:var; end; 我们也可以做一个小的优化, 如下所示,用ID INLIST的这种方式写SQL: select * from mytable where id in(:id1,id2,...,idn); 通过这样处理可以大大减少SQL请求的数量,从而提高性能。那如果有10000个ID,那是不是全部放在一条SQL里处理呢?答案肯定是否定的。首先大部份数据库都会有SQL长度和IN里个数的限制,如ORACLE的IN里就不允许超过1000个值。 另外当前数据库一般都是采用基于成本的优化规则,当IN数量达到一定值时有可能改变SQL执行计划,从索引访问变成全表访问,这将使性能急剧变化。随着SQL中IN的里面的值个数增加,SQL的执行计划会更复杂,占用的内存将会变大,这将会增加服务器CPU及内存成本。 评估在IN里面一次放多少个值还需要考虑应用服务器本地内存的开销,有并发访问时要计算本地数据使用周期内的并发上限,否则可能会导致内存溢出。 综合考虑,一般IN里面的值个数超过20个以后性能基本没什么太大变化,也特别说明不要超过100,超过后可能会引起执行计划的不稳定性及增加数据库CPU及内存成本,这个需要专业DBA评估。 3.3、设置Fetch Size 当我们采用select从数据库查询数据时,数据默认并不是一条一条返回给客户端的,也不是一次全部返回客户端的,而是根据客户端fetch_size参数处理,每次只返回fetch_size条记录,当客户端游标遍历到尾部时再从服务端取数据,直到最后全部传送完成。所以如果我们要从服务端一次取大量数据时,可以加大fetch_size,这样可以减少结果数据传输的交互次数及服务器数据准备时间,提高性能。 以下是jdbc测试的代码,采用本地数据库,表缓存在数据库CACHE中,因此没有网络连接及磁盘IO开销,客户端只遍历游标,不做任何处理,这样更能体现fetch参数的影响: String vsql ="select * from t_employee"; PreparedStatement pstmt = conn.prepareStatement(vsql,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY); pstmt.setFetchSize(1000); ResultSet rs = pstmt.executeQuery(vsql); int cnt = rs.getMetaData().getColumnCount(); Object o; while (rs.next()) { for (int i = 1; i <= cnt; i++) { o = rs.getObject(i); } } 测试示例中的employee表有100000条记录,每条记录平均长度135字节 以下是测试结果,对每种fetchsize测试5次再取平均值: fetchsize elapse_time(s) 1 20.516 2 11.34 4 6.894 8 4.65 16 3.584 32 2.865 64 2.656 128 2.44 256 2.765 512 3.075 1024 2.862 2048 2.722 4096 2.681 8192 2.715 Oracle jdbc fetchsize默认值为10,由上测试可以看出fetchsize对性能影响还是比较大的,但是当fetchsize大于100时就基本上没有影响了。fetchsize并不会存在一个最优的固定值,因为整体性能与记录集大小及硬件平台有关。根据测试结果建议当一次性要取大量数据时这个值设置为100左右,不要小于40。注意,fetchsize不能设置太大,如果一次取出的数据大于JVM的内存会导致内存溢出,所以建议不要超过1000,太大了也没什么性能提高,反而可能会增加内存溢出的危险。 注:图中fetchsize在128以后会有一些小的波动,这并不是测试误差,而是由于resultset填充到具体对像时间不同的原因,由于resultset已经到本地内存里了,所以估计是由于CPU的L1,L2 Cache命中率变化造成,由于变化不大,所以笔者也未深入分析原因。 iBatis的SqlMapping配置文件可以对每个SQL语句指定fetchsize大小,如下所示:
面向程序员的数据库访问性能优化法则
最新推荐文章于 2024-10-16 11:14:53 发布