关于分块分页开发的一些心得
概述:当数据量很大的情况下,传统的row_number()数据库分页会很慢。假设数据表有一千万,并且搜索出符合条件的数据集有上万、十万,甚至更多......这种情况如果用传统分页,不管是在数据库中使用row_number()函数,还是一次性载入APP服务器内存进行分页,对内存的消耗都是巨大的。
第一种,使用数据库row_number()分页,数据库服务器需要在上千万的数据中对符合条件的上万甚至更多数据进行row_number,这无疑是一个巨大的工程。
第二种,使用APP对进行分页,一次性将数据加载到APP服务器,对APP服务器来说也是一个巨大的考验。
那么有没有一种相对快速的方法可以解决这个问题呢?答案是肯定的。
第一次尝试:
使用DataReader,核心代码如下:
string cmdCountText = string.Format("SELECT COUNT(1) FROM ({0}) AS T1 WITH UR", cmdText); totalCount = Convert.ToInt32(DB2Helper.ExecuteScalar(connectionString, CommandType.Text, cmdCountText, paras.ToArray())); if (totalCountOnly) { return result; } cmdText += " order by E.CTime desc WITH UR"; using (DB2DataReader dr = DB2Helper.ExecuteReader(connectionString, CommandType.Text, cmdText, paras.ToArray())) { bool isAddEmptyRow = true; result.Rows.Clear(); int i = -1; while (dr.Read()) { i++; if (i < (pageIndex - 1) * pageSize) { continue; } if (i >= pageIndex * pageSize) { break; } isAddEmptyRow = false; DataRow row = result.NewRow(); row["ApplyID"] = dr["ApplyID"]; ......//此处省略row字段逐个赋值语句 result.Rows.Add(row); } return result; }
解析:DataReader会建立一个长连接,以独站方式访问数据库,向前逐条读取数据。这里使用循环逐条读取,一旦取到需要的数据将退出,不再继续读数据。很显然,使用这种方式访问时,相对row_number()节约了对数据进行编号处理的时间。而且这种方式会随着PageIndex的增大,空转(无用循环)的次数也会加大,特别是当结果数据集数据量比较大的时候,查找数值大的PageIndex非常耗时。为了解决这个问题,因此有了【分块分页】:核心思路是,对庞大的结果数据集进行分块读取,通过当前pageIndex在整个结果数据集中的位置计算出所处的分块,以及在块中的位置,这里的位置信息主要包括块中的BlockPageIndex(块中的PageIndex),BlockPageFrom(页中的开始位置),BlockPageTo(页中的结束位置)。注意:这里数据可能会跨多个块,也有可能会跨多个页。假设我们要搜索2016/03/30-2017/03/30一年的数据,我们按月份对数据进行分块,数据在每个月的分布情况如下:
1月 |
2月 |
3月 |
4月 |
5月 |
6月 |
7月 |
8月 |
9月 |
10月 |
11月 |
12月 |
13 |
2 |
6 |
5 |
18 |
7 |
4 |
45 |
5 |
2 |
1 |
10 |
现在假设PageSize=10,PageIndex=1,我们首先通过PageSize和PageIndex计算出当前数据在整个结果集中的位置并记录下来,这里From=1,To=10,我们开始循环分块分布数据,第一块13,From和To均小于它,所以直接返回Block1,并把块中的位置信息计算并记录下来,依次类推,核心代码如下:
/// <summary> /// 计算分块位置 /// </summary> /// <param name="sortOrder">排序方式</param> /// <returns></returns> public List<DistributionModel> GetPageModelDistribution(SortOrder sortOrder) { List<DistributionModel> pageModelDistribution = new List<DistributionModel>(); // ModelDistribution为分块分布数据,必须是按指定顺序排好的 foreach (var item in ModelDistribution) { var PrevIndex = 0; if (sortOrder == SortOrder.Ascending) { PrevIndex = ModelDistribution.Where(m => m.EndDate < item.BeginDate).Sum(m => m.TotalCount); } else { PrevIndex = ModelDistribution.Where(m => m.BeginDate > item.EndDate).Sum(m => m.TotalCount); } var CurrentIndex = PrevIndex + item.TotalCount; //计算开始位置 if (this.From > PrevIndex && this.From <= CurrentIndex) { DistributionModel model = (DistributionModel)item.Clone(); model.From = (this.From - PrevIndex); model.PageSize = this.PageSize; model.PageIndex = (Int32)Math.Ceiling((this.From - PrevIndex) * 1.0 / model.PageSize); //开始和结束位置在同一区块的情况 if (this.To <= CurrentIndex) { model.To = (this.To - PrevIndex); //跨页的情况处理 var addModel = CalcPagingMore(model); pageModelDistribution.Add(model); if (addModel != null) { pageModelDistribution.Add(addModel); } break; } else//跨区块 { model.To = model.TotalCount; model.PageFrom = (this.From - PrevIndex - 1) % model.PageSize + 1; //跨页的情况处理 var addModel = CalcPagingMore(model); pageModelDistribution.Add(model); if (addModel != null) { pageModelDistribution.Add(addModel); } } } //跨多个区块的情况 if (this.From <= PrevIndex && this.To > CurrentIndex) { DistributionModel model = (DistributionModel)item.Clone(); model.From = 1; model.PageSize = this.PageSize; model.PageIndex = 1; model.To = item.TotalCount; model.PageFrom = 1; model.PageTo = item.TotalCount; pageModelDistribution.Add(model); } //跨区块计算结束位置 if (this.To > PrevIndex && this.To <= CurrentIndex) { DistributionModel model = (DistributionModel)item.Clone(); model.From = 1; model.PageSize = this.PageSize; model.PageIndex = 1; model.PageFrom = 1; if (this.To <= CurrentIndex) { model.To = this.To - PrevIndex; } var addModel = CalcPagingMore(model); pageModelDistribution.Add(model); if (addModel != null) { pageModelDistribution.Add(addModel); } break; } } return pageModelDistribution; } /// <summary> /// 计算是否需要加分页 /// </summary> /// <param name="model"></param> /// <returns></returns> private DistributionModel CalcPagingMore(DistributionModel model) { DistributionModel addModel = null; var pageTo = (Int32)Math.Ceiling(model.To * 1.0 / model.PageSize); //当开始和结束不在区块中的同一页中 if (model.PageIndex != pageTo) { addModel = new DistributionModel(); addModel = ((DistributionModel)model.Clone()); addModel.PageFrom = 1; addModel.PageTo = (model.To - 1) % addModel.PageSize + 1; model.To = model.PageSize; model.PageTo = model.PageSize; model.PageFrom = (model.From - 1) % model.PageSize + 1; addModel.PageIndex = model.PageIndex + 1; } else//当开始和结束在区块中的同一页中 { model.PageTo = (model.To - 1) % model.PageSize + 1; model.PageFrom = (model.From - 1) % model.PageSize + 1; } return addModel; }
解析:分块分页的方法将数据进行分块,在一定程度上减少了初始分页方法中的空转(无用循环)所消耗的时间成本,但是增加了计算区块位置的时间成本(可以忽略不及,因为一般来说分块不会太多),在结果数据集量大胡情况下,性能提升明显。