Existem 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:
- verifique se existe uma linha
- verifique se uma linha não existe
- encontre a próxima linha começando em algum momento
- encontre a linha anterior começando em algum momento
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 seu BINARY(64)
em um BIGINT
e ele não gerará erros de conversão, mas o comportamento não está definido :
Não é garantido que as conversões entre qualquer tipo de dados e os tipos de dados binários sejam as mesmas entre as versões do SQL Server.
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
:
CREATE TABLE dbo.BINARY_PROBLEMS (
KeyCol BINARY(64) NOT NULL
);
INSERT INTO dbo.BINARY_PROBLEMS WITH (TABLOCK)
SELECT CAST(SUM(OFFSET) OVER (ORDER BY (SELECT NULL) ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS BINARY(64))
FROM
(
SELECT 1 + CASE WHEN t.RN > 50000000 THEN
CASE WHEN ABS(CHECKSUM(NewId()) % 20) = 10 THEN 1 ELSE 0 END
ELSE 0 END OFFSET
FROM
(
SELECT TOP (100000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3
) t
) tt
OPTION (MAXDOP 1);
CREATE UNIQUE CLUSTERED INDEX CI_BINARY_PROBLEMS ON dbo.BINARY_PROBLEMS (KeyCol);
-- add a value too large for BIGINT
INSERT INTO dbo.BINARY_PROBLEMS
SELECT CAST(0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000 AS BINARY(64));
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:
SELECT TOP (2) KeyColBigInt
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
FROM dbo.BINARY_PROBLEMS
) t
ORDER By KeyCol DESC;
Os resultados sugerem que o valor máximo em que convertemos BIGINT
é 102500672:
╔══════════════════════╗
║ KeyColBigInt ║
╠══════════════════════╣
║ -9223372036854775808 ║
║ 102500672 ║
╚══════════════════════╝
Existem 100 milhões de linhas com valores que se encaixam no BIGINT conforme o esperado:
SELECT COUNT(*)
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF;
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:
SELECT TOP (1) KeyCol
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
, ROW_NUMBER() OVER (ORDER BY KeyCol) RN
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF
) t
WHERE KeyColBigInt <> RN
ORDER BY KeyCol;
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 :
SELECT TOP 1 ca.KeyCol
FROM (
SELECT 1 bucket_min_value, 25625168 bucket_max_value
UNION ALL
SELECT 25625169, 51250336
UNION ALL
SELECT 51250337, 76875504
UNION ALL
SELECT 76875505, 102500672
) buckets
CROSS APPLY (
SELECT TOP 1 t.KeyCol
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
, buckets.bucket_min_value - 1 + ROW_NUMBER() OVER (ORDER BY KeyCol) RN
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol >= CAST(buckets.bucket_min_value AS BINARY(64)) AND KeyCol <= CAST(buckets.bucket_max_value AS BINARY(64))
) t
WHERE t.KeyColBigInt <> t.RN
ORDER BY t.KeyCol
) ca
ORDER BY ca.KeyCol
OPTION (QUERYTRACEON 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.
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 ?