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 o qual calcular). Esses valores geralmente são inseridos em ordem, mas, ocasionalmente, desejo reutilizar um valor anterior que foi excluído. É inviável modificar os registros excluídos com uma IsDeleted
coluna, pois algumas vezes é inserida uma linha com muitos milhões de valores à frente das linhas existentes no momento. Isso significa que os dados da amostra se pareceriam com:
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 na posição i
e testaria se esse valor era esperado. Para o contexto, meu terrível algoritmo: /codereview/174498/binary-search-for-a-missing-or-default-value-by-a-given-formula
Esse algoritmo executaria, por exemplo, 26 a 27 consultas SQL em uma tabela com 100.000.000 itens. (Isso não parece muito, mas ocorrerá com muita frequência.) Atualmente, esta tabela possui 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 que possui seus próprios obstáculos. (Eu tenho que escrever um BINARY(64) + BINARY(64)
algoritmo, além de várias outras coisas.) Isso seria doloroso, mas não inviável. Também considerei implementar o algoritmo de tradução com base ROW_NUMBER
, mas tenho um pressentimento muito ruim sobre isso. (A BIGINT
não é grande o suficiente para esses valores.)
Estou pronto para outras sugestões, pois realmente preciso que isso seja o mais rápido possível. No que vale a pena, a única coluna selecionada pela consulta C # é a KeyCol
, as outras são irrelevantes para esta parte.
Além disso, para o que vale a pena, a consulta atual que busca o registro apropriado segue as seguintes linhas:
SELECT [KeyCol]
FROM [Table]
ORDER BY [KeyCol] ASC
OFFSET <VALUE> ROWS FETCH FIRST 1 ROWS ONLY
Onde <VALUE>
está o índice fornecido pelo algoritmo. Eu também não tive o BIGINT
problema OFFSET
ainda, mas vou. (Apenas ter 50.000.000 de linhas no momento significa que nunca solicita um índice acima desse valor, mas em algum momento ele fica acima do BIGINT
intervalo.)
Alguns dados adicionais:
- Das exclusões, a
gap:sequential
proporção é de aproximadamente1:20
; - As últimas 35.000 linhas da tabela têm valores>
BIGINT
no máximo;
fonte
delete
gatilho na tabela que despejaria o binário agora disponível em uma tabela separada (por exemplo,create table available_for_reuse(id binary64)
), especialmente à luz do requisito de fazer essa pesquisa com muita frequência ?mynameisebrown
que significa que você obteria omynameisebrowo
que deseja. não gostaria se estivesseabc
disponível.select t1.keycol+1 as aa from t as t1 where not exists (select 1 from t as t2 where t2.keycol = t1.keycol+1) order by keycol fetch first 1 rows only
fornece a você?SELECT TOP 1 ([T1].[KeyCol] + 1) AS [AA] FROM [SearchTestTableProper] AS [T1] WHERE NOT EXISTS (SELECT 1 FROM [SearchTestTableProper] AS [T2] WHERE [T2].[KeyCol] = [T1].[KeyCol] + 1) ORDER BY [KeyCol]
, que sempre retorna1
.Respostas:
Joe já acertou na maioria dos pontos que passei uma hora digitando, em resumo:
KeyCol
valoresbigint
ficam < 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 idéia de pesquisa (repetida, intensiva em CPU e força bruta) em espera por um minuto e ver a foto maior.
O que eu gostaria de propor são algumas adições ao modelo de dados ...
KeyCol
, por exemplo:available_for_use(KeyCol binary(64) not null primary key)
KeyCol
valores (talvez criar um processo armazenado 'completada'?) [por exemplo, atualize aselect/top/row_number()
consulta de Joe para fazer umatop 100000
]available_for_use
caso você comece a ficar com valores baixosKeyCol
valores excluídos em nossa nova tabelaavailable_for_use
sempre que uma linha é excluída da tabela principalKeyCol
coluna, um acionador UPDATE novo / modificado na> main_table <também manterá nossa nova tabelaavailable_for_use
atualizadaKeyCol
valor, vocêselect min(KeyCol) from available_for_use
(obviamente, há um pouco mais disso desde que a) precisará codificar para problemas de simultaneidade - não queira duas cópias do seu processo agarrando o mesmomin(KeyCol)
eb) você precisará excluirmin(KeyCol)
da tabela; isso deve ser relativamente fácil de codificar, talvez como um processo armazenado e pode ser abordado em outra sessão de perguntas e respostas, se necessário)select min(KeyCol)
processo não encontrar linhas disponíveis, você poderá iniciar seu processo '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 pré-gerados da 'próxima chave'; e sim, existe um potencial para alguma disputa ao pegar o valor 'próximo', mas qualquer disputa a) é facilmente resolvida através do design adequado de tabela / índice / consulta eb) será menor / terá vida curta em comparação com a sobrecarga / atrasa com a ideia atual de pesquisas repetidas e de força bruta no índice.fonte
n
chaves (provavelmente 10 ou 20, para forçá-lo a procurar o que pode ser valores mais baixos e desejáveis). Realmente aprecio a resposta aqui, porém, você coloca os pensamentos por escrito! :)KeyCol
valores disponíveis ... sim, isso também funcionaria :-) e obviamente elimina a necessidade de uma alteração no modelo de dados ehKeyCol
gerenciador distribuído e a necessidade de codificar possíveis violações de PK se 2 (ou mais) instâncias simultâneas do aplicativo tentarem usar o mesmoKeyCol
valor ... eca ... definitivamente mais fácil com um único servidor de middleware ou um solução db-centricExistem alguns desafios com esta pergunta. Os índices no SQL Server podem fazer o seguinte de maneira 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. Para isso, é necessário rolar o seu próprio índice armazenado como uma tabela ou varrer as primeiras N linhas no índice. Seu código C # depende muito do fato de que você pode encontrar com eficiência o enésimo elemento da matriz, mas não pode fazer isso aqui. Eu 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)
em umBIGINT
e ele 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 gera resultados errados.É verdade que você tem valores maiores que 9223372036854775807. No entanto, se você está sempre começando em 1 e pesquisando o 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 naquele momento teria cerca de 2000 exabytes, portanto, para fins de resposta à sua pergunta, vou assumir que valores muito grandes não precisam ser pesquisados. Também farei a conversão de tipos de dados, porque eles parecem inevitáveis.
Para os dados de teste, inseri o equivalente a 50 milhões de números inteiros seqüenciais em uma tabela, além de mais 50 milhões de números inteiros com uma única diferença de valor a cada 20 valores. Também inseri um valor único que não cabe corretamente em um assinado
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 de desempenho. O código que eu usei para resolver o problema varre 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 em que 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 digitalizada para obter a primeira linha: somente as linhas até a primeira lacuna. Aqui está uma maneira de escrever código com probabilidade de obter esse plano de consulta:Por motivos que não se encaixam nessa resposta, essa consulta geralmente é executada em série pelo SQL Server e o SQL Server subestima 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. Aqui está a aparência do plano :
Minha única outra idéia era intimidar o SQL Server a usar o paralelismo para a consulta. Eu tenho quatro CPUs, então eu vou dividir os dados em quatro intervalos e fazer buscas nesses intervalos. Cada CPU receberá um intervalo. Para calcular os intervalos, peguei o valor máximo e assumi que os dados eram distribuídos igualmente. Se você quiser ser mais inteligente, consulte um histograma de estatísticas com amostra dos valores das colunas e crie seus intervalos dessa maneira. O código abaixo se baseia em 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 varrerá 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 .
Realmente não consigo pensar em uma boa maneira de resolver esse problema. Fazer o cálculo fora do SQL ou alterar o modelo de dados pode ser sua melhor aposta.
fonte
Aqui está uma resposta que provavelmente não vai funcionar para você, mas eu a adicionarei de qualquer maneira.
Embora BINARY (64) seja enumerável, há um suporte insuficiente para determinar o sucessor de um item. Como o BIGINT parece ser muito pequeno para o seu domínio, você pode considerar usar um DECIMAL (38,0), que parece ser o maior tipo de NUMBER no servidor SQL.
É fácil encontrar a primeira lacuna, pois podemos construir o número que estamos procurando:
Uma junção de loop aninhada sobre o índice pk deve ser suficiente para encontrar o primeiro item disponível.
fonte