Почему вставка 1M записей медленнее без транзакции, чем внутри транзакции?

Я делаю некоторые тесты производительности, используя .Net 3.5 против SQL Server. Я делаю вставку 1 миллиона записей. Когда я заключаю это в транзакцию (сериализуемую, RepeatabelRead или ReadUncommited), она выполняется менее чем за 80 секунд в моей системе. Когда я удаляю транзакцию, она выполняется примерно через 300 секунд. Я ожидаю, что использование отсутствия транзакций будет самым быстрым способом вставки строк в базу данных, потому что СУБД не нужно учитывать потенциальный откат. Что здесь происходит? Типично ли это для SQL Server, поставщика SQL Server ADO.Net, ADO.Net в целом, СУБД в целом?

У меня есть опыт работы с базами данных iSeries/DB2. В DB2 вы должны включить ведение журнала, прежде чем сможете получить управление фиксацией и транзакциями, а ведение журнала относительно дорого.

Что я на самом деле хотел сделать, так это сравнить вставки SqlCommand и вставки Entity Framework, но я был настолько удивлен этими результатами, что хотел сначала выяснить, что здесь происходит.

Ниже код, который я использую для запуска теста. Когда я запускаю приведенный ниже код, это занимает около 74 секунд (измеряется между строками журнала AtStart и строками журнала AtEnd).

using (SqlConnection sqlConnection = new SqlConnection(connectionString))
{
    sqlConnection.Open();
    SqlCommand deleteCommand = new SqlCommand("DELETE FROM LockTest");
    deleteCommand.Connection = sqlConnection;
    deleteCommand.ExecuteNonQuery();

    using (SqlTransaction transaction = sqlConnection.BeginTransaction(System.Data.IsolationLevel.Serializable))
    {
        try
        {
            if (DEBUG) LOG.Debug("AtStart");

            SqlCommand insertCommand = new SqlCommand();
            insertCommand.Connection = sqlConnection;
            insertCommand.Transaction = transaction;

            insertCommand.CommandText = "INSERT INTO LockTest (Id, Name, Description, Type) "  + 
                "VALUES (@id, @name, @description, @type)";
            SqlParameter idParameter = new SqlParameter("@id", System.Data.SqlDbType.UniqueIdentifier);
            insertCommand.Parameters.Add(idParameter);
            SqlParameter nameParameter = new SqlParameter("@name", System.Data.SqlDbType.NVarChar, 50);
            insertCommand.Parameters.Add(nameParameter);
            SqlParameter descriptionParameter = new SqlParameter("@description", System.Data.SqlDbType.NVarChar, Int32.MaxValue);
            insertCommand.Parameters.Add(descriptionParameter);
            SqlParameter typeParameter = new SqlParameter("@type", System.Data.SqlDbType.NChar, 20);
            insertCommand.Parameters.Add(typeParameter);

            insertCommand.Prepare();

            for (int i= 0; i < 1000000; i++)
            {
                Guid g = Guid.NewGuid();
                string s = g.ToString();
                insertCommand.Parameters["@id"].Value = g;
                insertCommand.Parameters["@name"].Value = s;
                insertCommand.Parameters["@description"].Value = DateTime.UtcNow.Ticks.ToString();
                insertCommand.Parameters["@type"].Value = "test";
                insertCommand.ExecuteNonQuery();
            }
            transaction.Commit();
        }
        catch
        {
            transaction.Rollback();
            throw;
        }

    }
    sqlConnection.Close();
}
if (DEBUG) LOG.Debug("AtEnd");

person Jeroen Huinink    schedule 25.06.2009    source источник
comment
Уровень изоляции транзакций по определению влияет только на чтение. Записи (т.е. INSERTS) ведут себя одинаково на всех уровнях изоляции.   -  person Remus Rusanu    schedule 25.06.2009
comment
Таблица очищается между каждым запуском? Последовательно каждый раз?   -  person gbn    schedule 25.06.2009
comment
Кстати, вы можете использовать TRUNCATE TABLE вместо DELETE FROM для подготовки тестов. И чтобы быть полностью точным, вы должны каждый раз создавать базу данных с нуля, предварительно увеличивая файлы данных и журналов (выделяйте начальные размеры, достаточно большие для теста). Одно событие роста базы данных или журнала во время одного из запусков приведет к сбросу всех результатов для этого запуска.   -  person Remus Rusanu    schedule 25.06.2009
comment
@Remus: вы правы, если бы я нашел относительно небольшую разницу между прогонами, то, что я нахожу сейчас, - это разница на порядок.   -  person Jeroen Huinink    schedule 25.06.2009


Ответы (4)


Сброс журнала.

При отсутствии явных транзакций неявные транзакции, запускаемые каждым оператором (например, INSERT), должны быть зафиксированы. Commit не может вернуться до тех пор, пока данные в журнале не будут записаны на диск, что означает, что каждый оператор INSERT должен ожидать операции записи на диск журнала.

Явные транзакции должны ждать только после того, как будет выдан оператор COMMIT, и к тому времени каждая полная страница журнала уже отправлена, а последняя страница журнала содержит, вероятно, несколько INSERT, поэтому стоимость записи амортизируется.

Обновлять:

Вы можете проверить время сброса журнала в счетчиках производительности: http://msdn.microsoft.com/en-us/library/ms189883.aspx:

  • Время ожидания очистки журнала Общее время ожидания (в миллисекундах) очистки журнала.
  • Ожидание очистки журнала/сек Количество коммитов в секунду, ожидающих очистки журнала.
  • Log Flushes/sec Количество сбросов журнала в секунду.
person Remus Rusanu    schedule 25.06.2009
comment
Спасибо за информацию: как насчет второй части моего вопроса. Является ли это типичным для SQL Server или другие СУБД ведут себя одинаково, например. MySQL, Оракул? - person Jeroen Huinink; 25.06.2009
comment
Это типично для всех баз данных на основе журналов с упреждающей записью (en.wikipedia.org/wiki/Write_ahead_logging). MySQL ведет себя одинаково с движком InnoDB и Oracle (в Oracle могут быть всевозможные ручки для управления этим, я ни в коем случае не эксперт Oracle). Альтернативой WAL является подкачка версий (en.wikipedia.org/wiki/Shadow_paging), но единственная коммерческая БД, о развертывании которой я знаю, это Informix. Системы, которые не используют WAL или страницы с версиями, не предлагают ACID, поэтому они обычно не предлагают транзакции (например, механизм MySQL ISAM). - person Remus Rusanu; 25.06.2009

Потому что каждая команда (если транзакция не задана явно) неявно обернута транзакцией, т.е. у вас есть 1M транзакций. По крайней мере, для sqLite

person Sergey Mirvoda    schedule 25.06.2009

Он должен получать и снимать блокировку для каждой вставки, если вы не транзакционны. С транзакцией он может держать блокировку открытой для нескольких вставок. Меньше накладных расходов.

person David M    schedule 25.06.2009
comment
Количество полученных/снятых блокировок одинаково в обоих случаях. - person Remus Rusanu; 25.06.2009
comment
Разве это не зависит от степени детализации получаемых замков? - person David M; 25.06.2009
comment
@ Ремус: наверное, нет. Это приведет к эскалации блокировки на страницу/таблицу. - person gbn; 25.06.2009
comment
@gbn правда, но это «особый случай» и может произойти, а может и не произойти на самом деле (вероятно, произойдет в 1M). Требуемые чисто «теоретические» блокировки идентичны (ключевые блокировки для вставленной записи). По моему опыту, стоимость очистки журнала в подавляющем большинстве случаев является фактором разницы в производительности пакетных транзакций. Время получения блокировки чрезвычайно низкое (несколько сотен циклов ЦП) по сравнению со временем ожидания ввода-вывода страницы журнала. - person Remus Rusanu; 25.06.2009
comment
@David: Нет, потому что тест делает отдельные вставки. Блокировка страниц будет рассматриваться только в том случае, если тест будет вставлять несколько записей (например, INSERT... SELECT...). - person Remus Rusanu; 25.06.2009
comment
@gbn: забыл, что тест OP выполняет одну вставку для каждого оператора, что также исключает возможность эскалации блокировки. - person Remus Rusanu; 25.06.2009
comment
@Remus: В рамках транзакции, скажем, после блокировки 500 тыс. строк, она, несомненно, будет увеличиваться ... - person gbn; 25.06.2009
comment
@gbn: нет. msdn.microsoft.com/en-us/library/ms184286 (SQL.90).aspx: ‹quote› Расширение блокировок запускается, когда инструкция Transact-SQL получает не менее 5000 блокировок на одну ссылку таблицы или индекса или, если таблица разделена, на одну ссылка на раздел таблицы или раздел индекса.‹/quote› Блокировки должны быть получены в одном операторе и одной ссылке на раздел. Операторы +5k insert ... values ​​... не вызовут его. Один оператор insert... select... со вставками +5k вызовет его. - person Remus Rusanu; 25.06.2009

Для быстрой загрузки используйте Use SqlBulkCopy:

http://www.sqlteam.com/article/use-sqlbulkcopy-to-quickly-load-data-from-your-client-to-sql-server

person A-K    schedule 28.06.2009