首页 面向程序员的数据库访问性能优化法则

面向程序员的数据库访问性能优化法则

举报
开通vip

面向程序员的数据库访问性能优化法则 面向程序员的数据库访问性能优化法则 特别说明: 1、 本文只是面对数据库应用开发的程序员,不适合专业 DBA,DBA 在数据库 性能优化方面需要了解更多的知识; 2、 本文许多示例及概念是基于 Oracle 数据库描述,对于其它关系型数据库也 可以参考,但许多观点不适合于 KV 数据库或内存数据库或者是基于 SSD 技 术的数据库; 3、 本文未深入数据库优化中最核心的执行计划分析技术。 读者对像: 开发人员:如果你是做数据库开发,那本文的内容非常适合,因为本文是从程 序员...

面向程序员的数据库访问性能优化法则
面向程序员的数据库访问性能优化法则 特别说明: 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 解析、 函数 excel方差函数excelsd函数已知函数     2 f x m x mx m      2 1 4 2拉格朗日函数pdf函数公式下载 或逻 辑运算; 网络:结果数据传输、SQL 请求、远程数据库访问(dblink); 硬盘:数据访问、数据写入、日志记录、大数据量排序、大表连接。 根据当前计算机硬件的基本性能指标及其在数据库中主要操作内容,可以 整理出如下图所示的性能基本优化法则: 这个优化法则归纳为 5 个层次: 1、 减少数据访问(减少磁盘访问) 2、 返回更少数据(减少网络传输或磁盘访问) 3、 减少交互次数(减少网络传输) 4、 减少服务器 CPU 开销(减少 CPU 及内存开销) 5、 利用更多资源(增加资源) 由于每一层优化法则都是解决其对应硬件的性能问题,所以带来的性能提升 比例也不一样。传统数据库系统 设计 领导形象设计圆作业设计ao工艺污水处理厂设计附属工程施工组织设计清扫机器人结构设计 是也是尽可能对低速设备提供优化方法, 因此针对低速设备问题的可优化手段也更多,优化成本也更低。我们任何一个 SQL 的性能优化都应该按这个规则由上到下来诊断问题并提出解决 方案 气瓶 现场处置方案 .pdf气瓶 现场处置方案 .doc见习基地管理方案.doc关于群访事件的化解方案建筑工地扬尘治理专项方案下载 ,而不 应该首先想到的是增加资源解决问题。 以下是每个优化法则层级对应优化效果及成本经验参考: 优化法则 性能提升效果 优化成本 减少数据访问 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、如果访问的所有字段刚好在一个索引里面,则可以使用纯索引访问提高性 能。 缺点:增加编码工作量 由于会增加一些编码工作量,所以一般需求通过开发 规范 编程规范下载gsp规范下载钢格栅规范下载警徽规范下载建设厅规范下载 来要求程序员这 么做,否则等项目上线后再整改工作量更大。 如果你的查询表中有大字段或内容较多的字段,如备注信息、文件内容等 等,那在查询表时一定要注意这方面的问题,否则可能会带来严重的性能问题。 如果表经常要查询并且请求大内容字段的概率很低,我们可以采用分表处理, 将一个大表分拆成两个一对一的关系表,将不常用的大内容字段放在一张单独 的表中。如一张存储上传文件的表: 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=10 0 Batch=100 0 Batch=1000 0 服务器事务处理时间 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_R EAD_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 大小,如 下所示: 3.4、使用存储过程 大型数据库一般都支持存储过程,合理的利用存储过程也可以提高系统性 能。如你有一个业务需要将 A 表的数据做一些加工然后更新到 B 表中,但是又 不可能一条 SQL 完成,这时你需要如下 3 步操作: a:将 A 表数据全部取出到客户端; b:计算出要更新的数据; c:将计算结果更新到 B 表。 如果采用存储过程你可以将整个业务逻辑封装在存储过程里,然后在客户 端直接调用存储过程处理,这样可以减少网络交互的成本。 当然,存储过程也并不是十全十美,存储过程有以下缺点: a、不可移植性,每种数据库的内部编程语法都不太相同,当你的系统需要 兼容多种数据库时最好不要用存储过程。 b、学习成本高,DBA 一般都擅长写存储过程,但并不是每个程序员都能写 好存储过程,除非你的团队有较多的开发人员熟悉写存储过程,否则后期系统 维护会产生问题。 c、业务逻辑多处存在,采用存储过程后也就意味着你的系统有一些业务逻 辑不是在应用程序里处理,这种架构会增加一些系统维护和调试成本。 d、存储过程和常用应用程序语言不一样,它支持的函数及语法有可能不能 满足需求,有些逻辑就只能通过应用程序处理。 e、如果存储过程中有复杂运算的话,会增加一些数据库服务端的处理成本, 对于集中式数据库可能会导致系统可扩展性问题。 f、为了提高性能,数据库会把存储过程代码编译成中间运行代码(类似于 java 的 class 文件),所以更像静态语言。当存储过程引用的对像(表、视图等等) 结构改变后,存储过程需要重新编译才能生效,在 24*7 高并发应用场景,一般 都是在线变更结构的,所以在变更的瞬间要同时编译存储过程,这可能会导致 数据库瞬间压力上升引起故障(Oracle 数据库就存在这样的问题)。 个人观点:普通业务逻辑尽量不要使用存储过程,定时性的 ETL 任务或报 表统计函数可以根据团队资源情况采用存储过程处理。 3.5、优化业务逻辑 要通过优化业务逻辑来提高性能是比较困难的,这需要程序员对所访问的 数据及业务流程非常清楚。 举一个案例: 某移动公司推出优惠套参,活动对像为 VIP 会员并且 2010 年 1,2,3 月平 均话费 20 元以上的客户。 那我们的检测逻辑为: select avg(money) as avg_money from bill where phone_no='13988888888' and date between '201001' and '201003'; select vip_flag from member where phone_no='13988888888'; if avg_money>20 and vip_flag=true then begin 执行套参(); end; 如果我们修改业务逻辑为: select avg(money) as avg_money from bill where phone_no='13988888888' and date between '201001' and '201003'; if avg_money>20 then begin select vip_flag from member where phone_no='13988888888'; if vip_flag=true then begin 执行套参(); end; end; 通过这样可以减少一些判断 vip_flag 的开销,平均话费 20 元以下的用户就 不需要再检测是否 VIP 了。 如果程序员分析业务,VIP 会员比例为 1%,平均话费 20 元以上的用户比例 为 90%,那我们改成如下: select vip_flag from member where phone_no='13988888888'; if vip_flag=true then begin select avg(money) as avg_money from bill where phone_no='13988888888' and date between '201001' and '201003'; if avg_money>20 then begin 执行套参(); end; end; 这样就只有 1%的 VIP 会员才会做检测平均话费,最终大大减少了 SQL 的交 互次数。 以上只是一个简单的示例,实际的业务总是比这复杂得多,所以一般只是 高级程序员更容易做出优化的逻辑,但是我们需要有这样一种成本优化的意识。 3.6、使用 ResultSet 游标处理记录 现在大部分 Java 框架都是通过 jdbc 从数据库取出数据,然后装载到一个 list 里再处理,list 里可能是业务 Object,也可能是 hashmap。 由于 JVM 内存一般都小于 4G,所以不可能一次通过 sql 把大量数据装载到 list 里。为了完成功能,很多程序员喜欢采用分页的方法处理,如一次从数据库 取 1000 条记录,通过多次循环搞定,保证不会引起 JVM Out of memory 问题。 以下是实现此功能的代码示例,t_employee 表有 10 万条记录,设置分页大小 为 1000: d1 = Calendar.getInstance().getTime(); vsql = "select count(*) cnt from t_employee"; pstmt = conn.prepareStatement(vsql); ResultSet rs = pstmt.executeQuery(); Integer cnt = 0; while (rs.next()) { cnt = rs.getInt("cnt"); } Integer lastid=0; Integer pagesize=1000; System.out.println("cnt:" + cnt); String vsql = "select count(*) cnt from t_employee"; PreparedStatement pstmt = conn.prepareStatement(vsql); ResultSet rs = pstmt.executeQuery(); Integer cnt = 0; while (rs.next()) { cnt = rs.getInt("cnt"); } Integer lastid = 0; Integer pagesize = 1000; System.out.println("cnt:" + cnt); for (int i = 0; i <= cnt / pagesize; i++) { vsql = "select * from (select * from t_employee where id>? order by id) where rownum<=?"; pstmt = conn.prepareStatement(vsql); pstmt.setFetchSize(1000); pstmt.setInt(1, lastid); pstmt.setInt(2, pagesize); rs = pstmt.executeQuery(); int col_cnt = rs.getMetaData().getColumnCount(); Object o; while (rs.next()) { for (int j = 1; j <= col_cnt; j++) { o = rs.getObject(j); } lastid = rs.getInt("id"); } rs.close(); pstmt.close(); } 以上代码实际执行时间为 6.516 秒 很多持久层框架为了尽量让程序员使用方便,封装了 jdbc 通过 statement 执行数据返回到 resultset 的细节,导致程序员会想采用分页的方式处理问题。 实际上如果我们采用 jdbc 原始的 resultset 游标处理记录,在 resultset 循环读取 的过程中处理记录,这样就可以一次从数据库取出所有记录。显著提高性能。 这里需要注意的是,采用 resultset 游标处理记录时,应该将游标的打开方 式设置为 FORWARD_READONLY 模式 (ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY),否则会把结果 缓存在 JVM 里,造成 JVM Out of memory 问题。 代码示例: String vsql ="select * from t_employee"; PreparedStatement pstmt = conn.prepareStatement(vsql,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_R EAD_ONLY); pstmt.setFetchSize(100); ResultSet rs = pstmt.executeQuery(vsql); int col_cnt = rs.getMetaData().getColumnCount(); Object o; while (rs.next()) { for (int j = 1; j <= col_cnt; j++) { o = rs.getObject(j); } } 调整后的代码实际执行时间为 3.156 秒 从测试结果可以看出性能提高了 1 倍多,如果采用分页模式数据库每次还 需发生磁盘 IO 的话那性能可以提高更多。 iBatis 等持久层框架考虑到会有这种需求,所以也有相应的解决方案,在 iBatis 里我们不能采用 queryForList 的方法,而应用该采用 queryWithRowHandler 加回调事件的方式处理,如下所示: MyRowHandler myrh=new MyRowHandler(); sqlmap.queryWithRowHandler("getAllEmployee", myrh); class MyRowHandler implements RowHandler { public void handleRow(Object o) { //todo something } } iBatis 的 queryWithRowHandler 很好的封装了 resultset 遍历的事件处理,效 果及性能与 resultset 遍历一样,也不会产生 JVM 内存溢出。 4、减少数据库服务器 CPU运算 4.1、使用绑定变量 绑定变量是指 SQL 中对变化的值采用变量参数的形式提交,而不是在 SQL 中直接拼写对应的值。 非绑定变量写法:Select * from employee where id=1234567 绑定变量写法: Select * from employee where id=? Preparestatement.setInt(1,1234567) Java 中 Preparestatement 就是为处理绑定变量提供的对像,绑定变量有以 下优点: 1、防止 SQL 注入 2、提高 SQL 可读性 3、提高 SQL 解析性能,不使用绑定变更我们一般称为硬解析,使用绑定变量我 们称为软解析。 第 1 和第 2 点很好理解,做编码的人应该都清楚,这里不详细说明。关于 第 3 点,到底能提高多少性能呢,下面举一个例子说明: 假设有这个这样的一个数据库主机: 2 个 4 核 CPU 100 块磁盘,每个磁盘支持 IOPS 为 160 业务应用的 SQL 如下: select * from table where pk=? 这个 SQL 平均 4 个 IO(3 个索引 IO+1 个数据 IO) IO 缓存命中率 75%(索引全在内存中,数据需要访问磁盘) SQL 硬解析 CPU 消耗:1ms (常用经验值) SQL 软解析 CPU 消耗:0.02ms(常用经验值) 假设 CPU 每核性能是线性增长,访问内存 Cache 中的 IO 时间忽略,要求计 算系统对如上应用采用硬解析与采用软解析支持的每秒最大并发数: 是否使用绑定变 量 CPU 支持最大并发数 磁盘 IO 支持最大并发数 不使用 2*4*1000=8000 100*160=16000 使用 2*4*1000/0.02=400000 100*160=16000 从以上计算可以看出,不使用绑定变量的系统当并发达到 8000 时会在 CPU 上产生瓶颈,当使用绑定变量的系统当并行达到 16000 时会在磁盘 IO 上产生瓶 颈。所以如果你的系统 CPU 有瓶颈时请先检查是否存在大量的硬解析操作。 使用绑定变量为何会提高 SQL 解析性能,这个需要从数据库 SQL 执行原理 说明,一条 SQL 在 Oracle 数据库中的执行过程如下图所示: 当一条 SQL 发送给数据库服务器后,系统首先会将 SQL 字符串进行 hash 运 算,得到 hash 值后再从服务器内存里的 SQL 缓存区中进行检索,如果有相同的 SQL 字符,并且确认是同一逻辑的 SQL 语句,则从共享池缓存中取出 SQL 对应 的执行计划,根据执行计划读取数据并返回结果给客户端。 如果在共享池中未发现相同的 SQL 则根据 SQL 逻辑生成一条新的执行计划 并保存在 SQL 缓存区中,然后根据执行计划读取数据并返回结果给客户端。 为了更快的检索 SQL 是否在缓存区中,首先进行的是 SQL 字符串 hash 值对 比,如果未找到则认为没有缓存,如果存在再进行下一步的准确对比,所以要 命中 SQL 缓存区应保证 SQL 字符是完全一致,中间有大小写或空格都会认为是 不同的 SQL。 如果我们不采用绑定变量,采用字符串拼接的模式生成 SQL,那么每条 SQL 都会产生执行计划,这样会导致共享池耗尽,缓存命中率也很低。 一些不使用绑定变量的场景: a、数据仓库应用,这种应用一般并发不高,但是每个 SQL 执行时间很长,SQL 解析的时间相比 SQL 执行时间比较小,绑定变量对性能提高不明显。数据仓库 一般都是内部分析应用,所以也不太会发生 SQL 注入的安全问题。 b、数据分布不均匀的特殊逻辑,如产品表,记录有 1 亿,有一产品状态字段, 上面建有索引,有审核中,审核通过,审核未通过 3 种状态,其中审核通过 9500 万,审核中 1 万,审核不通过 499 万。 要做这样一个查询: select count(*) from product where status=? 采用绑定变量的话,那么只会有一个执行计划,如果走索引访问,那么对 于审核中查询很快,对审核通过和审核不通过会很慢;如果不走索引,那么对 于审核中与审核通过和审核不通过时间基本一样; 对于这种情况应该不使用绑定变量,而直接采用字符拼接的方式生成 SQL, 这样可以为每个 SQL 生成不同的执行计划,如下所示。 select count(*) from product where status='approved'; //不使用索引 select count(*) from product where status='tbd'; //不使用索引 select count(*) from product where status='auditing';//使用索引 4.2、合理使用排序 Oracle 的排序算法一直在优化,但是总体时间复杂度约等于 nLog(n)。普通 OLTP 系统排序操作一般都是在内存里进行的,对于数据库来说是一种 CPU 的消 耗,曾在 PC 机做过测试,单核普通 CPU 在 1 秒钟可以完成 100 万条记录的全 内存排序操作,所以说由于现在 CPU 的性能增强,对于普通的几十条或上百条 记录排序对系统的影响也不会很大。但是当你的记录集增加到上万条以上时, 你需要注意是否一定要这么做了,大记录集排序不仅增加了 CPU 开销,而且可 能会由于内存不足发生硬盘排序的现象,当发生硬盘排序时性能会急剧下降, 这种需求需要与 DBA 沟通再决定,取决于你的需求和数据,所以只有你自己最 清楚,而不要被别人说排序很慢就吓倒。 以下列出了可能会发生排序操作的 SQL 语法: Order by Group by Distinct Exists 子查询 Not Exists 子查询 In 子查询 Not In 子查询 Union(并集),Union All 也是一种并集操作,但是不会发生排序,如果你确认 两个数据集不需要执行去除重复数据操作,那请使用 Union All 代替 Union。 Minus(差集) Intersect(交集) Create Index Merge Join,这是一种两个表连接的内部算法,执行时会把两个表先排序好再连 接,应用于两个大表连接的操作。如果你的两个表连接的条件都是等值运算, 那可以采用 Hash Join 来提高性能,因为 Hash Join 使用 Hash 运算来代替排序的 操作。具体原理及设置参考 SQL 执行计划优化专题。 4.3、减少比较操作 我们 SQL 的业务逻辑经常会包含一些比较操作,如 a=b,a
本文档为【面向程序员的数据库访问性能优化法则】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
该文档来自用户分享,如有侵权行为请发邮件ishare@vip.sina.com联系网站客服,我们会及时删除。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。
本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。
网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
下载需要: 免费 已有0 人下载
最新资料
资料动态
专题动态
is_274514
暂无简介~
格式:pdf
大小:855KB
软件:PDF阅读器
页数:27
分类:互联网
上传时间:2012-11-29
浏览量:7