Data

3 abr, 2014

SQL Server clustered columnstore Tuple Mover

Publicidade

Os índices columnstore cluster atualizáveis introduzidos no SQL Server 2014 dependem de uma tarefa em segundo plano chamada de Tuple Mover para comprimir periodicamente deltastores no formato mais eficiente de coluna. Deltastores armazenam dados no modo tradicional de linha (são B-Trees) e, como tal, são significativamente mais custosos de consultar do que os segmentos de colunas comprimidas. Quanto mais custoso? Eles são equivalentes a guardar os dados em um Heap descompactado e, devido ao pequeno tamanho (máximo de 1.048.576 linhas por rowset deltastore), recebem pouca tração de paralelismo e de read-aheads . É importante, para o seu upload, um seed inicial e atividades de ETL cotidianas para conseguir um índice columnstore saudável, ou seja, sem deltastores ou com apenas alguns deltastores. Para atingir esse estado desejado de um columnstore saudável, é de suma importância entender como deltastores são criados e removidos.

Como deltastores são criados

Deltastores aparecem através de um dos seguintes eventos:

  • INSERT corrente: INSERT comuns que não usam a API BULK INSERT. Isso inclui todas as instruções INSERT, exceto INSERT … SELECT…
  • Comandos UPDATE, MERGE: todas as atualizações são implementadas em columnstores clusterizados como uma exclusão de dados antigos e a inserção de novos dados (modificados). A inserção de novos dados como resultado de atualizações de dados será sempre uma inserção comum.
  • Operações subdimensionadas de BULK INSERT: um número insuficiente de linhas de dados inseridos usando a API do BULK INSERT (que está usando um dos IRowFastLoad, usam o cliente SQL nativo de extensões de operação ODBC em massa ou a classe .Net gerenciável SqlBulkCopy) e declarações INSERT … SELECT …. Essas APIs são frequentemente mais conhecidas pelas diversas ferramentas que as expõem, como bcp.exe ou a destinação “fast- load” SSIS OleDB. Linhas insuficientes significam que o número de linhas submetidas em um lote é demasiado pequeno para criar um segmento comprimido em uma partição. O número exato é, penso eu, em torno de 100 mil linhas. Abaixo dessa margem, mesmo quando a API BULK INSERT é usada, as linhas serão inseridas como um deltastore. Acima de 100 mil, um segmento comprimido é criado, mas note que um columnstore clusterizado composto por 100 mil segmentos de linha será sub-otimizado. O tamanho ideal do lote é de 1 milhão de linhas, e seu processo de ETL deve se esforçar para atingir esse tamanho por partição por thread (ex.: cada thread de insert deve inserir 1 milhão de linhas para cada partição) .

É claro que o processo de negócio simplesmente pode não ter dados suficientes em um ciclo de ETL para criar o tamanho desejado de 1 milhão de linhas por lote, ou até mesmo o mínimo 100 mil linhas. Este As linhas serão salvas como deltastores, e os deltastores serão posteriormente comprimidos, quando atingirem o limite (tamanho máximo de 1.048.576 linhas). O que é importante, porém, é que durante a população inicial (seeding), o processo de upload deve criar columnstores clusterizados que sejam saudáveis. A melhor opção é fazer o upload dos dados em uma estrutura de modo-linha (heap ou uma b-tree) e, em seguida, emitir uma instrução CREATE CLUSTERED COLUMNSTORE INDEX para construir um columnstore agrupado. Essa opção é a melhor porque só CREATE INDEX em dados existentes criará dicionários globais adequadamente relevantes (ou seja, úteis) para os segmentos comprimidos. Ter bons dicionários globais reduz o tamanho dos segmentos compactados de forma significativa, para melhorar o desempenho da consulta. Se você tem medo de executar CREATE CLUSTERED COLUMNSTORE INDEX em um conjunto grande devido a possíveis problemas de crescimento de registro de dados, lembre-se de duas coisas: CREATE INDEX é um excelente candidato para registro mínimo, e um índice columnstore clusterizado não vai gerar um enorme log linha por linha, mesmo em modo log completo, porque ele só grava os dados compactados.

Se você não pode fazer o upload dos dados iniciais em row-mode e então criar o índice do cluster, a alternativa é o BULK INSERT em um columnstore vazi. Isso resultará em um columnstore de pior qualidade porque ele não pode usar os dicionários globais, apenas dicionários locais podem. Estes, em geral, resultam em uma compressão pior. Claro que a compressão ainda será de ordens de magnitude melhor do que em row-mode, mas provavelmente não será tão boa quanto uma compilação recente do índice poderia alcançar. Mas o maior problema que você terá para popular inicialmente um columnstore vazio será alcançar os desejados 1 milhão de linhas por partição por thread. Pense no que isso significa em termos de um SSIS DefaultBufferSize e DefaultBufferMaxRow!

A pior opção de todas para popular inicialmente os dados é efetuar o upload em pequenos lotes em um índice columnstore existente, deixá-lo estabelecer-se em deltastores e deixar o Tuple Mover comprimi-los.

Como deltastores são comprimidos

Quando um deltastore atinge o limite (ou seja, atinge o tamanho máximo de 1.048.576 linhas), ele será fechado e se tornará disponível para o Tuple Mover para compressão. O Tuple Mover criará grandes segmentos saudáveis. Mas ele não foi projetado para ser um substituto para construção de índice. O que isso significa?

create event session cci_tuple_mover
on server
 add event sqlserver.columnstore_tuple_mover_begin_compress,
 add event sqlserver.columnstore_tuple_mover_end_compress
 add target package0.asynchronous_file_target
   (SET filename = 'c:\temp\cci_tuple_mover.xel', metadatafile = 'c:\temp\cci_tuple_mover.xem');

O Tuple Mover não expõe muito no que diz respeito ao monitoramento, mas expõe dois XEvents para quando ele começa a comprimir um segmento e quando completa uma compressão de segmento. Quando crio um par de deltastores prontos e espero o Tuple Mover para comprimi-los, isto é o que eu vejo na sessão XEvents:

with e as (select cast(event_data as xml) as x
	from sys.fn_xe_file_target_read_file ('c:\temp\*.xel', 'c:\temp\*.xem', null, null))
select x.value(N'(//event/@name)[1]', 'varchar(40)') as event,
	x.value(N'(//event/@timestamp)[1]', 'datetime') as time
	from e
	order by time desc;

event                                    time
---------------------------------------- -----------------------
columnstore_tuple_mover_end_compress     2013-12-02 11:26:25.047
columnstore_tuple_mover_begin_compress   2013-12-02 11:26:20.247
columnstore_tuple_mover_end_compress     2013-12-02 11:26:05.040
columnstore_tuple_mover_begin_compress   2013-12-02 11:26:00.183

Você pode ver que o Tuple Mover leva cerca de cinco segundos para comprimir um grupo de linhas, que é um intervalo de tempo enorme para você ficar esperando. Linhas mais amplas (muitas colunas) vão demorar mais. Ele não pode se beneficiar de paralelismo. Também é importante notar que o Tuple Mover não faz nada por cerca de 15 segundos entre os dois rowgroups que ele compacta. Ele não está à espera de algum recurso, é literalmente a forma como ele funciona: comprime um rowgroup de cada vez por tabela, e então dorme. Além disso, o Tuple Mover apenas acorda e procura por “trabalho” de tempos em tempos. Esse é um comportamento intencional, não um problema de pouca utilização de recursos. O Tuple Mover não está tentando correr para acompanhar a enorme inserção em massa. Para essas situações, o pipeline ETL deve criar os rowgroups direto formato comprimido, ou você deve usar operações de reconstrução/reorganização do índice. O Tuple Mover trabalha em um ritmo que é projetado para se manter de acordo com o ritmo normal do INSERT, restringindo-se a um consumo mínimo de recursos. Se o Tuple Mover tentasse ser uma tarefa agressiva, disposta a comprometer todos os núcleos de 5-6 segundos em 100% da CPU – e repetisse a tarefa logo em seguida, alguém poderia inesperadamente se deparar diante de um computador que não responderia quando os recursos fossem necessários para quem sabe lá qual tarefa fosse importante. Na verdade, não importa o quão agressiva ou tímida é uma tarefa, sempre haverá alguém reclamando que essa é a escolha errada para uma determinada carga de trabalho em particular. Ghost Cleanup, alguém?

Executando o Tuple Mover sob demanda

Se você fizer operações que resultam com um grande número de deltastores, o Tuple Mover só vai pegar os deltastores em um ritmo de 5-6 segmentos por minuto, talvez até menos. Se o seu índice tem centenas ou mesmo milhares de deltastores em CLOSED, então você deve considerar uma operação de reconstrução de índice. Esperar que o Tuple Mover consiga alcançar o ritmo vai demorar horas, durante as quais as consultas a partir dessa “columnstore” serão lentas. Afinal, um columnstore consistindo em nada além de centenas de deltastores não se beneficia de qualquer uma das otimizações de columnstore (sem compressão, sem leitura de apenas colunas relevantes, sem eliminação segmentos), e os dados terão de ser empurrados para dentro da execução da consulta como um formato em coluna “falso”, já que os deltastores são um conceito de armazenamento que não é entendido na cadeia superior de execução.

E se acontecer de você ficar com centenas, milhares de deltastores fechados? Como expliquei, a melhor opção é a de reconstruir o índice, porque isso vai construir novos dicionários globais mais relevantes para os dados presentes na tabela. O melhor a se fazer em seguida é REORGANIZE. REORGANIZE em um índice columnstore clusterizado tem a semântica de “execute o Tuple Mover agora nesta tabela a toda velocidade”. Quando o REORGANIZE é executado, o motor tem uma luz verde bem clara para usar todos os recursos à sua disposição para comprimir os segmentos fechados e agirá em conformidade.

Inclusive, você sabia que as operações de reconstrução offline de índices columnstore são, na verdade, semi-online? Elas só adquirem bloqueios S no índice durante a fase de reconstrução e, em seguida, escalonam para um curto bloqueio SCH-M no final, quando trocam o índice velho pelo novo. As consultas podem ler o índice antigo durante a reconstrução, mas updates/deletes/inserts não são permitidos.

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://rusanu.com/2013/12/02/sql-server-clustered-columnstore-tuple-mover/