Eu sou principalmente um desenvolvedor .NET usando Entity Framework ORM. Porém, como não quero falhar na utilização do ORM , estou tentando entender o que acontece dentro da camada de dados (banco de dados). Basicamente, durante o desenvolvimento eu inicio o profiler e verifico o que algumas partes do código geram em termos de consultas.
Se eu detectar algo totalmente complicado (ORM pode gerar consultas horríveis mesmo de instruções LINQ bastante simples, se não cuidadosamente escritas) e/ou pesada (duração, CPU, leituras de página), eu o pego no SSMS e verifico seu plano de execução.
Funciona bem para o meu nível de conhecimento de banco de dados. No entanto, BULK INSERT parece ser uma criatura especial, pois não parece produzir um SHOWPLAN .
Vou tentar ilustrar um exemplo muito simples:
Definição da tabela
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)
)
Nota: nenhum outro índice está definido na tabela
A inserção em massa (o que eu pego no profiler, apenas um lote)
insert bulk [dbo].[ImportingSystemFileLoadInfo] ([EnvironmentId] Int, [ImportingSystemId] Int, [FileName] NVarChar(64) COLLATE Latin1_General_CI_AS, [FileImportTime] DateTime2(7))
Métricas
- 695 itens inseridos
- CPU = 31
- Leituras = 4271
- Grava = 24
- Duração = 154
- Contagem total de mesas = 11.500
Para o meu aplicativo, tudo bem, embora as leituras pareçam bastante grandes (eu sei muito pouco sobre os componentes internos do SQL Server, então comparo com o tamanho da página de 8K e as pequenas informações de registro que tenho)
Pergunta: como posso investigar se este BULK INSERT pode ser otimizado? Ou não faz sentido, já que é sem dúvida a maneira mais rápida de enviar grandes dados de um aplicativo cliente para o SQL Server?
Até onde eu sei, você pode otimizar uma inserção em massa de uma maneira muito semelhante à que você otimizaria uma inserção regular. Normalmente, um plano de consulta para uma inserção simples não é muito informativo, portanto, não se preocupe em não ter o plano. Abordarei algumas maneiras de otimizar uma inserção, mas a maioria delas provavelmente não se aplica à inserção especificada na pergunta. No entanto, eles podem ser úteis se, no futuro, você precisar carregar grandes quantidades de dados.
1. Insira os dados na ordem das chaves de cluster
O SQL Server geralmente classifica os dados antes de inseri-los em uma tabela com um índice clusterizado. Para algumas tabelas e aplicativos, você pode melhorar o desempenho classificando os dados no arquivo simples e informando ao SQL Server que os dados são classificados por meio do
ORDER
argumento deBULK INSERT
:Como você está usando uma
IDENTITY
coluna como chave clusterizada, não precisa se preocupar com isso.2. Use
TABLOCK
se possívelSe você tiver a garantia de ter apenas uma sessão inserindo dados em sua tabela, você pode especificar o
TABLOCK
argumento paraBULK INSERT
. Isso pode reduzir a contenção de bloqueio e pode levar ao registro mínimo em alguns cenários. No entanto, você está inserindo em uma tabela com um índice clusterizado que já contém dados para que você não obtenha log mínimo sem o sinalizador de rastreamento 610, mencionado posteriormente nesta resposta.Se
TABLOCK
não for possível, porque você não pode alterar o código , nem toda a esperança está perdida. Considere usarsp_table_option
:Outra opção é habilitar o sinalizador de rastreamento 715 .
3. Use um tamanho de lote apropriado
Às vezes, você poderá ajustar as inserções alterando o tamanho do lote.
Aqui está a citação de mais tarde no artigo:
Pessoalmente, eu apenas inseriria todas as 695 linhas em um único lote. Ajustar o tamanho do lote pode fazer uma grande diferença ao inserir muitos dados.
4. Certifique-se de que você precisa da
IDENTITY
colunaNão sei nada sobre seu modelo de dados ou requisitos, mas não caia na armadilha de adicionar uma
IDENTITY
coluna a cada tabela. Aaron Bertrand tem um artigo sobre isso chamado Maus hábitos para chutar: colocar uma coluna IDENTIDADE em cada tabela . Para ser claro, não estou dizendo que você deve remover aIDENTITY
coluna desta tabela. No entanto, se você determinar que aIDENTITY
coluna não é necessária e removê-la, isso pode melhorar o desempenho da inserção.5. Desabilitar índices ou restrições
Se você estiver carregando uma grande quantidade de dados em uma tabela em comparação com o que já possui, pode ser mais rápido desabilitar índices ou restrições antes do carregamento e habilitá-los após o carregamento. Para grandes quantidades de dados, geralmente é mais ineficiente para o SQL Server criar um índice de uma só vez, em vez de quando os dados são carregados na tabela. Parece que você inseriu 695 linhas em uma tabela com 11.500 linhas, então eu não recomendaria essa técnica.
6. Considere TF 610
O sinalizador de rastreamento 610 permite o registro mínimo em alguns cenários adicionais. Para sua tabela com uma
IDENTITY
chave clusterizada, você obteria um registro mínimo para quaisquer novas páginas de dados, desde que seu modelo de recuperação fosse simples ou registrado em massa. Acredito que esse recurso não esteja ativado por padrão porque pode prejudicar o desempenho em alguns sistemas. Você precisaria testar cuidadosamente antes de habilitar esse sinalizador de rastreamento. A referência recomendada da Microsoft ainda parece ser The Data Loading Performance GuideTanto quanto posso dizer, isso não tem nada a ver com o sinalizador de rastreamento 610, mas com o próprio registro mínimo. Acredito que a citação anterior sobre o
ROWS_PER_BATCH
ajuste estava chegando a esse mesmo conceito.Em conclusão, provavelmente não há muito que você possa fazer para ajustar seu
BULK INSERT
. Eu não estaria preocupado com a contagem de leitura que você observou com sua inserção. O SQL Server relatará as leituras sempre que você inserir dados. Considere o seguinte muito simplesINSERT
:Saída de
SET STATISTICS IO, TIME ON
:Eu tenho 11.428 leituras relatadas, mas isso não é uma informação acionável. Às vezes, o número de leituras relatadas pode ser reduzido por um registro mínimo, mas é claro que a diferença não pode ser traduzida diretamente em um ganho de desempenho.
Vou começar a responder a essa pergunta, com a intenção de atualizar continuamente essa resposta à medida que construo uma base de conhecimento de truques. Espero que outros se deparem com isso e me ajudem a melhorar meu próprio conhecimento no processo.
Gut Check: Seu firewall está fazendo uma inspeção profunda de pacotes com estado? Você não encontrará muito na Internet sobre isso, mas se suas inserções em massa forem cerca de 10 vezes mais lentas do que deveriam, é provável que você tenha um dispositivo de segurança fazendo inspeção profunda de pacotes de nível 3-7 e verificando "Prevenção de injeção de SQL genérica ".
Meça o tamanho dos dados que você planeja inserir em massa, em bytes, por lote. E verifique se você está armazenando algum dado LOB, pois essa é uma operação de busca e gravação de página separada.
Várias razões pelas quais você deve fazer isso dessa maneira:
uma. Na AWS, as IOPS do Elastic Block Storage são divididas em bytes, não em linhas.
b. Enquanto a maioria das bibliotecas ou whitepapers testam com base no número de linhas, é realmente o número de páginas que podem ser gravadas para esse assunto e, para calcular isso, você precisa saber quantos bytes por linha e o tamanho da página (geralmente 8 KB , mas sempre verifique se você herdou o sistema de outra pessoa.)
Preste atenção em avg_record_size_in_bytes e page_count.
c. Como Paul White explica em https://sqlperformance.com/2019/05/sql-performance/minimal-logging-insert-select-heap , "Para habilitar o log mínimo com
INSERT...SELECT
, o SQL Server deve esperar mais de 250 linhas com um tamanho total de pelo menos uma extensão (8 páginas)."Se você tiver índices com restrições de verificação ou restrições exclusivas, use
SET STATISTICS IO ON
andSET STATISTICS TIME ON
(ou SQL Server Profiler ou SQL Server Extended Events) para capturar informações como se sua inserção em massa tem alguma operação de leitura. As operações de leitura são devidas ao mecanismo de banco de dados do SQL Server, garantindo que as restrições de integridade sejam aprovadas.Tente criar um banco de dados de teste em que o PRIMARY
FILEGROUP
esteja montado em uma unidade RAM. Isso deve ser um pouco mais rápido que o SSD, mas também elimina qualquer dúvida sobre se o seu controlador RAID pode estar adicionando sobrecarga. Em 2018, não deveria, mas ao criar várias linhas de base diferenciais como esta, você pode ter uma ideia geral de quanta sobrecarga seu hardware está adicionando.Coloque também o arquivo de origem em uma unidade RAM.
Colocar o arquivo de origem em uma unidade RAM descartará quaisquer problemas de contenção se você estiver lendo o arquivo de origem da mesma unidade em que o FILEGROUP do servidor de banco de dados está.
Verifique se você formatou seu disco rígido usando extensões de 64 KB.
Use UserBenchmark.com e compare seu SSD. Isso vai:
Se você estiver chamando "INSERT BULK" de C# por meio de Entity Framework Extensions, certifique-se de "aquecer" o JIT primeiro e "jogar fora" os primeiros resultados.
Tente criar contadores de desempenho para o seu programa. Com o .NET, você pode usar o benchmark.NET e ele criará automaticamente o perfil de várias métricas básicas. Você pode então COMPARTILHAR suas tentativas de criação de perfil com a comunidade de código aberto e ver se as pessoas que executam hardware diferente relatam as mesmas métricas (viz. do meu ponto anterior sobre o uso do UserBenchmark.com para comparar).
Tente usar pipes nomeados e executá-lo como localhost.
Se você estiver direcionando o SQL Server e usando o .NET Core, considere a possibilidade de criar um Linux com SQL Server Std Edition - isso custa menos de um dólar por hora, mesmo para hardware sério. A principal vantagem de tentar o mesmo código com o mesmo hardware com um sistema operacional diferente é verificar se a pilha TCP/IP do kernel do sistema operacional está causando problemas.
Use as consultas de diagnóstico do SQL Server de Glen Barry para medir a latência da unidade que armazena o FILEGROUP da tabela do banco de dados.
uma. Certifique-se de medir antes do teste e após o teste. O "antes do teste" apenas informa se você tem características de E/S horríveis como linha de base.
b. Para medir "durante o teste", você realmente precisa usar os contadores de desempenho PerfMon.
Por quê? Porque a maioria dos servidores de banco de dados usa algum tipo de armazenamento conectado à rede (NAS). Na nuvem, na AWS, o Elastic Block Storage é exatamente isso. Você pode estar vinculado ao IOPS de sua solução de volume/NAS EBS.
Use alguma ferramenta para medir as estatísticas de espera. Red Gate SQL Monitor , SolarWinds Database Performance Analyzer , ou mesmo consultas de diagnóstico SQL Server de Glen Barry, ou consulta de estatísticas de espera de Paul Randal .
uma. Os tipos de espera mais comuns provavelmente serão Memory/CPU, WRITELOG, PAGEIOLATCH_EX e ASYNC_NETWORK_IO .
b. Você pode incorrer em tipos de espera adicionais se estiver executando grupos de disponibilidade.
Meça os efeitos de vários
INSERT BULK
comandos simultâneos comTABLOCK
desabilitado (TABLOCK provavelmente forçará a serialização de comandos INSERT BULK). Seu gargalo pode estar esperandoINSERT BULK
a conclusão de um; você deve tentar enfileirar tantas dessas tarefas quanto o modelo de dados físico do seu servidor de banco de dados pode manipular.Considere particionar sua tabela. Como um exemplo específico: se sua tabela de banco de dados for somente anexada, Andrew Novick sugeriu criar um "TODAY"
FILEGROUP
e particionar em pelo menos dois grupos de arquivos, TODAY e BEFORE_TODAY. Dessa forma, se seusINSERT BULK
dados são apenas dados de hoje, você pode filtrar em um campo CreatedOn para forçar todas as inserções a atingir um únicoFILEGROUP
e, assim, reduzir o bloqueio ao usarTABLOCK
. Essa técnica é descrita com mais detalhes em um whitepaper da Microsoft: Estratégias de tabela e índice particionadas usando o SQL Server 2008Se você estiver usando índices columnstore, desative
TABLOCK
e carregue dados em 102.400 linhas Tamanho do lote. Você pode então carregar todos os seus dados em paralelo diretamente em rowgroups columnstore. Esta sugestão (e documentada racional) vem dos índices Columnstore da Microsoft - Orientação de carregamento de dados :A partir do SQL Server 2016, não é mais necessário habilitar o sinalizador de rastreamento 610 para log mínimo na tabela indexada . Citando o engenheiro da Microsoft Parikshit Savjani ( grifo meu ):
Se você estiver usando SqlBulkCopy em C# ou EntityFramework.Extensions (que usa SqlBulkCopy nos bastidores), verifique sua configuração de compilação. Você está executando seus testes no modo Release? A arquitetura de destino está definida como Qualquer CPU/x64/x86?
Considere usar sp_who2 para ver se a transação INSERT BULK está SUSPENDED. Pode ser SUSPENSO porque está bloqueado por outro spid. Considere a leitura de Como minimizar o bloqueio do SQL Server . Você também pode usar o sp_WhoIsActive de Adam Machanic, mas sp_who2 fornecerá as informações básicas que você precisa.
You might just have bad disk I/O. If your doing a bulk insert and your disk utilization is not hitting 100%, and is stuck at around 2%, then you probably have either bad firmware, or defective I/O device. (This happened to a coworker of mine.) Use [SSD UserBenchmark] to compare with others for hardware performance, especially if you can replicate the slowness on your local dev machine. (I put this last in the list because most companies do not allow developers to run databases on their local machine due to IP risk.)
If your table uses compression, you can try running multiple sessions, and in each session, start off with using an existing transaction and run this before the SqlBulkCopy command:
ALTER SERVER CONFIGURATION SET PROCESS AFFINITY CPU=AUTO;
For Continuous Loading, one stream of ideas, first outlined in a Microsoft whitepaper, Partitioned Table and Index Strategies Using SQL Server 2008:
Microsoft CAT Team's The Data Loading Performance Guide
Make sure your statistics are up to date. Use FULLSCAN if you can after each index build.
SAN Performance Tuning with SQLIO and also make sure if you are using mechanical disks that your disk partitions are aligned. See Microsoft's Disk Partition Alignment Best Practices.
COLUMNSTORE
INSERT
/UPDATE
performanceThe reads are likely to be the unique & FK constraints being checked during insert - you may get an speed improvement if you can disable/drop them during the insert & enable/recreate them afterwards. You'll need to test if this makes it slower overall compared to keeping them active. This also may not be a good idea if other processes are writing to the same table concurrently. - Gareth Lyons
According to the Q & A Foreign keys become untrusted after bulk insert, FK constraints become untrusted after a
BULK INSERT
with noCHECK_CONSTRAINTS
option (my case as I ended with untrusted constraints). It is not clear, but it would not make sense to check them and still make them untrusted. However, PK and UNIQUE will still be checked (see BULK INSERT (Transact-SQL)). - Alexei