- 数据合并程序调优
关键词:系统问题定位 系统速度调优 消除java程序中循环的数据库操作
背景:程序主要功能是将业务数据定时的从数据库中进行提取,合并,并且导入到接口库中的一个java service程序。随着业务系统以及业务数据的增加,程序执行速度下降显著。由于是数据增加以后出现的这个问题,所以问题定位在数据库操作之上。
调优攻略:
- 查询历史执行日志,发现日志使用log4j打出了debug信息,其中包含每一步的操作时间,调查时间间隔比较长的几个步骤。
- 查询目标步骤涉及的表数据,发现目标表数据量都很大,有3张表数据达到千万。
- 查询其中一张表的全部打印日志,发现操作随着时间变慢,基本都是新增或更新数据的操作。
- 根据SQL,对抽取涉及到查询的列创建索引,发现速度基本没有变化。
- 由于有源码,查看源码逻辑,发现源码中对每条数据都先进行一次查询有没有的数据库操作,然后才进行一次增加或者更新操作。业务系统每天产生约1W条数据,那么光这一张表就会进行至少2W次操作,并且是在一张1000W大小的表上。
- 更改上面逻辑,将更新和新增统一变为先删除,后新增的操作。业务系统无论产生多少数据,只进行2次数据库操作。
- 问题解决。
后话:更改以后的操作受到了限制,如果源数据库和目标数据库不在同一台服务器,就会无法完成合并,即:SQL是跨数据库的。但是考虑了实际环境,两个库不可能分开,所以这么写了。
- 查询统计调优
关键词: 统计信息更新 索引创建 执行计划 性能监视器 查询跟踪器
背景:之前一个统计分析子系统对数据仓库有大量的查询操作。某一天,用户提出某一个模块查询过慢,导致程序无法正常运行。
服务器操作系统:winserver2003
数据库:MSSQLServer 2008。
调优攻略:
- 使用性能监视器进行监视,开始执行目标操作以后,观察计数器获取线索:
- Memory Page Faults/sec 每次执行都会保持很高的值
- Memory Page /sec 第一次执行会有值,以后则没有
- 说明这个SQL执行过程会进行大量数据的扫描。
- 使用SQL Server Profiler开始跟踪,模板选择TSQL,这样会过滤掉一些其他的操作,然后执行目标操作,对SQL进行捕获。
- 分析目标SQL,综合索引,查询的表数据量较大,800W左右,但是SQL没有不同,之前查询经过优化,对应分组和过滤的列上也创建有索引。
- 对近期客户数据进行比对,发现目标表数据增加了部分,总量的1/20左右,不会对查询造成过大影响。
- 偶尔发现查询更改作为参数的查询日期会得出不同的查询速度。分析执行计划,发现执行计划不同。
- 查看目标表对应索引的统计信息,发现统计信息不完整,是抽样1.5%得出,只有200行左右。
- 目标表每日有合并操作,操作内容是截断表和新增数据,这个操作会引发更新统计信息的操作。
- 在合并过程中增加全量更新统计信息操作解决。
深入剖析:
问题本质: 过时或者抽样率过小的统计信息会导致效率低下的执行计划.
truncate操作以后表的统计信息会过时, 新的统计信息收集会在第一次查询时候进行收集,收集信息策略如下:
数据库设置了参数AUTO_CREATE_STATISTICS, 统计信息会自动进行更新具体更新阈值Recompilation threshold (RT)如下:
- Permanent table 永久表, 数据量 = n
- If n <= 500, RT = 500.
- If n > 500, RT = 500 + 0.20 * n.
- Temporary table 临时数据库表 , 数据量=n
- If n < 6, RT = 6.
- If 6 <= n <= 500, RT = 500.
- If n > 500, RT = 500 + 0.20 * n.
- Table variable 表变量不进行统计
查看原文
对于数据量比较大的表, 自动更新策略会比较缩水,以下是微软的原文:
“ 当 AUTO_UPDATE_STATISTICS 数据库选项设置为 ON(默认值)时,查询优化器会在表中的数据发生变化时自动定期更新这些统计信息。每当查询执行计划中使用的统计信息没有通过针对当前统计信息的测试时就会启动统计信息更新。 采样是在各个数据页上随机进行的,取自表或统计信息所需列的最小非聚集索引。 从磁盘读取一个数据页后,该数据页上的所有行都被用来更新统计信息。常规情况是:在大约有 20% 的数据行发生变化时更新统计信息。但是,查询优化器始终确保采样的行数尽量少。对于小于 8 MB 的表,则始终进行完整扫描来收集统计信息。”
这也就是为什么数据的统计信息始终只有很少的原因。
ps: 创建索引时候自动创建的统计信息抽样率为100%
ps2: 无论使用什么手动办法更新statistics, 如果不明文指定FULLSCAN, 那么数据库始终使用抽样的方式进行信息收集。╮(╯_╰)╭
调优感想:
- 统计信息的查看方式有很多
DBCC SHOW_STATISTICS ( table_or_indexed_view_name , target ) [ WITH [ NO_INFOMSGS ] < option > [ , n ] ] < option > :: = STAT_HEADER | DENSITY_VECTOR | HISTOGRAM | STATS_STREAM
- 也可以查询动态视图
select
ind.name,
ind.type_desc,
a.page_count,
a.record_count,
a.index_depth from
sys.indexes ind , sys.dm_db_index_physical_stats(DB_ID('TEST'),OBJECT_ID(N'dbo.User1'),null,null,'DETAILED') a where
ind.index_id = a.index_id and ind.object_id = OBJECT_ID(N'dbo.User1')
- 查看统计信息时候要注意,数据库忙的时候,会使用抽样的方式来进行统计信息,此时的统计信息虽然无可厚非,但是对于数据量比较大的表,单靠索引存活,那么很可能导致严重的查询问题。
- 这种问题的典型例子就是对于同样的SQL不同的参数,执行计划不同。
- 解决办法暂时只是想到了定时进行数据库的统计信息更新,另外,选择TRUNCATE操作一定要慎重。
通过存储过程
sp_createstats [ [ @indexonly = ] 'indexonly' ] [ , [ @fullscan = ] 'fullscan' ] [ , [ @norecompute = ] 'norecompute' ]
或者通过T-SQL
UPDATE STATISTICS table_or_indexed_view_name [ { { index_or_statistics__name } | ( { index_or_statistics_name } [ ,...n ] ) } ] [WITH [ FULLSCAN | SAMPLE number { PERCENT | ROWS } | RESAMPLE | <update_stats_stream_option> [ ,...n ] ] [ [ , ] [ ALL | COLUMNS | INDEX ] [ [ , ] NORECOMPUTE ] ] ;
常用的系统存储过程说明
sp_who (sp_who2)报告有关当前 SQL Server 用户和进程的快照信息,包括当前正在执行的语句以及该语句是否被阻塞。
sp_lock 报告有关锁的快照信息,包括对象 ID、索引 ID、锁的类型以及锁应用于的类型或资源。
sp_spaceused 显示对表(或整个数据库)所用的当前磁盘空间量的估计。
sp_monitor 显示统计信息,包括 CPU 使用率、I/O 使用率以及自上次执行 sp_monitor 以来的空闲时间。
常用的动态管理视图说明
常规服务器动态管理对象包括:
dm_db_*:数据库和数据库对象
dm_exec_*:执行用户代码和关联的连接
dm_os_*:内存、锁定和时间安排
dm_tran_*:事务和隔离
dm_io_*:网络和磁盘的输入/输出
以下举几个比较重要的动态视图。
sys.dm_exec_query_stats DMV 提供缓存查询计划的汇总性能统计信息,包括有关物理和逻辑读/写以及查询执行次数的详细信息。它包含从实际 SQL 的父 SQL 中提取实际 SQL 所使用的偏移量。此 DMV 已联接到sys.dm_exec_sql_text DMF,后者包含与 I/O 有关的 SQL 批处理的信息。
示例1:查询最费IO的查询
SELECT TOP 10 [Average IO] = (total_logical_reads + total_logical_writes) / qs.execution_count ,[Total IO] = (total_logical_reads + total_logical_writes) ,[Execution count] = qs.execution_count ,[Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2, (CASE WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2 ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) ,[Parent Query] = qt.text ,DatabaseName = DB_NAME(qt.dbid) FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt ORDER BY [Average IO] DESC;
示例2:查询最费CPU的查询
SELECT TOP 10 [Average CPU used] = total_worker_time / qs.execution_count ,[Total CPU used] = total_worker_time ,[Execution count] = qs.execution_count ,[Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2, (CASE WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2 ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) ,[Parent Query] = qt.text ,DatabaseName = DB_NAME(qt.dbid) FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt ORDER BY [Average CPU used] DESC;
示例3:使用 CLR(包括存储过程、函数和触发器)的语句
SELECT TOP 10 [Average CLR Time] = total_clr_time / execution_count ,[Total CLR Time] = total_clr_time ,[Execution count] = qs.execution_count ,[Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2, (CASE WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2 ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) ,[Parent Query] = qt.text ,DatabaseName = DB_NAME(qt.dbid) FROM sys.dm_exec_query_stats as qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt WHERE total_clr_time <> 0 ORDER BY [Average CLR Time] DESC;
示例4:最常执行的SQL
SELECT TOP 10 [Execution count] = execution_count ,[Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2, (CASE WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2 ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) ,[Parent Query] = qt.text ,DatabaseName = DB_NAME(qt.dbid) FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt ORDER BY [Execution count] DESC;
示例5:受阻塞影响的查询
SELECT TOP 10 [Average Time Blocked] = (total_elapsed_time - total_worker_time) / qs.execution_count ,[Total Time Blocked] = total_elapsed_time - total_worker_time ,[Execution count] = qs.execution_count ,[Individual Query] = SUBSTRING (qt.text,qs.statement_start_offset/2, (CASE WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2 ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) ,[Parent Query] = qt.text ,DatabaseName = DB_NAME(qt.dbid) FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) as qt ORDER BY [Average Time Blocked] DESC;
sys.dm_db_missing_index_group_stats DMV 记录了 SQL 尝试使用特定缺失索引的次数。sys.dm_db_missing_index_details DMV 详细显示缺失索引的结构,例如查询所需的列。这两个 DMV 通过sys.dm_db_missing_index_groups DMV 联系在一起。缺失索引的开销(总开销列)的计算方法是,用户平均总开销与用户平均影响的积,再乘以用户搜寻次数与用户扫描次数的和。
以下脚本来确定开销最高的缺失索引。此查询的结果(按“总开销”排序)显示最重要缺失索引的成本以及有关数据库/架构/表和缺失索引中所需列的信息。特别是,此脚本可确定哪些列在相等和不相等 SQL 语句中使用。另外,它还报告应将哪些其他列用作缺失索引中的包含性列。使用包含性列可以在不从基础页获取数据的情况下满足更多的覆盖查询,因而使用的 I/O 操作更少,从而提高性能。
示例6:高开销的缺失索引
SELECT TOP 10 [Total Cost] = ROUND(avg_total_user_cost * avg_user_impact * (user_seeks + user_scans),0) , avg_user_impact , TableName = statement , [EqualityUsage] = equality_columns , [InequalityUsage] = inequality_columns , [Include Cloumns] = included_columns FROM sys.dm_db_missing_index_groups g INNER JOIN sys.dm_db_missing_index_group_stats s ON s.group_handle = g.index_group_handle INNER JOIN sys.dm_db_missing_index_details d ON d.index_handle = g.index_handle ORDER BY [Total Cost] DESC;
从sys.dm_exec_requests DMV中读取正在运行的请求,以及这个请求的句柄(sql_handle),通过句柄,可以从sys.dm_exec_sql_textDMF得到请求的具体内容。同时,通过(SPID)联接sys.processes这个表,可以得到运行语句的用户、数据库、及应用程序名。
示例7:查询正在执行的SQL
SELECT [Spid] = session_Id , ecid , [Database] = DB_NAME(sp.dbid) , [User] = nt_username , [Status] = er.status , [Wait] = wait_type , [Individual Query] = SUBSTRING (qt.text, er.statement_start_offset/2, (CASE WHEN er.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2 ELSE er.statement_end_offset END - er.statement_start_offset)/2) ,[Parent Query] = qt.text , Program = program_name , Hostname , nt_domain , start_time FROM sys.dm_exec_requests er INNER JOIN sys.sysprocesses sp ON er.session_id = sp.spid CROSS APPLY sys.dm_exec_sql_text(er.sql_handle)as qt WHERE session_Id > 50 -- Ignore system spids. AND session_Id NOT IN (@@SPID) -- Ignore this current statement. ORDER BY 1, 2