Existe-t-il un bon moyen de vérifier les règles par rapport aux n colonnes?
-
09-09-2019 - |
Question
Supposons que vous ayez des règles de table avec 3 colonnes A, B et C. À mesure que les données entrent dans le système, je veux savoir si une ligne du tableau des règles correspond à mes données avec la condition que si la colonne correspondante dans le tableau des règles est nul , toutes les données correspondent. Le SQL évident est:
SELECT * FROM RULES
WHERE (A = :a OR A IS NULL)
AND (B = :b OR B IS NULL)
AND (C = :c OR C IS NULL)
Donc, si j'ai des règles:
RULE A B C 1 50 NULL NULL 2 51 xyz NULL 3 51 NULL 123 4 NULL xyz 456
Une entrée de (50, XYZ, 456) correspondra aux règles 1 et 4.
Question: Y a-t-il une meilleure manière de faire cela? Avec seulement 3 champs, ce n'est pas un problème. Mais le tableau réel aura 15 colonnes et je m'inquiète de la façon dont ces échelles SQL.
Spéculation: Une autre instruction SQL que j'ai inventée impliquée ajoutant une colonne supplémentaire à la table avec un compte du nombre de champs ne sont pas nuls. (Ainsi, dans l'exemple, cette valeur de colonnes pour les règles 1 à 4 est respectivement 1, 2, 2 et 2.) Avec cette colonne "col_count", la sélection pourrait être:
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
Malheureusement, je n'ai pas suffisamment d'échantillons de données pour trouver notre autre approche. Avant de commencer à créer des règles aléatoires, je pensais que je demanderais ici s'il y avait une meilleure approche.
Noter: Les techniques d'exploration de données et les contraintes de colonne ne sont pas possibles ici. Les données doivent être vérifiées lorsqu'elles entrent dans le système et peuvent donc être signalées de passage / échouer immédiatement. Et, les utilisateurs contrôlent l'addition ou la suppression des règles afin que je ne puisse pas convertir les règles en contraintes de colonne ou autres instructions de définition de données.
Une dernière chose, à la fin, j'ai besoin d'une liste de toutes les règles que les données ne réussissent pas. La solution ne peut pas interrompre à la première défaillance.
Merci.
La solution
La première requête que vous avez fournie est parfaite. Je doute vraiment que l'ajout de la colonne dont vous parliez vous donnerait plus de vitesse, car la propriété non nul de chaque entrée est de toute façon vérifiée, car chaque comparaison avec Null donne faux. Alors je suppose que x=y
est étendu à x IS NOT NULL AND x=y
intérieurement. Peut-être que quelqu'un d'autre peut clarifier cela.
Toutes les autres optimisations auxquelles je peux penser impliqueraient une précalcul ou une mise en cache. Vous pouvez créer des tables [temporaires] correspondant à certaines règles ou ajouter d'autres colonnes contenant des règles de correspondance.
Autres conseils
Y a-t-il trop de lignes / règles? Si ce n'est pas le cas (c'est subjectif, mais dire moins de 10 000), vous pouvez créer des index pour toutes les colonnes.
Cela augmenterait considérablement la vitesse et les index ne prendront pas beaucoup de place.
Si vous ne prévoyez pas de faire une énorme table de règles, je parie que votre approche est ok à condition d'indexer toutes les colonnes.
Pourquoi ne pas faire des indices de votre table de règles par les valeurs? Ensuite vous pouvez
SELECT myvalue FROM RULES_A
Il semble que ce que vous avez vraiment, ce sont des règles et des ensembles de règles. Le modéliser de cette façon rendra non seulement ce codage particulier beaucoup plus simple, mais rendra également le modèle extensible lorsque vous décidez que vous avez besoin de 16 colonnes.
Par exemple:
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)
)
Certaines données correspondraient à vos règles données:
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 test qui confirme la même réponse que vous attendez:
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 vous pouvez présenter les données d'entrée sous une forme basée sur un ensemble plutôt que comme des paramètres individuels, l'instruction SQL finale peut être plus dynamique et n'aurait pas à croître à mesure que vous ajoutez des colonnes supplémentaires.
SELECT * FROM RULES
WHERE (A = :a OR A IS NULL)
AND (B = :b OR B IS NULL)
AND (C = :c OR C IS NULL);
Selon vos RBDM, cela peut ou non être plus efficace, mais pas beaucoup:
SELECT * FROM RULES
WHERE coalesce(A, :a) = :a
AND coalesce(B, :b) = :b
AND coalesce(C, :c) = :c ;
Dans MySQL (vos RBDM peuvent le faire différemment), cette requête permet un index
scan plutôt qu'un ref_or_null
Scan, s'il existe un index applicable. Si l'index couvre toutes les colonnes, il permet à l'index entier d'être utilisé (et en effet, si l'index couvre toutes les colonnes, l'index est la table).
Avec votre requête, un ref_or_null
l'accès est effectué plutôt qu'un index
Accès, et seule la première colonne d'un index multi-colonne est utilisée. Avec ref_or_null
, MySQL doit rechercher l'index pour les correspondances, puis rechercher à nouveau Nulls. Nous utilisons donc deux fois l'index, mais n'utilisons jamais l'index entier.
Mais avec Coalesce, vous avez les frais généraux d'exécuter la fonction de coalesce sur chaque valeur de colonne. Ce qui est plus rapide dépend probablement du nombre de règles que vous avez, du nombre de colonnes dans chaque ligne et de l'index utilisé, le cas échéant.
Que ce soit plus lisible est une question d'opinion.