Hay algunos desafíos con esta pregunta. Los índices en SQL Server pueden hacer lo siguiente de manera muy eficiente con solo unas pocas lecturas lógicas cada uno:
- comprobar que existe una fila
- comprobar que no existe una fila
- encuentra la siguiente fila que comienza en algún momento
- encontrar la fila anterior comenzando en algún momento
Sin embargo, no se pueden usar para encontrar la enésima fila en un índice. Para hacerlo, debe rodar su propio índice almacenado como una tabla o escanear las primeras N filas en el índice. Su código C # depende en gran medida del hecho de que puede encontrar eficientemente el enésimo elemento de la matriz, pero no puede hacerlo aquí. Creo que ese algoritmo no es utilizable para T-SQL sin un cambio de modelo de datos.
El segundo desafío se relaciona con las restricciones sobre los BINARY
tipos de datos. Por lo que puedo decir, no puedes realizar sumas, restas o divisiones de la forma habitual. Puede convertir su BINARY(64)
en BIGINT
ay no arrojará errores de conversión, pero el comportamiento no está definido :
No se garantiza que las conversiones entre cualquier tipo de datos y los tipos de datos binarios sean las mismas entre las versiones de SQL Server.
Además, la falta de errores de conversión es un problema aquí. Puede convertir cualquier cosa más grande que el mayor BIGINT
valor posible , pero le dará resultados incorrectos.
Es cierto que tiene valores en este momento que son mayores que 9223372036854775807. Sin embargo, si siempre comienza en 1 y busca el valor mínimo más pequeño, esos valores grandes no pueden ser relevantes a menos que su tabla tenga más de 9223372036854775807 filas. Esto parece poco probable porque su tabla en ese momento estaría alrededor de 2000 exabytes, por lo que para responder a su pregunta voy a suponer que no es necesario buscar los valores muy grandes. También voy a hacer la conversión del tipo de datos porque parecen ser inevitables.
Para los datos de la prueba, inserté el equivalente de 50 millones de enteros secuenciales en una tabla junto con 50 millones de enteros más con una sola brecha de valor cada 20 valores. También inserté un valor único que no cabe correctamente en un signo 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));
Ese código tardó unos minutos en ejecutarse en mi máquina. Hice que la primera mitad de la tabla no tuviera huecos para representar un caso peor para el rendimiento. El código que usé para resolver el problema escanea el índice en orden para que termine muy rápidamente si el primer espacio está al principio de la tabla. Antes de llegar a eso, verifiquemos que los datos estén como deberían ser:
SELECT TOP (2) KeyColBigInt
FROM
(
SELECT KeyCol
, CAST(KeyCol AS BIGINT) KeyColBigInt
FROM dbo.BINARY_PROBLEMS
) t
ORDER By KeyCol DESC;
Los resultados sugieren que el valor máximo al que convertimos BIGINT
es 102500672:
╔══════════════════════╗
║ KeyColBigInt ║
╠══════════════════════╣
║ -9223372036854775808 ║
║ 102500672 ║
╚══════════════════════╝
Hay 100 millones de filas con valores que se ajustan a BIGINT como se esperaba:
SELECT COUNT(*)
FROM dbo.BINARY_PROBLEMS
WHERE KeyCol < 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007FFFFFFFFFFFFFFF;
Un enfoque para este problema es escanear el índice en orden y salir tan pronto como el valor de una fila no coincida con el ROW_NUMBER()
valor esperado . No es necesario escanear toda la tabla para obtener la primera fila: solo las filas hasta el primer espacio. Aquí hay una forma de escribir código que probablemente obtenga ese plan 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 razones que no encajan en esta respuesta, esta consulta a menudo se ejecutará en serie por SQL Server y SQL Server a menudo subestimará el número de filas que deben analizarse antes de encontrar la primera coincidencia. En mi máquina, SQL Server escanea 50000022 filas del índice antes de encontrar la primera coincidencia. La consulta tarda 11 segundos en ejecutarse. Tenga en cuenta que esto devuelve el primer valor más allá de la brecha. No está claro qué fila desea exactamente, pero debería poder cambiar la consulta para que se ajuste a sus necesidades sin muchos problemas. Así es como se ve el plan :
Mi única otra idea era intimidar a SQL Server para que usara paralelismo para la consulta. Tengo cuatro CPU, así que dividiré los datos en cuatro rangos y haré búsquedas en esos rangos. A cada CPU se le asignará un rango. Para calcular los rangos, simplemente tomé el valor máximo y asumí que los datos se distribuían de manera uniforme. Si desea ser más inteligente al respecto, puede mirar un histograma de estadísticas muestreadas para los valores de columna y construir sus rangos de esa manera. El siguiente código se basa en muchos trucos indocumentados que no son seguros para la producción, incluido el indicador de seguimiento 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);
Así es como se ve el patrón de bucle anidado paralelo:
En general, la consulta hace más trabajo que antes, ya que escaneará más filas en la tabla. Sin embargo, ahora se ejecuta en 7 segundos en mi escritorio. Podría paralelizar mejor en un servidor real. Aquí hay un enlace al plan real .
Realmente no puedo pensar en una buena manera de resolver este problema. Hacer el cálculo fuera de SQL o cambiar el modelo de datos pueden ser sus mejores apuestas.
delete
disparador en la tabla que volcaría el binario ahora disponible a una tabla separada (por ejemplo,create table available_for_reuse(id binary64)
), especialmente a la luz del requisito de hacer esta búsqueda con mucha frecuencia ?