关于大数据查询与导出

上周末,帮朋友处理了一个关于大数据的查询与导出问题,整理一下,在此记录一下用以备忘,同时也为有类似需要的朋友提供一个参考.

背景:

  1. 数据库服务使用: SqlServer2008 ;
  2. 查询的流水表总数据量约在 800W 条左右 ;
  3. 需要展示的字段需要从流水表+基础资料表中
  4. 导出需要 加载指定模板 ;
  5. 要求查询响应时间<=2s,导出<=10s; (当然每次仅处理符合条件的数据) .
  6. 该系统运行了大概2年时间,系统刚上线的时候,各项性能指标还ok,目前该功能点查询和导出时直接卡死.
  7. 该项目为 常规 winform 类型,普通三层架构.

改造步骤:

  1. 数据库,

    1. 该功能主要查询的表为日常业务流水,增长比较大,且查询多已产生的时间段作为查询条件,首先考虑使用创建时间段(每半年做一个分区)做分区表处理.
    2. 为提高数据文件读写性能,将该业务流水表存储为独立的数据文件,
    3. 创建查询条件 ”创建时间”,“付款公司Id”(int 类型)  和 “付款方式” 字段的索引.
  2. 程序方面,
    1. 首先引入后台线程, 将耗时的查询从主线程[UI线程]中移除,转为后台线程处理,
    2. 采用分页查询数据,每次固定加载1000条数据,待滚动条滚动至当前结果集中最后一页的时候,自动加载下一页数据,
    3. 导出需要处理 查询和填充文件 两个操作.而这两个操作都比较耗时.引入队列+生产/消费模式处理.
    4. 去掉关联查询,查询的数据,采用仅查询业务流水表,内存中读取缓存的基础数据组合为前段UI需要的数据,
    5. 简化程序,同时也为降低sql语句复杂度,引入ORM,这里引入微软自家的EntityFramework(版本6.0) .
  3. 下面略微介绍一下程序中的处理代码片段.
    1. 在原界面上增加一个友好提示,
    2. 查询功能按钮代码片段
      1. 查询按钮点击.

        /// <summary>
                /// 响应查询按钮事件
                /// </summary>
                /// <param name="sender"></param>
                /// <param name="e"></param>
                private void btnQuery_Click(object sender, EventArgs e)
                {
                    if (txtCompany.Tag == null)
                    {
                        MessageBox.Show(this, "请选择指定的结算公司", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
                        return;
                    }
                    _pageIndex = 0;
                    lblTotalCount.Text = "当前共[0]条数据";
                    gridData.Rows.Clear();
                    LoadQueryData();
                }
        
                /// <summary>
                /// 开始查询数据
                /// </summary>
                private void LoadQueryData()
                {
                    plProcessStatus.Visible = true;     //展示进度panel
                    SetControlStatus(true);             //设置其他功能控件暂时为只读状态
                    _isCurrentLoadEnd = false;          //标识正在加载数据
                    bgwQuery.RunWorkerAsync();          //开始执行后台查询
                }

      2. 引入BackgroundWorker组件执行后台查询 ,关于构建EF查询条件可参见之前文章[使用EF构建企业级应用(三)].

        /// <summary>
        /// 后台线程查询数据
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void bgwQuery_DoWork(object sender, DoWorkEventArgs e)
        {
           int total;
           _queryList = ExecQuery(_pageIndex, _pageSize, out total);
           _rowCount = total;
        }
        
        /// <summary>
        /// 执行分页查询方法,返回当前查询结果,
        /// </summary>
        /// <param name="pgIndex">当前页码</param>
        /// <param name="pgSize">每次查询分页大小</param>
        /// <param name="total">记录总数</param>
        /// <returns></returns>
        private List<OrderDetail> ExecQuery(int pgIndex, int pgSize, out int total)
        {
            List<OrderDetail> lst = null;
        
            var queryParam = BuildQueryExpression(pgIndex, pgSize);
            using (var services = new KYEService())
            {
                lst = services.GetOrderDetailList(queryParam, out total);
            }
            return lst;
        }
        
        /// <summary>
        /// 构建查询条件
        /// </summary>
        /// <param name="pgIndex">当前查询第几页</param>
        /// <param name="pgSize">当前查询分页大小</param>
        /// <returns>当前查询条件</returns>
        private EFQueryParam<OrderDetail> BuildQueryExpression(int pgIndex, int pgSize)
        {
          //计算查询时间段
          var queryBeginDate = new DateTime(_queryYear, _queryMonth, 1);
          var queryEndDate = queryBeginDate.AddMonths(1).AddDays(-1);
        
          //构建查询条件
          var exp = QueryBuilder.Create<OrderDetail>();
          exp = exp.Equals(t => t.PaymentCompanyId, (int)txtCompany.Tag);//结算公司
          exp = exp.GreaterThanOrEqual(t => t.FromDate, queryBeginDate); //纳入月份转化为开始日期
          exp = exp.LessThanOrEqual(t => t.FromDate, queryEndDate);      //纳入月份沾化为结束日期
          if (_queryPaymentType != EPaymentType.ALL)
          {
              exp = exp.Equals(t => t.PaymentType, _queryPaymentType);   //付款方式
          }
        
          //执行查询
          var queryParam = new EFQueryParam<OrderDetail>(exp, "FromDate", true, pgIndex, pgSize);
          return queryParam;
        }
        
        /// <summary>
        /// 查询数据线程结束后,开始UI绑定数据
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void bgwQuery_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            plProcessStatus.Visible = false;                 //隐藏进度条
            if (_queryList != null && _queryList.Count > 0)  //当前查询有数据
            {
                //后台线程异步展示数据到UI
                Thread thBindGrid = new Thread(() =>
                {
                    lblTotalCount.Invoke(new Action(() =>
                    {
                        lblTotalCount.Text = string.Format("当前共[{0}]条数据", _rowCount);
                    }));
                    //循环绑定数据
                    for (int i = 0; i < _queryList.Count; i++)
                    {
                        gridData.Invoke(new Action<OrderDetail>(FillData), _queryList[i]);
                    }
                    _isCurrentLoadEnd = true;     //标识当前查询加载结束
        
                    //绑定结束恢复其他功能按钮为可用状态(设置为非只读)
                    btnExport.Invoke(new Action(() =>
                    {
                        SetControlStatus(false);
                    }));
                });
                thBindGrid.IsBackground = true;
                thBindGrid.Start();
            }
            else
            {
                SetControlStatus(false);
            }
        }

        /// <summary>
        /// 绑定具体行数据
        /// </summary>
        /// <param name="detail">具体行数据</param>
        private void FillData(OrderDetail detail)
        {
            var index = gridData.Rows.Add();
        
            gridData["dgcIndex", index].Value = index + 1;
        
            //TODO... 具体绑定到Grid的代码略
        }
        /// <summary>
        /// 处理滚动条移动的时候,自动加载下一页数据
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void gridData_Scroll(object sender, ScrollEventArgs e)
        {
            //已经加载结束或者水平滚动,则不处理
            if (!_isCurrentLoadEnd                      //当前查询数据还未绑定结束
                || _rowCount <= gridData.Rows.Count     //当前符合条件的数据已经查询完毕
                || e.ScrollOrientation == ScrollOrientation.HorizontalScroll)
            {
                return;
            }
        
            var gridPageRowCount = gridData.DisplayRectangle.Height;    //Grid每页能显示的记录数
        
            //当前滚动到最后一页
            if (gridData.FirstDisplayedScrollingRowIndex >= gridData.Rows.Count - gridPageRowCount - 1)
            {
                _pageIndex += 1;
                LoadQueryData();            //加载下一页数据
            }
        }

    3. 导出功能,队列+生产/消费模式处理

      /// <summary>
      /// 执行导出操作
      /// </summary>
      /// <param name="p"></param>
      private void ExecExport(string fileName)
      {
          plProcessStatus.Visible = true;
          SetControlStatus(true);
      
          IExport rpter = new ExcelExporter();
          var formater = BuildExportFormater();
          bool isQueryEnd = false;                    //当前是否查询结束
          var templateFieName = Path.Combine(Application.StartupPath, "Template", "Rpt_CustomerList.xls");
      
          //创建供导出的队列
          Queue<List<OrderDetail>> exportQueue = new Queue<List<OrderDetail>>();
      
          #region 查询线程
          //处理后台查询
          Thread thQuery = new Thread(() =>
             {
                 int tempTotal = 0;
                 int tempPgIndex = 0;
                 int queryPageSize = 3000;      //每次查询3k
                 var tempList = ExecQuery(tempPgIndex, queryPageSize, out tempTotal);
                 if (tempList != null && tempList.Count > 0)
                 {
                     lock (locker)
                     {
                         exportQueue.Enqueue(tempList);
                         Monitor.PulseAll(locker);
                     }
      
                     tempPgIndex += 1;
      
                     //循环查询直至查询结束
                     while (tempPgIndex * _pageSize < tempTotal)
                     {
                         var temp_tempList = ExecQuery(tempPgIndex, queryPageSize, out tempTotal);
                         if (temp_tempList != null && temp_tempList.Count > 0)
                         {
                             lock (locker)
                             {
                                 exportQueue.Enqueue(temp_tempList);      //将查询结果加入到队列
                                 Monitor.PulseAll(locker);
                             }
                         }
                         tempPgIndex += 1;
                     }
                 }
                 isQueryEnd = true;
             });
          #endregion
      
          #region 导出excel线程
          //处理将查询的结果写入到文件中
          Thread thExport = new Thread(() =>
             {
                 rpter.Export(templateFieName, fileName, formater);//读取模板,并创建新文件,
                 int tempRowIndex = 0;
                 while (!isQueryEnd || exportQueue.Count > 0)     //未查询结束及队列不为空,执行导出
                 {
                     if (exportQueue.Count > 0)
                     {
                         List<OrderDetail> tempExpotLst = null;
                         lock (locker)
                         {
                             tempExpotLst = exportQueue.Dequeue();        //取队列数据,导出excel操作
                         }
                         if (tempExpotLst != null && tempExpotLst.Count > 0)
                         {
                             formater.DetailRowBeginIndex += tempRowIndex;
                             rpter.ExportByAppend(fileName, formater, tempExpotLst);          //执行导出操作(追加形式)
                             tempRowIndex = tempExpotLst.Count;
                         }
                     }
                     else
                     {
                         Thread.Sleep(200);
                     }
                 }
      
                 //导出贴图片
                 var imgRow = formater.DetailRowBeginIndex + tempRowIndex + 8;
                 formater.ImageCellFormaters.Add(new ImageCellFormater(imgRow, 2, Resources.ywz));
                 rpter.ExportByAppend(fileName, formater, null);
      
                 //导出结束 恢复按钮可用状态
                 btnExport.Invoke(new Action(() =>
                 {
                     plProcessStatus.Visible = false;         //隐藏进度栏
                     SetControlStatus(false);
                     if (MessageBox.Show(this, "数据已成功导出至[" + fileName + "],是否立即打开导出文件?",
                         "提示",
                         MessageBoxButtons.YesNo,
                         MessageBoxIcon.Question) == DialogResult.Yes)
                     {
                         Process.Start(fileName);
                     }
                 }));
             });
          #endregion
      
          thQuery.IsBackground = true;
          thExport.IsBackground = true;
      
          thQuery.Start();            //开始查询线程
          thExport.Start();           //开始导出线程
      }

  4. 后台查询数据方法 (基于EF实现),

    /// <summary>
    /// 获取物流明细记录
    /// </summary>
    /// <param name="queryParam">查询条件</param>
    /// <param name="total">返回符合条件的总记录数量</param>
    /// <returns></returns>
    public List<OrderDetail> GetOrderDetailList(EFQueryParam<OrderDetail> queryParam, out int total)
    {
        total = 0;
        var lst = GetRepository<OrderDetail, Int64>().Get(queryParam, out total).ToList();
    
        //组织其他冗余数据
        if (lst != null && lst.Count > 0)
        {
            //冗余公司信息,供前台UI使用
            var companyList = GetCompanyList();
            if (companyList != null && companyList.Count > 0)
            {
                var companyDic = companyList.ToDictionary(p => p.Id);               //转化为字典,提高效率
                var tempbgIndex = queryParam.PageIndex * queryParam.PageSize + 1;   //生成排序
                lst.ForEach(t =>
                {
                    t.Index = tempbgIndex;
                    //寄件公司
                    if (companyDic.ContainsKey(t.FromCompanyId))
                    {
                        t.FromComoany = companyDic[t.FromCompanyId];
                    }
                    //收件公司
                    if (companyDic.ContainsKey(t.ToCompanyId))
                    {
                        t.ToCompany = companyDic[t.ToCompanyId];
                    }
                    //付款公司
                    if (companyDic.ContainsKey(t.PaymentCompanyId))
                    {
                        t.PaymentCompany = companyDic[t.PaymentCompanyId];
                    }
                    tempbgIndex += 1;
                });
            }
        }
        return lst;
    }
    
    /// <summary>
    /// 获取公司信息
    /// </summary>
    /// <returns></returns>
    public List<Company> GetCompanyList()
    {
        //从缓存中获取
        var lst = ApplicationRuntime.Instance.CurrentCache.Get<List<Company>>(KYEConsts.CachesKey.Company,
            () => this.GetRepository<Company>().Get().ToList());
        return lst;
    }

结语: 经过这一系列改造后, 性能上大大改进了,查询响应耗时<=1s, 导出<=8s, 每次符合条件的数据大概在2W条左右.

至于本文中提到的

1. EF构建查询条件表达式,及查询数据库方式,可参见之前文章.http://www.cnblogs.com/xie-zhonglai/archive/2012/04/07/2435903.html

2. 导出Excel.本文使用了NPOI 这个组件,详情可参见文章: http://www.cnblogs.com/xie-zhonglai/p/3979771.html

时间: 2024-10-20 08:24:53

关于大数据查询与导出的相关文章

开源大数据查询分析引擎现状

引言 大数据查询分析是云计算中核心问题之一,自从Google在2006年之前的几篇论文奠定云计算领域基础,尤其是GFS.Map-Reduce.Bigtable被称为云计算底层技术三大基石.GFS.Map-Reduce技术直接支持了Apache Hadoop项目的诞生.Bigtable和Amazon Dynamo直接催生了NoSQL这个崭新的数据库领域,撼动了RDBMS在商用数据库和数据仓库方面几十年的统治性地位.FaceBook的Hive项目是建立在Hadoop上的数据仓库基础构架,提供了一系列

POI实现大数据EXCLE导入导出,解决内存溢出问题

使用POI能够导出大数据保证内存不溢出的一个重要原因是SXSSFWorkbook生成的EXCEL为2007版本,修改EXCEL2007文件后缀为ZIP打开可以看到,每一个Sheet都是一个xml文件,单元格格式和单元格坐标均用标签表示.直接使用SXSSFWorkbook来到导出EXCEL本身就是POI为了大数据量导出而量身定制的,所以导出可以直接使用SXSSFWorkbook方式. 为了保险起见可以采用多Sheet的方式保证内存不溢出.需要注意的是Sheet名称不能重复:下载的时候需要定义好返回

mysql 大数据 查询方面的测试

---方法1: 直接使用数据库提供的SQL语句---语句样式: MySQL中,可用如下方法: SELECT * FROM 表名称 LIMIT M,N---适应场景: 适用于数据量较少的情况(元组百/千级)---原因/缺点: 全表扫描,速度会很慢 且 有的数据库结果集返回不稳定(如某次返回1,2,3,另外的一次返回2,1,3). Limit限制的是从结果集的M位置处取出N条输出,其余抛弃. ---方法2: 建立主键或唯一索引, 利用索引(假设每页10条)---语句样式: MySQL中,可用如下方法

大数据查询——HBase读写设计与实践

背景介绍 本项目主要解决 check 和 opinion2 张历史数据表(历史数据是指当业务发生过程中的完整中间流程和结果数据)的在线查询.原实现基于 Oracle 提供存储查询服务,随着数据量的不断增加,在写入和读取过程中面临性能问题,且历史数据仅供业务查询参考,并不影响实际流程,从系统结构上来说,放在业务链条上游比较重.本项目将其置于下游数据处理 Hadoop 分布式平台来实现此需求.下面列一些具体的需求指标: 1.数据量:目前 check 表的累计数据量为 5000w+ 行,11GB:op

大数据系列之分布式大数据查询引擎Presto

关于presto部署及详细介绍请参考官方链接 http://prestodb-china.com PRESTO是什么? Presto是一个开源的分布式SQL查询引擎,适用于交互式分析查询,数据量支持GB到PB字节. Presto的设计和编写完全是为了解决像Facebook这样规模的商业数据仓库的交互式分析和处理速度的问题. 它可以做什么? Presto支持在线数据查询,包括Hive, Cassandra, 关系数据库以及专有数据存储. 一条Presto查询可以将多个数据源的数据进行合并,可以跨越

sql 大数据查询慎用 order by

今天在sql 查询中一个表中查询花了至少20秒时间,数据为620000行,sql语句如下: 测试一:使用order by  单单只是查询0,10行数据,耗时27.888s select a.id,a.county_id,a.county_name,a.town_id,a.town_name,a.village_id,a.village_name,b.province as province_name,b.name as city_name from place a left join city

关于poi导出大数据量EXCEL导出缓慢的解决方案--生成xml格式的表格文件

今天遇到了一个POI导出EXCEL文件结果文件大小太大导致系统运行缓慢的问题.想到了使用输出XML格式的表格来完成. 首先,我们需要理解一下为什么POI导出EXCEL文件会导致文件大小变大.最主要的原因应该是POI变成中经常会对无关的单元格设置属性.例如:一个EXCEL工作表的最大列数为256,我们只使用10列.而编程时经常会将256列全部渲染格式,造成很大的资源浪费.当然应该还有其他很多原因也会造成表格变大. 使用这个方案的前提是对于表格的格式要求不高,因为使用的是自己的工具,没有POI优化了

【java】itoo项目实战之大数据查询之使用 new map 优化hibernate之级联查询

在我的上一篇博客<[java]itoo项目实战之hibernate 懒加载优化性能>中,我曾提到过学生数据有2万条,查询数据十分的慢,这是让人很受不了的事情,看着页面进度条一直转着圈圈,那种着急的感觉真的没法形容.最开始考虑着使用lazy 来优化,因为前台框架的原因,lazy 优化并没有起到什么左右,后来就想着有select new map 优化.我先来画画关于查询学生的级联树 这个树的意思就是查询学生的时候它的深度是4级. 在没有优化之前,使用的是hibernate的hql 语句:From

大数据查询思路

$limit = 500; while (true) { $offset = ($page - 1) * $limit; $tmp_sellers = $store->sellers($where, $offset, $limit,$supplier_id); if (empty($tmp_sellers)) { break; }