2010年4月21日 星期三

在 DataTable 進行 Group by 並且取 Sum 的運算

今天幫忙同事以 DataTable 進行 Group by 並且取 Sum 的運算

知道方法有很多,我就自己懂的部分,寫下純 ADO.NET 的作法,先以 DataTable 的 ToTable 方法取得 distinct 過的群組鍵,接下來建立 Relation 關連回原 DataTable,最後利用在群組鍵 DataTable 中增加自動計算欄位 (Expression 屬性,透過 DataColumnCollection.Add method 設定) 完成關聯記錄的計算

見範例程式中 TestByPureAdoNet method 中寫得密密麻麻高亮的那幾行就是了

我知道 LINQ to DataSet 能幫我做到一樣的事情,但是僅停留在大概知道怎麼做的階段,雖然同事的 .NET 2.0 環境也用不上,還是借機小練了一下,這才發現效能差距不小...

LINQ 的作法可以從範例中的 TestByLinq method 看到

using System;
using System.Linq;
using System.Data;
using System.Diagnostics;

class Program
{
    static void Main(string[] args)
    {
        Stopwatch stopWatch = new Stopwatch();

        Console.WriteLine("Testing by pure ADO.NET...");
        stopWatch.Start();
        TestByPureAdoNet();
        stopWatch.Stop();
        Console.WriteLine(stopWatch.ElapsedMilliseconds + "ms");

        Console.WriteLine("Testing by LINQ...");
        stopWatch.Reset();
        stopWatch.Start();
        TestByLinq();
        stopWatch.Stop();
        Console.WriteLine(stopWatch.ElapsedMilliseconds + "ms");

        Console.WriteLine("Press any key to exit...");
        Console.ReadKey();
    }

    private static void TestByPureAdoNet()
    {
        for (int i = 0; i < 10000; i++)
        {
            DataSet ds = GetDataSet();
            DataTable dt = ds.Tables[0];

            // 分組並計算 
            DataTable grouped = dt.DefaultView.ToTable("DistinctTable", 
                                                       true, 
                                                       new[]{"GroupKey"});
            ds.Tables.Add(grouped);
            ds.Relations.Add("Relation1", 
                             grouped.Columns["GroupKey"], 
                             dt.Columns["GroupKey"]);
            grouped.Columns.Add("SumValue", 
                                typeof(int), 
                                "Sum(Child.Value)");

            foreach (DataRow dr in grouped.Rows)
            {
                var key = dr["GroupKey"];
                var value = dr["SumValue"];
                if (i == 0) System.Console.WriteLine(key + " = " + value);
            }
        }
    }

    private static void TestByLinq()
    {
        for (int i = 0; i < 10000; i++)
        {
            DataSet ds = GetDataSet();
            DataTable dt = ds.Tables[0];

            // 分組並計算 
            var grouped = from row in dt.AsEnumerable()
                          group row by row["GroupKey"] into g
                          select new 
                          {
                            GroupKey = g.Key, 
                            SumValue = g.Sum(p => (int)p["Value"]) 
                          };

            foreach (var group in grouped)
            {
                var key = group.GroupKey;
                var value = group.SumValue;
                if (i == 0) System.Console.WriteLine(key + " = " + value);
            }
        }
    }

    // 產生測試資料集
    public static DataSet GetDataSet()
    {
        DataSet ds = new DataSet();

        DataTable dt = new DataTable();
        dt.Columns.Add("GroupKey", typeof(String));
        dt.Columns.Add("Value", typeof(int));
        ds.Tables.Add(dt);

        dt.Rows.Add(new object[] { "P1", 10 });
        dt.Rows.Add(new object[] { "P2", 40 });
        dt.Rows.Add(new object[] { "P3", 50 });
        dt.Rows.Add(new object[] { "P1", 20 });
        dt.Rows.Add(new object[] { "P2", 5 });

        return ds;
    }
}

最後既然 method 都實做了,當然要來量測一下效能,在 main 中使用 Stopwatch 取得測試 method 的執行花費時間,每個測試 method 中均反覆執行 10000 次以放大效能表現數值,發現以 LINQ to DataSet 進行查詢的效能平均約為純 ADO.NET 方式的 5~6 倍!



不過這種作業要反覆執行這麼多次的機會並不高,如果僅執行一次而言,initial cost + 執行時間,兩者相當,以我測試環境下測得均為 3ms

感嘆 LINQ 果然是好物啊!見到發明人 Anders Hejlsberg 快拜就對了!

參考資料:
101 LINQ Samples

1 則留言: