Я запускаю импорт, который будет иметь 1000 записей при каждом запуске. Просто ищу подтверждение моим предположениям:
Что из этого имеет наибольшее значение:
AddToClassName()
SaveChanges()
каждый AddToClassName()
. SaveChanges()
каждый n номеров AddToClassName()
. SaveChanges()
после всех AddToClassName()
. Первый вариант, вероятно, медленный, верно? Так как для этого нужно будет анализировать объекты EF в памяти, генерировать SQL и т. Д.
Я предполагаю, что второй вариант является лучшим из обоих миров, поскольку мы можем обернуть попытку перехватить этот вызов SaveChanges()
и потерять только n записей за раз, если одна из них завершится неудачно. Возможно сохранить каждую партию в списке <>. Если вызов SaveChanges()
завершился успешно, избавьтесь от списка. Если это не удается, зарегистрируйте элементы.
Последний вариант, вероятно, также будет очень медленным, так как каждый отдельный объект EF должен находиться в памяти до SaveChanges()
. И если сохранение не удастся, ничего не будет совершено, верно?
Я бы сначала проверил это, чтобы быть уверенным. Производительность не должна быть такой плохой.
Если вам нужно ввести все строки в одной транзакции, вызовите ее после всего класса AddToClassName. Если строки можно вводить независимо, сохраняйте изменения после каждой строки. Согласованность базы данных важна.
Второй вариант мне не нравится. Для меня было бы непонятно (с точки зрения конечного пользователя), если бы я выполнял импорт в систему, и он уменьшил бы 10 строк из 1000, просто потому что 1 - это плохо. Вы можете попробовать импортировать 10, и если это не удалось, попробуйте один за другим, а затем войдите.
Проверьте, если это займет много времени. Не пишите «вероятно». Ты еще этого не знаешь. Только когда это действительно проблема, подумайте о другом решении (marc_s).
РЕДАКТИРОВАТЬ
Я сделал несколько тестов (время в миллисекундах):
10000 строк:
SaveChanges () после 1 строки: 18510,534
SaveChanges () после 100 строк: 4350,3075
SaveChanges () после 10000 строк: 5233,0635
50000 строк:
SaveChanges () после 1 строки: 78496,929
SaveChanges () после 500 строк: 22302,2835
SaveChanges () после 50000 строк: 24022,8765
Таким образом, на самом деле быстрее сделать коммит после n строк, чем после всех.
Я рекомендую:
Тестовые занятия:
ТАБЛИЦА:
CREATE TABLE [dbo].[TestTable](
[ID] [int] IDENTITY(1,1) NOT NULL,
[SomeInt] [int] NOT NULL,
[SomeVarchar] [varchar](100) NOT NULL,
[SomeOtherVarchar] [varchar](50) NOT NULL,
[SomeOtherInt] [int] NULL,
CONSTRAINT [PkTestTable] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
Учебный класс:
public class TestController : Controller
{
//
// GET: /Test/
private readonly Random _rng = new Random();
private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private string RandomString(int size)
{
var randomSize = _rng.Next(size);
char[] buffer = new char[randomSize];
for (int i = 0; i < randomSize; i++)
{
buffer[i] = _chars[_rng.Next(_chars.Length)];
}
return new string(buffer);
}
public ActionResult EFPerformance()
{
string result = "";
TruncateTable();
result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(10000, 1).TotalMilliseconds + "<br/>";
TruncateTable();
result = result + "SaveChanges() after 100 rows:" + EFPerformanceTest(10000, 100).TotalMilliseconds + "<br/>";
TruncateTable();
result = result + "SaveChanges() after 10000 rows:" + EFPerformanceTest(10000, 10000).TotalMilliseconds + "<br/>";
TruncateTable();
result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(50000, 1).TotalMilliseconds + "<br/>";
TruncateTable();
result = result + "SaveChanges() after 500 rows:" + EFPerformanceTest(50000, 500).TotalMilliseconds + "<br/>";
TruncateTable();
result = result + "SaveChanges() after 50000 rows:" + EFPerformanceTest(50000, 50000).TotalMilliseconds + "<br/>";
TruncateTable();
return Content(result);
}
private void TruncateTable()
{
using (var context = new CamelTrapEntities())
{
var connection = ((EntityConnection)context.Connection).StoreConnection;
connection.Open();
var command = connection.CreateCommand();
command.CommandText = @"TRUNCATE TABLE TestTable";
command.ExecuteNonQuery();
}
}
private TimeSpan EFPerformanceTest(int noOfRows, int commitAfterRows)
{
var startDate = DateTime.Now;
using (var context = new CamelTrapEntities())
{
for (int i = 1; i <= noOfRows; ++i)
{
var testItem = new TestTable();
testItem.SomeVarchar = RandomString(100);
testItem.SomeOtherVarchar = RandomString(50);
testItem.SomeInt = _rng.Next(10000);
testItem.SomeOtherInt = _rng.Next(200000);
context.AddToTestTable(testItem);
if (i % commitAfterRows == 0) context.SaveChanges();
}
}
var endDate = DateTime.Now;
return endDate.Subtract(startDate);
}
}
Я только что оптимизировал очень похожую проблему в своем собственном коде и хотел бы указать на оптимизацию, которая работала для меня.
Я обнаружил, что большая часть времени при обработке SaveChanges, будь то обработка 100 или 1000 записей одновременно, связана с процессором. Таким образом, обработав контексты с использованием шаблона «производитель / потребитель» (реализованного с помощью BlockingCollection), я смог значительно лучше использовать ядра ЦП и получил в общей сложности 4000 изменений в секунду (согласно возвращаемому значению SaveChanges) в более 14 000 изменений в секунду. Загрузка процессора увеличилась с 13% (у меня 8 ядер) до 60%. Даже используя несколько потоков пользователей, я едва облагал налогами (очень быструю) систему дискового ввода-вывода, а загрузка ЦП SQL Server не превышала 15%.
Разгрузив сохранение в несколько потоков, вы можете настроить как количество записей до фиксации, так и количество потоков, выполняющих операции фиксации.
Я обнаружил, что создание 1 потока производителя и (# ядер ЦП) -1 потоков потребителя позволило мне настроить количество записей, зафиксированных в пакете, так, чтобы число элементов в BlockingCollection колебалось от 0 до 1 (после того, как поток потребителя занял один вещь). Таким образом, было достаточно работы, чтобы потребляющие потоки работали оптимально.
Этот сценарий, конечно, требует создания нового контекста для каждого пакета, который я считаю более быстрым даже в однопоточном сценарии для моего варианта использования.