Pergunta

Suponha que você tenha uma tabela RULES com 3 colunas A, B e C.À medida que os dados entram no sistema, quero saber se alguma linha da tabela RULES corresponde aos meus dados com a condição de que, se a coluna correspondente na tabela RULES for nula, todos os dados correspondam.O SQL óbvio é:

SELECT * FROM RULES
WHERE (A = :a OR A IS NULL)
  AND (B = :b OR B IS NULL)
  AND (C = :c OR C IS NULL)

Então, se eu tiver regras:

RULE    A        B        C
1       50       NULL     NULL
2       51       xyz      NULL
3       51       NULL     123
4       NULL     xyz      456

Uma entrada de (50, xyz, 456) corresponderá às regras 1 e 4.

Pergunta: Existe uma maneira melhor de fazer isso?Com apenas 3 campos isso não é problema.Mas a tabela real terá 15 colunas e me preocupo com o desempenho do SQL.

Especulação: Uma instrução SQL alternativa que criei envolvia adicionar uma coluna extra à tabela com uma contagem de quantos campos não são nulos.(Portanto, no exemplo, o valor desta coluna para as regras 1-4 é 1, 2, 2 e 2 respectivamente.) Com esta coluna "col_count", a seleção poderia 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

Infelizmente, não tenho dados de amostra suficientes para descobrir qual dessas abordagens teria melhor desempenho.Antes de começar a criar regras aleatórias, pensei em perguntar aqui se existe uma abordagem melhor.

Observação: Técnicas de mineração de dados e restrições de colunas não são viáveis ​​aqui.Os dados devem ser verificados assim que entram no sistema e para que possam ser sinalizados como aprovado/reprovado imediatamente.E os usuários controlam a adição ou remoção de regras, portanto não posso converter as regras em restrições de coluna ou outras instruções de definição de dados.

Uma última coisa, no final preciso de uma lista de todas as regras que os dados não conseguem passar.A solução não pode ser abortada na primeira falha.

Obrigado.

Foi útil?

Solução

A primeira consulta que você forneceu é perfeita.Eu realmente duvido que adicionar a coluna da qual você estava falando lhe daria mais velocidade, já que a propriedade NOT NULL de cada entrada é verificada de qualquer maneira, já que toda comparação com NULL resulta em falso.Então eu acho que x=y é expandido para x IS NOT NULL AND x=y internamente.Talvez alguém possa esclarecer isso.

Todas as outras otimizações que consigo imaginar envolveriam pré-cálculo ou armazenamento em cache.Você pode criar tabelas [temporárias] que correspondam a certas regras ou adicionar mais colunas contendo regras correspondentes.

Outras dicas

Existem muitas linhas/regras?Se não for o caso (isso é subjetivo, mas digamos menos de 10.000), você poderá criar índices para todas as colunas.

Isso aumentaria significativamente a velocidade e os índices não ocupariam muito espaço.

Se você não planeja criar uma enorme tabela de regras, aposto que sua abordagem está correta, desde que você indexe todas as colunas.

Por que não fazer índices da sua tabela de regras pelos valores?Então você pode

SELECT myvalue FROM RULES_A

Parece que o que você realmente tem são regras e conjuntos de regras.Modelá-lo dessa maneira não apenas tornará essa codificação específica muito mais simples, mas também tornará o modelo expansível quando você decidir que precisa de 16 colunas.

Por exemplo:

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)
)

Alguns dados que corresponderiam às regras fornecidas:

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)

Um script de teste que confirma a mesma resposta esperada:

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)

Se você puder apresentar os dados de entrada em um formato baseado em conjunto, em vez de parâmetros individuais, a instrução SQL final poderá ser mais dinâmica e não precisará crescer à medida que você adiciona colunas adicionais.

SELECT * FROM RULES
 WHERE (A = :a OR A IS NULL)
   AND (B = :b OR B IS NULL)
   AND (C = :c OR C IS NULL);

Dependendo do seu RBDMS, isso pode ou não ser mais eficiente, embora não muito:

SELECT * FROM RULES
 WHERE coalesce(A, :a) = :a
   AND coalesce(B, :b) = :b 
   AND coalesce(C, :c) = :c ;

No MySQL (seu RBDMS pode fazer isso de forma diferente), esta consulta permite uma index digitalizar em vez de um ref_or_null scan, se houver um índice aplicável.Se o índice cobrir todas as colunas, ele permitirá que todo o índice seja usado (e, de fato, se o índice cobrir todas as colunas, o índice é a mesa).

Com sua consulta, um ref_or_null o acesso é feito em vez de um index acesso e apenas a primeira coluna em um índice de múltiplas colunas é usada.Com ref_or_null, o MySQL precisa procurar correspondências no índice e, em seguida, procurar novamente por nulos.Portanto, usamos o índice duas vezes, mas nunca usamos o índice inteiro.

Mas com coalescer, você tem a sobrecarga de executar a função coalescer em cada valor de coluna.O que é mais rápido provavelmente depende de quantas regras você possui, de quantas colunas em cada linha e do índice usado, se houver.

Se é mais legível é uma questão de opinião.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top