O cenário
Era uma vez um banco de dados de armazenamento temporário em uma pequena empresa que participava de um processo de ETL, atuando como um catálogo de recebimento para os vários formatos de arquivos de várias fontes de terceiros. O E foi tratado através de pacotes DTS, com poucas estruturas de controle para auditoria ou controle, mas foi considerado "Bom o suficiente" e, para todos os efeitos, foi.
Os dados fornecidos pela parte E destinavam-se ao consumo por um aplicativo singular, desenvolvido e gerenciado por um punhado de programadores jovens e capazes. Embora não possuam experiência ou conhecimento das técnicas de data warehousing da época, eles estabeleceram e criaram seus próprios processos T e L a partir do código do aplicativo. À vista, esses novatos engenheiros de software inventaram o que as pessoas de fora poderiam chamar de "roda menos do que o ideal", mas com "Bom o suficiente" como um nível de serviço sempre presente, eles foram capazes de fornecer uma estrutura operacional.
Por um tempo, tudo ficou bom no domínio fortemente acoplado, com o catálogo Staging se deliciando com os dados de uma dúzia de terceiros, sendo alimentado pelo aplicativo. À medida que a aplicação crescia, também aumentavam seus apetites, mas com os hábeis desenvolvedores de cavaleiros brancos vigiando o sistema, esses apetites foram tratados rapidamente e, em muitos casos, até bem.
Mas a idade de ouro não poderia durar para sempre, é claro. Com a prosperidade concedida pelo aplicativo bem-sucedido, os negócios cresceram e cresceram. À medida que crescia, o ambiente e o aplicativo de armazenamento temporário eram forçados a crescer com ele. Apesar de toda a vigilância, o mero punhado de desenvolvedores de heróis não conseguia manter a manutenção do sistema agora expansivo, e os consumidores tinham direito a seus dados. Não era mais uma questão do que eles precisavam ou até queriam, mas a população achava que eles mereciam, exigindo ainda mais.
Armado com pouco mais do que cofres cheios de ganhos, os negócios chegaram ao mercado, contratando desenvolvedores e administradores para ajudar a apoiar o sistema sempre crescente. Mercenários de todos os etos afluíam à empresa, mas com esse surto de crescimento veio pouco o caminho das orientações especializadas disponíveis. Novos desenvolvedores e administradores lutaram para entender os meandros da suíte caseira, até que as frustrações resultaram em guerra total. Cada departamento começou a tentar resolver todos os problemas sozinho, trabalhando mais um contra o outro do que trabalhando um com o outro. Um único projeto ou iniciativa seria implementado de várias maneiras diferentes, cada uma ligeiramente diferente da seguinte. A tensão de tudo isso provou ser demais para alguns dos cavaleiros brancos e, quando caíram, o império desmoronou. Logo, o sistema estava em frangalhos,
Apesar da transformação desses campos de promessa em código de espaguete sangrento, a empresa resistiu. Afinal, era "Bom o suficiente".
O desafio
Mais algumas mudanças de regime e contratações depois, me encontro no emprego da empresa. Faz muitos anos desde as grandes guerras, mas o dano causado ainda é muito visível. Eu consegui resolver alguns dos pontos fracos da parte E do sistema e adicionar algumas tabelas de controle sob o pretexto de atualizar os pacotes DTS para o SSIS, que agora estão sendo usados por alguns profissionais reais de data warehousing, pois eles criam um ambiente normal. e substituição T e L documentada.
O primeiro obstáculo foi importar os dados dos arquivos de terceiros de uma maneira que não truncasse os valores ou alterasse os tipos de dados nativos, mas também incluísse algumas teclas de controle para recarregamentos e expurgos. Tudo estava bem, mas os aplicativos precisavam acessar essas novas tabelas de maneira transparente e transparente. Um pacote DTS pode preencher uma tabela, que é então lida diretamente pelo aplicativo. As atualizações do SSIS precisam ser feitas paralelamente por motivos de controle de qualidade, mas esses novos pacotes incluem várias chaves de controle e também utilizam um esquema de particionamento, sem mencionar que as mudanças reais de metadados por si só podem ser significativas o suficiente para garantir uma nova tabela de qualquer maneira. nova tabela foi usada para os novos pacotes SSIS.
Com as importações de dados confiáveis agora trabalhando e sendo usadas pela equipe de armazenamento, o verdadeiro desafio vem ao servir os novos dados para os aplicativos que acessam diretamente o ambiente de armazenamento temporário, com um impacto mínimo (também conhecido como "Não") no código do aplicativo. Para isso, optou por vistas de uso, renomear uma tabela, como dbo.DailyTransaction
a dbo.DailyTranscation_LEGACY
e reutilizar o dbo.DailyTransaction
nome do objeto para uma vista, o que efetivamente apenas seleciona tudo, desde o agoraLEGACY
tabela designada. Como recarregar os anos de dados contidos nessas tabelas não é uma opção da perspectiva dos negócios, como as novas tabelas particionadas e particionadas pelo SSIS entram na produção, as importações antigas do DTS são desativadas e os aplicativos precisam poder acesse também os novos dados nas novas tabelas. Nesse ponto, as visualizações são atualizadas para selecionar os dados das novas tabelas ( dbo.DailyTransactionComplete
por exemplo, quando disponíveis) e selecionar nas tabelas herdadas quando não estiverem.
Com efeito, algo como o seguinte está sendo feito:
CREATE VIEW dbo.DailyTransaction
AS SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.DailyTransactionComplete
UNION ALL
SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.DailyTransaction_LEGACY l
WHERE NOT EXISTS ( SELECT 1
FROM dbo.DailyTransactionComplete t
WHERE t.FileDate = l.FileDate );
Embora logicamente correto, isso não apresenta um bom desempenho em vários casos de agregação, geralmente resultando em um plano de execução que executa uma varredura completa do índice nos dados da tabela herdada. Provavelmente, isso é bom para algumas dezenas de milhões de registros, mas não tanto para algumas dezenas de milhões de registros. Como o último é de fato o caso, tive que recorrer a ser ... "criativo", levando-me a criar uma exibição indexada.
Aqui está o pequeno caso de teste que eu configurei, incluindo a FileDate
chave de controle que foi portada para a DateCode_FK
porta compatível com o Data Warehouse para ilustrar o quão pouco me importo com o fato de as consultas contra a nova tabela serem sargáveis por enquanto:
USE tempdb;
GO
SET NOCOUNT ON;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransaction_LEGACY'
AND type = 'U' )
BEGIN
--DROP TABLE dbo.DailyTransaction_LEGACY;
CREATE TABLE dbo.DailyTransaction_LEGACY
(
DailyTransaction_PK BIGINT IDENTITY( 1, 1 ) NOT NULL,
FileDate DATETIME NOT NULL,
Foo INT NOT NULL
);
INSERT INTO dbo.DailyTransaction_LEGACY ( FileDate, Foo )
SELECT DATEADD( DAY, ( 1 - ROW_NUMBER()
OVER( ORDER BY so1.object_id ) - 800 ) % 1000,
CONVERT( DATE, GETDATE() ) ),
so1.object_id % 1000 + so2.object_id % 1000
FROM sys.all_objects so1
CROSS JOIN sys.all_objects so2;
ALTER TABLE dbo.DailyTransaction_LEGACY
ADD CONSTRAINT PK__DailyTrainsaction
PRIMARY KEY CLUSTERED ( DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 );
END;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransactionComplete'
AND type = 'U' )
BEGIN
--DROP TABLE dbo.DailyTransactionComplete;
CREATE TABLE dbo.DailyTransactionComplete
(
DailyTransaction_PK BIGINT IDENTITY( 1, 1 ) NOT NULL,
DateCode_FK INTEGER NOT NULL,
Foo INTEGER NOT NULL
);
INSERT INTO dbo.DailyTransactionComplete ( DateCode_FK, Foo )
SELECT TOP 100000
CONVERT( INTEGER, CONVERT( VARCHAR( 8 ), DATEADD( DAY,
( 1 - ROW_NUMBER() OVER( ORDER BY so1.object_id ) ) % 100,
GETDATE() ), 112 ) ),
so1.object_id % 1000
FROM sys.all_objects so1
CROSS JOIN sys.all_objects so2;
ALTER TABLE dbo.DailyTransactionComplete
ADD CONSTRAINT PK__DailyTransaction
PRIMARY KEY CLUSTERED ( DateCode_FK, DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 );
END;
GO
Na minha sandbox local, a tabela acima me fornece uma tabela herdada com cerca de 4,4 milhões de linhas e uma nova tabela contendo 0,1 milhão de linhas, com alguma sobreposição dos valores DateCode_FK
/ FileDate
.
Um MAX( FileDate )
contra a tabela herdada sem índices adicionais é executado sobre o que eu esperaria.
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput DATETIME;
SELECT @ConsumeOutput = MAX( FileDate )
FROM dbo.DailyTransaction_LEGACY;
SET STATISTICS IO, TIME OFF;
GO
Tabela 'DailyTransaction_LEGACY'. Contagem de varredura 1, leituras lógicas 9228, leituras físicas 0, leituras antecipadas 0, leituras lógicas lob 0, leituras físicas lob 0, leituras físicas lob 0, leituras antecipadas lob.
Tempos de execução do SQL Server: tempo de CPU = 889 ms, tempo decorrido = 886 ms.
Jogar um índice simples em cima da mesa torna as coisas muito melhores. Ainda uma varredura, mas uma varredura em um registro, em vez dos 4,4 milhões de registros. Eu sou legal com isso.
CREATE NONCLUSTERED INDEX IX__DailyTransaction__FileDate
ON dbo.DailyTransaction_LEGACY ( FileDate );
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput DATETIME;
SELECT @ConsumeOutput = MAX( FileDate )
FROM dbo.DailyTransaction_LEGACY;
SET STATISTICS IO, TIME OFF;
GO
Tempo de análise e compilação do SQL Server: tempo da CPU = 0 ms, tempo decorrido = 1 ms. Tabela 'DailyTransaction_LEGACY'. Contagem de varreduras 1, leituras lógicas 3, leituras físicas 0, leituras antecipadas 0, leituras lógicas lob 0, leituras lógicas lob 0, leituras físicas lob 0, leituras antecipadas lob.
Tempos de execução do SQL Server: tempo de CPU = 0 ms, tempo decorrido = 0 ms.
E agora, criando a visualização para que os desenvolvedores não precisem alterar nenhum código, porque aparentemente seria o fim do mundo como o conhecemos. Um tipo de cataclismo.
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransaction'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.DailyTransaction AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.DailyTransaction
AS SELECT DailyTransaction_PK, FileDate = CONVERT(
DATETIME, CONVERT( VARCHAR( 8 ), DateCode_FK ), 112 ), Foo
FROM dbo.DailyTransactionComplete
UNION ALL
SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.DailyTransaction_LEGACY l
WHERE NOT EXISTS ( SELECT 1
FROM dbo.DailyTransactionComplete t
WHERE CONVERT( DATETIME, CONVERT( VARCHAR( 8 ),
t.DateCode_FK ), 112 ) = l.FileDate );
GO
Sim, a subconsulta é péssima, mas esse não é o problema e provavelmente vou simplesmente criar uma coluna computada persistente e lançar um índice nela para esse fim quando o problema real for resolvido. Então, sem mais delongas,
O problema
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT @ConsumeOutput1 = MAX( FileDate )
FROM dbo.DailyTransaction;
SET STATISTICS IO, TIME OFF;
GO
Tempo de análise e compilação do SQL Server: tempo da CPU = 0 ms, tempo decorrido = 4 ms. Tabela 'DailyTransaction_LEGACY'. Contagem de varredura 1, leituras lógicas 11972, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de 0. Tabela 'Worktable'. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de 0. Tabela 'Arquivo de Trabalho'. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada 0. 0. Tabela 'DailyTransactionComplete'. Contagem de varredura 2, leituras lógicas 620, leituras físicas 0, leituras antecipadas 0, leituras lógicas lob 0, leituras lógicas lob 0, leituras físicas lob 0, leituras antecipadas lob.
Tempos de execução do SQL Server: tempo de CPU = 983 ms, tempo decorrido = 983 ms.
Ah, entendo, o Sql Server está tentando me dizer que o que estou fazendo é idiota. Embora eu concorde amplamente, isso não muda minha situação. Na verdade, isso funciona de maneira brilhante para consultas FileDate
em que a dbo.DailyTransaction
exibição está incluída no predicado, mas, embora o MAX
plano seja ruim o suficiente, o TOP
plano envia tudo para o sul. Sul verdadeiro.
SET STATISTICS IO, TIME ON;
SELECT TOP 10 FileDate
FROM dbo.DailyTransaction
GROUP BY FileDate
ORDER BY FileDate DESC
SET STATISTICS IO, TIME OFF;
GO
Tabela 'DailyTransactionComplete'. Contagem de varredura 2, leituras lógicas 1800110, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada 0. 0. Tabela 'DailyTransaction_LEGACY'. Contagem de varredura 1, leituras lógicas 1254, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de zero 0. Tabela 'Worktable'. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de 0. Tabela 'Arquivo de Trabalho'. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de lob 0.
Tempos de execução do SQL Server: tempo de CPU = 109559 ms, tempo decorrido = 109664 ms.
Eu mencionei ser "criativo" anteriormente, o que provavelmente foi enganoso. O que eu quis dizer foi "mais estúpido", então minhas tentativas de fazer essa visualização funcionar durante operações de agregação foram criar visualizações nas tabelas dbo.DailyTransactionComplete
e dbo.DailyTransaction_LEGACY
, ligar o esquema e indexar a última e, em seguida, usá-las em outra visualização com uma NOEXPAND
dica na visão herdada. Embora esteja trabalhando mais ou menos para o que precisa fazer por enquanto, considero toda a "solução" bastante perturbadora, culminando com o seguinte:
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'v_DailyTransactionComplete'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.v_DailyTransactionComplete AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.v_DailyTransactionComplete
AS SELECT DailyTransaction_PK, FileDate = CONVERT( DATETIME,
CONVERT( VARCHAR( 8 ), DateCode_FK ), 112 ),
Foo
FROM dbo.DailyTransactionComplete;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'v_DailyTransaction_LEGACY'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.v_DailyTransaction_LEGACY AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.v_DailyTransaction_LEGACY
WITH SCHEMABINDING
AS SELECT l.DailyTransaction_PK,
l.FileDate,
l.Foo,
CountBig = COUNT_BIG( * )
FROM dbo.DailyTransaction_LEGACY l
INNER JOIN dbo.DailyTransactionComplete n
ON l.FileDate <> CONVERT( DATETIME, CONVERT( VARCHAR( 8 ),
n.DateCode_FK ), 112 )
GROUP BY l.DailyTransaction_PK,
l.FileDate,
l.Foo;
GO
CREATE UNIQUE CLUSTERED INDEX CI__v_DailyTransaction_LEGACY
ON dbo.v_DailyTransaction_LEGACY ( FileDate, DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 80 );
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransaction'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.DailyTransaction AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.DailyTransaction
AS SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.v_DailyTransactionComplete
UNION ALL
SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.v_DailyTransaction_LEGACY WITH ( NOEXPAND );
GO
Forçar o otimizador a usar o índice fornecido pela exibição indexada diminui os problemas MAX
e TOP
, mas deve haver uma maneira melhor de conseguir o que estou tentando fazer aqui. Absolutamente qualquer sugestão / bronca seria muito apreciada !!
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT @ConsumeOutput1 = MAX( FileDate )
FROM dbo.DailyTransaction;
SET STATISTICS IO, TIME OFF;
GO
Tabela 'v_DailyTransaction_LEGACY'. Contagem de varredura 1, leituras lógicas 3, leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras de lob de leitura antecipada 0. Tabela 'DailyTransactionComplete'. Contagem de varredura 1, leituras lógicas 310, leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de lob 0.
Tempos de execução do SQL Server: tempo de CPU = 31 ms, tempo decorrido = 36 ms.
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT TOP 10 @ConsumeOutput1 = FileDate
FROM dbo.DailyTransaction
GROUP BY FileDate
ORDER BY FileDate DESC
SET STATISTICS IO, TIME OFF;
GO
Tabela 'v_DailyTransaction_LEGACY'. Contagem de varredura 1, leituras lógicas 101, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de 0. Tabela 'Worktable'. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de 0. Tabela 'Arquivo de Trabalho'. Contagem de varreduras 0, leituras lógicas 0, leituras físicas 0, leituras de leitura antecipada 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada 0. 0. Tabela 'DailyTransactionComplete'. Contagem de varredura 1, leituras lógicas 310, leituras físicas 0, leituras de read-ahead 0, leituras lógicas de lob 0, leituras físicas de lob 0, leituras físicas de lob 0, leituras de leitura antecipada de lob 0.
Tempos de execução do SQL Server: tempo de CPU = 63 ms, tempo decorrido = 66 ms.
TL; DR:
Ajude-me a entender o que preciso fazer para que as consultas de agregação na primeira exibição que eu mencionei sejam executadas em quantidades razoáveis de tempo com utilização razoável de recursos de E / S.
fonte
Respostas:
Reescrever
NOT EXISTS
comoDISTINCT
uma junção de desigualdade permite que a exibição seja indexada, mas há boas razões para isso não ser feito com frequência.O plano de execução gerado para criar o índice na visualização é inevitavelmente horrível. A desigualdade força uma junção física de loops aninhados, que, com exceção de um valor, é uma junção cruzada. O recolhimento do produto com um grupo distinto ou equivalente produzirá os resultados corretos, supondo que a coluna de junção não seja anulável (como no código de exemplo), mas nunca será eficiente. Essa ineficiência só piora com o passar do tempo e as tabelas envolvidas se tornam maiores.
Problemas semelhantes afetam o plano de execução de qualquer instrução DML que afeta uma tabela referenciada pela exibição (porque a exibição deve ser sincronizada com as tabelas base o tempo todo no SQL Server). Veja o plano de execução gerado para adicionar ou modificar uma única linha em qualquer tabela para ver o que quero dizer.
Em um nível alto, o problema que você está enfrentando é que o otimizador de consultas do SQL Server nem sempre gera bons planos sobre as visualizações que incluem a
UNION ALL
. Muitas das otimizações que tomamos como garantidas (comoMAX
->TOP (1)
) simplesmente não são implementadas em toda a união.Para cada problema resolvido, você encontrará outro caso em que uma otimização normal e esperada não ocorre, resultando em um plano de execução com desempenho desesperado. A solução óbvia é evitar o uso de união nas visualizações. Como você implementa isso no seu caso depende de detalhes que, apesar dos detalhes da pergunta, provavelmente são conhecidos apenas por você.
Se você tiver espaço, uma solução é manter
complete
elegacy
basear as tabelas separadamente (incluindo a lógica não existe). Isso resulta em duplicação de dados e vem com problemas de sincronização, mas, na minha experiência, eles são muito mais fáceis de resolver com robustez do que tentar obter visualizações de união para gerar bons planos de execução para uma ampla gama de consultas em todas (ou mesmo na maioria) circunstâncias.O SQL Server fornece vários recursos para ajudar na sincronização de dados, como tenho certeza, incluindo rastreamento de alterações, captura de dados alterados, gatilhos ... e assim por diante. As especificidades da implementação estão além deste fórum. O ponto importante é apresentar o otimizador com tabelas de base, não unir todas as visualizações.
fonte