当DataGrid遇见100万笔资料

DataGrid遇见100万笔资料会发生什么事呢?以下是在蓝色小铺的应答
【作者:奚江华.圣殿祭司】
原问题网址:http://www.blueshop.com.tw/board/show.asp?subcde=BRD20050216141103C3Y&fumcde=FUM20041006161839LRJ

效能永远不够,所以Performance的议题永远存在,而原因分为两个部分:
1.人为
2.系统(软件和硬件)
而你的8000笔的资料量的问题,刚好同时Cover到上面两个原因。

我有个ASP.NET网页报表软件,使用MSDE 2000,数据库档案大小为900MB,里面有二个庞大的Table,一个约1,700,000笔(30多个字段),另一个为380,000笔(约15个字段),每个月资料比数以几十万比的成长率在持续增加;在P4-2.8G而内存512MB环境中执行,每次查询会有3~5万个Table Cells显示,但查询在一秒内就可以显示完毕(但这中间是经过许多层次的最佳化和调校才能达成的,注:Table没有作Join查询);而和你的8000笔资料+DataGrid对ASP.NET而言绝对不会有问题,只是手法要改一下,不能老是依赖Default的东西。

但为了为了要和你的问题有个客观比对,依你的条件,我首先在我的NB做了同样的仿真测试,NB测试环境如下:
1.OS:Windows 2003 Server标准版
2.Visual Studio.NET 2003
3.MS SQL Server 2000标准版
4.CPU:Pentium-M 1.8G
5.内存:512MB

我开机后内存使用量为205MB,而有个数据库有近160,000笔的资料,字段数则有15个,我的程序代码如下:
SqlConnection conn=new SqlConnection("data source=.;initial catalog=LK;user id=sa;password="admin");
conn.Open();
SqlDataAdapter da1=new SqlDataAdapter("select top 8000 * from lm1200detail",conn);
DataSet ds=new DataSet();

da1.Fill(ds,"ABC");

conn.Close();
DataGrid1.DataSource=ds.Tables["ABC"].DefaultView;
DataGrid1.DataBind();

da1.Dispose();
ds.Dispose();
以上为8000笔资料,15个字段,执行结果分几点来说明:
1.在我的SQL Analyzer中执行row笔数的计算select Top 10000 * from lm1200detail,第一次不计,以第二次以后是SQL Analyzer显示为0秒,也就是不到1秒。
2.执行ASP.NET的DataGrid显示后,CPU的使用率立刻爆增到100%,持续了20秒才完成,但DataGrid依然把所有的资料量显示完。
3.内存由205MB爆增到422MB,一共增加了217MB(吃得凶)。

所以你应隐约知道这样子的系统存在了哪些问题了吧?

OK,有了上面的数据样本我们可以开始讨论正题了(这样的测试数据应比你的数据还严苛吧),问题在哪里呢?
基本上为了厘清你的问题,必须切割和隔离成三个层次,如下:
1.SQL Server层
2.ADO.NET层
3.DataGrid控件层

为何要切割成这三个层次?因为这三层每个地方都有问题,简述如下:
1.SQL Server->没有最佳化问题
2.ADO.NET->DataSet对大量数据处理的问题
3.DataGrid控件->对大量数据处理及分页的问题

接着开始探讨你所碰到的问题:
SQL Server部分的问题:
1.SQL Analyzer执行5秒有点久,但如果你做8000多笔多个Table Join的话则Maybe合理,但还需进一步分析是否有改进空间。
2.你用的是View,View上面当然不能加索引(Index),但这样一来你Join多个Table时,就会造成效能上的问题,速度比起丛集索引Clustered及非丛集索引Non Clustered可能会慢上3~10倍不等的可能性,其决定于SQL Server的最佳化引擎是否有作用。
3.解决索引问题当然不能在View上加,而是要在Join的相关Table加上索引,而索引建立的原则是依T-SQL查询语法的where条件式为依据,通常是Relation的Key上去建立索引;然后你可以去比对有索引和没索引的执行秒数,应会差不少,但最客观的是你必须去查看SQL Server的最佳化引擎是如何Parse你的 T-SQL语句,用Execution Plan去分析SQL Server是以何种方式执行你的SQL Server语句,若是用Table Scan执行的表示,你一点也没用到索引,若用到任何的索引则表示SQL Server最佳化引擎有在作用。
4.Server的最佳化引擎是否有作用取决于1.你的T-SQL语法后的where条作子句2.你建立的索引对不对?!
Execution Plan图片参考网址: 
 


ADO.NET的问题:
1.在ADO.NET之中DataSet是一个In Memory的Database,也就是存在于内存中的数据库,一般数据库中有的基本特性它都有,资料量几千几万笔难不倒它,但若几十万笔~100万笔呢?可能在ASP.NET环境中DataSet就是一个大问题,会非常的不经济(和ASP.NET、IIS及AP Server运作的方式原理有关,但在这不深入说明),但是它的承载性究竟被设计到多大是一个未知数?!有空大家可以去测试一下…
2.SqlDataAdapter.Fill(ds)这样的动作,要耗费多少内存空间和运算成本cost?且在你的程序代码之中,因为ASP.NET”无状态”的特性,若每次DataSet中Fill 8000笔资料,耗费几十MB~100MB,用完一次就丢弃了,你觉得能负荷多少人?若以我的经验,如果是DataSet每次用掉100MB内存空间,不到10次就会Timeout掉了。
3.对DataSet而言,即使你一页只看30笔资料,8000笔-30笔=7970笔,对系统资源而言,不但7970笔资料也要吃掉记体不说,最后耗费系统成本来处理这些没用的7970笔废物般的资料,活似个拖油瓶。
4.DataGrid不分页一次显示的话会更惨。

DataGrid的问题:
1.DataGrid不论有没有做分页Page,它都必须先照单全收这8000笔资料,然后再进行加工处理,选出这个Page所需的30笔,你想这样会快吗?等你资料成长到16000笔时,就是从16000笔中选出你分页Page要的30笔…会更慢。从SQL Server->ADO.NET DataSet->DataGrid都在传送非常不经济且大量的资料,对系统内存、效能与CPU运算有着很大的伤害。
2.DataGrid分页Page换页时又是什么动作?它会触发一个PageIndexChange的事件,里面又要重作一次Fill(DataSet)的动作,又来一次8000笔的Fill,然后再丢给DataGrid,所以说程序怎么会快得了呢?(在大量资料用DataGrid来处理)。
3.在Http技术的Stateless无状态特性下,所有资源,除非你用状态管理(Application、Session、ViewState)来处理、宣告为Static或组件的技术,否则所有资源用过后即会被丢弃,不能再重用了。
4.DataGrid在大量只读数据时,对于EnableViewState属性应设为false,因为ViewState会耗费大量可观的内存,你用DataGrid不分页来一次显示8000笔你就会知道true与false两者的差别了,两者落差可能会高达30~60MB不等(对30000~60000个Table Cell而言)。

解决之道:
首先我必需说明这不是ASP.NET或任何系统的错,纯粹是没有好的方法来处理这样的问题,是手法面要改一下!

SQL Server部分:
1.请建立有效索引,无论是丛集索引或非丛集索引索引,或是复合索引都好,甚至必要时还要指定索引的升降序,减少不必要的Order By。
2.请用Execution Plan来确认最佳化引擎有采用到你的索引,where条件子句是最佳化引擎分析的关键。
3.你的5秒在上面两点作用后,一定要有显著的改进,才算解决SQL Server这层的问题!

DataGrid及DataSet的问题:
1.基本上DataGrid+DataSet在遇见大量资料时,会变得气喘如牛,系统资源钜幅的耗尽(前面已说过了),所以若资料很大时,请用top xxx来限制有效资料量,缓解DataSet及DataGrid的压力。
2.不能用Top时,请用T-SQL或Store Procedure来取回该页Page所需的30笔资料即可,这就需要一点加工了:
(1)先计算出总笔数,然后总笔数/30=Page页数,再以自订方式产生DataGrid作页码分页用。
(2)依DataGrid目前所在的PageIndex去呼叫资料存取层,也就是只要Fill(DataSet)填入30笔回传即可。
3.最后记得Connection要close,SqlDataAdapter和DataSet也要Dispose,释放掉系统资源。

基本上这样你的系统就会变得非常的”轻”,速度会飞驰增加十倍以上都不成问题,即使你资料量成长到100万笔以上,依然Power!

注:
1.SQL Server 2005就支持了ADO.NET的Page,只取回你Page所需的Row,对系统效能和设计会有很大的帮助。
2.关于分页只取回该页Page及SP的程序代码,请花点时间做功课,真的研究不出来时再回我,我再Post参考程序代码给你。

有空参观我的Blog吧http://blog.sina.com.tw/weblog.php?blog_id=4907

其实在这有很多高手,大家各有所长,互相交流一下而已,刚好曾经花了很多时间研究大量资料时,ASP.NET与ADO.NET效能的问题,因为我系统再过一阵子就破200万笔了,顺道是测试一下MSDE 2000的能耐,没想道竟这么Power,MSDE 2000+ADO.NET离线式真的可以搭配得很好,又可以说免钱的,还可以用Enterprise Manager来作管理,真的是省了一笔银子,把心得公布给大家知道,承蒙各位不弃就好!

分页的技巧有两种,一种是直接透过T-SQL,另一种是透过Store Procedure,在这Post出来跟大家分享一下:
T-SQL:
假设Northwind有一个Customer的Table,你需要取回41~50笔的记录,T-SQL语法该如何作呢?

Select Top 10 CustomerID,CompanyName,ContactName,Country from Customers where CustomerID Not in
(Select top 40 CustomerID from Customers order by Country,CustomerID)
Order by Country,CustomerID

Store Procedure:
出自MSDN Magazine,是别人的智能
CREATE PROCEDURE northwind_OrdersPaged
(
@PageIndex int, 
@PageSize int
)
AS
BEGIN
DECLARE @PageLowerBound int
DECLARE @PageUpperBound int
DECLARE @RowsToReturn int

-- First set the rowcount
SET @RowsToReturn = @PageSize * (@PageIndex + 1)
SET ROWCOUNT @RowsToReturn

-- Set the page bounds
SET @PageLowerBound = @PageSize * @PageIndex
SET @PageUpperBound = @PageLowerBound + @PageSize + 1

-- Create a temp table to store the select results
CREATE TABLE #PageIndex 
(
IndexId int IDENTITY (1, 1) NOT NULL,
OrderID int
)

-- Insert into the temp table
INSERT INTO #PageIndex (OrderID)
SELECT 
OrderID
FROM 
Orders
ORDER BY 
OrderID DESC

-- Return total count
SELECT COUNT(OrderID) FROM Orders

-- Return paged results
SELECT 
O.*
FROM 
Orders O,
#PageIndex PageIndex
WHERE 
O.OrderID = PageIndex.OrderID AND
PageIndex.IndexID > @PageLowerBound AND
PageIndex.IndexID < @PageUpperBound
ORDER BY 
PageIndex.IndexID

END


以上的东西不藏私和大家分享一下,其实我相信如果你自己花多点心思说不定可以写出比上面更好的算法,算是拋砖引玉啰,有更好的方法希望大家贴出来一起分享!

你可能感兴趣的:(datagrid)