最近有客户反馈系统导入EXECL进行数据处理超时了,我当时的第一反应,不可能啊我明明是做过性能优化的啊,怎么还会超时呢,这是要有多少条数据才可能发生啊!于是找客户要来了EXECL,发现有7500多条数据,备份完客户数据库进行代码调试找出性能差的地方。都是一些平时老生常谈的东西,可是又是很容易忽略的地方,这里面就只谈两个点,使用String还是StringBuilder,校验数据正确性是在循环里面一条一条的使用SQL取数呢,还是一次性取出来在代码里面进行校验!下面将用实际数据结合图表,给出准确的答案。
阅读目录
String和StringBuilder的差别这里就不提了,学习和工作中常常会听到拼接字符串要使用StringBuilder对象速度很快,但是可能你只是知道这个知识,实际开发工作中有关注过这一点吗?我也是当客户反馈之后自己跟踪用实际效果才学会这个知识,后续开发中也会铭记这一点!下面的实际数据或许能说明些问题。
分别调用了这个函数, 循环次数为 1,5,15,200,500,1500,2500,5500,8500,20000 后面数据可以下载最后的DEMO实验一下,String在这时已经是慢到不行了。为了保证数据的准确性,这里每个量级的数据都取了十次值,然后求出平均值。
/// <summary> /// 对比String和StringBuilder拼接字符串的速度 /// 每种量级测试,取十次时间平均值 /// </summary> /// <param name="Total">循环次数</param> public static void StringSpeedComparer(int Total){ List<string> list = new List<string>(); for (int i = 0; i < Total; i++) { list.Add(Guid.NewGuid().ToString()); } int iTest = 10; //总执行时间 ms double TotalMilliseconds = 0; //String拼接 string strGUID = String.Empty; while (iTest > 0) { DateTime dtBegin = DateTime.Now; foreach (string temp in list) { strGUID = strGUID + temp + ";"; } DateTime dtEnd = DateTime.Now; TotalMilliseconds += (dtEnd - dtBegin).TotalMilliseconds; iTest--; } Console.WriteLine("String拼接{0}个字符串耗时{1}ms", Total, TotalMilliseconds / 10); //StringBuilder拼接 StringBuilder sb = new StringBuilder(); iTest = 10; TotalMilliseconds = 0; while (iTest > 0) { DateTime dtBegin = DateTime.Now; foreach (string temp in list) { sb.AppendFormat("{0};", temp); } DateTime dtEnd = DateTime.Now; TotalMilliseconds += (dtEnd - dtBegin).TotalMilliseconds; iTest--; } Console.WriteLine("StringBuilder拼接{0}个字符串耗时{1}ms", Total, TotalMilliseconds / 10); }
执行结果如下图:
绘制成曲线图:
从上图可直观看出来,String拼接是呈几何形递增的,而StringBuilder呈线性的,递增趋势很慢。在循环次数多的情况下使用哪种拼接,相信大家都清楚了吧!在7500的数量时,可以节省整整4s的时间,性能是不是提升很多呢?
回到顶部背景:EXECL中有7500行学生信息数据,要把这些数据导入到学生表(p_Student)里面,但是要保证学生编号(StudentNo)唯一,不唯一导入的时候需要给出提示信息。这就需要在后台代码里面读取EXECL里面的学生信息然后校验学生编码在数据库中是否存在,当然EXECL中填写的学生编号也要校验唯一。下面就来模拟这个过程,以两种方式比较性能。、
首先创建学生信息表,插入7500条数据,下面是SQL脚本,学生编号这里插入的是newid,实际情况不会是这样的,这里只是会了保证唯一,但是又是无序的,尽可能模拟真实情形。
/*---------------------------数据字典生成工具(V2.1)--------------------------------*/ GO IF NOT EXISTS(SELECT 1 FROM sysobjects WHERE id=OBJECT_ID('[p_Student]')) BEGIN /*==============================================================*/ /* Table: p_Student */ /*==============================================================*/ CREATE TABLE [dbo].[p_Student]( [StudentGUID] uniqueidentifier , [Name] varchar(40) , [Major] varchar(100) , [Sex] varchar(8) , [StudentNo] varchar(100) , PRIMARY KEY(StudentGUID) ) declare @CurrentUser sysname select @CurrentUser = user_name() execute sp_addextendedproperty 'MS_Description', '学生信息表','user', @CurrentUser, 'table', 'p_Student' execute sp_addextendedproperty 'MS_Description', '学生信息GUID' ,'user', @CurrentUser, 'table', 'p_Student', 'column', 'StudentGUID' execute sp_addextendedproperty 'MS_Description', '姓名' ,'user', @CurrentUser, 'table', 'p_Student', 'column', 'Name' execute sp_addextendedproperty 'MS_Description', '专业' ,'user', @CurrentUser, 'table', 'p_Student', 'column', 'Major' execute sp_addextendedproperty 'MS_Description', '性别' ,'user', @CurrentUser, 'table', 'p_Student', 'column', 'Sex' execute sp_addextendedproperty 'MS_Description', '学生编号' ,'user', @CurrentUser, 'table', 'p_Student', 'column', 'StudentNo' END GO --插入7500条模拟数据 DECLARE @Count AS INT SELECT @Count=COUNT(1) FROM p_Student IF @Count=0 BEGIN DECLARE @i AS INT SET @i=7500 WHILE @i>0 BEGIN INSERT INTO dbo.p_Student ( StudentGUID , Name , Major , Sex , StudentNo ) VALUES ( NEWID() , -- StudentGUID - uniqueidentifier @i , -- Name - varchar(40) '软件工程' , -- Major - varchar(100) '男' , -- Sex - varchar(8) NEWID() -- StudentNo - varchar(100) ) SET @i=@i-1 END END GO
基础信息准备好以后,进入后台代码
/// <summary> /// 统计循环校验和一次性校验性能差异 /// </summary> public static void Check(int Total) { //这里模拟学生编号 List<string> listStudetNo = new List<string>(); for (int i = 0; i < Total; i++) { listStudetNo.Add(Guid.NewGuid().ToString()); } using (SqlConnection con = new SqlConnection(SqlCon)) { con.Open(); string strSQL = "SELECT COUNT(1) FROM dbo.p_Student WHERE StudentNo='{0}'"; SqlCommand cmd = con.CreateCommand(); //循环校验 double TotalMilliseconds = 0; for (int i = 0; i < 10; i++) { foreach (string studentNo in listStudetNo) { DateTime dtBegin = DateTime.Now; cmd.CommandText = String.Format(strSQL, studentNo); int count = (int)cmd.ExecuteScalar(); if (count > 0) { Console.WriteLine("{0}编号重复,请重新录入!", studentNo); return; } DateTime dtEnd = DateTime.Now; TotalMilliseconds += (dtEnd - dtBegin).TotalMilliseconds; } } Console.WriteLine("循环校验{0}个学生编号耗时{1}ms", Total, TotalMilliseconds / 10); //一次性校验 TotalMilliseconds = 0; strSQL = "SELECT TOP 1 StudentNo FROM dbo.p_Student WHERE StudentNo IN ('{0}')"; for (int i = 0; i < 10; i++) { DateTime dtBegin = DateTime.Now; StringBuilder sb = new StringBuilder(); foreach (string studentNo in listStudetNo) { sb.AppendFormat("{0};", studentNo); } cmd.CommandText = String.Format(strSQL,sb.ToString().Substring(0, sb.ToString().Length - 1).Replace(";","','")); string no = (string)cmd.ExecuteScalar(); if (!string.IsNullOrEmpty(no)) { Console.WriteLine("{0}编号重复,请重新录入!", no); return; } DateTime dtEnd = DateTime.Now; TotalMilliseconds += (dtEnd - dtBegin).TotalMilliseconds; } Console.WriteLine("一次性校验{0}个学生编号耗时{1}ms", Total, TotalMilliseconds / 10); } }
从上图可直观看出来,循环校验和一次性校验都是线性递增的,一次性校验速度差不多比循环的快一倍左右。
回到顶部示例sql,示例代码DEMO
其实性能优化不仅仅只有这么一点,需要在日常工作中总结,这次性能优化还有一点也令我惊叹,有一条SQL未优化之前执行需要20s左右,给表添加了索引,速度刷的一下变成0s了,最终性能问题圆满解决了。
性能优化思想:
1:大量字符串拼接请采用StringBuilder
2:千万不要在大量循环里面循环查SQL,考虑是否能用一次性查询代替,或者一次性把数据查询出来在代码里面进行逻辑判断
3:SQL执行速度慢,可以采用执行计划看看是否表缺少索引。
好了本篇到这里就要结束了,如果觉得对你有益,记住点赞哦!
相关阅读:附加没有日志文件的数据库方法 删除数据库日志文件的方法 数据字典生成工具系列文章