MS SQL Server umożliwia szybkie wstawianie dużych ilości danych. Nazywa się ono kopią zbiorczą (ang. bulk copy) i jest wykonywane przez klasę SqlBulkCopy
. Porównałem już, jak szybko działa ta klasa w porównaniu z EF Core 5 w tym poście: https://www.michalbialecki.com/2020/05/06/entity-framework-core-5-vs-sqlbulkcopy/, ale tym razem chcę sprawdzić coś innego – bibliotekę linq2db.
Czym jest Linq2db
Sprawdźmy, jak Linq2db jest opisane na swojej stronie internetowej:
LINQ to DB to najszybsza biblioteka dostępu do bazy danych LINQ oferująca prostą, lekką, szybką i bezpieczną dla typów warstwę między obiektami POCO a bazą danych.
LINQ to DB is the fastest LINQ database access library offering a simple, light, fast, and type-safe layer between your POCO objects and your database (wersja oryginalna)
Brzmi imponująco i niedawno odkryłem, że istnieje pakiet rozszerzeń EF Core linq2db.EntityFrameworkCore, który jest zintegrowany z EF Core i wzbogaca DbContext o kilka fajnych funkcji.
Najważniejsze możliwości linq2db to:
- Bulk copy (bulk insert)
- szybkie wczesne ładowanie (nieporównywalnie szybsze niż wbudowane polecenie
Include
) - wsparcie dla polecenia MERGE
- wsparcie dla tabel tymczasowych
- rozszerzenia do wyszukiwania w tekście (Full-Text search)
- oraz jeszcze kilka
Napiszmy kod
W projekcie PrimeHotel zaimplementowałem już metodę z SqlBulkCopy
do wstawiania profili do bazy danych. Najpierw generuję profile za pomocą biblioteki Bogus, a następnie wstawiam je. W tym przykładzie użyję 3 metod z ProfileController
:
- GenerateAndInsert – zaiplementowana przy pomocy EF Core
- GenerateAndInsertWithSqlCopy – zaimplementowana z użyciem klasy
SqlBulkCopy
- GenerateAndInsertWithLinq2db – zaimplementowana z użyciem Linq2db
Pokażę szybko, jak wyglądają wspomniane metody. Pierwszą z nich jest GenerateAndInsert
, zaimplementowana przy pomocy czystego Entity Framework Core 5.
[HttpPost("GenerateAndInsert")] public async Task<IActionResult> GenerateAndInsert([FromBody] int count = 1000) { Stopwatch s = new Stopwatch(); s.Start(); var profiles = GenerateProfiles(count); var gererationTime = s.Elapsed.ToString(); s.Restart(); primeDbContext.Profiles.AddRange(profiles); var insertedCount = await primeDbContext.SaveChangesAsync(); return Ok(new { inserted = insertedCount, generationTime = gererationTime, insertTime = s.Elapsed.ToString() }); }
Używam klasy Stopwatch
, aby zmierzyć, ile czasu zajmuje wygenerowanie profili metodą GenerateProfiles
i ile czasu zajmuje ich wstawienie.
GenerateAndInsertWithSqlCopy
jest zaimplementowana przy pomocy klasy SqlBulkCopy
:
[HttpPost("GenerateAndInsertWithSqlCopy")] public async Task<IActionResult> GenerateAndInsertWithSqlCopy([FromBody] int count = 1000) { Stopwatch s = new Stopwatch(); s.Start(); var profiles = GenerateProfiles(count); var gererationTime = s.Elapsed.ToString(); s.Restart(); var dt = new DataTable(); dt.Columns.Add("Id"); dt.Columns.Add("Ref"); dt.Columns.Add("Forename"); dt.Columns.Add("Surname"); dt.Columns.Add("Email"); dt.Columns.Add("TelNo"); dt.Columns.Add("DateOfBirth"); foreach (var profile in profiles) { dt.Rows.Add(string.Empty, profile.Ref, profile.Forename, profile.Surname, profile.Email, profile.TelNo, profile.DateOfBirth); } using var sqlBulk = new SqlBulkCopy(connectionString); sqlBulk.DestinationTableName = "Profiles"; await sqlBulk.WriteToServerAsync(dt); return Ok(new { inserted = dt.Rows.Count, generationTime = gererationTime, insertTime = s.Elapsed.ToString() }); }
Zauważ, że ta implementacja jest znacznie dłuższa i musiałem utworzyć obiekt DataTable
, aby przekazać moje dane w postaci tabeli.
I wreszcie implementacja GenerateAndInsertWithLinq2db
, która wykorzystuje bibliotekę linq2db.
[HttpPost("GenerateAndInsertWithLinq2db")] public async Task<IActionResult> GenerateAndInsertWithLinq2db([FromBody] int count = 1000) { Stopwatch s = new Stopwatch(); s.Start(); var profiles = GenerateProfiles(count); var gererationTime = s.Elapsed.ToString(); s.Restart(); using (var db = primeDbContext.CreateLinqToDbConnection()) { await db.BulkCopyAsync(new BulkCopyOptions { TableName = "Profiles" }, profiles); } return Ok(new { inserted = profiles.Count(), generationTime = gererationTime, insertTime = s.Elapsed.ToString() }); }
Ta metoda jest prawie tak krótka jak implementacja z użyciem EF Core, ale tworzy obiekt DataConnection
z metodą CreateLinqToDbConnection
.
Wyniki
Porównałem te 3 metody z wstawianiem 1k, 10k, 50k, 100k i 500k rekordów. Jak myśliś, jak szybko udało się to osiągnąć? Sprawdźmy, poniższe dane są wyrażone w sekundach.
EF Core | Bulk insert | linq2db bulk insert | |
1000 | 0.22 | 0.035 | 0.048 |
10000 | 1.96 | 0.2 | 0.318 |
50000 | 9.63 | 0.985 | 1.54 |
100000 | 19.35 | 1.79 | 3.18 |
500000 | 104 | 9.47 | 16.56 |
Oto tabela z wynikami, w sekundach. Sam EF Core nie jest wcale imponujący, ale w połączeniu z biblioteką Linq2db jest prawie tak szybki, jak klasa SqlBulkCopy
.
A oto wykres, im niższa wartość, tym lepiej.
Zabawne – podczas testów zauważyłem, że generowanie danych testowych jest faktycznie wolniejsze niż wstawianie do bazy danych, wow 😀
Podsumowanie
Linq2db to imponująca biblioteka, która oferuje już całkiem sporo. Z tego co można zobaczyć na GitHub wygląda na to, że jest to dobrze ugruntowany projekt z wieloma współtwórcami. Wiedząc to, jestem zdziwiony, że wcześniej na niego nie trafiłem.
Wstawianie zbiorcze z linq2db jest prawie tak szybkie, jak użycie klasy SqlBulkCopy
, ale jest znacznie czystsze i krótsze. Jest też mniej podatne na błędy i na pewno użyłbym go w swoich projektach.
Cały zamieszczony tutaj kod jest dostępny na moim GitHub.
Mam nadzieję, że ci się przyda, pozdro! 😄