如何对DataTable进行动态分组和动态统计[C#]

来源:互联网 发布:apache下载安装 win7 编辑:程序博客网 时间:2024/05/21 14:04

    C#的DataTable中,没有提供分组统计的功能,因此需要对此进行功能扩展。 方法无非是两种,一种是直接利用DataTable的PrimaryKey对DataTable原始数据进行分组统计,一种是使用linq的group by功能进行统计。

    使用linq的好处是代码简洁。但由于实际需求中,分组的字段是动态的,而且被统计的字段也是动态,每个被统计字段的统计算法也不同,简单的linq无法满足此实际需求。

    我们假设原始的表格如下所示:

            dt = new DataTable();
            dt.Columns.AddRange(new DataColumn[] {
               new DataColumn("A1", typeof(double)),
               new DataColumn("A2", typeof(double)),
               new DataColumn("A3", typeof(double)),
               new DataColumn("S1", typeof(double)),
               new DataColumn("S2", typeof(double)),
               new DataColumn("S3", typeof(double)),
               new DataColumn("S4", typeof(double))
            });
            dt.Rows.Add(new object[] { 1, 1, 1, 1, 1, 1, 1 });
            dt.Rows.Add(new object[] { 1, 1, 1, 2, 2, 2, 2 });
            dt.Rows.Add(new object[] { 1, 2, 1, 3, 3, 3, 3 });
            dt.Rows.Add(new object[] { 1, 2, 1, 4, 4, 4, 4 });
            dt.Rows.Add(new object[] { 1, 2, 2, 5, 5, 5, 5 });
            dt.Rows.Add(new object[] { 1, 2, 2, 6, 6, 6, 6 });
            dt.Rows.Add(new object[] { 1, 2, 2, 7, 7, 7, 7 });

    需要进行的查询如下所示:

   

            DataTable result = new DataTableGroupBy().GroupByLinq(dt, new string[] { "A1", "A2", "A3" },
                                                                      new string[] { "S1", "S2", "S3", "S4" },
                                                                      new string[] { "AVG", "MAX", "MIN", "COUNT" });
            Assert.AreEqual(result.Rows[0][3].ToString(), "1.5");
            Assert.AreEqual(result.Rows[0][4].ToString(), "2");
            Assert.AreEqual(result.Rows[0][5].ToString(), "1");
            Assert.AreEqual(result.Rows[0][6].ToString(), "2");

            Assert.AreEqual(result.Rows[2][3].ToString(), "6");
            Assert.AreEqual(result.Rows[2][4].ToString(), "7");
            Assert.AreEqual(result.Rows[2][5].ToString(), "5");
            Assert.AreEqual(result.Rows[2][6].ToString(), "3");

    即以A1,A2,A3进行分组,对S1,S2,S3,S4进行统计,它们各自的统计算法均不同。

    那么如何实现?

    首先看如何实现对动态字段的分组。一个基本的思路是,在分组时,利用linq的key信息,把每个分组的A1,A2,A3进行字符串连接,形成一个字符串key,然后再对结果集中的key进行拆分,得到分组中的A1,A2和A3。

        private string GroupData(DataRow dataRow)
        {
            StringBuilder builder = new StringBuilder();
            builder.Remove(0, builder.Length);
            foreach (string field in groupByFields)
            {
                builder.Append(dataRow[field].ToString() + ";");
            }
            string key = builder.ToString();
            return key.Remove(key.Length - 1);
        }

    再看如何实现对动态字段的统计。在上述思路的基础上,只需要把结果集中的每个分组的item数据得到,就能够形成一个新的DataTable,对其中的统计字段,可以使用DataTable的Compute方法进行计算。Compute方法,只需要提供一个计算的表达式字符串即可,这样就可以根据输入的算法,动态生成Compute的参数。

        private void FillResultStatistics(ref DataRow resultRow, DataTable temp)
        {
            for (int i = 0; i < statisticsFields.Length; i++)
            {
                string statisticsFieldCaption = GetStatisticsCaption(statisticsFields[i], methods[i]);
                resultRow[statisticsFieldCaption] = temp.Compute(statisticsFieldCaption, "");
                if (methods[i] == "AVG")
                {
                    object obj = resultRow[statisticsFieldCaption];
                    if (obj.ToString() != "")
                    {
                        double t = Math.Round(Convert.ToDouble(obj), 2);
                        resultRow[statisticsFieldCaption] = t;
                    }
                }
            }
        }

    在此基本思路基础上,形成的完整计算过程如下:   

        public DataTable GroupByLinq(DataTable source, string[] groupByFields, string[] statisticsFields, string[] methods)
        {
            TimeSpan start = new TimeSpan(DateTime.Now.Ticks);
            Init(groupByFields, statisticsFields, methods);
            DataTable result = new DataTable();
            AddGroupByFieldsHead(ref result);
            AddStatisticsFieldsHead(ref result);

            EnumerableDataRowList<DataRow> enumerableRowCollection = new EnumerableDataRowList<DataRow>(source.Rows);
            Func<DataRow, String> groupingFunction = GroupData;
            var groupedDataRows = enumerableRowCollection.GroupBy(groupingFunction);
            foreach (var keys in groupedDataRows)
            {
                DataRow resultRow = result.NewRow();
                FillResultGroupBy(ref resultRow, keys.Key.Split(';'));
                DataTable temp = source.Clone();
                ChangeDataType(ref temp);//因原始数据从csv中读出,默认都是string,所以需要转换下,否则求AVG会出异常。
                foreach (var item in keys)
                {
                    temp.ImportRow(item);
                }
                FillResultStatistics(ref resultRow, temp);
                result.Rows.Add(resultRow);
            }
            TimeSpan end = new TimeSpan(DateTime.Now.Ticks);
            double allTime = end.Subtract(start).Duration().TotalSeconds;
            return result;
        }

        internal class EnumerableDataRowList<T> : IEnumerable<T>, IEnumerable
        {
        IEnumerable dataRows;
        internal EnumerableDataRowList(IEnumerable items)
        {
            dataRows = items;
        }
        IEnumerator<T> IEnumerable<T>.GetEnumerator()
        {
            foreach (T dataRow in dataRows)
                yield return dataRow;
        }
        IEnumerator IEnumerable.GetEnumerator()
        {
            IEnumerable<T> iEnumerable = this;
            return iEnumerable.GetEnumerator();
        }
    }

    另外一种不使用linq的方法如下。其基本思路是,遍历DataTable中的每一行,查看其PrimaryKey是否已经在结果集的DataTable中,如果没有,则在原始DataTable中,Select所有此PrimaryKey的行,对这些行进行Compute,然后把计算结果放入结果集的DataTable中。

        public DataTable GroupByImp(DataTable source, string[] groupByFields, string[] statisticsFields, string[] methods)
        {
            Init(groupByFields, statisticsFields, methods);
            DataTable result = new DataTable();
            AddGroupByFieldsHead(ref result);
            AddStatisticsFieldsHead(ref result);
            result.PrimaryKey = CreatePrimary(result);
            foreach (DataRow row in source.Rows)
            {
                DataRow complete = result.Rows.Find(CreateObjects(row));
                if (complete == null)
                {
                    DataRow[] rows = source.Select(CreateSelect(row));
                    DataTable temp = source.Clone();
                    for (int i = 0; i < rows.Length; i++)
                    {
                        temp.Rows.Add(rows[i].ItemArray);
                    }
                    DataRow resultRow = result.NewRow();
                    FillResultGroupBy(ref resultRow, row);
                    FillResultStatistics(ref resultRow, temp);
                    result.Rows.Add(resultRow);
                }
            }
            return result;
        }

    其实,还可以使用另外一种思路,就是直接把csv数据,bulk insert到数据库中,直接使用select A1,A2,A3,AVG(S1),MAX(S2),MIN(S3),COUNT(S4) from a group by A1,A2,A3进行查询。在此不展开讨论。

    对DataTable进行Groupby的查询,还需要满足一定的性能要求。

    利用linq的性能测试结果大致如下(不能保证此性能测试结果可以在其他场景下适用):

    75484行70列的DataTable(原始文件大约20.8M的csv),对2个字段进行分组,对4个字段进行统计,大约需要1.8s。
    19124行70列的DataTable(原始文件大约5.41M的csv),对2个字段进行分组,对4个字段进行统计,大约需要0.6s。

     可以看出,此性能不是非常的理想,因此主要看数据量大小,如果数据量控制在2w行以内,0.6s的结果,对于数据展示来讲,基本是可以接受的。

     如果需要对10w以上的DataTable进行分组,建议还是先对原始数据进行Filter,再在小结果集上进行Group by。

0 0
原创粉丝点击