[linq2db] lInq2db、EF、Dapper 查詢大量資料效能比較

新玩具 linq2db 入手,一定要拿來測測效能的,我準備了三張表,裡面各有一百萬筆,有兩種讀取方式,一個是讀取某一張表,一個是Join兩張表

開發環境

  • VS 2017 15.9.5
  • .Net Framework 4.7.2
  • Entity Framework 6.2.0
  • Dapper 1.60.1
  • linq2db 2.6.4

實作

為了方便測試,建立了一個接口,為了要把 ADO.NET 也一併納進來測試,用到了 DataTable

public interface IEmployeeRepository
{
    string ConnectionName { get; set; }
 
    object GetAll(out int count);
 
    IEnumerable<EmployeeViewModel> GetAllEmployees(out int count);
 
    IEnumerable<EmployeeViewModel> GetAllEmployeesDetail(out int count);
}
 
public interface IAdoEmployeeRepository
{
    string ConnectionName { get; set; }
 
    DataTable GetAllEmployees(out int count);
 
    DataTable GetAllEmployeesDetail(out int count);
}

 

EF

https://github.com/yaochangyu/sample.dotblog/blob/master/ORM/ORM.Benchmark/UnitTestProject2/Repository/Ef/EfNoTrackEmployeeRepository.cs
 

  • 因為別的框架沒有追蹤,比較時,也應該要關掉,這樣才公平
  • LazyLoadingEnabled 、ProxyCreationEnabled:關閉消極式載入。
  • AutoDetectChangesEnabled :停用異動追蹤,不過這次的情境並不會用到它。
  • AsNoTracking:關閉追蹤,使用查詢最關鍵的一個指令,只要查詢後沒有要立即異動,都要關掉。
public IEnumerable<EmployeeViewModel> GetAllEmployees(out int count)
{
    IEnumerable<EmployeeViewModel> results = null;
    using (var dbContext = new LabDbContext(this.ConnectionName))
    {
        dbContext.Configuration.LazyLoadingEnabled = false;
        dbContext.Configuration.ProxyCreationEnabled = false;
        dbContext.Configuration.AutoDetectChangesEnabled = false;
 
        var selector = dbContext.Employees
                                .Select(p => new EmployeeViewModel
                                {
                                    Id = p.Id,
                                    Name = p.Name,
                                    Age = p.Age,
                                    SequenceId = p.SequenceId,
                                });
 
     
        results = selector.AsNoTracking().ToList();
        count = results.Count();
    }
 
    return results;
}

 

Linq2Db

https://github.com/yaochangyu/sample.dotblog/blob/master/ORM/ORM.Benchmark/UnitTestProject2/Repository/Linq2Db/Linq2EmployeeRepository.cs
 

因為它沒有狀態追蹤,所以看起來乾淨了一些

public IEnumerable<EmployeeViewModel> GetAllEmployees(out int count)
{
    IEnumerable<EmployeeViewModel> results = null;
    using (var db = new LabEmployeeDB(this.ConnectionName))
    {
        var selector = db.Employees
                         .Select(p => new EmployeeViewModel
                         {
                             Id = p.Id,
                             Name = p.Name,
                             Age = p.Age,
                             SequenceId = p.SequenceId
                         })
                         .Where(p=>p.SequenceId>0)
                         .OrderBy(p=>p.SequenceId);
        results = selector.ToList();
        count = results.Count();
    }
 
    return results;
}

 

Dapper

https://github.com/yaochangyu/sample.dotblog/blob/master/ORM/ORM.Benchmark/UnitTestProject2/Repository/Dapper/DapperEmployeeRepository.cs
 

public IEnumerable<EmployeeViewModel> GetAllEmployees(out int count)
{
    IEnumerable<EmployeeViewModel> results = null;
 
    count = 0;
 
    using (var db = DbManager.CreateConnection(this.ConnectionName))
    {
        results = db.Query<EmployeeViewModel>(SqlEmployeeText.AllEmployee);
        count = results.Count();
    }
 
    return results;
}

 

查詢腳本 ADO.NET 跟 Dapper 都用相同的寫法

    class SqlEmployeeText
    {
        public static readonly string AllEmployee = @"
SELECT
	[p].[Id],
	[p].[Name],
	[p].[Age],
	[p].[SequenceId]
FROM
	[dbo].[Employee] [p]
WHERE
	[p].[SequenceId] > 0
ORDER BY
	[p].[SequenceId]
";
 
        public static readonly string EmployeeCount = "SELECT COUNT(1) FROM Employee";
    }

 

DataReader 轉 DataTable

https://github.com/yaochangyu/sample.dotblog/blob/master/ORM/ORM.Benchmark/UnitTestProject2/Repository/Ado/DataReaderToTableEmployeeRepository.cs
 

public DataTable GetAllEmployees(out int count)
{
    DataTable result = null;
 
    using (var dbConnection = DbManager.CreateConnection(this.ConnectionName))
    using (var dbCommand = dbConnection.CreateCommand())
    {
        dbCommand.CommandType = CommandType.Text;
 
        dbCommand.CommandText = SqlEmployeeText.AllEmployee;
        var reader = dbCommand.ExecuteReader(CommandBehavior.SequentialAccess);
        result = ToDataTable(reader);
        count = result.Rows.Count;
    }
 
    return result;
}

只要不轉 DataTable 他是最快的,但不是快就好,後面還要拿出來用

 

Adapter.Fill

https://github.com/yaochangyu/sample.dotblog/blob/master/ORM/ORM.Benchmark/UnitTestProject2/Repository/Ado/AdapterEmployeeRepository.cs
 

public DataTable GetAllEmployees(out int count)
{
    DataTable result = null;
 
    using (var dbConnection = DbManager.CreateConnection(this.ConnectionName))
    using (var dbCommand = dbConnection.CreateCommand())
    {
        dbCommand.CommandType = CommandType.Text;
 
        dbCommand.CommandText = SqlEmployeeText.AllEmployee;
        result = this.ExecuteDataTable(dbCommand);
        count = result.Rows.Count;
    }
 
    return result;
}

 

LoadDataRow

https://github.com/yaochangyu/sample.dotblog/blob/master/ORM/ORM.Benchmark/UnitTestProject2/Repository/Ado/LoadDataEmployeeRepository.cs
 

public DataTable GetAllEmployees(out int count)
{
    DataTable result = null;
 
    using (var dbConnection = DbManager.CreateConnection(this.ConnectionName))
    using (var dbCommand = dbConnection.CreateCommand())
    {
        dbCommand.CommandType = CommandType.Text;
 
        dbCommand.CommandText = SqlEmployeeText.AllEmployee;
        var reader = dbCommand.ExecuteReader(CommandBehavior.SequentialAccess);
        result = ToDataTable(reader);
 
        count = result.Rows.Count;
    }
 
    return result;
}

 

Load

https://github.com/yaochangyu/sample.dotblog/blob/master/ORM/ORM.Benchmark/UnitTestProject2/Repository/Ado/LoadEmployeeRepository.cs
 

public DataTable GetAllEmployees(out int count)
{
    DataTable result = null;
 
    using (var dbConnection = DbManager.CreateConnection(this.ConnectionName))
    using (var dbCommand = dbConnection.CreateCommand())
    {
        dbCommand.CommandType = CommandType.Text;
 
        dbCommand.CommandText = SqlEmployeeText.AllEmployee;
        var reader = dbCommand.ExecuteReader(CommandBehavior.SequentialAccess);
        result = new DataTable();
        result.Load(reader);
        count = result.Rows.Count;
    }
 
    return result;
}

 

測試程式

https://github.com/yaochangyu/sample.dotblog/blob/master/ORM/ORM.Benchmark/UnitTestProject2/BenchmarkTest.cs
 

建構函數

  1. 我把所有的 repository 放在集合,然後跑迴圈呼叫 BenchmarkManager.Add 傳入需要量測的 Method。
  2. 關閉 EF 的檢查
  3. 先載入 EF 的 Entity Model 對應
  4. 暖機一下,主要是在排除第一次執行所花費的時間,大家先跑一下
  5. 暖機的 db 是小量資料只放一筆,暖完之後就切到一百萬筆的連線字串
[TestClass]
public class BenchmarkTest
{
    private static readonly BenchmarkManager BenchmarkManager = new BenchmarkManager();
 
    static BenchmarkTest()
    {
        string connectionName = "LabDbContext";
 
        //呼叫 BenchmarkManager.Add 傳入需要量測的 Method
        foreach (var repository in Utility.AdoRepositories)
        {
            BenchmarkManager.Add(repository.Key.ToString(),
                                 () =>
                                 {
                                     var employees = repository.Value.GetAllEmployees(out var count);
                                     return new Report {RowCount = count};
                                 });
        }
 
        foreach (var repository in Utility.Repositories)
        {
            BenchmarkManager.Add(repository.Key.ToString(),
                                 () =>
                                 {
                                     var employees = repository.Value.GetAllEmployees(out var count);
                                     return new Report {RowCount = count};
                                 });
        }
 
        //不檢查migration table
        Database.SetInitializer<LabDbContext>(null);
 
        //載入對應
        using (var dbcontext = new LabDbContext(connectionName))
        {
            var objectContext = ((IObjectContextAdapter) dbcontext).ObjectContext;
            var mappingCollection =
                (StorageMappingItemCollection) objectContext.MetadataWorkspace.GetItemCollection(DataSpace.CSSpace);
            mappingCollection.GenerateViews(new List<EdmSchemaError>());
        }
 
        //暖機
        BenchmarkManager.Warm();
        Utility.Warm();
        Utility.SwitchLargeDb();
    }
 
    [TestMethod]
    public void Benchmark_Statistics_10()
    {
        BenchmarkManager.Statistics(10);
    }
}

 

BenchmarkManager.cs

https://github.com/yaochangyu/sample.dotblog/blob/master/ORM/ORM.Benchmark/UnitTestProject2/BenchmarkManager.cs
 

用 Stopwatch 量測執行時間,這裡有用到 Console Table,請參考
https://stackoverflow.com/questions/856845/how-to-best-way-to-draw-table-in-console-app-c

把要量測的方法放進 s_targets 集合

public void Add(string name, Func<Report> target)
{
    if (s_targets.ContainsKey(name))
    {
        s_targets[name] = target;
    }
    else
    {
        s_targets.Add(name, target);
    }
}

 

量測,要量測的東西由外部程試跟他說,要先調用 Add

public List<Report> Statistics(int count = 1)
{
    var firstReports = new Dictionary<string, List<Report>>();
    var currentCount = count;
    var watch = new Stopwatch();
    foreach (var target in s_targets)
    {
        var index = 1;
        while (count-- > 0)
        {
            watch.Restart();
 
            var report = target.Value.Invoke();
 
            watch.Stop();
 
            report.CostTime = watch.Elapsed.TotalMilliseconds;
            report.Name = target.Key;
            report.Index = index;
            if (firstReports.ContainsKey(target.Key) == false)
            {
                firstReports.Add(target.Key, new List<Report> {report});
            }
            else
            {
                firstReports[target.Key].Add(report);
            }
 
            index++;
        }
 
        count = currentCount;
    }
 
    var totalReports = GenerateTotalReports(firstReports);
    var detailReports = GenerateDetailReport(totalReports);
    return totalReports;
}

 

一次一百萬,量測10次

可以看到三套 ORM,相差無幾,可視為相等,不過最令我驚訝的是 EF 的 Tracking,我記得 Tracking 會花很多時間阿,這個版本似乎跟以前不一樣,找機會再來研究研究

 

Join 兩張一百萬筆的資料,量測10次

專案內有我這次測試的資料結構,然後我用 dbForge Data Generator for SQL Server 分別產出一筆及一百萬筆的資料庫

範例專案

https://github.com/yaochangyu/sample.dotblog/tree/master/ORM/ORM.Benchmark

效能測試

https://dotblogs.com.tw/yc421206/2019/03/13/query_large_data_compare_ef_dapper_linq2db
 

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo