Introdução
- como a indexação no ClickHouse difere da indexação em sistemas tradicionais de gerenciamento de bancos de dados relacionais
- como o ClickHouse cria e usa o índice primário esparso de uma tabela
- quais são algumas das melhores práticas de indexação no ClickHouse
Conjunto de dados
- Usaremos um subconjunto de 8,87 milhões de linhas (eventos) do conjunto de dados de exemplo.
- O tamanho dos dados não compactados é de 8,87 milhões de eventos e cerca de 700 MB. Esse volume é compactado para 200 MB quando armazenado no ClickHouse.
- Em nosso subconjunto, cada linha contém três colunas que indicam um usuário da internet (coluna
UserID) que clicou em uma URL (colunaURL) em um momento específico (colunaEventTime).
- “Quais são as 10 URLs mais clicadas por um usuário específico?”
- “Quais são os 10 usuários que mais clicaram em uma URL específica?”
- “Quais são os horários mais populares (por exemplo, dias da semana) em que um usuário clica em uma URL específica?”
Máquina de teste
Varredura completa da tabela
insert.
Isso usa a função de tabela URL para carregar um subconjunto do conjunto de dados completo hospedado remotamente em clickhouse.com:
Design de índices no ClickHouse
Um design de índices para grandes escalas de dados
B(+)-Tree tem complexidade de tempo média O(log n); mais precisamente, log_b n = log_2 n / log_2 b, em que b é o fator de ramificação da B(+)-Tree e n é o número de linhas indexadas. Como b normalmente fica entre algumas centenas e alguns milhares, as B(+)-Trees são estruturas muito rasas, e são necessárias poucas operações de seek em disco para localizar registros. Com 8,87 milhões de linhas e um fator de ramificação de 1000, são necessárias, em média, 2,3 operações de seek em disco. Essa capacidade tem um custo: sobrecarga adicional de disco e memória, custos de inserção mais altos ao adicionar novas linhas à tabela e novas entradas ao índice e, às vezes, rebalanceamento da B-Tree.
Considerando os desafios associados aos índices B-Tree, os motores de tabela do ClickHouse utilizam uma abordagem diferente. A família de motores MergeTree do ClickHouse foi projetada e otimizada para lidar com volumes massivos de dados. Essas tabelas foram projetadas para receber milhões de inserções de linhas por segundo e armazenar volumes muito grandes (centenas de petabytes) de dados. Os dados são gravados rapidamente em uma tabela parte por parte, com regras aplicadas para mesclar as partes em segundo plano. No ClickHouse, cada parte tem seu próprio índice primário. Quando as partes são mescladas, os índices primários da parte mesclada também são mesclados. Na escala extremamente grande para a qual o ClickHouse foi projetado, é fundamental ser altamente eficiente em termos de disco e memória. Por isso, em vez de indexar cada linha, o índice primário de uma parte tem uma entrada de índice (conhecida como ‘mark’) por grupo de linhas (chamado de ‘granule’) - essa técnica é chamada de índice esparso.
A indexação esparsa é possível porque o ClickHouse armazena em disco as linhas de uma parte ordenadas pelas colunas da chave primária. Em vez de localizar diretamente linhas individuais (como um índice baseado em B-Tree), o índice primário esparso permite identificar rapidamente (por meio de uma busca binária nas entradas do índice) grupos de linhas que podem corresponder à consulta. Os grupos localizados de linhas potencialmente correspondentes (grânulos) são então transmitidos em paralelo para o mecanismo do ClickHouse a fim de encontrar as correspondências. Esse design de índice permite que o índice primário seja pequeno (ele pode, e deve, caber completamente na memória principal), ao mesmo tempo que ainda acelera significativamente o tempo de execução das consultas: especialmente no caso de consultas de intervalo, típicas em cenários de análise de dados.
A seguir, mostramos em detalhes como o ClickHouse constrói e usa seu índice primário esparso. Mais adiante neste artigo, discutiremos algumas boas práticas para escolher, remover e ordenar as colunas da tabela usadas para construir o índice (colunas da chave primária).
Uma tabela com chave primária
Detalhes da instrução DDL
Detalhes da instrução DDL
Para simplificar as discussões mais adiante neste guia, bem como tornar os diagramas e resultados reproduzíveis, a instrução DDL:
- Especifica uma chave de ordenação composta para a tabela por meio de uma cláusula
ORDER BY. - Controla explicitamente quantas entradas o índice primário terá por meio das seguintes configurações:
index_granularity: definido explicitamente com seu valor padrão de 8192. Isso significa que, para cada grupo de 8192 linhas, o índice primário terá uma entrada de índice. Por exemplo, se a tabela contiver 16384 linhas, o índice terá duas entradas de índice.index_granularity_bytes: definido como 0 para desabilitar a granularidade adaptativa do índice. Isso significa que o ClickHouse cria automaticamente uma entrada de índice para um grupo de n linhas se qualquer uma destas condições for verdadeira:- Se
nfor menor que 8192 e o tamanho combinado dos dados dessasnlinhas for maior ou igual a 10 MB (o valor padrão deindex_granularity_bytes). - Se o tamanho combinado dos dados de
nlinhas for menor que 10 MB, masnfor 8192.
- Se
compress_primary_key: definido como 0 para desabilitar a compressão do índice primário. Isso nos permitirá, se desejado, inspecionar seu conteúdo mais adiante.
Em seguida, insira os dados:
E otimize a tabela:
Podemos usar a consulta a seguir para obter metadados sobre nossa tabela:
- Os dados da tabela são armazenados em formato wide em um diretório específico no disco, o que significa que haverá um arquivo de dados (e um arquivo de marcação) para cada coluna da tabela dentro desse diretório.
- A tabela tem 8,87 milhões de linhas.
- O tamanho dos dados não compactados de todas as linhas somadas é 733.28 MB.
- O tamanho compactado em disco de todas as linhas somadas é 206.94 MB.
- A tabela tem um índice primário com 1083 entradas (chamadas de ‘marcas’), e o tamanho do índice é 96.93 KB.
- No total, os dados da tabela, os arquivos de marcação e o arquivo de índice primário ocupam juntos 207.07 MB em disco.
Os dados são armazenados em disco ordenados pelas colunas da chave primária
- uma chave primária composta
(UserID, URL)e - uma chave de ordenação composta
(UserID, URL, EventTime).
- Se tivéssemos especificado apenas a chave de ordenação, a chave primária seria implicitamente definida como igual à chave de ordenação.
- Para otimizar o uso de memória, especificamos explicitamente uma chave primária que contém apenas as colunas usadas nos filtros das nossas consultas. O índice primário baseado na chave primária é carregado integralmente na memória principal.
- Para manter a consistência nos diagramas do guia e maximizar a taxa de compressão, definimos uma chave de ordenação separada que inclui todas as colunas da tabela (se, em uma coluna, dados semelhantes ficarem próximos uns dos outros, por exemplo, por meio da ordenação, esses dados serão comprimidos melhor).
- A chave primária precisa ser um prefixo da chave de ordenação se ambas forem especificadas.
EventTime da chave de ordenação).
EventTime.- na representação em disco, há um único arquivo de dados (*.bin) por coluna da tabela, no qual todos os valores dessa coluna são armazenados em formato compactado, e
- as 8,87 milhões de linhas são armazenadas em disco em ordem lexicográfica crescente pelas colunas da chave primária (e pelas colunas adicionais da chave de ordenação), ou seja, neste caso
- primeiro por
UserID, - depois por
URL, - e por fim por
EventTime:
- primeiro por
UserID.bin, URL.bin e EventTime.bin são os arquivos de dados em disco onde os valores das colunas UserID, URL e EventTime são armazenados.
- Como a chave primária define a ordem lexicográfica das linhas em disco, uma tabela pode ter apenas uma chave primária.
- Estamos numerando as linhas a partir de 0 para manter o alinhamento com o esquema interno de numeração de linhas do ClickHouse, que também é usado em mensagens de log.
Os dados são organizados em grânulos para processamento paralelo de dados
index_granularity (definida com o valor padrão de 8192).
As primeiras 8192 linhas (com base na ordem física em disco) (seus valores de coluna) pertencem logicamente ao grânulo 0; as 8192 linhas seguintes (seus valores de coluna) pertencem ao grânulo 1; e assim por diante.
- O último grânulo (grânulo 1082) “contém” menos de 8192 linhas.
- Mencionamos no início deste guia, em “Detalhes da instrução DDL”, que desativamos a granularidade adaptativa do índice (para simplificar as discussões neste guia, bem como tornar os diagramas e os resultados reproduzíveis). Portanto, todos os grânulos (exceto o último) da nossa tabela de exemplo têm o mesmo tamanho.
- Para tabelas com granularidade adaptativa do índice (a granularidade do índice é adaptativa por padrão), o tamanho de alguns grânulos pode ser menor que 8192 linhas, dependendo do tamanho dos dados das linhas.
-
Marcamos alguns valores de coluna das nossas colunas de chave primária (
UserID,URL) em laranja. Esses valores de coluna marcados em laranja são os valores das colunas de chave primária da primeira linha de cada grânulo. Como veremos abaixo, esses valores de coluna marcados em laranja serão as entradas no índice primário da tabela. - Estamos numerando os grânulos a partir de 0 para manter o alinhamento com o esquema de numeração interno do ClickHouse, que também é usado nas mensagens de log.
O índice primário tem uma entrada por grânulo
- a primeira entrada do índice (‘marca 0’ no diagrama abaixo) armazena os valores das colunas da chave da primeira linha do grânulo 0 do diagrama acima;
- a segunda entrada do índice (‘marca 1’ no diagrama abaixo) armazena os valores das colunas da chave da primeira linha do grânulo 1 do diagrama acima; e assim por diante.
- Para tabelas com granularidade adaptativa do índice, há também uma marca adicional “final” armazenada no índice primário, que registra os valores das colunas da chave primária da última linha da tabela. Mas, como desativamos a granularidade adaptativa do índice (para simplificar a discussão neste guia e também tornar os diagramas e os resultados reproduzíveis), o índice da nossa tabela de exemplo não inclui essa marca final.
- O arquivo do índice primário é carregado completamente na memória principal. Se o arquivo for maior que o espaço livre de memória disponível, o ClickHouse gerará um erro.
Inspecionando o conteúdo do índice primário
Inspecionando o conteúdo do índice primário
Em um cluster ClickHouse autogerenciado, podemos usar a table function file para inspecionar o conteúdo do índice primário da nossa tabela de exemplo.Para isso, primeiro precisamos copiar o arquivo do índice primário para o user_files_path de um nó do cluster ativo:
- Passo 1: Obter o caminho da parte que contém o arquivo do índice primário
- Passo 2: Obter user_files_path O user_files_path padrão no Linux é
- Passo 3: Copiar o arquivo do índice primário para o user_files_path
SELECT path FROM system.parts WHERE table = 'hits_UserID_URL' AND active = 1retorna /Users/tomschreiber/Clickhouse/store/85f/85f4ee68-6e28-4f08-98b1-7d8affa1d88c/all_1_9_4 na máquina de teste./var/lib/clickhouse/user_files/e, no Linux, você pode verificar se ele foi alterado: $ grep user_files_path /etc/clickhouse-server/config.xmlNa máquina de teste, o caminho é /Users/tomschreiber/Clickhouse/user_files/cp /Users/tomschreiber/Clickhouse/store/85f/85f4ee68-6e28-4f08-98b1-7d8affa1d88c/all_1_9_4/primary.idx /Users/tomschreiber/Clickhouse/user_files/primary-hits_UserID_URL.idxAgora podemos inspecionar o conteúdo do índice primário via SQL:
- Obter o número de entradas
- Obter as duas primeiras marcas do índice
- Obter a última marca do índice
SELECT count( )<br/>FROM file('primary-hits_UserID_URL.idx', 'RowBinary', 'UserID UInt32, URL String');
retorna 1083SELECT UserID, URL<br/>FROM file('primary-hits_UserID_URL.idx', 'RowBinary', 'UserID UInt32, URL String')<br/>LIMIT 0, 2;retorna240923, http://showtopics.html%3...<br/> 4073710, http://mk.ru&pos=3_0SELECT UserID, URL FROM file('primary-hits_UserID_URL.idx', 'RowBinary', 'UserID UInt32, URL String')<br/>LIMIT 1082, 1;
retorna
4292714039 │ http://sosyal-mansetleri...Isso corresponde exatamente ao nosso diagrama do conteúdo do índice primário da nossa tabela de exemplo:
-
Marcas de índice de UserID:
Os valores
UserIDarmazenados no índice primário estão em ordem crescente.
Assim, a ‘marca 1’ no diagrama acima indica que os valores deUserIDde todas as linhas da tabela no grânulo 1 e em todos os grânulos seguintes são garantidamente maiores ou iguais a 4.073.710.
-
Marcas de índice de URL:
A cardinalidade bastante semelhante das colunas da chave primária
UserIDeURLsignifica que, em geral, as marcas de índice de todas as colunas-chave após a primeira só indicam um intervalo de dados enquanto o valor da coluna-chave anterior permanecer o mesmo para todas as linhas da tabela em pelo menos o grânulo atual.
Por exemplo, como os valores de UserID da marca 0 e da marca 1 são diferentes no diagrama acima, o ClickHouse não pode presumir que todos os valores de URL de todas as linhas da tabela no grânulo 0 sejam maiores ou iguais a'http://showtopics.html%3...'. No entanto, se os valores de UserID da marca 0 e da marca 1 fossem os mesmos no diagrama acima (ou seja, se o valor de UserID permanecesse o mesmo para todas as linhas da tabela dentro do grânulo 0), o ClickHouse poderia presumir que todos os valores de URL de todas as linhas da tabela no grânulo 0 sejam maiores ou iguais a'http://showtopics.html%3...'. Discutiremos em mais detalhes, adiante, as consequências disso para o desempenho da execução de consultas.
O índice primário serve para selecionar grânulos
749927693 na coluna UserID. Isso requer 19 passos, com complexidade de tempo média de O(log2 n):
Detalhes do Log de Trace
Detalhes do Log de Trace
A marca 176 foi identificada (a ‘marca do limite esquerdo encontrada’ é inclusiva, e a ‘marca do limite direito encontrada’ é exclusiva) e, portanto, todas as 8192 linhas do grânulo 176 (que começa na linha 1.441.792 — veremos isso mais adiante neste guia) são então lidas pelo ClickHouse para encontrar as linhas reais com o valor 749927693 na coluna UserID.
Como discutido acima, o ClickHouse usa seu índice primário esparso para selecionar rapidamente (via busca binária) grânulos que possam conter linhas correspondentes a uma consulta. Este é o primeiro estágio (seleção de grânulos) da execução de consultas no ClickHouse. No segundo estágio (leitura de dados), o ClickHouse localiza os grânulos selecionados para transmitir todas as linhas deles ao mecanismo do ClickHouse, a fim de encontrar as linhas que realmente correspondem à consulta. Abordamos esse segundo estágio em mais detalhes na seção a seguir.
Arquivos de marcação são usados para localizar grânulos
Detalhes da seleção de grânulos
Detalhes da seleção de grânulos
O diagrama acima mostra que a marca 176 é a primeira entrada do índice em que tanto o valor mínimo de UserID do grânulo 176 associado é menor que 749.927.693 quanto o valor mínimo de UserID do grânulo 177, da marca seguinte (marca 177), é maior que esse valor. Portanto, apenas o grânulo 176 correspondente à marca 176 pode conter linhas com o valor 749.927.693 na coluna UserID.
UserID.mrk, URL.mrk e EventTime.mrk, que armazenam as localizações físicas dos grânulos das colunas UserID, URL e EventTime da tabela.
Já vimos que o índice primário é um arquivo de array simples, não compactado (primary.idx), que contém marcas de índice numeradas a partir de 0.
Da mesma forma, um arquivo de marcação também é um arquivo de array simples, não compactado (*.mrk), contendo marcas numeradas a partir de 0.
Depois que o ClickHouse identifica e seleciona a marca de índice de um grânulo que pode conter linhas correspondentes a uma consulta, é possível realizar uma busca posicional no array nos arquivos de marcação para obter as localizações físicas do grânulo.
Cada entrada do arquivo de marcação para uma coluna específica armazena duas localizações na forma de offsets:
-
O primeiro offset (
block_offsetno diagrama acima) localiza o bloco no arquivo de dados da coluna compactado que contém a versão compactada do grânulo selecionado. Esse bloco compactado pode conter alguns grânulos compactados. O bloco compactado localizado é descompactado na memória principal durante a leitura. -
O segundo offset (
granule_offsetno diagrama acima), do arquivo de marcação, fornece a localização do grânulo dentro dos dados do bloco descompactado.
- Para tabelas com formato wide e sem granularidade adaptativa do índice, o ClickHouse usa arquivos de marcação
.mrk, como mostrado acima, que contêm entradas com dois endereços de 8 bytes por entrada. Essas entradas são localizações físicas de grânulos que têm todos o mesmo tamanho.
-
Para tabelas com formato wide e com granularidade adaptativa do índice, o ClickHouse usa arquivos de marcação
.mrk2, que contêm entradas semelhantes às dos arquivos de marcação.mrk, mas com um terceiro valor adicional por entrada: o número de linhas do grânulo ao qual a entrada atual está associada. -
Para tabelas com formato compact, o ClickHouse usa arquivos de marcação
.mrk3.
EventTime.Na nossa consulta de exemplo, o ClickHouse precisa apenas dos dois offsets de localização física do grânulo 176 no arquivo de dados UserID (UserID.bin) e dos dois offsets de localização física do grânulo 176 no arquivo de dados URL (URL.bin).A indireção fornecida pelos arquivos de marcação evita armazenar, diretamente no índice primário, entradas com as localizações físicas de todos os 1083 grânulos das três colunas, evitando assim manter dados desnecessários (potencialmente não utilizados) na memória principal.Como usar vários índices primários
Colunas secundárias da chave podem (não) ser ineficientes
Usamos uma consulta que calcula os 10 usuários que mais clicaram na URL “http://public_search”:
Algoritmo de busca por exclusão genérica
- uma consulta que procura linhas com valor de URL = “W3”.
- uma versão abstrata da nossa tabela hits com valores simplificados para UserID e URL.
- a mesma chave primária composta (UserID, URL) para o índice. Isso significa que as linhas são ordenadas primeiro pelos valores de UserID. As linhas com o mesmo valor de UserID são então ordenadas por URL.
- um tamanho de grânulo de dois, ou seja, cada grânulo contém duas linhas.
- A marca de índice 0, para a qual o valor de URL é menor que W3 e o valor de URL da marca de índice imediatamente seguinte também é menor que W3, pode ser excluída porque as marcas 0 e 1 têm o mesmo valor de UserID. Observe que essa pré-condição de exclusão garante que o grânulo 0 seja composto inteiramente por valores de UserID U1, de modo que o ClickHouse também possa assumir que o valor máximo de URL no grânulo 0 é menor que W3 e excluir o grânulo.
- A marca de índice 1, para a qual o valor de URL é menor (ou igual) a W3 e o valor de URL da marca de índice imediatamente seguinte é maior (ou igual) a W3, é selecionada porque isso significa que o grânulo 1 possivelmente contém linhas com URL W3.
- As marcas de índice 2 e 3, para as quais o valor de URL é maior que W3, podem ser excluídas, já que as marcas de índice de um índice primário armazenam os valores das colunas-chave da primeira linha da tabela de cada grânulo, e as linhas da tabela são ordenadas em disco pelos valores das colunas-chave; portanto, os grânulos 2 e 3 não podem conter o valor de URL W3.
Observação sobre índice de salto de dados
GRANULARITY 4 na instrução ALTER TABLE acima) — os valores mínimo e máximo de URL:
A primeira entrada do índice (‘marca 0’ no diagrama acima) armazena os valores mínimo e máximo de URL das linhas pertencentes aos primeiros 4 grânulos da nossa tabela.
A segunda entrada do índice (‘marca 1’) armazena os valores mínimo e máximo de URL das linhas pertencentes aos 4 grânulos seguintes da nossa tabela, e assim por diante.
(O ClickHouse também criou um arquivo de marcas especial para o índice de data skipping, para localizar os grupos de grânulos associados às marcas do índice.)
Devido à cardinalidade igualmente alta de UserID e URL, esse índice secundário de data skipping não ajuda a excluir grânulos da seleção quando nossa consulta filtrando por URL é executada.
É muito provável que o valor específico de URL que a consulta procura (ou seja, ‘http://public_search') esteja entre o valor mínimo e o máximo armazenados pelo índice para cada grupo de grânulos, fazendo com que o ClickHouse seja forçado a selecionar esse grupo de grânulos (porque ele pode conter linhas que correspondam à consulta).
A necessidade de usar vários índices primários
Opções para criar índices primários adicionais
UserID específico e a que filtra linhas com uma URL específica — precisaremos usar vários índices primários por meio de uma destas três opções:
- Criar uma segunda tabela com uma chave primária diferente.
- Criar uma visão materializada na tabela existente.
- Adicionar uma projeção à tabela existente.
Opção 1: Tabelas secundárias
UserIDs também não será executada com muita eficiência na nova tabela adicional, porque UserID agora é a segunda coluna da chave no índice primário dessa tabela e, portanto, o ClickHouse usará busca por exclusão genérica para selecionar grânulos, o que não é muito eficaz para a cardinalidade igualmente alta de UserID e URL.
Abra a caixa de detalhes para ver mais informações.
A consulta com filtro por UserIDs agora tem mau desempenho
A consulta com filtro por UserIDs agora tem mau desempenho
UserIDs e consultas com filtro por URLs:
Opção 2: Visões materializadas
- trocamos a ordem das colunas da chave (em comparação com nossa tabela original) na chave primária da visão
- a visão materializada usa uma tabela criada implicitamente, cuja ordem das linhas e cujo índice primário são baseados na definição de chave primária fornecida
- a tabela criada implicitamente é listada pela consulta
SHOW TABLESe tem um nome que começa com.inner - também é possível primeiro criar explicitamente a tabela subjacente de uma visão materializada; em seguida, a visão pode apontar para essa tabela por meio da cláusula
TO [db].[table] - usamos a palavra-chave
POPULATEpara preencher imediatamente a tabela criada implicitamente com todas as 8,87 milhões de linhas da tabela de origem hits_UserID_URL - se novas linhas forem inseridas na tabela de origem hits_UserID_URL, essas linhas também serão inseridas automaticamente na tabela criada implicitamente
- na prática, a tabela criada implicitamente tem a mesma ordem de linhas e o mesmo índice primário da tabela secundária que criamos explicitamente:
Opção 3: Projeções
- a projeção cria uma tabela oculta cuja ordem das linhas e cujo índice primário são baseados na cláusula
ORDER BYdefinida na projeção - a tabela oculta não é listada pela consulta
SHOW TABLES - usamos a palavra-chave
MATERIALIZEpara preencher imediatamente a tabela oculta com todas as 8,87 milhões de linhas da tabela de origem hits_UserID_URL - se novas linhas forem inseridas na tabela de origem hits_UserID_URL, essas linhas também serão inseridas automaticamente na tabela oculta
- uma consulta sempre aponta (sintaticamente) para a tabela de origem hits_UserID_URL, mas, se a ordem das linhas e o índice primário da tabela oculta permitirem uma execução mais eficiente da consulta, essa tabela oculta será usada
- observe que as projeções não tornam mais eficientes as consultas que usam
ORDER BY, mesmo que oORDER BYcorresponda à cláusulaORDER BYda projeção (consulte https://github.com/ClickHouse/ClickHouse/issues/47333) - Na prática, a tabela oculta criada implicitamente tem a mesma ordem das linhas e o mesmo índice primário que a tabela secundária que criamos explicitamente:
Resumo
Ordenando com eficiência as colunas da chave
- a eficiência da filtragem em colunas de chave secundária nas consultas; e
- a taxa de compressão dos arquivos de dados da tabela.
UserID) a uma URL (coluna URL) foi marcado como tráfego de bot (coluna IsRobot).
Usaremos uma chave primária composta contendo as três colunas mencionadas acima, que pode ser usada para acelerar consultas típicas de análise da web que calculam:
- quanto do tráfego para uma URL específica (em porcentagem) vem de bots; ou
- qual é o grau de confiança de que um usuário específico é (ou não) um bot (qual porcentagem do tráfego desse usuário é, ou não, considerada tráfego de bot).
clickhouse client:
URL e IsRobot e, portanto, a ordem dessas colunas em uma chave primária composta é importante tanto para acelerar com eficiência as consultas que filtram por essas colunas quanto para alcançar taxas de compressão ideais para os arquivos de dados das colunas da tabela.
Para demonstrar isso, vamos criar duas versões de tabela para nossos dados de análise de tráfego de bots:
- uma tabela
hits_URL_UserID_IsRobotcom a chave primária composta(URL, UserID, IsRobot), em que ordenamos as colunas da chave por cardinalidade em ordem decrescente - uma tabela
hits_IsRobot_UserID_URLcom a chave primária composta(IsRobot, UserID, URL), em que ordenamos as colunas da chave por cardinalidade em ordem crescente
hits_URL_UserID_IsRobot com a chave primária composta (URL, UserID, IsRobot):
hits_IsRobot_UserID_URL com a chave primária composta por (IsRobot, UserID, URL):
Filtragem eficiente em colunas secundárias da chave
UserID da tabela em que ordenamos as colunas da chave (URL, UserID, IsRobot) por cardinalidade em ordem decrescente:
(IsRobot, UserID, URL) por cardinalidade em ordem crescente:
Taxa de compressão ideal dos arquivos de dados
UserID entre as duas tabelas que criamos acima:
UserID é significativamente maior na tabela em que ordenamos as colunas da chave (IsRobot, UserID, URL) por cardinalidade em ordem crescente.
Embora exatamente os mesmos dados estejam armazenados em ambas as tabelas (inserimos as mesmas 8,87 milhões de linhas nas duas tabelas), a ordem das colunas da chave na chave primária composta influencia significativamente quanto espaço em disco os dados comprimidos nos arquivos de dados de coluna da tabela exigem:
- na tabela
hits_URL_UserID_IsRobot, com a chave primária composta(URL, UserID, IsRobot), em que ordenamos as colunas da chave por cardinalidade em ordem decrescente, o arquivo de dadosUserID.binocupa 11.24 MiB de espaço em disco - na tabela
hits_IsRobot_UserID_URL, com a chave primária composta(IsRobot, UserID, URL), em que ordenamos as colunas da chave por cardinalidade em ordem crescente, o arquivo de dadosUserID.binocupa apenas 877.47 KiB de espaço em disco
cl, e as linhas que têm o mesmo valor de cl são ordenadas pelo valor de ch. E, como a primeira coluna-chave cl tem baixa cardinalidade, é provável que existam linhas com o mesmo valor de cl. Por isso, também é provável que os valores de ch estejam ordenados (localmente — para linhas com o mesmo valor de cl).
Se, em uma coluna, dados semelhantes ficarem próximos uns dos outros, por exemplo por meio de ordenação, esses dados serão comprimidos melhor.
Em geral, um algoritmo de compressão se beneficia do comprimento das sequências de dados (quanto mais dados ele vê, melhor para a compressão)
e da localidade (quanto mais semelhantes os dados forem, melhor será a taxa de compressão).
Em contraste com o diagrama acima, o diagrama abaixo mostra a ordem das linhas em disco para uma chave primária em que as colunas da chave são ordenadas por cardinalidade em ordem decrescente:
Agora, as linhas da tabela são ordenadas primeiro pelo valor de ch, e as linhas que têm o mesmo valor de ch são ordenadas pelo valor de cl.
Mas, como a primeira coluna-chave ch tem alta cardinalidade, é improvável que existam linhas com o mesmo valor de ch. E, por causa disso, também é improvável que os valores de cl estejam ordenados (localmente — para linhas com o mesmo valor de ch).
Portanto, os valores de cl provavelmente estarão em ordem aleatória e, consequentemente, terão baixa localidade e uma taxa de compressão ruim, respectivamente.
Resumo
Identificando linhas individuais com eficiência
Um exemplo concreto
- a ordem de inserção das linhas quando o conteúdo muda (por exemplo, devido às teclas pressionadas ao digitar o texto na área de texto) e
- a ordem em disco dos dados das linhas inseridas quando
PRIMARY KEY (hash)é usado:
hash é usada como coluna de chave primária,
- linhas específicas podem ser recuperadas muito rapidamente, mas
- as linhas da tabela (os dados de suas colunas) são armazenadas em disco em ordem crescente pelos valores de hash (únicos e aleatórios). Portanto, os valores da coluna de conteúdo também são armazenados em ordem aleatória, sem localidade de dados, o que resulta em uma taxa de compressão subótima para o arquivo de dados da coluna de conteúdo.
- um hash do conteúdo, como discutido acima, que é distinto para dados distintos, e
- um hash sensível à localidade (fingerprint) que não muda com pequenas alterações nos dados.
- a ordem de inserção das linhas quando o conteúdo muda (por exemplo, devido às teclas pressionadas ao digitar o texto na área de texto) e
- a ordem em disco dos dados das linhas inseridas quando a
PRIMARY KEY (fingerprint, hash)composta é usada:
fingerprint e, para linhas com o mesmo valor de fingerprint, o valor de hash determina a ordem final.
Como dados que diferem apenas em pequenas alterações recebem o mesmo valor de fingerprint, dados semelhantes agora são armazenados em disco próximos uns dos outros na coluna de conteúdo. E isso é muito bom para a taxa de compressão da coluna de conteúdo, já que, em geral, um algoritmo de compressão se beneficia da localidade dos dados (quanto mais semelhantes forem os dados, melhor será a taxa de compressão).
A contrapartida é que dois campos (fingerprint e hash) são necessários para recuperar uma linha específica, a fim de utilizar de forma ideal o índice primário que resulta da PRIMARY KEY (fingerprint, hash) composta.