🚀 Fast SQL Bulk Inserts With C# and EF Core
فرقی نمیکند دارید یک پلتفرم تحلیل داده میسازید، یک سیستم قدیمی را مهاجرت میدهید، یا یک موج بزرگ از کاربران جدید وارد سیستمتان میشوند—بهاحتمال زیاد زمانی میرسد که باید مقدار عظیمی داده را وارد دیتابیس کنید.
وارد کردن رکوردها یکییکی مثل این است که خشکشدن رنگ را در حرکت آهسته تماشا کنید! روشهای سنتی اصلاً جواب نمیدهند.
پس یاد گرفتن تکنیکهای bulk insert سریع با #C و EF Core تبدیل به یک مهارت ضروری میشود.
در این مقاله، چندین روش برای انجام bulk insert در #C بررسی میکنیم:
• Dapper
• EF Core
• EF Core Bulk extensions
• SQL Bulk Copy
• Entity Framework Extensions
تمام مثالها بر اساس یک کلاس User و جدول Users در SQL Server هستند:
public class User
{
public int Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string PhoneNumber { get; set; }
}
این لیست کامل تمام روشهای bulk insert نیست. چند گزینه دیگر بررسی نشدهاند، مثل تولید دستی SQL یا استفاده از Table-Valued parameters.
📌EF Core Simple Approach
بیایید با یک مثال ساده با EF Core شروع کنیم. ما یک ApplicationDbContext میسازیم، یک شی User اضافه میکنیم و SaveChangesAsync را صدا میزنیم. این کار باعث میشود هر رکورد بهصورت جداگانه در دیتابیس insert شود. یعنی برای هر رکورد یک round trip به دیتابیس نیاز داریم.
using var context = new ApplicationDbContext();
foreach (var user in GetUsers())
{
context.Users.Add(user);
await context.SaveChangesAsync();
}
و نتایج دقیقاً همانقدر ضعیف هستند که انتظار دارید:
EF Core - Add one and save برای 100 کاربر: 20 ms
EF Core - Add one and save برای 1,000 کاربر: 260 ms
EF Core - Add one and save برای 10,000 کاربر: 8,860 ms
نتایج 100,000 و 1,000,000 رکورد حذف شدند چون اجرای آنها بیش از حد طولانی شد.
این مثال را بهعنوان «چگونه bulk insert انجام ندهیم» در نظر بگیرید.
🧩Dapper Simple Insert
Dapper
یک SQL-to-object mapper ساده برای NET. است. این ابزار اجازه میدهد مجموعهای از اشیا را بهراحتی در دیتابیس insert کنیم.
من از قابلیت Dapper برای unwrap کردن یک collection داخل یک SQL INSERT statement استفاده میکنم.
using var connection = new SqlConnection(connectionString);
connection.Open();
const string sql =
@"
INSERT INTO Users (Email, FirstName, LastName, PhoneNumber)
VALUES (@Email, @FirstName, @LastName, @PhoneNumber);
";
await connection.ExecuteAsync(sql, GetUsers());
نتایج بسیار بهتر از مثال ابتدایی هستند:
Dapper - Insert range برای 100 کاربر: 10 ms
Dapper - Insert range برای 1,000 کاربر: 113 ms
Dapper - Insert range برای 10,000 کاربر: 1,028 ms
Dapper - Insert range برای 100,000 کاربر: 10,916 ms
Dapper - Insert range برای 1,000,000 کاربر: 109,065 ms
🔸️EF Core Add and Save
اما EF Core هم هنوز کنار نکشیده. مثال اول عمداً خیلی ضعیف پیادهسازی شده بود. EF Core میتواند چندین SQL statement را با هم batch کند، پس از این قابلیت استفاده میکنیم.
اگر یک تغییر ساده بدهیم، میتوانیم عملکرد بهمراتب بهتری بگیریم. اول، همهٔ اشیا را به ApplicationDbContext اضافه میکنیم. سپس فقط یکبار SaveChangesAsync را صدا میزنیم.
این EF یک batched SQL statement میسازد—یعنی چندین INSERT را با هم گروه میکند—و همه را یکجا به دیتابیس میفرستد. این کار تعداد round trip ها را کم میکند و باعث بهبود سرعت میشود.
using var context = new ApplicationDbContext();
foreach (var user in GetUsers())
{
context.Users.Add(user);
}
await context.SaveChangesAsync();
نتایج benchmark این پیادهسازی:
EF Core - Add all and save برای 100 کاربر: 2 ms
EF Core - Add all and save برای 1,000 کاربر: 18 ms
EF Core - Add all and save برای 10,000 کاربر: 203 ms
EF Core - Add all and save برای 100,000 کاربر: 2,129 ms
EF Core - Add all and save برای 1,000,000 کاربر: 21,557 ms
به یاد داشته باشید: Dapper برای insert کردن 1,000,000 رکورد حدود 109 ثانیه زمان گرفت. با EF Core و batched queries، همان کار را در حدود 21 ثانیه انجام میدهیم.