LINQ の INSERT が遅いときは AutoDetectChangesEnabled を False にする

とあるシステムで1万件程度のテーブルを構成しなおして別の複数のテーブルに移すことをやっていた。いわゆる、正規化していないテーブルをデータ移行の際に正規化しようと思って、複数のテーブルに分けたのだが、たかだか1万件しかないのに非常に遅い。正規化のロジックが遅いのかもしれないけど、1万件程度をデータ挿入するのに30分位掛かってしまうのである。
相手が SQL Server なので SqlBulkCopy を使えば結構なスピードになるはずなのだが、100万件の場合ならばそうかもしれないけど、たかだか1万件の挿入でこんなに遅いのは変。ということで、LINQ の INSERT について調べなおしてみる。

結論

結論から言えば、AutoDetectChangesEnabled と ValidateOnSaveEnabled を OFF(false) にすればよい。

DbContextConfiguration.AutoDetectChangesEnabled Property
https://docs.microsoft.com/en-us/dotnet/api/system.data.entity.infrastructure.dbcontextconfiguration.autodetectchangesenabled?redirectedfrom=MSDN&view=entity-framework-6.2.0#overloads

ent.Configuration.AutoDetectChangesEnabled = false;
ent.Configuration.ValidateOnSaveEnabled = false;

LINQ で INSERT/DELETE/UPDATE をする場合、EF の内部で整合性をチェックしている。System.Data.Entity.DbSet.Add などを呼び出したときに、DetectChanges() でチェックをしているというわけだ。どうやらこれが遅い原因なので、素直に System.Data.Entity.Infrastructure.DbContextConfiguration.AutoDetectChangesEnabled の値を false にして呼び出さないようにすればよい。
デフォルトでは、AutoDetectChangesEnabled が true となっている。

実験

1
2
3
4
5
CREATE TABLE [dbo].[BulkT](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [GUID] [varchar](100) NOT NULL,
    [Created] [datetime] NOT NULL
)

この BulkT テーブルに1万件のデータを挿入する。SaveChanges を1回の挿入ごとに行っているが、テーブルが複雑な場合は最後に1回だけだとメモリを食いすぎたりするため、何度かに分ける(100回毎など)必要がある。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private void clickNormal(object sender, RoutedEventArgs e)
{
    var ent = new testdbEntities();
    ent.Database.ExecuteSqlCommand("delete BulkT");
    var start = DateTime.Now;
    for ( int i=0; i<10000; i++ )
    {
        var t = new BulkT()
        {
            GUID = Guid.NewGuid().ToString("N"),
            Created = DateTime.Now,
        };
        ent.BulkT.Add(t);
        ent.SaveChanges();
    }
    var tend = DateTime.Now;
    var span = (tend - start).TotalSeconds;
    System.Diagnostics.Debug.WriteLine(span.ToString());
}
 
private void clickNoAutoDetect(object sender, RoutedEventArgs e)
{
    var ent = new testdbEntities();
    ent.Database.ExecuteSqlCommand("delete BulkT");
    var start = DateTime.Now;
    ent.Configuration.AutoDetectChangesEnabled = false;
    ent.Configuration.ValidateOnSaveEnabled = false;
    for (int i = 0; i < 10000; i++)
    {
        var t = new BulkT()
        {
            GUID = Guid.NewGuid().ToString("N"),
            Created = DateTime.Now,
        };
        ent.BulkT.Add(t);
        ent.SaveChanges();
    }
    var tend = DateTime.Now;
    var span = (tend - start).TotalSeconds;
    System.Diagnostics.Debug.WriteLine(span.ToString());
}

DetectChanges のチェックあり(AutoDetectChangesEnabled = true)
50.1799167
48.9856614
48.5278795

DetectChanges のチェック無し(AutoDetectChangesEnabled = false)
6.1251368
6.1039912
6.2945836

このように8倍ぐらいの差がでてくる。100件程度ならば特に問題もでないだろうが、1万件以上ある場合は気を付けたおいたほうがよいだろう。データチェックが入らないので、挿入データに気を付ける必要があるが、今回のように空のテーブルに挿入する場合はプログラム内でチェックが済んでいるので特に問題はない。

ちなみに、30分以上掛かっていたデータコンバートは1分以内に終わるようになった。

参考リンク

Entity Framework のパフォーマンス #2 更新処理 | C#.NET vs VB.NET
http://csharpvbcomparer.blogspot.com/2015/04/net-ef-performance-2-updating.html

カテゴリー: 開発, C# パーマリンク