Data

29 ago, 2013

Índices de Columnstore agrupáveis do SQL Server

Publicidade

Agora que as apresentações do TechEd 2013 estão online no Channel9, você pode assistir à apresentação de Brian Mitchell sobre “O que há de novo nos índices Columnstore e no modo de processamento em lote” (Batch Mode). Brian faz um ótimo trabalho ao apresentar as atualizações dos índices columnstore agrupáveis (clustered) e as melhorias feitas na execução de query em vetores (também chamada de batch mode, ou mode em lote). Além das apresentações da TechEd, há outras excelentes fontes disponíveis online para o seu aprendizado sobre o tema: o paper Enhancements to SQL Server Column Stores do SIGMOD2013. Além de estar obviamente atualizado, cita algumas melhorias que estão disponíveis em columnstores agrupáveis:

  • Criação melhorada de índices
  • Suporte para amostragem
  • Suporte para bookmark
  • Suporte e esquemas de modificação
  • Suporte para pequenas strings
  • Modo de execução mista
  • Suporte para Hash Join
  • Melhorias nos filtros bitmap
  • Suporte para arquivamento

Não deve ser surpresa para alguém estudando armazenamento em colunas que os columnstores agrupáveis e atualizáveis que virão na próxima versão do SQL Server serão baseados em deltastores. Eu falei anteriormente sobre SQL Server 2012 Columnstore internals e expliquei por que o formato de alta compressão que torna o armazenamento em colunas tão rápido, também os torna praticamente impossível atualizar no local. A técnica de ter um “deltastore” que armazena alterações e, durante as varreduras, consolida as atualizações com os dados nas colunas não é nova e é empregada por muitos fabricantes de soluções de armazenamento em forma de coluna.

Deltastores

Columnstores introduzem uma nova unidade de organização, o row-group. Um row-group é uma unidade lógica que agrupa até 220 linhas (aproximadamente 1 milhão de linhas). No SQL Server 2012, os row-groups estavam implícitos e havia a visão do catálogo (catalog view) para visualizá-los. Como você pode ver na apresentação do Brian, o SQL Server acrescenta 14 novas visualizações de catálogos: sys.column_store_row_groups. Essa visualização do catálogo exibe o estado de cada row-group para todos os columnstores (incluindo os não agrupados). Agrupamentos de columnstores atualizáveis podem exibir os row groups no estado COMPRESSED ou no estado OPEN/CLOSED. O row group OPEN/CLOSED é um deltastore (sim, pode haver múltiplos deltastores por columnstore, veja o paper do SIGMOD 2013 mencionado anteriormente). Row groups OPEN estão prontos para aceitar mais inserções, enquanto que row groups em CLOSED estão cheios e aguardando compressão. A estrutura de um deltastore é explicada no paper do SIGMOD:

Um delta store contém as mesmas colunas que o índice column store correspondente. A chave B-tree é o ID da fila e um número inteiro exclusivo gerado pelo sistema (column stores não possuem uma chave única).

Se você quer saber o que é um ID de fila cujo número inteiro exclusivo é gerado pelo sistema, recorde como o uniqueifier column funciona. Deltastores são gerenciados exclusivamente pelo engine, não há DDL para controlar a criação e a exclusão de deltastores. O engine cria um novo deltastore sempre que precisa de um para manipular inserts, fecha-o quando está cheio (com 1 milhão de filas) e um processo em segundo plano chamado de Tuple Mover comprime essas deltastores fechadas em um formato columnar storage.

Ao manipular deltastores, as vantagens do columnar storage são minimizadas. Deltastores são armazenamentos do tipo fila (row), portanto toda a fila precisa ser lida, não apenas as colunas de interesse. Eliminação de segmentos não acontecem para deltastores desde que os deltastores não possuam metadados sobre valores mínimos e máximos dentro de cada coluna. Varreduras paralelas distribuirão os deltastores entre as threads para que múltiplos deltastores sejam varridos em paralelo, mas não há tentativa de paralelismo dentro de um único deltastore. No tamanho máximo de 1 milhão de linhas, eles são simplesmente muito pequenos para justificar as complicações de engenharia para manipular paralelismo dentro de um deltastore. Tudo isso é explicado no paper SIGMOD.

Varreduras paralelas designam a cada delta store uma única thread de execução. Um único delta store é pequeno demais para justificar a varredura em paralelo, mas múltiplos delta stores podem ser varridos em paralelo. Varrer delta stores é mais lento do que varrer dados em forma de colunas, porque registros completos precisam ser lidos e não apenas e não apenas as colunas necessárias para a query.

Tuple Mover

O processo em segundo plano Tuple Mover é responsável por comprimir os deltastores cheios. O Tuple Mover é uma operação online e não evita que os dados sejam lidos a partir de delta stores sendo comprimidas. Isso é descrito no paper do SIGMOD:

O Tuple Mover lê um delta store fechado por vez e começa a construir os segmentos comprimidos correspondentes.  Durante esse momento, varreduras continuam a ver e a ler os delta stores. Quando o Tuple Mover terminou de comprimir o delta store, os novos segmentos criados tornam-se visíveis, e o delta store torna-se invisível em uma única operação atômica. Novas varreduras irão ver e varrer o formato comprimido. O Tuple Mover espera então que todas as varreduras continuem operando até que o delta store acabe, após o delta store ser removido.

Exclusões concorrentes ou atualizações são bloqueadas enquanto o Tuple Mover comprime um deltastore. Inserts concorrentes não são bloqueados pelo Tuple Mover. Como parte do “processo em segundo plano” do Tuple Mover, a analogia mais próxima é o processo de limpeza do Ghost. Veteranos sabem que as limpezas do Ghost nunca são ajustadas para a minha tarefa, ou são muito agressivas para alguns usuários ou muito lentas para outros. Será que o Tuple Mover sofrerá do mesmo problema? Eu espero que não, principalmente porque a unidade de trabalho é grande. Leva-se tempo para acumular 1 milhão de linhas em uma única linha através de inserts.

O processo de remoção de deltastores comprimidos é muito eficiente: basicamente uma desalocação (tão eficiente quanto um DROP). Isso é importante para que o Tuple Mover não gere demasiados logs durante a compressão.

Exclusão de bitmaps

O bitmap excluído é outra árvore-B (B-Tree) associada com columnstore agrupados. Há apenas um bitmap deletado para toda o columnstore, e ele cobre todos os row-groups (todos os segmentos). Apenas segmentos comprimidos usam um bitmap deletado. A operação DELETE é, na verdade, um insert no bitmap apagado; o row-group e o tuple number das filas apagadas são inseridos no bitmap deletado. Varreduras (leituras) cumprem os bitmaps deletados ao filtrar qualquer fila (n) marcada como apagada. É recomendável que os índices de columnstores agrupados que tiveram um alto número de exclusões sejam refeitos para restaurar a “saúde” dos segmentos ao remover as linhas apagadas. Operações de UPDATE em columnstore agrupados são sempre atualizações divididas, o que significa que o Otimizador de Query criará um plano que contém uma exclusão e um insert para qualquer UPDATE.

Bitmaps apagados não cobrem deltastores. Com árvores-B, os deltastores suportam exclusão direta, portanto implementam a exclusão ao remover a fila apagada.

Insert em lote

Grandes lotes de operações de insert não inserem linhas dentro dos delta stores, mas convertem os grupos de linhas diretamente para o formato coluna. A operação armazena as linhas em buffer até que um número suficiente de linhas tenha sido acumulado, converte-as para o formato coluna, e escreve os segmentos resultantes e o dicionário no disco. Isso é muito eficiente, reduz necessidades de IO e produz imediatamente o formato coluna necessário para varreduras rápidas. O lado ruim é a grande quantidade de memória necessária para colocar as linhas em buffer.

Para columnstores agrupados, o desempenho de inserts em lotes é algo crítico, por ser o ganha pão de cenários de ETL (sigla em inglês para extract, transform e load) de data centers de dados. Você terá que dar atenção para o seu fluxo ETL e se voltar para alcançar o formato de compressão ideal. Isso necessita que os inserts em lote sejam capazes de fazer o upload de 1 milhão de filas por partição em cada lote. Se os lotes forem pequenos demais, então o resultado será deltastores sub-otimizadas em vez de segmentos comprimidos e otimizados. Com o SISS (SQL Server Integration Service), você terá de prestar atenção ao tamanho do buffer de fluxo de dados, uma vez que o padrão de 10Mb é muito pequeno para arquivar eficientemente inserts em lote de segmentos de columnstores. É claro que haverá casos em que simplesmente não haverá dados suficientes para fazer o upload, e em tais casos o insert em lote resultará em um deltastore em vez de um columnstore comprimido. Os deltastores permanecerão como OPEN, e os lotes de insert subsequentes os reutilizarão até o fim de sua capacidade, fechando-os e para que o Tuple Mover comprima-os. Apesar de o formato de compressão direta ser desejável durante os inserts em lotes, você não deve forçar o seu ETL e sua lógica de negócios para fora disso apenas para conseguir tal cenário. Chegar ao estágio de um deltastore intermediário remediará a si mesmo automaticamente quando os dados acumulados forem suficientes.

Aproveite! SELECT … INTO sempre foi uma operação de massa (e minimamente registrada em logs), mas isso só pode criar um monte de modos de filas. Com INSERT …SELECT sendo otimizado para índices columnstores agrupados para usar a API de inserção em lotes, seu fluxo ETL pode construir os dados de troca diretamente no formato columnar storage em um passo simples, sem ter que lançar mão de reconstruções. Isso está, mais uma vez, descrito no paper do SIGMOD.

Tricle Insert

Trickle inserts são todos os inserts que não chegam através da API de inserções em lote. INSERT INTO… VALUES … são trickle insert. Trickle inserts são manipulados através por um deltastore e nunca podem criar dados comprimidos diretamente. Comandos UPDATE (assim como MERGE) resultam em atualizações divididas (delete e insert) e efetuarão os inserts em modo trickle.

Construção melhorada de índice

A criação de índices columnstore agrupados é inteligente. Ela resolve diversos problemas de novas formas:

Dicionários relevantes

As colunas de um armazenamento desse tipo utilizam um dicionário global, compartilhado por todos os segmentos, e dicionários locais compartilhados com segmentos específicos. Quanto mais relevante for o dicionário global (quanto mais atual forem os valores dos dados cobertos por ele), melhor será a compressão de dados alcançada, uma vez que os dicionários secundários são menores e até mesmo desnecessários. No SQL Server 2012, o dicionário global era apenas o primeiro dicionário criado, portanto ele poderia conter dados desviantes resultando em baixa relevância. Como os índices dos columnstores agrupados são criam todos os dados no primeiro estágio, os dicionários globais são feitos para todas as colunas que necessitam dele, e então a criação inicia da forma apropriada. Isso propicia um dicionário global muito melhor, que resulta em melhoras significativas no armazenamento (melhor compressão).

Minimizando o bloqueio

Esse problema não é específico de índices de columnar agrupados: durante a reconstrução offline de um índice, não há razão para bloquear as
leituras. Sim, há um risco de deadlock ao final da reconstrução do índice, mas sempre há formas de resolver esse problema. Com índices de
columnstore agrupados, operações de reconstruções offline são semi-online, o que significa que são permitidas leituras, mas atualizações são bloqueadas.

Variação de fluxo de trabalho

A criação de índices de columnstores é uma tarefa de uso intensivo da memória. O modelo de execução tradicional determina o DOP (degrees of parallelism) no início da execução, e então a query executa com o DOP fornecido. Com a criação do columnstore melhorada, a execução atual do DOP varia na medida em que a criação progride e o processo de criação pode ativa e voluntariamente reduzir seu DOP (ao “estacionar” a execução das threads) para se ajustar a condições de baixa memória.

Suporte para sampling

Requerido pela criação de índices de dois estágios, conforme mencionado anteriormente, para a criação de dicionários globais relevantes. De forma semelhante a como árvores-B e heap selecionam páginas inteiras para amostragem, columnstores podem selecionar grupos de filas (segmentos) para fazer o mesmo.

Suporte para bookmarks

Necessário para a implementação de atualizações separadas. O heap do bookmark é um localizador físico (file_id:page_id:slot_id), o bookmark da árvore-B é a chave valor atual e o bookmark do columnstore é o próprio par (row_group_id:tuple_id). Em deltastores, o tuple_id é o valor da coluna única para que o bookmark esteja alocado eficientemente com uma operação de busca (seek). Ter bookmarks também libera o otimizador para explorar opções adicionais como spools do tipo eager.

Suporte para esquemas de modificações

Acrescentar uma coluna, alterar coluna, excluir uma coluna são operações suportadas por índices columnstore.

Suporte para strings pequenas

Strings pequenas, como abreviações de estados americanos, são frequentes em tabelas informativas e, anteriormente, apenas dicionário de códigos estava disponível para eles, o que é ineficiente. Agora, strings podem ser codificadas por valores, se houver a necessidade de dicionário.

Modo de execução misto

Queries podem agora executar uma mistura de estágios de modo de lote e modo de fila (batch-mode e row-mode). Operadores de adaptadores especiais podem trocar filas por lotes e vice-versa. Isso dá ao otimizador a liberdade de misturar modo-lote com operadores não suportados sem ter que lançar mão de reverter toda a query para modo-fila.

Hash Join e melhoria nos filtros bitmap

O hash join é o join preferido de cargas de trabalho de data centers de dados, dominados por grandes conjuntos de dados e agregações. O hash-join em modo-lote manipula inner, outer, semi e anti-semi joins (ex.: todo o espectro de operadores join possíveis (não confundi-los com a sintaxe do join). Recomendo ver os capítulos relevantes no paper do SIGMOD para mais detalhes sobre esse tópico.

Suporte para arquivamento

De forma simples, adicione outra camada de compressão utilizando XPress8 (uma variação do LZ77) sobre os dicionários e segmentos já comprimidos. Recomendado para dados “frios”, pode ser aplicado por partições e oferecer uma redução adicional de até 66% do tamanho (os resultados podem variar).

***

Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://rusanu.com/2013/06/11/sql-server-clustered-columnstore-indexes-at-teched-2013/