Eu preciso ser capaz de localizar um elemento ausente de uma tabela com dezenas de milhões de linhas e ter uma chave primária de uma BINARY(64)
coluna (que é o valor de entrada para calcular). Esses valores geralmente são inseridos em ordem, mas de vez em quando quero reutilizar um valor anterior que foi excluído. É inviável modificar os registros excluídos com uma IsDeleted
coluna, pois às vezes uma linha é inserida muitos milhões de valores à frente das linhas existentes no momento. Isso significa que os dados de amostra seriam algo como:
KeyCol : BINARY(64)
0x..000000000001
0x..000000000002
0x..FFFFFFFFFFFF
Portanto, inserir todos os valores ausentes entre 0x000000000002
e 0xFFFFFFFFFFFF
é inviável, a quantidade de tempo e espaço usados seria indesejável. Essencialmente, quando executo o algoritmo, espero que ele retorne 0x000000000003
, que é a primeira abertura.
Eu criei um algoritmo de pesquisa binária em C#, que consultaria o banco de dados para cada valor em position i
e testaria se esse valor era esperado. Para contextualizar, meu terrível algoritmo: https://codereview.stackexchange.com/questions/174498/binary-search-for-a-missing-or-default-value-by-a-given-formula
Esse algoritmo executaria, por exemplo, 26-27 consultas SQL em uma tabela com 100.000.000 itens. (Isso não parece muito, mas vai ocorrer com muita frequência.) Atualmente, esta tabela tem aproximadamente 50.000.000 de linhas e o desempenho está se tornando perceptível .
Meu primeiro pensamento alternativo é traduzir isso para um procedimento armazenado, mas isso tem seus próprios obstáculos. (Eu tenho que escrever um BINARY(64) + BINARY(64)
algoritmo, assim como uma série de outras coisas.) Isso seria doloroso, mas não inviável. Também considerei implementar o algoritmo de traduçãoROW_NUMBER
baseado em , mas tenho um pressentimento muito ruim sobre isso. (A BIGINT
não é grande o suficiente para esses valores.)
Aceito outras sugestões, pois preciso muito que isso seja o mais rápido possível. Pelo que vale a única coluna selecionada pela consulta C# é a KeyCol
, as demais são irrelevantes para esta parte.
Além disso, vale a pena, a consulta atual que busca o registro apropriado segue as linhas de:
SELECT [KeyCol]
FROM [Table]
ORDER BY [KeyCol] ASC
OFFSET <VALUE> ROWS FETCH FIRST 1 ROWS ONLY
Onde <VALUE>
é o índice fornecido pelo algoritmo. Eu também não tive o BIGINT
problema com OFFSET
ainda, mas eu vou. (Apenas ter 50.000.000 de linhas agora significa que ele nunca solicita um índice acima desse valor, mas em algum momento ele ficará acima do BIGINT
intervalo.)
Alguns dados adicionais:
- A partir de exclusões, a
gap:sequential
proporção é de cerca de1:20
; - As últimas 35.000 linhas da tabela possuem valores >
BIGINT
's máximo;
Existem alguns desafios com esta pergunta. Os índices no SQL Server podem fazer o seguinte de forma muito eficiente com apenas algumas leituras lógicas cada:
No entanto, eles não podem ser usados para localizar a enésima linha em um índice. Fazer isso requer que você role seu próprio índice armazenado como uma tabela ou verifique as primeiras N linhas no índice. Seu código C# depende muito do fato de que você pode encontrar eficientemente o N-ésimo elemento da matriz, mas não pode fazer isso aqui. Acho que esse algoritmo não é utilizável para T-SQL sem uma alteração no modelo de dados.
O segundo desafio está relacionado às restrições sobre os
BINARY
tipos de dados. Tanto quanto posso dizer, você não pode realizar adição, subtração ou divisão da maneira usual. Você pode converter seuBINARY(64)
para aBIGINT
e não gerará erros de conversão, mas o comportamento não está definido :Além disso, a falta de erros de conversão é um problema aqui. Você pode converter qualquer coisa maior que o maior
BIGINT
valor possível, mas isso lhe dará os resultados errados.É verdade que você tem valores agora que são maiores que 9223372036854775807. No entanto, se você está sempre começando em 1 e procurando pelo menor valor mínimo, esses valores grandes não podem ser relevantes, a menos que sua tabela tenha mais de 9223372036854775807 linhas. Isso parece improvável porque sua tabela nesse ponto estaria em torno de 2.000 exabytes, portanto, para responder à sua pergunta, vou assumir que os valores muito grandes não precisam ser pesquisados. Eu também vou fazer a conversão de tipos de dados porque eles parecem ser inevitáveis.
Para os dados de teste, inseri o equivalente a 50 milhões de inteiros sequenciais em uma tabela junto com mais 50 milhões de inteiros com uma única diferença de valor a cada 20 valores. Também inseri um único valor que não caberá corretamente em um sinal
BIGINT
:Esse código levou alguns minutos para ser executado na minha máquina. Fiz com que a primeira metade da tabela não tivesse lacunas para representar uma espécie de pior caso para o desempenho. O código que usei para resolver o problema verifica o índice em ordem para que ele termine muito rapidamente se a primeira lacuna estiver no início da tabela. Antes de chegarmos a isso, vamos verificar se os dados estão como deveriam:
Os resultados sugerem que o valor máximo para o qual convertemos
BIGINT
é 102500672:Existem 100 milhões de linhas com valores que se encaixam no BIGINT conforme o esperado:
Uma abordagem para esse problema é verificar o índice em ordem e sair assim que o valor de uma linha não corresponder ao
ROW_NUMBER()
valor esperado. A tabela inteira não precisa ser escaneada para obter a primeira linha: apenas as linhas até o primeiro intervalo. Aqui está uma maneira de escrever código que provavelmente obterá esse plano de consulta:Por motivos que não se encaixam nesta resposta, essa consulta geralmente será executada em série pelo SQL Server e o SQL Server geralmente subestimará o número de linhas que precisam ser verificadas antes que a primeira correspondência seja encontrada. Na minha máquina, o SQL Server verifica 50000022 linhas do índice antes de encontrar a primeira correspondência. A consulta leva 11 segundos para ser executada. Observe que isso retorna o primeiro valor após o intervalo. Não está claro qual linha você deseja exatamente, mas você deve poder alterar a consulta para atender às suas necessidades sem muitos problemas. Veja como é o plano :
Minha única outra ideia era forçar o SQL Server a usar paralelismo para a consulta. Eu tenho quatro CPUs, então vou dividir os dados em quatro intervalos e fazer buscas nesses intervalos. Cada CPU será atribuída a um intervalo. Para calcular os intervalos, apenas peguei o valor máximo e assumi que os dados estavam distribuídos uniformemente. Se você quiser ser mais esperto sobre isso, você pode olhar para um histograma de estatísticas de amostra para os valores da coluna e construir seus intervalos dessa maneira. O código abaixo depende de muitos truques não documentados que não são seguros para produção, incluindo o sinalizador de rastreamento 8649 :
Aqui está a aparência do padrão de loop aninhado paralelo:
No geral, a consulta funciona mais do que antes, pois verifica mais linhas na tabela. No entanto, agora ele é executado em 7 segundos na minha área de trabalho. Pode paralelizar melhor em um servidor real. Aqui está um link para o plano real .
Eu realmente não consigo pensar em uma boa maneira de resolver este problema. Fazer o cálculo fora do SQL ou alterar o modelo de dados pode ser sua melhor aposta.
Joe já acertou na maioria dos pontos que passei uma hora digitando, resumindo:
KeyCol
valores <bigint
max (9.2e18), portanto, as conversões (se necessário) de/parabigint
não devem ser um problema, desde que você limite as pesquisas aKeyCol <= 0x00..007FFFFFFFFFFFFFFF
Então o que fazer?
Vamos colocar a ideia de pesquisa (repetida, com uso intensivo de CPU e força bruta) em espera por um minuto e olhar para o quadro maior.
O que eu gostaria de propor são algumas adições ao modelo de dados...
KeyCol
valores 'disponíveis para uso', por exemplo:available_for_use(KeyCol binary(64) not null primary key)
KeyCol
valores (talvez crie um proc armazenado 'top off'?) [por exemplo, atualize aselect/top/row_number()
consulta de Joe para fazer umtop 100000
]available_for_use
caso você comece a ficar com poucos valoresKeyCol
valores excluídos em nossa nova tabelaavailable_for_use
sempre que uma linha é excluída da tabela principalKeyCol
coluna, um gatilho UPDATE novo/modificado no >main_table< para também manter nossa nova tabelaavailable_for_use
atualizadaKeyCol
valor, vocêselect min(KeyCol) from available_for_use
(obviamente, há um pouco mais disso, pois a) você precisará codificar para problemas de simultaneidade - não queira 2 cópias do seu processo pegando o mesmomin(KeyCol)
e b) você 'precisará deletarmin(KeyCol)
da tabela; isso deve ser relativamente fácil de codificar, talvez como um proc armazenado, e pode ser abordado em outro Q&A, se necessário)select min(KeyCol)
processo não encontrar linhas disponíveis, você poderá iniciar seu proc 'top off' para gerar um novo lote de linhasCom essas alterações propostas no modelo de dados:
available_for_use
tabela para garantir que você nunca fique sem novos valoresSim, a
available_for_use
tabela proposta é apenas uma tabela de valores de 'próxima chave' pré-gerados; e sim, há um potencial para alguma contenção ao pegar o valor 'próximo', mas qualquer contenção a) é facilmente abordada por meio do design adequado de tabela/índice/consulta e b) será menor/de curta duração em comparação com a sobrecarga/ atrasos com a ideia atual de buscas repetidas, de força bruta, de índice.Aqui está uma resposta que provavelmente não funcionará para você, mas vou adicioná-la de qualquer maneira.
Embora BINARY(64) seja enumerável, há um suporte ruim para determinar o sucessor de um item. Como BIGINT parece ser muito pequeno para o seu domínio, você pode considerar usar um DECIMAL(38,0), que parece ser o maior tipo NUMBER no SQL-server.
Encontrar a primeira lacuna é fácil, pois podemos construir o número que estamos procurando:
Uma junção de loop aninhado sobre o índice pk deve ser suficiente para encontrar o primeiro item disponível.