Как исследовать производительность оператора BULK INSERT?

12

Я в основном разработчик .NET, использующий Entity Framework ORM. Однако, поскольку я не хочу терпеть неудачу в использовании ORM , я пытаюсь понять, что происходит на уровне данных (базе данных). По сути, во время разработки я запускаю профилировщик и проверяю, какие части кода генерируются в терминах запросов.

Если я замечаю что-то очень сложное (ORM может генерировать ужасные запросы даже из довольно простых операторов LINQ, если они не написаны тщательно) и / или тяжело (длительность, загрузка ЦП, чтение страниц), я беру это в SSMS и проверяю план его выполнения.

Это прекрасно работает для моего уровня знаний базы данных. Тем не менее, BULK INSERT, кажется, особенное существо, так как оно не производит SHOWPLAN .

Я попытаюсь проиллюстрировать очень простой пример:

Определение таблицы

CREATE TABLE dbo.ImportingSystemFileLoadInfo
(
    ImportingSystemFileLoadInfoId INT NOT NULL IDENTITY(1, 1) CONSTRAINT PK_ImportingSystemFileLoadInfo PRIMARY KEY CLUSTERED,
    EnvironmentId INT NOT NULL CONSTRAINT FK_ImportingSystemFileLoadInfo REFERENCES dbo.Environment,
    ImportingSystemId INT NOT NULL CONSTRAINT FK_ImportingSystemFileLoadInfo_ImportingSystem REFERENCES dbo.ImportingSystem,
    FileName NVARCHAR(64) NOT NULL,
FileImportTime DATETIME2 NOT NULL,
    CONSTRAINT UQ_ImportingSystemImportInfo_EnvXIs_TableName UNIQUE (EnvironmentId, ImportingSystemId, FileName, FileImportTime)
)

Примечание: никакие другие индексы не определены в таблице

Основная вставка (что я ловлю в профилировщике, только одна партия)

insert bulk [dbo].[ImportingSystemFileLoadInfo] ([EnvironmentId] Int, [ImportingSystemId] Int, [FileName] NVarChar(64) COLLATE Latin1_General_CI_AS, [FileImportTime] DateTime2(7))

метрика

  • Вставлено 695 предметов
  • CPU = 31
  • Читает = 4271
  • Пишет = 24
  • Продолжительность = 154
  • Общее количество столов = 11500

Для моего приложения это нормально, хотя чтение кажется довольно большим (я очень мало знаю о внутренностях SQL Server, поэтому я сравниваю с размером страницы 8 КБ и имеющейся у меня информацией о записях)

Вопрос: как я могу узнать, можно ли оптимизировать этот BULK INSERT? Или это не имеет никакого смысла, поскольку это, пожалуй, самый быстрый способ передачи больших данных из клиентского приложения в SQL Server?

Алексей
источник

Ответы:

14

Насколько я могу сказать, вы можете оптимизировать массовую вставку очень похожим образом, как если бы вы оптимизировали обычную вставку. Как правило, план запроса для простой вставки не очень информативен, поэтому не беспокойтесь об отсутствии плана. Я рассмотрю несколько способов оптимизации вставки, но большинство из них, вероятно, не подходят для вставки, указанной вами в вопросе. Однако они могут быть полезны, если в будущем вам потребуется загружать большие объемы данных.

1. Вставьте данные в порядке ключей кластеризации.

SQL Server часто сортирует данные перед вставкой в ​​таблицу с кластерным индексом. Для некоторых таблиц и приложений вы можете повысить производительность, отсортировав данные в плоском файле и сообщив SQL Server, что данные отсортированы по ORDERаргументу BULK INSERT:

ЗАКАЗ ({столбец [ASC | DESC]} [, ... n])

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

Поскольку вы используете IDENTITYстолбец в качестве кластерного ключа, вам не нужно беспокоиться об этом.

2. Используйте, TABLOCKесли возможно

Если у вас гарантированно будет только один сеанс вставки данных в вашу таблицу, вы можете указать TABLOCKаргумент для BULK INSERT. Это может уменьшить конфликт блокировок и может привести к минимальному ведению журнала в некоторых сценариях. Однако вы вставляете в таблицу с кластеризованным индексом, который уже содержит данные, поэтому вы не получите минимальное ведение журнала без флага трассировки 610, который упоминается далее в этом ответе.

Если TABLOCKэто невозможно, потому что вы не можете изменить код , не вся надежда потеряна. Рассмотрите возможность использования sp_table_option:

EXEC [sys].[sp_tableoption]
    @TableNamePattern = N'dbo.BulkLoadTable' ,
    @OptionName = 'table lock on bulk load' , 
    @OptionValue = 'ON'

Другой вариант - включить флаг трассировки 715 .

3. Используйте соответствующий размер партии

Иногда вы сможете настроить вставки, изменив размер пакета.

ROWS_PER_BATCH = row_per_batch

Указывает приблизительное количество строк данных в файле данных.

По умолчанию все данные в файле данных отправляются на сервер в виде одной транзакции, а количество строк в пакете неизвестно оптимизатору запросов. Если вы укажете ROWS_PER_BATCH (со значением> 0), сервер использует это значение для оптимизации операции массового импорта. Значение, указанное для ROWS_PER_BATCH, должно приблизительно соответствовать фактическому количеству строк. Дополнительные сведения о производительности см. В разделе «Замечания» далее в этом разделе.

Вот цитата позже в статье:

Если число страниц, подлежащих очистке в одном пакете, превышает внутренний порог, может произойти полное сканирование пула буферов, чтобы определить, какие страницы необходимо очистить при фиксации пакета. Такое полное сканирование может снизить производительность массового импорта. Вероятный случай превышения внутреннего порога происходит, когда большой буферный пул объединяется с медленной подсистемой ввода-вывода. Чтобы избежать переполнения буфера на больших компьютерах, либо не используйте подсказку TABLOCK (которая удалит массовые оптимизации), либо используйте меньший размер пакета (который сохраняет массовые оптимизации).

Поскольку компьютеры различаются, мы рекомендуем вам тестировать пакеты разных размеров при загрузке данных, чтобы выяснить, что лучше всего подходит для вас.

Лично я бы просто вставил все 695 строк в одну партию. Настройка размера пакета может иметь большое значение при вставке большого количества данных.

4. Убедитесь, что вам нужен IDENTITYстолбец

Я ничего не знаю о вашей модели данных или требованиях, но не попадаюсь в ловушку добавления IDENTITYстолбца к каждой таблице. Аарон Бертран имеет статью об этом, которая называется « Плохие привычки»: размещение столбца IDENTITY на каждой таблице . Чтобы было ясно, я не говорю, что вы должны удалить IDENTITYстолбец из этой таблицы. Однако, если вы определите, что IDENTITYстолбец не нужен, и удалите его, это может повысить производительность вставки.

5. Отключить индексы или ограничения

Если вы загружаете большой объем данных в таблицу по сравнению с тем, что у вас уже есть, то может быть быстрее отключить индексы или ограничения перед загрузкой и включить их после загрузки. Для больших объемов данных, как правило, более неэффективно для SQL Server создавать индекс сразу, а не по мере загрузки данных в таблицу. Похоже, вы вставили 695 строк в таблицу с 11500 строками, поэтому я бы не рекомендовал этот метод.

6. Рассмотрим TF 610

Trace Flag 610 позволяет минимально регистрировать в некоторых дополнительных сценариях. Для вашей таблицы с IDENTITYкластеризованным ключом вы получите минимальное ведение журнала для любых новых страниц данных, если ваша модель восстановления является простой или с массовой регистрацией. Я считаю, что эта функция не включена по умолчанию, потому что она может ухудшить производительность в некоторых системах. Вам нужно тщательно протестировать, прежде чем включить этот флаг трассировки. Рекомендуемое руководство Microsoft по-прежнему выглядит как Руководство по повышению производительности загрузки данных.

Влияние ввода-вывода на минимальное ведение журнала под флагом трассировки 610

Когда вы фиксируете транзакцию массовой загрузки, которая была минимально записана в журнал, все загруженные страницы должны быть сброшены на диск до завершения фиксации. Любые сброшенные страницы, не уловленные более ранней операцией контрольной точки, могут создавать много случайных операций ввода-вывода. Сравните это с полностью зарегистрированной операцией, которая вместо этого создает последовательный ввод-вывод для записей журнала и не требует загрузки загруженных страниц на диск во время фиксации.

Если ваш сценарий загрузки - небольшие операции вставки на деревьях, которые не пересекают границы контрольных точек, и у вас медленная система ввода-вывода, использование минимального ведения журнала может фактически снизить скорость вставки.

Насколько я могу судить, это не имеет ничего общего с флагом трассировки 610, а скорее с минимальным ведением журнала. Я полагаю, что предыдущая цитата о ROWS_PER_BATCHтюнинге была связана с этой концепцией.

В заключение, вероятно, не так много, что вы можете сделать, чтобы настроить свой BULK INSERT. Я не буду беспокоиться о количестве операций чтения, которые вы наблюдали со своей вставкой. SQL Server будет сообщать о чтениях каждый раз, когда вы вставляете данные. Рассмотрим следующее очень просто INSERT:

DROP TABLE IF EXISTS X_TABLE;

CREATE TABLE X_TABLE (
VAL VARCHAR(1000) NOT NULL
);

SET STATISTICS IO, TIME ON;

INSERT INTO X_TABLE WITH (TABLOCK)
SELECT REPLICATE('Z', 1000)
FROM dbo.GetNums(10000); -- generate 10000 rows

Выход из SET STATISTICS IO, TIME ON:

Таблица «X_TABLE». Сканирование 0, логическое чтение 11428

У меня 11428 сообщений о прочтении, но это не действующая информация. Иногда количество зарегистрированных операций чтения может быть уменьшено путем минимального ведения журнала, но, конечно, разницу нельзя напрямую перевести в выигрыш в производительности.

Джо Оббиш
источник
12

Я собираюсь начать отвечать на этот вопрос с намерением постоянно обновлять этот ответ, поскольку я строю базу знаний трюков. Надеюсь, другие столкнутся с этим и помогут мне улучшить мои собственные знания в процессе.

  1. Gut Check: Ваш брандмауэр выполняет глубокую проверку пакетов? В Интернете вы не найдете много информации об этом, но если ваши массовые вставки примерно в 10 раз медленнее, чем они должны быть, скорее всего, у вас есть устройство безопасности, которое выполняет глубокую проверку пакетов уровня 3-7 и проверяет "Generic SQL Injection Prevention ».

  2. Измерьте размер данных, которые вы планируете массово вставить, в байтах на пакет. И проверьте, храните ли вы какие-либо большие объекты, так как это отдельная операция извлечения и записи страницы.

    Несколько причин, почему вы должны сделать это таким образом:

    а. В AWS IOPS Elastic Block Storage разбивается на байты, а не строки.

    1. См. Производительность Amazon EBS в экземплярах Linux »Характеристики ввода-вывода и мониторинг, чтобы узнать, что такое модуль EBS IOPS
    2. В частности, тома SSD общего назначения (gp2) имеют концепцию «Кредиты ввода-вывода и производительность пакета», и для обработки тяжелых ETL характерно истощение кредитов «пакетного баланса». Ваша продолжительность пакета измеряется в байтах, а не в строках SQL Server :)

    б. В то время как большинство библиотек или технических документов тестируют на основе количества строк, на самом деле это количество страниц, которые можно записать, и, чтобы рассчитать это, вам нужно знать, сколько байтов в строке и размер вашей страницы (обычно 8 КБ). , но всегда дважды проверяйте, унаследовали ли вы систему от кого-то другого.)

    SELECT *
    FROM 
    sys.dm_db_index_physical_stats(DB_ID(),OBJECT_ID(N'YourTable'), NULL, NULL, 'DETAILED')

    Обратите внимание на avg_record_size_in_bytes и page_count.

    с. Как объясняет Пол Уайт в https://sqlperformance.com/2019/05/sql-performance/minimal-logging-insert-select-heap : «Чтобы включить минимальное ведение журнала INSERT...SELECT, SQL Server должен ожидать более 250 строк с общим размером по крайней мере, одной степени (8 страниц). "

  3. Если у вас есть какие-либо индексы с проверочными или уникальными ограничениями, используйте SET STATISTICS IO ONи SET STATISTICS TIME ON(или SQL Server Profiler или расширенные события SQL Server) для сбора информации, например, содержит ли ваша массовая вставка какие-либо операции чтения. Операции чтения выполняются благодаря тому, что ядро ​​базы данных SQL Server обеспечивает соблюдение ограничений целостности.

  4. Попробуйте создать тестовую базу данных, в которой PRIMARYFILEGROUP смонтирован на диске RAM. Это должно быть немного быстрее SSD, но также устраняет любые вопросы относительно того, может ли ваш RAID-контроллер увеличивать накладные расходы. В 2018 году этого не должно быть, но, создав несколько различных базовых показателей, подобных этой, вы можете получить общее представление о том, сколько накладных расходов добавляет ваше оборудование.

  5. Также поместите исходный файл на RAM-диск.

    Помещение исходного файла на RAM-диск исключит любые конфликтные ситуации, если вы читаете исходный файл с того же диска, на котором работает FILEGROUP сервера базы данных.

  6. Убедитесь, что вы отформатировали жесткий диск с использованием экстентов размером 64 КБ.

  7. Используйте UserBenchmark.com и сравните ваш SSD. Это будет:

    1. Добавьте больше знаний другим поклонникам производительности о том, какую производительность ожидать от устройства
    2. Помогите выяснить, если производительность вашего диска ниже, чем у аналогичных дисков.
    3. Помочь вам выяснить, не соответствует ли производительность вашего диска другим дискам той же категории (SSD, HDD и т. Д.)
  8. Если вы вызываете «INSERT BULK» из C # через Entity Framework Extensions, убедитесь, что вы сначала «прогреваете» JIT и «отбрасываете» первые несколько результатов.

  9. Попробуйте создать счетчики производительности для вашей программы. С .NET вы можете использовать benchmark.NET, и он автоматически профилирует кучу базовых метрик. Затем вы можете поделиться своими попытками профилировщика с сообществом открытого исходного кода и посмотреть, сообщают ли люди, работающие на другом оборудовании, одни и те же метрики (а именно, из моего предыдущего пункта об использовании UserBenchmark.com для сравнения).

  10. Попробуйте использовать именованные каналы и запустить его как localhost.

  11. Если вы ориентируетесь на SQL Server и используете .NET Core, рассмотрите возможность раскрутки Linux с помощью SQL Server Std Edition - это стоит меньше доллара в час даже для серьезного оборудования. Основным преимуществом использования одного и того же кода на одном и том же оборудовании с другой ОС является проверка наличия проблем в стеке TCP / IP ядра ОС.

  12. Используйте диагностические запросы SQL Server Глена Барри, чтобы измерить задержку диска для диска, хранящего FILEGROUP вашей таблицы базы данных.

    а. Обязательно проводите измерения до и после теста. «Перед тестом» просто говорит вам, есть ли у вас ужасные характеристики ввода-вывода в качестве базовой линии.

    б. Для измерения «во время теста» вам действительно необходимо использовать счетчики производительности PerfMon.

    Почему? Потому что большинство серверов баз данных используют какое-то сетевое хранилище (NAS). В облаке, в AWS, Elastic Block Storage - именно это. Вы можете быть связаны IOPS вашего EBS тома / NAS-решения.

  13. Используйте некоторый инструмент для измерения статистики ожидания. Red Gate SQL Monitor , анализатор производительности баз данных SolarWinds или даже диагностические запросы Глена Барри к SQL Server или запрос статистики Пола Рэндала .

    а. Наиболее распространенными типами ожидания, вероятно, будут Memory / CPU, WRITELOG, PAGEIOLATCH_EX и ASYNC_NETWORK_IO .

    б. Вы можете получить дополнительные типы ожидания, если вы используете группы доступности.

  14. Измерьте эффекты нескольких одновременных INSERT BULKкоманд с TABLOCKотключенным (TABLOCK, скорее всего, принудит сериализацию команд INSERT BULK). Ваше узкое место может быть в ожидании INSERT BULKзавершения; Вы должны попытаться поставить в очередь столько задач, сколько может справиться физическая модель данных сервера баз данных.

  15. Подумайте о разделении вашего стола. В качестве конкретного примера: если ваша таблица базы данных предназначена только для добавления, Эндрю Новик предложил создать «TODAY» FILEGROUPи разбить его как минимум на две файловые группы, TODAY и BEFORE_TODAY. Таким образом, если ваши INSERT BULKданные являются только данными за сегодня, вы можете фильтровать по полю CreatedOn, чтобы заставить все вставки попадать по одному FILEGROUP, и, таким образом, уменьшать блокировку при использовании TABLOCK. Этот метод более подробно описан в официальном документе Microsoft: стратегии секционированной таблицы и индексации с использованием SQL Server 2008

  16. Если вы используете индексы columnstore, выключите TABLOCKи загрузите данные в 102 400 строк Batch Size. Затем вы можете параллельно загружать все свои данные непосредственно в группы строк columnstore. Это предложение (и документированное рациональное) исходит из индексов Columnstore Microsoft - Руководство по загрузке данных :

    При массовой загрузке предусмотрены следующие встроенные функции оптимизации производительности:

    Параллельные загрузки: у вас может быть несколько одновременных массовых загрузок (bcp или массовая вставка), каждая из которых загружает отдельный файл данных. В отличие от массовых загрузок хранилища строк в SQL Server, указывать их не нужно, TABLOCKпоскольку каждый поток массового импорта будет загружать данные исключительно в отдельные группы строк (сжатые или дельта-группы строк) с исключительной блокировкой. Использование TABLOCKзаставит монопольную блокировку таблицы, и вы не сможете импортировать данные параллельно.

    Минимальная регистрация:Массовая загрузка использует минимальное ведение журнала данных, которые направляются непосредственно в сжатые группы строк. Все данные, поступающие в дельта-группу строк, полностью регистрируются. Это включает в себя любые размеры партий, которые составляют менее 102 400 строк. Однако при массовой загрузке цель состоит в том, чтобы большая часть данных обходила дельта-группы строк.

    Оптимизация блокировок: при загрузке в сжатую группу строк фиксируется блокировка X для группы строк. Однако при массовой загрузке в дельта-группу строк в группе строк получается X-блокировка, но SQL Server все еще блокирует блокировки PAGE / EXTENT, потому что X-блокировка группы строк не является частью иерархии блокировок.

  17. Начиная с SQL Server 2016, больше не нужно включать флаг трассировки 610 для минимального входа в индексированную таблицу . Цитирую инженера Microsoft Парикшита Савяни ( выделено мое ):

    Одна из целей разработки SQL Server 2016 заключалась в улучшении производительности и масштабируемости движка из коробки, чтобы он работал быстрее без необходимости каких-либо ручек или флагов трассировки для клиентов. В рамках этих улучшений одним из улучшений, внесенных в код механизма SQL Server, стало включение контекста массовой загрузки (также называемого быстрым вставлением или контекстом быстрой загрузки) и минимального ведения журнала по умолчанию при выполнении операций массовой загрузки в базе данных с простыми или Массовая регистрация модели восстановления. Если вы не знакомы с минимальным ведением журнала, я настоятельно рекомендую прочитать этот пост от Sunil Agrawal, где он объясняет, как минимальное ведение журнала работает в SQL Server. Чтобы минимально регистрировать объемные вставки, они все равно должны соответствовать предварительным условиям, которые описаны здесь.

    В рамках этих усовершенствований в SQL Server 2016 вам больше не нужно включать флаг трассировки 610 для минимального входа в индексированную таблицу.и он присоединяется к некоторым другим флагам трассировки (1118, 1117, 1236, 8048), чтобы стать частью истории. В SQL Server 2016, когда операция массовой загрузки приводит к выделению новой страницы, все строки, последовательно заполняющие эту новую страницу, минимально регистрируются, если выполнены все другие предварительные условия для минимального ведения журнала, обсужденные ранее. Строки, вставленные в существующие страницы (без выделения новых страниц) для поддержания порядка индекса, по-прежнему полностью регистрируются, как и строки, которые перемещаются в результате разбиения страниц во время загрузки. Также важно, чтобы параметр ALLOW_PAGE_LOCKS был включен для индексов (который по умолчанию включен), чтобы минимальная операция регистрации работала, так как блокировки страниц приобретаются во время размещения, и, таким образом, регистрируются только выделения страниц или экстентов.

  18. Если вы используете SqlBulkCopy в C # или EntityFramework.Extensions (где используется SqlBulkCopy), проверьте конфигурацию сборки. Вы запускаете свои тесты в режиме выпуска? Установлена ​​ли целевая архитектура Any CPU / x64 / x86?

  19. Подумайте об использовании sp_who2, чтобы увидеть, является ли транзакция INSERT BULK SUSPENDED. Это может быть приостановлено, потому что он заблокирован другим спидом. Подумайте о том, как минимизировать блокировку SQL Server . Вы также можете использовать sp_WhoIsActive Адама Мачаника, но sp_who2 предоставит вам необходимую базовую информацию.

  20. Возможно, у вас просто плохой дисковый ввод-вывод. Если вы выполняете массовую вставку и загрузка диска не достигает 100%, а застряла на уровне около 2%, то, вероятно, у вас либо плохая прошивка, либо неисправное устройство ввода-вывода. (Это случилось с моим коллегой.) Используйте [SSD UserBenchmark], чтобы сравнить с другими по производительности оборудования, особенно если вы можете повторить медлительность на вашем локальном компьютере разработчика. (Я поместил это последнее в списке, потому что большинство компаний не позволяют разработчикам запускать базы данных на своем локальном компьютере из-за риска IP.)

  21. Если в вашей таблице используется сжатие, вы можете попробовать запустить несколько сеансов, и в каждом сеансе начните с использования существующей транзакции и выполните это перед командой SqlBulkCopy:

    НАСТРОЙКА КОНФИГУРАЦИИ ALTER SERVER ПРОЦЕСС AFFINITY CPU = AUTO;

  22. Для непрерывной загрузки один поток идей, впервые изложенный в официальном документе Microsoft « Секционированная таблица и стратегии индексирования с использованием SQL Server 2008» :

    Непрерывная загрузка

    В сценарии OLTP новые данные могут поступать непрерывно. Если пользователи также запрашивают новейший раздел, непрерывная вставка данных может привести к блокировке: пользовательские запросы могут блокировать вставки, и аналогичным образом вставки могут блокировать пользовательские запросы.

    Конфликт в загрузочной таблице или разделе может быть уменьшен с помощью изоляции моментальных снимков, в частности READ COMMITTED SNAPSHOTуровня изоляции. В условиях READ COMMITTED SNAPSHOTизоляции вставки в таблицу не вызывают активности в хранилище версий tempdb , поэтому накладные расходы tempdb минимальны для вставок, но общие запросы не будут приниматься пользовательскими запросами в одном разделе.

    В других случаях, когда данные вставляются в многораздельную таблицу непрерывно с высокой скоростью, вы все равно сможете размещать данные в течение коротких периодов времени в промежуточных таблицах, а затем многократно вставлять эти данные в самый новый раздел, пока не появится окно для текущий раздел проходит, а затем данные вставляются в следующий раздел. Например, предположим, что у вас есть две промежуточные таблицы, каждая из которых получает данные по 30 секунд на альтернативной основе: одна таблица для первой половины минуты, вторая таблица для второй половины минуты. Хранимая процедура вставки определяет полминуты текущей вставки, а затем вставляет ее в первую промежуточную таблицу. По истечении 30 секунд процедура вставки определяет, что она должна быть вставлена ​​во вторую промежуточную таблицу. Затем другая хранимая процедура загружает данные из первой промежуточной таблицы в самый новый раздел таблицы, а затем усекает первую промежуточную таблицу. Еще через 30 секунд та же хранимая процедура вставляет данные из второй хранимой процедуры и помещает их в текущий раздел, а затем усекает вторую промежуточную таблицу.

  23. Microsoft CAT Team: руководство по производительности загрузки данных

  24. Убедитесь, что ваша статистика актуальна. Используйте FULLSCAN, если можете после каждой сборки индекса.

  25. SAN Performance Tuning с SQLIO, а также убедитесь, что вы используете механические диски для выравнивания разделов диска. См. Рекомендации по выравниванию дисковых разделов Microsoft .

  26. COLUMNSTORE INSERT/ UPDATEпроизводительность

Джон Заброски
источник
2

Чтения, вероятно, будут уникальными & FK-ограничениями, проверяемыми во время вставки - вы можете получить улучшение скорости, если вы можете отключить / удалить их во время вставки и включить / воссоздать их впоследствии. Вам нужно будет проверить, замедляет ли это процесс в целом по сравнению с их активностью. Это также не может быть хорошей идеей, если другие процессы записывают в одну и ту же таблицу одновременно. - Гарет Лайонс

В соответствии с Q & A Внешние ключи становятся ненадежными после массовой вставки , ограничения FK становятся ненадежными после опции BULK INSERTбез CHECK_CONSTRAINTSопции (мой случай, когда я закончил с ненадежными ограничениями). Это не ясно, но было бы бессмысленно проверять их и все же не доверять им. Однако PK и UNIQUE по-прежнему будут проверяться (см. BULK INSERT (Transact-SQL) ). - Алексей

user126897
источник