.NET 6 работает быстро. Очень быстро! Поскольку я ❤️ .NET, я собираюсь показать вам, насколько быстр .NET 6!
В этой короткой статье или, скажем так, примере (исходный код), я собираюсь показать вам, как вставить 1.5 миллиона записей, а затем получить, массировать и снова вставить за очень короткое время.
Прежде всего, я собираюсь создать простую таблицу в SQL Server 2019, как вы можете видеть ниже.
- Имя базы данных : BenchMarkDB
- Имя таблицы должно быть : StockMarket
Структуру таблицы вы можете увидеть на рисунке.
Сценарий таблицы также предоставляется.
CREATE TABLE [dbo].[StockMarket](
[ID] [int] IDENTITY(1,1) NOT NULL,
[StockShare] [int] NULL,
[Name] [nvarchar](50) NULL,
[Family] [nvarchar](50) NULL,
[State] [nvarchar](50) NULL,
[LogTime] [datetime] NULL,
CONSTRAINT [PK_StockMarketNormal] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
Поскольку я собираюсь использовать таблицу с оптимизацией памяти, вам следует преобразовать вашу таблицу в таблицу с оптимизацией памяти, чтобы получить наилучший результат производительности. Как это сделать? Следуйте рисунку и инструкции ниже.
Нажмите на уважаемый пункт, после чего вы увидите следующую страницу.
Если вы нажмете на Next, следующей страницей должна быть следующая страница, как я показал ниже.
Продолжайте нажимать на кнопку Далее
Как видно на скриншоте, SQL Server создаст новую таблицу и изменит имя текущей таблицы с суффиксом «_old», чтобы сохранить все, что вы уже создали, как есть.
Нажмите кнопку Далее, после чего вы увидите следующую страницу.
Работа с SQL Server почти закончена, нажмите кнопку Далее, чтобы увидеть следующую страницу.
Нажмите кнопку Migrate, и вы закончите работу с SQL Server.
Нажмите на кнопку OK, чтобы увидеть новую таблицу.
Это должен быть последний снимок экрана, касающийся создания оптимизированной таблицы In-Memory.
Как я уже сказал, SQL Server вполне достаточно. Я создал простое консольное приложение .NET 6 и назвал его «EF_6_BenchMark». Затем, как вы можете видеть, есть папка, которую я назвал «EF_6_Performance».
Все, что находится вне этой папки, кроме Program.cs, совершенно не имеет отношения к данному образцу и должно быть проигнорировано.
Прежде чем начать разговор о кодировании, давайте добавим все необходимое из Nuget. Прежде всего, пожалуйста, установите пакет Nuget, который я представил ниже.
Если вам сложно работать с картинками, вы можете скопировать и вставить название Nuget, как я указал ниже. Я не говорю установить его командой, я говорю, что вы можете скопировать название аддона из текста ниже, но если вы заинтересованы в добавлении командой, продолжайте, я уже предоставил то, что вам нужно.
Install-Package EFCore.BulkExtensions -Version 6.5.6
Следующим пакетом Nuget будет этот:
Вам нужен текст, он уже здесь:
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 7.0.0-preview.6.22329.4
Я создал папку и назвал ее : Entity, поэтому давайте посмотрим, что происходит внутри этой папки. Здесь есть простая модель/сущность, связанная с моей таблицей. Взгляните:
public class StockMarket
{
[Key]
public int ID { get; set; }
[Column(TypeName = "varchar")]
public string Name { get; set; }
[Column(TypeName = "varchar")]
public string Family { get; set; }
public int StockShare { get; set; }
[Column(TypeName = "varchar")]
public string State { get; set; }
public DateTime LogTime { get; set; }
}
Следующая папка — : DbContext, которая содержит мой DbContext и его конфигурации, как показано ниже.
public partial class StockMarketContext : DbContext
{
public StockMarketContext()
{
}
public StockMarketContext(DbContextOptions<StockMarketContext> options)
: base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=ALIKOLAHDOOZAN;Database=BenchMarkDB;Trusted_Connection=True;",
options => options.EnableRetryOnFailure(
maxRetryCount: 4,
maxRetryDelay: TimeSpan.FromSeconds(1),
errorNumbersToAdd: new int[] { }
));
base.OnConfiguring(optionsBuilder);
}
public virtual DbSet<StockMarket> StockMarket { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<StockMarket>(entity =>
{
entity.ToTable("StockMarket");
entity.Property(e => e.ID).HasColumnName("ID");
entity.Property(e => e.Name)
.HasMaxLength(50)
.IsUnicode(false);
entity.Property(e => e.Family)
.HasMaxLength(100)
.IsUnicode(false);
entity.Property(e => e.StockShare)
.IsUnicode(false);
entity.Property(e => e.State)
.HasMaxLength(10)
.IsUnicode(false);
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
Здесь находится моя строка подключения и все конфигурации, касающиеся моей сущности.
Следующая папка : Service. Внутри папки Service у меня есть две вложенные папки.
- Интерфейс
- Конкретный
Давайте посмотрим на содержимое интерфейса.
public interface IStockMarketService
{
Task BulkInsertAsync(int NumberOfRows, string state);
void BulkUpdate(int NumberOfRows);
void BulkDelete(int NumberOfRows);
Task BulkInsertfromlist(IEnumerable<StockMarket> Data);
List<StockMarket> LoadStockBasedOnState(string State);
Task<IEnumerable<StockMarket>> LoadStockBasedOnStateByRawSQL(string State);
}
Как вы видите, я должен реализовать интерфейс, но прежде чем это сделать, давайте посмотрим на сценарий, который я собираюсь реализовать.
У моей сущности есть поле State. Это означает, что моя таблица в базе данных также имеет колонку State. Если вы посмотрите на метод BulkInsertAsync в моем интерфейсе, у метода есть два входных параметра. NumberOfRows и state, поэтому после реализации интерфейса я могу вставить столько записей, сколько захочу для каждого штата, используя входные параметры NumberOfRows и state (мой план — 150_000 строк для каждого штата), так что это означает, что, имея 10 различных штатов, я могу вставить 1,5 миллиона строк, затем я получу все данные, принадлежащие каждому штату, изменю значение сущности StockShare и затем снова вставлю строку в таблицу, так что в конце истории у меня будет 3 миллиона строк в моей таблице SQL. Как я уже пробовал, в .NET 6 это делается очень быстро! Давайте вернемся к коду и посмотрим, что происходит в реализации Service.
public class StockMarketService : IStockMarketService
{
public async Task BulkInsertAsync(int NumberOfRows, string state)
{
var _StockMarketContext = new StockMarketContext();
var stockMarkets = Enumerable.Range(0, NumberOfRows).Select(i => new StockMarket
{
Name = i.ToString(),
Family = i.ToString(),
State = state,
StockShare = i,
LogTime=DateTime.Now
}).ToList();
await _StockMarketContext.BulkInsertAsync(stockMarkets);
}
public async Task BulkInsertfromlist(IEnumerable<StockMarket> Data)
{
var _StockMarketContext = new StockMarketContext();
await _StockMarketContext.BulkInsertAsync(Data.ToList());
}
public async Task<IEnumerable<StockMarket>> LoadStockBasedOnStateByRawSQL(string State)
{
var _StockMarketContext = new StockMarketContext();
var data = await _StockMarketContext.StockMarket
.FromSqlRaw("SELECT [ID],[StockShare],[Name],[Family],[State],[LogTime] FROM [dbo].[StockMarket] Where [State] = {0}", State)
.AsNoTracking().ToArrayAsync();
return data;
}
public List<StockMarket> LoadStockBasedOnState(string State)
{
var _StockMarketContext = new StockMarketContext();
var data = _StockMarketContext.StockMarket
.Where(x=>x.State==State)
.AsNoTracking().ToList();
return data;
}
public void BulkDelete(int NumberOfRows)
{
throw new NotImplementedException();
}
public void BulkUpdate(int NumberOfRows)
{
throw new NotImplementedException();
}
//public Array LoadStockBasedOnStateCompiledQuery(string State)
//{
// using (var db = new StockMarketContext())
// {
// var Data = ListOfStockBasedOnStateByCompiledQuery(db, State);
// return Data;
// }
//}
//public List<StockMarket> LoadStockBasedOnStateByRawSQL(string State)
//{
// StockMarketContext _StockMarketContext = new StockMarketContext();
// var data = _StockMarketContext.StockMarket.FromSqlRaw("SELECT [ID],[StockShare],[Name],[Family],[State] FROM[dbo].[StockMarket] Where [State] = {0}", State)
// .AsNoTracking()
// .ToList();
// return data;
//}
//private static Func<StockMarketContext, string, Array> ListOfStockBasedOnStateByCompiledQuery =
// EF.CompileQuery((StockMarketContext db, string State) => db.StockMarket
// .Where(x => x.State == State)
// .AsNoTracking().ToArray());
}
Я собираюсь использовать сервис внутри program.cs для вставки, массажа и повторной вставки строк для каждого состояния. Поскольку мне нужно наблюдать за производительностью и тем, что происходит во время выполнения, я создал папку Helper, которая содержит очень простой пользовательский логгер, как показано ниже:
public static class CustomLogger
{
static Dictionary<DateTime, string> ListOfTimes = new();
public static void SaveTime(string Message)
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fffffff} : {Message}");
ListOfTimes.Add(DateTime.Now,Message);
}
public static void SaveToFile()
{
var sb = new StringBuilder();
ListOfTimes.ToList().ForEach(kvp => sb.AppendLine($"{kvp.Key:HH:mm:ss.fffffff} : {kvp.Value}"));
var LogWriter = System.IO.File.CreateText(@"C:tempBenchMarkLogs.txt");
LogWriter.Write(sb.ToString());
LogWriter.Close();
}
}
}
Далее представлен мой файл program.cs ! Прежде всего, напомню, что мы будем иметь дело с 10 состояниями!
string[] States = {
"Alabama",
"Alaska",
"Arizona",
"Arkansas",
"California",
"Colorado",
"Connecticut",
"Delaware",
"Florida",
"Georgia"
};
Это вся моя программа.cs
using EF_6_BenchMark.EF_6_Performance.Entity;
using EF_6_BenchMark.EF_6_Performance.Helper;
using EF_6_BenchMark.EF_6_Performance.Service.Concrete;
using System.Diagnostics;
//Start Time
CustomLogger.SaveTime("Main Thread: Application has just been Started...");
Stopwatch stw = new Stopwatch();
IEnumerable<StockMarket> data;
StockMarketService Sr = new StockMarketService();
const int Row_Numbers = 150_000;
string[] States = {
"Alabama",
"Alaska",
"Arizona",
"Arkansas",
"California",
"Colorado",
"Connecticut",
"Delaware",
"Florida",
"Georgia"
};
stw.Start();
CustomLogger.SaveTime("Main Thread: StopWatch has just been Started...");
await Parallel.ForEachAsync(States, (state, tmp) =>
{
CustomLogger.SaveTime($"Thread of State {state} : Iteration has just been Started");
Parallel.Invoke(
async () =>
{
CustomLogger.SaveTime($"Thread of operation for State:{state} - Iteration has just been Started");
await Sr.BulkInsertAsync(Row_Numbers, state);
//
data = await Sr.LoadStockBasedOnStateByRawSQL(state);
Parallel.ForEach(data, x => x.StockShare += 10);
await Sr.BulkInsertfromlist(data);
CustomLogger.SaveTime($"Thread of operation for State:{state} - Iteration has just been Finished");
}
);
CustomLogger.SaveTime($"Thread of State {state} : Iteration has just been Finished");
return ValueTask.CompletedTask;
});
stw.Stop();
CustomLogger.SaveTime("Main Thread: StopWatch has just been FINISHED");
Thread.Sleep(10000);
CustomLogger.SaveTime("Main Thread: Application has just been FINISHED");
CustomLogger.SaveToFile();
Console.ReadKey();
Вы можете видеть, что я собираюсь работать с 150 000 строк для каждого штата:
const int Row_Numbers = 150_000;
Вся моя программа.cs состоит из протоколирования и Console.Write или использования StopWatch. Наиболее важной частью является использование метода BulkInsertAsync для вставки 150,000 строк для каждого штата, а затем получение данных с помощью LoadStockBasedOnStateByRawSQL для каждого штата, массирование данных с помощью :
Parallel.ForEach(data, x => x.StockShare += 10);
и затем, я должен BulkInsert весь список с помощью : BulkInsertfromlist и готово!
Это означает, что наиболее важной частью кода является :
await Parallel.ForEachAsync(States, (state, tmp) =>
{
CustomLogger.SaveTime($"Thread of State {state} : Iteration has just been Started");
Parallel.Invoke(
async () =>
{
CustomLogger.SaveTime($"Thread of operation for State:{state} - Iteration has just been Started");
await Sr.BulkInsertAsync(Row_Numbers, state);
//
data = await Sr.LoadStockBasedOnStateByRawSQL(state);
Parallel.ForEach(data, x => x.StockShare += 10);
await Sr.BulkInsertfromlist(data);
CustomLogger.SaveTime($"Thread of operation for State:{state} - Iteration has just been Finished");
}
);
CustomLogger.SaveTime($"Thread of State {state} : Iteration has just been Finished");
return ValueTask.CompletedTask;
});
Не забудьте изменить строку подключения в методе OnConfiguring внутри StockMarketContext.cs, затем вы можете запустить код и посмотреть результат.
Я также собрал результат в текстовый файл, поэтому вы должны держать консоль открытой, чтобы иметь возможность просмотреть результат. Кроме того, вы можете увидеть, сколько времени .NET 6 и SQL Server требуется для вставки первой строки, а затем последней строки после получения и изменения значения StockShare в таблице SQL в миллисекундах.
Используя эти 3 T-SQL Statement, вы можете просто увидеть результат.
Select Top 100 * from [BenchMarkDB].[dbo].[StockMarket] Where State='Alabama'
Order By ID Desc
Select FORMAT(Count(*),'N0')
From [BenchMarkDB].[dbo].[StockMarket]
Select DATEDIFF(MILLISECOND,Min(LogTime),MAX(LogTime)) As TimeDiff
From [BenchMarkDB].[dbo].[StockMarket]
Я знаю, что вы ищете репозиторий GitHub: GitHub repository
Если ссылка по какой-то причине не работает, найдите ссылку ниже:
https://github.com/AliCharper/DOTNET_6_Performance
Если вы хотите узнать спецификацию моего оборудования, посмотрите на картинку ниже!
Я желаю, чтобы всем понравился контент, и на этом я заканчиваю.
Наслаждайтесь и продолжайте кодить на .NET!
Али Колахдоозан
Я ❤️ .NET
KUL, Малайзия