Necesito poder localizar un elemento faltante de una tabla con decenas de millones de filas, y tiene una clave primaria de una BINARY(64)
columna (que es el valor de entrada para calcular). Estos valores se insertan principalmente en orden, pero en ocasiones quiero reutilizar un valor anterior que se eliminó. No es factible modificar los registros eliminados con una IsDeleted
columna, ya que a veces se inserta una fila que tiene muchos millones de valores por delante de las filas existentes actualmente. Esto significa que los datos de muestra se verían así:
KeyCol : BINARY(64)
0x..000000000001
0x..000000000002
0x..FFFFFFFFFFFF
Por lo tanto, insertar todos los valores faltantes entre 0x000000000002
y 0xFFFFFFFFFFFF
es inviable, la cantidad de tiempo y espacio utilizado sería indeseable. Esencialmente, cuando ejecuto el algoritmo, espero que regrese 0x000000000003
, que es la primera apertura.
Se me ocurrió un algoritmo de búsqueda binaria en C #, que consultaría la base de datos para cada valor en la posición i
y probaría si se esperaba ese valor. Para el contexto, mi algoritmo terrible: /codereview/174498/binary-search-for-a-missing-or-default-value-by-a-given-formula
Este algoritmo ejecutaría, por ejemplo, 26-27 consultas SQL en una tabla con 100,000,000 artículos. (Eso no parece mucho, pero ocurrirá con mucha frecuencia). Actualmente, esta tabla tiene aproximadamente 50,000,000 filas, y el rendimiento se está volviendo notable .
Mi primer pensamiento alternativo es traducir esto a un procedimiento almacenado, pero eso tiene sus propios obstáculos. (Tengo que escribir un BINARY(64) + BINARY(64)
algoritmo, así como una serie de otras cosas). Esto sería doloroso, pero no inviable. También he considerado implementar el algoritmo de traducción basado en ROW_NUMBER
, pero tengo un presentimiento realmente malo sobre esto. (A BIGINT
no es lo suficientemente grande como para estos valores).
Estoy preparado para otras sugerencias, ya que realmente necesito que esto sea lo más rápido posible. Por lo que vale, la única columna seleccionada por la consulta de C # es KeyCol
, las otras son irrelevantes para esta parte.
Además, por lo que vale, la consulta actual que obtiene el registro apropiado está en la línea de:
SELECT [KeyCol]
FROM [Table]
ORDER BY [KeyCol] ASC
OFFSET <VALUE> ROWS FETCH FIRST 1 ROWS ONLY
¿Dónde <VALUE>
está el índice proporcionado por el algoritmo? Tampoco he tenido el BIGINT
problema OFFSET
todavía, pero lo haré. (Solo tener 50,000,000 filas en este momento significa que nunca pide un índice por encima de ese valor, pero en algún momento superará el BIGINT
rango).
Algunos datos adicionales:
- A partir de eliminaciones, la
gap:sequential
relación es aproximadamente1:20
; - Las últimas 35,000 filas en la tabla tienen valores>>
BIGINT
máximo;
fuente
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 ?mynameisebrown
lo que significaría que obtendríamynameisebrowo
, que no querría siabc
está disponible.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
?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 siempre vuelve1
.Respuestas:
Joe ya ha acertado en la mayoría de los puntos que acabo de pasar una hora escribiendo, en resumen:
KeyCol
valores <bigint
max (9.2e18), por lo que las conversiones (si es necesario) a / desdebigint
no deberían ser un problema siempre que limite las búsquedas aKeyCol <= 0x00..007FFFFFFFFFFFFFFF
¿Entonces lo que hay que hacer?
Pongamos la idea de búsqueda (repetida, intensiva en CPU, fuerza bruta) en espera por un minuto y veamos la imagen más grande.
Lo que me gustaría proponer es algunas adiciones al modelo de datos ...
KeyCol
valores 'disponibles para usar' , por ejemplo:available_for_use(KeyCol binary(64) not null primary key)
KeyCol
valores (¿quizás crear un proceso almacenado 'superior'?) [por ejemplo, actualizar laselect/top/row_number()
consulta de Joe para hacer untop 100000
]available_for_use
en caso de que alguna vez comience a quedarse sin valoresKeyCol
valores eliminados en nuestra nueva tablaavailable_for_use
cada vez que se elimina una fila de la tabla principalKeyCol
columna, entonces un desencadenador de ACTUALIZACIÓN nuevo / modificado en> main_table <para también manteneravailable_for_use
actualizada nuestra nueva tablaKeyCol
valor que ustedselect min(KeyCol) from available_for_use
(obviamente, hay un poco más de esto, ya que a) necesitará codificar para problemas de concurrencia; no quiera que 2 copias de su proceso agarren lo mismomin(KeyCol)
yb) usted necesitará eliminarmin(KeyCol)
de la tabla; esto debería ser relativamente fácil de codificar, tal vez como un proceso almacenado, y se puede abordar en otras preguntas y respuestas si es necesario)select min(KeyCol)
proceso no encuentra filas disponibles, puede iniciar su proceso 'top off' para generar un nuevo lote de filasCon estos cambios propuestos al modelo de datos:
available_for_use
tabla para asegurarse de que nunca se quede sin nuevos valoresSí, la
available_for_use
tabla propuesta es solo una tabla de valores de 'próxima clave' pregenerados; y sí, existe la posibilidad de cierta controversia cuando se toma el valor 'siguiente', pero cualquier contención a) se aborda fácilmente a través del diseño adecuado de tabla / índice / consulta yb) será menor / de corta duración en comparación con la sobrecarga / retrasos con la idea actual de búsquedas repetidas de fuerza bruta e índice.fuente
n
claves (probablemente 10 o 20, para forzarlo a buscar valores más bajos y más deseables). Realmente aprecio la respuesta aquí, ¡pones los pensamientos por escrito! :)KeyCol
valores disponibles ... sí, eso también funcionaría :-) y obviamente eliminaría la necesidad de un cambio de modelo de datos ehKeyCol
administrador distribuido , y la necesidad de codificar por posibles violaciones de PK si 2 (o más) instancias concurrentes de la aplicación intentan usar el mismoKeyCol
valor ... qué asco ... definitivamente más fácil con un solo servidor de middleware o un solución centrada en dbHay 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:
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 suBINARY(64)
enBIGINT
ay no arrojará errores de conversión, pero el comportamiento no está definido :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
: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:
Los resultados sugieren que el valor máximo al que convertimos
BIGINT
es 102500672:Hay 100 millones de filas con valores que se ajustan a BIGINT como se esperaba:
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: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 :
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.
fuente
Aquí hay una respuesta que probablemente no funcione para ti, pero la agregaré de todos modos.
A pesar de que BINARY (64) es enumerable, hay poco apoyo para determinar el sucesor de un elemento. Dado que BIGINT parece ser demasiado pequeño para su dominio, puede considerar usar un DECIMAL (38,0), que parece ser el tipo NUMBER más grande en el servidor SQL.
Encontrar la primera brecha es fácil ya que podemos construir el número que estamos buscando:
Una unión de bucle anidado sobre el índice pk debería ser suficiente para encontrar el primer elemento disponible.
fuente