¿Hay una buena manera de verificar las reglas contra N columnas?
-
09-09-2019 - |
Pregunta
Suponga que tiene reglas de tabla con 3 columnas A, B y C. A medida que los datos ingresan al sistema, quiero saber si alguna fila de la tabla de reglas coincide con mis datos con la condición de que si la columna correspondiente en la tabla de reglas es nula , todas las coincidencias de datos. El obvio SQL es:
SELECT * FROM RULES
WHERE (A = :a OR A IS NULL)
AND (B = :b OR B IS NULL)
AND (C = :c OR C IS NULL)
Entonces, si tengo reglas:
RULE A B C 1 50 NULL NULL 2 51 xyz NULL 3 51 NULL 123 4 NULL xyz 456
Una entrada de (50, xyz, 456) coincidirá con las reglas 1 y 4.
Pregunta: ¿Hay una mejor manera de hacer esto? Con solo 3 campos, esto no es un problema. Pero la tabla real tendrá 15 columnas y me preocupa qué tan bien se escala SQL.
Especulación: Una declaración alternativa de SQL que se me ocurrió involucrada agregar una columna adicional a la tabla con un recuento de cuántos campos no son nulos. (Entonces, en el ejemplo, el valor de esta columna para las reglas 1-4 es 1, 2, 2 y 2 respectivamente). Con esta columna "col_count", la selección podría ser:
SELECT * FROM RULES
WHERE (CASE WHEN A = :a THEN 1 ELSE 0 END)
+ (CASE WHEN B = :b THEN 1 ELSE 0 END)
+ (CASE WHEN C = :c THEN 1 ELSE 0 END)
= COL_COUNT
Desafortunadamente, no tengo suficientes datos de muestra para encontrar nuestros enfoques que funcionarían mejor. Antes de comenzar a crear reglas aleatorias, pensé en preguntar aquí si había un mejor enfoque.
Nota: Las técnicas de minería de datos y las restricciones de columna no son factibles aquí. Los datos deben verificarse cuando ingrese al sistema y, por lo tanto, se puede marcar el paso/falla de inmediato. Y, los usuarios controlan la adición o eliminación de reglas para que no pueda convertir las reglas en restricciones de columna u otras declaraciones de definición de datos.
Una última cosa, al final, necesito una lista de todas las reglas que los datos no pasan. La solución no puede abortar en la primera falla.
Gracias.
Solución
La primera consulta que proporcionó es perfecta. Realmente dudo que agregar la columna de la que estabas hablando te daría más velocidad, ya que la propiedad no nula de cada entrada se verifica de todos modos, ya que cada comparación con NULL produce falso. Entonces supongo que x=y
se expande a x IS NOT NULL AND x=y
internamente. Quizás alguien más pueda aclarar eso.
Todas las demás optimizaciones en las que puedo pensar implicarían precalculación o almacenamiento en caché. Puede crear tablas [temporales] que coincidan con ciertas reglas o agregar más columnas que contienen reglas coincidentes.
Otros consejos
¿Hay demasiadas filas/reglas? Si no es el caso (eso es subjetivo, pero digamos menos de 10,000), podría crear índices para todas las columnas.
Eso aumentaría significativamente la velocidad y los índices no tomarán mucho espacio.
Si no planea hacer una gran tabla de reglas, apuesto a que su enfoque está bien siempre que indexe todas las columnas.
¿Por qué no hacer índices de su tabla de reglas por los valores? Entonces tú puedes
SELECT myvalue FROM RULES_A
Parece que lo que realmente tienes son reglas y conjuntos de reglas. Modelarlo de esa manera no solo hará que esta codificación en particular sea mucho más simple, sino que también hará que el modelo se pueda ampliar cuando decida que necesita un 16 columnas.
Por ejemplo:
CREATE TABLE Rules (
rule_id INT NOT NULL,
rule_category CHAR(1) NOT NULL, -- This is like your column idea
rule_int_value INT NULL,
rule_str_value VARCHAR(20) NULL,
CONSTRAINT PK_Rules PRIMARY KEY CLUSTERED (rule_id),
CONSTRAINT CK_Rules_one_value CHECK (rule_int_value IS NULL OR rule_str_value IS NULL)
)
CREATE TABLE Rule_Sets (
rule_set_id INT NOT NULL,
rule_id INT NOT NULL,
CONSTRAINT PK_Rule_Sets PRIMARY KEY CLUSTERED (rule_set_id, rule_id)
)
Algunos datos que coincidirían con sus reglas dadas:
INSERT INTO Rules (rule_id, rule_category, rule_int_value, rule_str_value)
VALUES (1, 'A', 50, NULL)
INSERT INTO Rules (rule_id, rule_category, rule_int_value, rule_str_value)
VALUES (2, 'A', 51, NULL)
INSERT INTO Rules (rule_id, rule_category, rule_int_value, rule_str_value)
VALUES (3, 'B', NULL, 'xyz')
INSERT INTO Rules (rule_id, rule_category, rule_int_value, rule_str_value)
VALUES (4, 'C', 123, NULL)
INSERT INTO Rules (rule_id, rule_category, rule_int_value, rule_str_value)
VALUES (5, 'C', 456, NULL)
INSERT INTO Rule_Sets (rule_set_id, rule_id) VALUES (1, 1)
INSERT INTO Rule_Sets (rule_set_id, rule_id) VALUES (2, 2)
INSERT INTO Rule_Sets (rule_set_id, rule_id) VALUES (2, 3)
INSERT INTO Rule_Sets (rule_set_id, rule_id) VALUES (3, 2)
INSERT INTO Rule_Sets (rule_set_id, rule_id) VALUES (3, 4)
INSERT INTO Rule_Sets (rule_set_id, rule_id) VALUES (4, 3)
INSERT INTO Rule_Sets (rule_set_id, rule_id) VALUES (4, 5)
Un script de prueba que confirma la misma respuesta que espera:
DECLARE
@a INT,
@b VARCHAR(20),
@c INT
SET @a = 50
SET @b = 'xyz'
SET @c = 456
SELECT DISTINCT
rule_set_id AS failed_rule_set_id
FROM
Rule_Sets RS
WHERE
NOT EXISTS (SELECT * FROM Rules R WHERE R.rule_id = RS.rule_id AND @a = R.rule_int_value) AND
NOT EXISTS (SELECT * FROM Rules R WHERE R.rule_id = RS.rule_id AND @b = R.rule_str_value) AND
NOT EXISTS (SELECT * FROM Rules R WHERE R.rule_id = RS.rule_id AND @c = R.rule_int_value)
Si puede presentar los datos de entrada en una forma basada en establecimientos en lugar de como parámetros individuales, la instrucción SQL final puede ser más dinámica y no tendría que crecer a medida que agrega columnas adicionales.
SELECT * FROM RULES
WHERE (A = :a OR A IS NULL)
AND (B = :b OR B IS NULL)
AND (C = :c OR C IS NULL);
Dependiendo de sus RBDM, esto podría o no ser más eficiente, aunque no por mucho:
SELECT * FROM RULES
WHERE coalesce(A, :a) = :a
AND coalesce(B, :b) = :b
AND coalesce(C, :c) = :c ;
En MySQL (su RBDMS puede hacer esto de manera diferente), esta consulta permite una index
escanear en lugar de un ref_or_null
escanear, si hay un índice aplicable. Si el índice cubre todas las columnas, permite que se use todo el índice (y de hecho, si el índice cubre todas las columnas, el índice es la mesa).
Con tu consulta, un ref_or_null
El acceso se realiza en lugar de un index
Acceso, y solo se utiliza la primera columna en un índice de múltiples columnas. Con ref_or_null
, MySQL tiene que buscar partidos en el índice, luego buscar nuevamente nulos. Entonces usamos el índice dos veces, pero nunca usamos el índice completo.
Pero con fusele, tiene la sobrecarga de ejecutar la función de fuselón en cada valor de columna. Lo cual es más rápido probablemente depende de cuántas reglas tenga, cuántas columnas en cada fila y el índice utilizado, si las hay.
Si es más legible es una cuestión de opinión.