O LindormTable é um mecanismo de dados distribuídos que particiona informações com base nas chaves primárias. Se a chave primária de uma tabela contiver várias colunas, o LindormTable as utilizará da esquerda para a direita durante as consultas. Um projeto inadequado de chave primária concentra leituras e gravações em poucas partições, criando hot spots que degradam o desempenho. Este tópico aborda os princípios para projetar chaves primárias eficientes e fornece exemplos para cargas de trabalho comuns.
Conceitos principais
Consultas GET vs. SCAN
O projeto da chave primária determina os métodos de consulta disponíveis.
|
Método de consulta |
Funcionamento |
Requisito |
|
GET |
Busca pontual por chave primária |
Especifique todas as colunas da chave primária com valores explícitos |
|
SCAN |
Varredura de intervalo na chave primária |
Especifique o intervalo na primeira coluna da chave primária; o sistema rejeita varreduras completas de tabela por padrão |
Exemplos de consultas:
-- GET: retrieve a single order
SELECT * FROM table WHERE userid='abc' AND orderid=123
-- SCAN: retrieve a range of orders
SELECT * FROM table WHERE userid='abc' AND 123<orderid<456
-- SCAN with reverse order: retrieve most recent orders first
SELECT * FROM table WHERE userid='abc' AND 123<orderid<456 ORDER BY orderid DESC
Para consultas impossíveis de expressar com GET ou SCAN, utilize uma tabela de índice ou um índice secundário. Para obter mais informações, consulte SELECT.
O uso do método Scan retorna dados que não atendem aos critérios da consulta?
Não. O SQL de tabelas largas do Lindorm suporta diversos tipos de dados, cada um com codificação individual. Para ver os tipos de dados suportados, consulte Tipos de dados básicos.
Princípios de projeto
Unicidade
Diferentes versões de linha compartilham a mesma chave primária. Por padrão, uma consulta GET retorna apenas a versão mais recente. Projete chaves primárias exclusivas, a menos que pretenda usar intencionalmente o recurso de multiversão.
Uma chave primária pode ser uma única coluna ou uma composição de várias colunas:
[userid]— um registro por usuário[userid][orderid]— vários registros por usuário, um por pedido
Quantidade de colunas e tamanho dos valores
Mantenha as chaves primárias compactas para reduzir custos de armazenamento e melhorar o desempenho de gravação.
Quantidade de colunas: Utilize de 1 a 3 colunas na chave primária.
Tamanho dos valores: Prefira valores de tamanho fixo, como inteiros longos. Para valores de tamanho variável, mantenha cada coluna abaixo de 2 KB.
Distribuição da primeira coluna
O Lindorm distribui os dados armazenados com base nas chaves primárias. Se a chave primária de uma tabela contiver várias colunas, a distribuição seguirá a ordem das colunas da esquerda para a direita. Quando os valores na primeira coluna da chave primária são enviesados ou aumentam monotonicamente, as gravações se acumulam em uma única partição.
A tabela a seguir mostra se os tipos comuns de primeira coluna produzem distribuição uniforme ou enviesada:
|
Primeira coluna da chave primária |
Distribuição |
Adequado? |
|
ID de usuário (alta cardinalidade, aleatório) |
Uniforme |
Sim |
|
ID de dispositivo (muitos dispositivos, acesso equilibrado) |
Uniforme |
Sim |
|
Timestamp ou valor autoincremental |
Sequencial — todas as gravações vão para uma partição |
Não |
|
Tipo de pedido ou código de status (poucos valores possíveis) |
Enviesada — a maioria das gravações vai para poucas partições |
Não |
|
Coluna com prefixo compartilhado |
Enviesada — valores similares caem na mesma partição |
Não |
Caso seja necessário usar uma coluna de timestamp ou autoincremental como primeira coluna da chave primária, aplique um prefixo hash para distribuir as gravações entre as partições. Consulte Evitar hot spots.
Evitar hot spots
Quando a primeira coluna da chave primária aumenta monotonicamente ou possui baixa cardinalidade, utilize uma das técnicas a seguir para distribuir as gravações uniformemente. Cada técnica apresenta diferentes compensações para as leituras.
Prefixo hash
Adicione um hash da chave original no início para dispersar as gravações entre as partições. Crie uma coluna derivada pk1 usando:
pk1 = hash(pk).substring(0, 4) + pk
Os primeiros quatro caracteres do hash funcionam como um prefixo aleatório. As gravações se espalham uniformemente e as leituras usam pk1 diretamente.
Chave primária: [pk1][...]
Compensação: Para consultar pela pk original, calcule o prefixo hash no momento da consulta para construir pk1.
Prefixo MD5
Uma variante comum do prefixo hash usando MD5:
Chave primária: [md5(userid).subStr(0,4)][userId][orderid]
Índice reverso
Inverta a representação em string da chave para quebrar prefixos sequenciais:
Chave primária: [reverse(userid)][orderid]
Compensação: Varreduras de intervalo em userid tornam-se menos naturais. Funciona melhor quando buscas por correspondência exata predominam.
Bucket modular
Divida as gravações em um número fixo de buckets usando aritmética modular. Atribua cada linha a um bucket com base em um valor monotonicamente crescente:
long bucket = timestamp % numBuckets
Chave primária: [bucket][timestamp][hostname][log-event]
Compensação: Para consultar todos os dados de um intervalo de tempo, emita uma consulta por bucket e mescle os resultados.
Salt aleatório
Anexe um número aleatório para espalhar as gravações entre as partições:
Chave primária: [userId][orderid][random(100)]
Compensação: Leituras para um único [userId][orderid] exigem varredura em vários valores de salt. Use este padrão apenas quando a distribuição de gravação for mais crítica do que a simplicidade de leitura.
Simplificar chaves primárias
Reduzir o tamanho da chave acelera as gravações e diminui os custos de armazenamento. Duas abordagens comuns:
Substitua STRING por LONG ou INT. Exemplo:
'2015122410'→Long(2015122410).Substitua nomes longos por códigos curtos. Exemplo:
'taobao'→'tb'.
Considerações sobre problemas
-
P: A chave primária é única?
R: No Lindorm, chaves primárias idênticas são consideradas múltiplas versões do mesmo dado. Como as consultas retornam por padrão a versão mais recente, geralmente é necessário garantir que a chave primária seja única, exceto ao utilizar o recurso de multiversão.
Exemplo de melhor prática de projeto: A chave primária pode ser uma única coluna ou uma combinação de várias colunas. Cada chave primária representa um registro.
[userid]: Indica que a chave primária tem apenas uma coluna, com um único registro por usuário.[userid][orderid]: Indica que a chave primária é uma combinação de duas colunas, permitindo múltiplos registros por usuário.
-
P: Quais cenários de consulta são atendidos?
R: O projeto da chave primária limita a forma de consulta dos dados. O servidor do Lindorm compila uma instrução
SELECTem dois métodos de consulta.-
Consulta por chave primária completa (método get), por exemplo
SELECT * FROM table WHERE userid='abc' AND orderid=123.NotaO método get exige o conhecimento de todas as colunas da chave primária. Ou seja, os valores de todos os campos que compõem a chave primária devem ser determinados.
-
Consulta por intervalo de chave primária (método scan), por exemplo
SELECT * FROM table WHERE userid='abc' AND 123<orderid<456.NotaO método scan requer a especificação do intervalo da primeira coluna da chave primária. Caso contrário, o Lindorm rejeitará por padrão consultas ineficientes de varredura completa da tabela. Para detalhes, consulte Instruções de consulta ineficientes e explicações.
Exemplo de melhor prática de projeto: Como implementar consultas complexas com métodos de consulta limitados? Os métodos a seguir podem ajudar.
Crie uma nova tabela para servir como tabela de índice.
Se as condições da consulta especificarem um intervalo em colunas que não são chave primária, o servidor usará filtros para descartar dados desnecessários.
Utilize índices secundários.
-
Use o método
ORDER BYpara implementar ordem decrescente (colocando dados novos primeiro), por exemploSELECT * FROM table WHERE userid='abc' AND 123<orderid<456 ORDER BY orderid DESC.NotaComo o desempenho da ordenação inversa à ordem original dos campos da tabela é inferior ao da ordenação direta, considere refletir isso no projeto da chave primária se a maioria dos cenários exigir ordem decrescente. Defina a chave primária como
[userid][orderid DESC].
-
-
P: Quais fatores devem ser considerados ao projetar a chave primária?
R: Considere o tamanho dos valores das colunas da chave primária e a quantidade de colunas.
Tamanho dos valores das colunas da chave primária: Mantenha os valores das colunas da chave primária o mais curtos possível. Prefira tipos de tamanho fixo, como inteiros longos. Para tipos de tamanho variável, limite o tamanho do valor da coluna da chave primária a 2 KB para reduzir custos de armazenamento e melhorar o desempenho de gravação.
Quantidade de colunas da chave primária: Menos colunas resultam em maior desempenho de gravação e menores custos de armazenamento. Limite a quantidade de colunas da chave primária a 1-3.
-
Quais situações evitar ao projetar a chave primária?
Lindorm é um banco de dados distribuído onde os dados são particionados conforme a chave primária. Se houver múltiplas colunas na chave primária, a distribuição segue o princípio de correspondência mais à esquerda do banco de dados. Para evitar problemas de hot spots de gravação, siga as condições abaixo:
A primeira coluna da chave primária deve ser o mais dispersa possível; não use nomes de chave primária com o mesmo prefixo.
Evite usar dados com prefixos comuns ou autoincrementais como primeira coluna da chave primária ou coluna de índice (por exemplo, colunas de timestamp).
-
Evite usar campos com prefixos óbvios ou enumerações (como order_type) como primeira chave primária.
Se situações semelhantes forem inevitáveis, adicione um Hash para dispersar os dados. Exemplo:
Suponha que a chave primária original pk seja uma string crescente. Defina uma nova chave primária
pk1 = hash(pk1).substring(0,4)+pk, ou seja, primeiro disperse com um algoritmo Hash e selecione os primeiros 4 dígitos como prefixo concatenado à chave primária original pk.
-
P: Se os dados estiverem suficientemente dispersos, ainda haverá fenômenos de hot spots com acúmulo?
R: O objetivo do hash é dispersar os dados em diferentes partições, evitando hot spots que deixariam um servidor sobrecarregado enquanto outros permanecem ociosos. Isso aproveita as vantagens da distribuição e concorrência.
Exemplos de melhores práticas de projeto:
Projete um algoritmo de hash md5, definindo a chave primária como
[md5(userid).subStr(0,4)][userId][orderid].Projete uma inversão, definindo a chave primária como
[reverse(userid)][orderid].Projete uma operação modular, definindo a chave primária como
[bucket][timestamp][hostname][log-event]; long bucket = timestamp % numBuckets.Adicione números aleatórios, definindo a chave primária como
[userId][orderid][random(100)].
-
P: A chave primária pode ser mais simplificada?
R: Colunas de chave primária simplificadas reduzem o volume de dados e aumentam a eficiência de consultas e gravações.
Exemplos de melhores práticas de projeto:
Use Long ou Int em vez de String, por exemplo
'2015122410' => Long(2015122410).Use codificações em vez de nomes, por exemplo
'淘宝'=> 'tb'.
-
P: O uso do método scan retorna dados indesejados?
R: Não. O SQL de tabelas largas do Lindorm suporta diversos tipos de dados, cada um com codificação individual. Para ver os tipos de dados suportados, consulte Tipos de dados básicos.
Projetos comuns
Dados de log e séries temporais
|
Objetivo da consulta |
Projeto da chave primária |
|
Consultar todos os registros de uma métrica em um intervalo de tempo |
|
|
Recuperar os registros mais recentes de uma métrica |
|
|
Consultar dados temporais com alto volume de gravação (timestamp quente) |
|
Para consultas de "registros mais recentes", defina timestamp DESC na chave primária para classificar novos registros no topo. Um SCAN então retorna os registros mais recentes sem necessidade de ordenação adicional.
Para séries temporais de alto volume onde uma única coluna de timestamp criaria uma partição quente, o padrão de bucket modular distribui as gravações. Para ler todos os registros em um intervalo de tempo, consulte cada bucket separadamente e mescle os resultados.
Dados transacionais
Cargas de trabalho transacionais normalmente exigem múltiplos padrões de acesso — por vendedor, por comprador e por ID de pedido. Projete tabelas separadas para cada padrão de acesso:
|
Padrão de acesso |
Tabela |
Projeto da chave primária |
|
Registros de transação do vendedor por tempo |
Tabela do vendedor |
|
|
Registros de transação do comprador por tempo |
Tabela do comprador |
|
|
Busca de pedido por ID |
Tabela de pedidos |
|
Una as três tabelas para realizar consultas quando precisar acessar dados em todos os três padrões de acesso.