基于性能的编程技巧点滴
基于性能的编程技巧点滴 企业应用软件开发具有两个显著的特点:
(1)业务复杂;
(2)数据量大。
由这两个特点派生出另一特点——数据存储复杂。
在一般的结构或编程习惯下,我们总是尽量让程序的重用性尽可能高,算法的结构尽可能简单明了。基于这两点考虑,在程序中会尽量采用重载和多步处理的方法。这里所说的“多步处理”是指将一个对象,对象集或数据集进行二次处理得到满足需要的格式的结果。合理的多步处理会让程序的可读性明显增强。
首先,我们来看重载。勿庸置疑,重载使得程序本身的复用性大大提高,但不是每个重载都是最佳的设计。比如有这样一个简单问题:
例1
根据给定开始时间(dtBegin)和结束时间(dtEnd)来查询数据,假设查询语句类似“select A.somefields from A where A.fdBeginDate>=@dtBegin and A.fdEndDate<=@dtEnd”;
现在提供给用户的操作界面是两个时间可以不输入,即查询请求提交可能有三种情形:
不妨设用户查询接口为UserQuery;
UserQuery()
//没有查询条件;
UserQuery(dtBegin,-)
//只有开始时间;
UserQuery(-,dtEnd)
//只有结束时间;
UserQuery(dtBegin,dtEnd)
//指定每一时间;
这时的实现(重载)可这样表示
UserQuery()
//没有查询条件;
UserQuery(DateTime dtInput,bool bIsBegin)
//bIsBegin表示是否是开始时间,否则代表结束时间;
UserQuery(DateTime dtBegin,DateTime dtEnd)
//具体指定了每个查询条件;
现在我们给其中一个做具体实现,一般是UserQuery(DateTime dtBegin,DateTime dtEnd);而其他两个则变为
public DataSet UserQuery()
{
DateTime dtBegin = new DateTime(1900,1,1,0,0,0);
DateTime dtEnd = new DateTime(9999,12,31,23,59,59);
return UserQuery(dtBegin,dtEnd);
}
public DataSet UserQuery(DateTime dtInput, bool bIsBegin)
{
DateTime dtBegin;
DateTime dtEnd;
if(bIsBegin)
{
dtBegin = dtInput;
dtEnd = new DateTime(9999,12,31,23,59,59);
}
else
{
dtBegin = new DateTime(1900,1,1,0,0,0);
dtEnd = dtInput;
}
return UserQuery(dtBegin,dtEnd);
}
我们这里不讨论将(1900,1,1,0,0,0)设为最小时间和(9999,12,31,23,59,59)设为最大时间的合理性,只是举例说明很多情况下的条件查询我们会采用类似做法进行重载。
这种做法在结构和复用性都是不错的。但一旦查询的处理过程很复杂(如需要很多表之间的关联和筛选),则加一个条件的过滤即会耗费一定的时间,也就是说对于用户没有
指定条件的查询,如果硬套上一个边界条件,则会影响处理速度。实际上,我们可以对上面的写法做一些改进,把关于条件的判断加进实现函数里,即在采用下面的实现方法。
public DataSet UserQuery()
{
DateTime dtTemp = new DateTime(1900,1,1,0,0,0);
return UserQuery(dtTemp,dtTemp,false,false);
}
public DataSet UserQuery(DateTime dtInput, bool bIsBegin)
{
if(bIsBegin)
{
return UserQuery(dtInput,dtInput,true,false);
}
else
{
return UserQuery(dtInput,dtInput,false,true);
}
}
public DataSet UserQuery(DateTime dtBegin,DateTime dtEnd)
{
return UserQuery(dtBegin,dtEnd,true,true);
}
private DataSet UserQuery(DateTime dtBegin,DateTime dtEnd, bool bNeedBegin,bool bNeedEnd)
{
string strSQL = 'select A.somefields from A where 1=1';
if(bNeedBegin)
{
strSQL += ' and A.fdBeginDate>=@dtBegin';
}
if(bNeedEnd)
{
strSQL += ' and A.fdEndDate<=@dtEnd';
}
//do some query
....
return a DataSet;
}
再来看第二点——多步处理。程序设计中如果能用一次数据库访问或对象方法调用就得到所需数据是很理想的情况,然而业务不可能总是那样简洁易操作的。在很多业务处理中存在大量的数据比较、数据转换,这些数据处理有时并不复杂但数据量会很大。比如根据一个ID到数据库查询一个字符串,这种查询不同于前面所讲的查询,是一种简单但频繁的查询。
来看这样一个例子:
例2
已知数据集DataSet1
{OBJ1_ID, OBJ1_NAME, OBJ2_ID, OBJ2_NAME}
现在要求根据DataSet1得到数据集DataSet2
{OBJ1_ID, OBJ1_NAME, OBJ2_ID, OBJ2_NAME, OBJ3_NAME, OBJ4_NAME}
假设这里获取OBJ3_NAME, OBJ4_NAME的过程很简单,就是根据OBJ1_ID, OBJ2_ID去拼一段SQL出来,然后连接数据库查询一下即可。按照这个思路,一般会提供两种方法供调用,形如:
QueryCaption(string strID)
//单参数;
QueryCaption(string[] strIDArr)
//数组参数;
数组参数可以用IN或OR将多次查询转成一次查询,这样当数据量大的时候可以减少数据库连接次数。但对于上面的例子,因为有两个字段(OBJ3_NAME, OBJ4_NAME)要取,所以至少还是要调用两次,而且还要后续处理。这时可以用Union来实现进一步的优化
假设取OBJ3_NAME、OBJ4_NAME的SQL分别为
SELECT TB_NAMELIB_3.NAME OBJ3_NAME FROM TB_NAMELIB_3 WHERE TB_NAMELIB_3.NAME_ID= OBJ1_ID;
SELECT TB_NAMELIB_4.NAME OBJ4_NAME FROM TB_NAMELIB_4 WHERE TB_NAMELIB_4.NAME_ID= OBJ2_ID;
下面用Union合成取数据过程
DataRowCollection objRows = DataSet1.Tables[0].Rows;
int iCount = objRows.Count;
string[] strSQLArr = new string[iCount];
string strSQL;
string strFields;
string strObjID1;
string strObjID2;
DataSet objDataSet;
for(int i=0;i<iCount;i++)
{
strObjID1 = objRows[i][“OBJ1_ID”].ToString();
strObjID2 = objRows[i][“OBJ2_ID”].ToString();
strFields = “’”+strObjID1 +'’ OBJ1_ID, ';
strFields+= “’”+objRows[i][“OBJ1_NAME”].ToString()+'’ OBJ1_NAME, ';
strFields+= “’”+strObjID2 +'’ OBJ2_ID, ';
strFields+= “’”+objRows[i][“OBJ2_NAME”].ToString()+'’ OBJ2_NAME, ';
strSQLArr[i] = ' UNION SELECT '+ strFields +' TEMPU. OBJ3_NAME,TEMPJ.OBJ4_NAME, FROM (SELECT 1 PID, TB_NAMELIB_3.NAME OBJ3_NAME FROM TB_NAMELIB_3 WHERE TB_NAMELIB_3.NAME_ID='''+ strObjID1 +'''') TEMPU LEFT JOIN (SELECT 1 PID, TB_NAMELIB_4.NAME OBJ4_NAME FROM TB_NAMELIB_4 WHERE TB_NAMELIB_4.NAME_ID= '''+ strObjID2 +''') TEMPJ ON TEMPU.PID = TEMPJ.PID UNION SELECT '+ strFields +' TEMPU.OBJ3_NAME,TEMPJ.OBJ4_NAME, FROM (SELECT 1 PID, TB_NAMELIB_3.NAME OBJ3_NAME FROM TB_NAMELIB_3 WHERE TB_NAMELIB_3.NAME_ID= '''+ strObjID1 +''') TEMPU RIGHTER JOIN (SELECT 1 PID, TB_NAMELIB_4.NAME OBJ4_NAME FROM TB_NAMELIB_4 WHERE TB_NAMELIB_4.NAME_ID= '''+ strObjID2 +''') TEMPJ ON TEMPU.PID = TEMPJ.PID ';
}
if(iCount>0)
{
if(null==strSQLArr[0])
{
return null;
}
strSQLArr[0] = strSQLArr[0].Substring(6);
strSQL = String.Concat(strSQLArr);
//创建连接 PersistConn m_objPersistConn = new PersistConn();
//执行查询 objDataSet = m_objPersistConn.ExecuteDataSet(strSQL);
//释放连接 m_objPersistConn.Dispose();
return objDataSet;
}
可以看到,这样一次SQL执行就可以得到目的数据集。这里不用Cross Join的原因是为了兼容有些数据库中不用Cross Join来表示全连接。在实际情况中类似这种问题其实很多,当然不一定问题描述完全一样。
以上通过两个简单的例子来说明实际编程中一些体会,类似这些点滴相信每个程序员都有自己的体会。我觉得优秀的代码就像学习英语一样,是慢慢积累起来的。近年来,有关性能编程方面的资料逐渐开始丰富起来。很多好的编程思路可以让我们得到很好的借鉴,同时我们还可以在实际开发中对其进行进一步拓展。比如:Lazy Loading的思想实际上可以应用到多层架构的多个面上,从数据实体到业务实体,再到UI层。
另外一个比较重要的问题是:如何在“有限”的情况下检验程序的性能。这里的“有限”是指不能够快速地使系统具有超大数据量或者复杂度。这时,我们可以考虑进行“逆境测试”,即通过降低Server的配置或程序的简单修改从而能够在某种程度上模拟出一些性能问题。