Alternativas à concatenação de strings ou a procedimentos para impedir a repetição de código de consulta SQL?

19

Disclaimer: Por favor, tenha paciência comigo como alguém que usa apenas bancos de dados uma pequena fração do seu tempo de trabalho. (Na maioria das vezes, faço programação C ++ no meu trabalho, mas todos os meses ímpares preciso pesquisar / corrigir / adicionar algo em um banco de dados Oracle.)

Eu precisei repetidamente escrever consultas SQL complexas, tanto para consultas ad-hoc quanto para consultas embutidas em aplicativos, onde grandes partes das consultas apenas repetiam "código".

Escrever essas abominações em uma linguagem de programação tradicional causaria muitos problemas, mas eu ( muitos I ) ainda não consegui encontrar nenhuma técnica decente para impedir a repetição do código de consulta SQL.


Edit: 1º, quero agradecer aos respondentes que forneceram excelentes melhorias no meu exemplo original . No entanto, esta questão não é sobre o meu exemplo. É sobre repetitividade em consultas SQL. Como tal, as respostas ( JackP , Leigh ) até agora fazem um ótimo trabalho ao mostrar que você pode reduzir a repetitividade escrevendo melhores consultas . No entanto, mesmo assim, você enfrenta alguma repetitividade que aparentemente não pode ser removida: isso sempre me incomodou com o SQL. Nas linguagens de programação "tradicionais", posso refatorar bastante para minimizar a repetitividade no código, mas com o SQL parece que não existem ferramentas (?) Que permitam isso, exceto para escrever uma instrução menos repetitiva para começar.

Observe que removi a tag Oracle novamente, pois estaria realmente interessado se não há banco de dados ou linguagem de script que permita algo mais.


Aqui está uma dessas pedras preciosas que juntei hoje. Ele basicamente relata a diferença em um conjunto de colunas de uma única tabela. Percorra o código a seguir, esp. a consulta grande no final. Eu continuarei abaixo.

--
-- Create Table to test queries
--
CREATE TABLE TEST_ATTRIBS (
id NUMBER PRIMARY KEY,
name  VARCHAR2(300) UNIQUE,
attr1 VARCHAR2(2000),
attr2 VARCHAR2(2000),
attr3 INTEGER,
attr4 NUMBER,
attr5 VARCHAR2(2000)
);

--
-- insert some test data
--
insert into TEST_ATTRIBS values ( 1, 'Alfred',   'a', 'Foobar', 33, 44, 'e');
insert into TEST_ATTRIBS values ( 2, 'Batman',   'b', 'Foobar', 66, 44, 'e');
insert into TEST_ATTRIBS values ( 3, 'Chris',    'c', 'Foobar', 99, 44, 'e');
insert into TEST_ATTRIBS values ( 4, 'Dorothee', 'd', 'Foobar', 33, 44, 'e');
insert into TEST_ATTRIBS values ( 5, 'Emilia',   'e', 'Barfoo', 66, 44, 'e');
insert into TEST_ATTRIBS values ( 6, 'Francis',  'f', 'Barfoo', 99, 44, 'e');
insert into TEST_ATTRIBS values ( 7, 'Gustav',   'g', 'Foobar', 33, 44, 'e');
insert into TEST_ATTRIBS values ( 8, 'Homer',    'h', 'Foobar', 66, 44, 'e');
insert into TEST_ATTRIBS values ( 9, 'Ingrid',   'i', 'Foobar', 99, 44, 'e');
insert into TEST_ATTRIBS values (10, 'Jason',    'j', 'Bob',    33, 44, 'e');
insert into TEST_ATTRIBS values (12, 'Konrad',   'k', 'Bob',    66, 44, 'e');
insert into TEST_ATTRIBS values (13, 'Lucas',    'l', 'Foobar', 99, 44, 'e');

insert into TEST_ATTRIBS values (14, 'DUP_Alfred',   'a', 'FOOBAR', 33, 44, 'e');
insert into TEST_ATTRIBS values (15, 'DUP_Chris',    'c', 'Foobar', 66, 44, 'e');
insert into TEST_ATTRIBS values (16, 'DUP_Dorothee', 'd', 'Foobar', 99, 44, 'e');
insert into TEST_ATTRIBS values (17, 'DUP_Gustav',   'X', 'Foobar', 33, 44, 'e');
insert into TEST_ATTRIBS values (18, 'DUP_Homer',    'h', 'Foobar', 66, 44, 'e');
insert into TEST_ATTRIBS values (19, 'DUP_Ingrid',   'Y', 'foo',    99, 44, 'e');

insert into TEST_ATTRIBS values (20, 'Martha',   'm', 'Bob',    33, 88, 'f');

-- Create comparison view
CREATE OR REPLACE VIEW TA_SELFCMP as
select 
t1.id as id_1, t2.id as id_2, t1.name as name, t2.name as name_dup,
t1.attr1 as attr1_1, t1.attr2 as attr2_1, t1.attr3 as attr3_1, t1.attr4 as attr4_1, t1.attr5 as attr5_1,
t2.attr1 as attr1_2, t2.attr2 as attr2_2, t2.attr3 as attr3_2, t2.attr4 as attr4_2, t2.attr5 as attr5_2
from TEST_ATTRIBS t1, TEST_ATTRIBS t2
where t1.id <> t2.id
and t1.name <> t2.name
and t1.name = REPLACE(t2.name, 'DUP_', '')
;

-- NOTE THIS PIECE OF HORRIBLE CODE REPETITION --
-- Create comparison report
-- compare 1st attribute
select 'attr1' as Different,
id_1, id_2, name, name_dup,
CAST(attr1_1 AS VARCHAR2(2000)) as Val1, CAST(attr1_2 AS VARCHAR2(2000)) as Val2
from TA_SELFCMP
where attr1_1 <> attr1_2
or (attr1_1 is null and attr1_2 is not null)
or (attr1_1 is not null and attr1_2 is null)
union
-- compare 2nd attribute
select 'attr2' as Different,
id_1, id_2, name, name_dup,
CAST(attr2_1 AS VARCHAR2(2000)) as Val1, CAST(attr2_2 AS VARCHAR2(2000)) as Val2
from TA_SELFCMP
where attr2_1 <> attr2_2
or (attr2_1 is null and attr2_2 is not null)
or (attr2_1 is not null and attr2_2 is null)
union
-- compare 3rd attribute
select 'attr3' as Different,
id_1, id_2, name, name_dup,
CAST(attr3_1 AS VARCHAR2(2000)) as Val1, CAST(attr3_2 AS VARCHAR2(2000)) as Val2
from TA_SELFCMP
where attr3_1 <> attr3_2
or (attr3_1 is null and attr3_2 is not null)
or (attr3_1 is not null and attr3_2 is null)
union
-- compare 4th attribute
select 'attr4' as Different,
id_1, id_2, name, name_dup,
CAST(attr4_1 AS VARCHAR2(2000)) as Val1, CAST(attr4_2 AS VARCHAR2(2000)) as Val2
from TA_SELFCMP
where attr4_1 <> attr4_2
or (attr4_1 is null and attr4_2 is not null)
or (attr4_1 is not null and attr4_2 is null)
union
-- compare 5th attribute
select 'attr5' as Different,
id_1, id_2, name, name_dup,
CAST(attr5_1 AS VARCHAR2(2000)) as Val1, CAST(attr5_2 AS VARCHAR2(2000)) as Val2
from TA_SELFCMP
where attr5_1 <> attr5_2
or (attr5_1 is null and attr5_2 is not null)
or (attr5_1 is not null and attr5_2 is null)
;

Como você pode ver, a consulta para gerar um "relatório de diferenças" usa o mesmo bloco SQL SELECT 5 vezes (pode ser facilmente 42 vezes!). Isso me parece absolutamente com morte cerebral (posso dizer isso depois de tudo que escrevi o código), mas não consegui encontrar nenhuma boa solução para isso.

  • Se isso fosse uma consulta em algum código de aplicativo real, eu poderia escrever uma função que agrupasse essa consulta como uma string e depois executaria a consulta como uma sequência.

    • -> Construir strings é horrível e horrível para testar e manter. Se o "código do aplicativo" for escrito em uma linguagem como PL / SQL, parecerá tão errado que dói.
  • Como alternativa, se usado a partir de PL / SQL ou similar, eu acho que existem alguns procedimentos para tornar essa consulta mais sustentável.

    • -> Desenrolar algo que pode ser expresso em uma única consulta em etapas processuais, apenas para evitar que a repetição de código também pareça errada.
  • Se essa consulta fosse necessária como uma exibição no banco de dados, então - pelo que entendi - não haveria outra maneira senão manter a definição da exibição, como postado acima. (!!?)

    • -> Na verdade, tive que fazer alguma manutenção em uma definição de visualização de 2 páginas, uma vez que não estava muito acima da declaração. Obviamente, alterar qualquer coisa nessa visualização exigia uma pesquisa de texto regexp sobre a definição de visualização para saber se a mesma sub-instrução havia sido usada em outra linha e se precisava mudar lá.

Então, como diz o título - que técnicas existem para evitar a necessidade de escrever essas abominações?

Martin
fonte

Respostas:

13

Você é muito modesto - seu SQL é escrito de forma concisa e concisa, dada a tarefa que você está realizando. Algumas dicas:

  • t1.name <> t2.namesempre é verdade se t1.name = REPLACE(t2.name, 'DUP_', '')- você pode largar o antigo
  • geralmente você quer union all. unionsignifica union allentão soltar duplicatas. Pode não fazer diferença nesse caso, mas sempre usar union allé um bom hábito, a menos que você queira eliminar explicitamente as duplicatas.
  • se você deseja que as comparações numéricas ocorram após a conversão para varchar, vale a pena considerar o seguinte:

    create view test_attribs_cast as 
    select id, name, attr1, attr2, cast(attr3 as varchar(2000)) as attr3, 
           cast(attr4 as varchar(2000)) as attr4, attr5
    from test_attribs;
    
    create view test_attribs_unpivot as 
    select id, name, 1 as attr#, attr1 as attr from test_attribs_cast union all
    select id, name, 2, attr2 from test_attribs_cast union all
    select id, name, 3, attr3 from test_attribs_cast union all
    select id, name, 4, attr4 from test_attribs_cast union all
    select id, name, 5, attr5 from test_attribs_cast;
    
    select 'attr'||t1.attr# as different, t1.id as id_1, t2.id as id_2, t1.name, 
           t2.name as name_dup, t1.attr as val1, t2.attr as val2
    from test_attribs_unpivot t1 join test_attribs_unpivot t2 on(
           t1.id<>t2.id and 
           t1.name = replace(t2.name, 'DUP_', '') and 
           t1.attr#=t2.attr# )
    where t1.attr<>t2.attr or (t1.attr is null and t2.attr is not null)
          or (t1.attr is not null and t2.attr is null);

    a segunda visualização é um tipo de unpivotoperação - se você tiver pelo menos 11g, poderá fazer isso de forma mais concisa com a unpivotcláusula - veja aqui um exemplo

  • Eu digo que não siga a rota processual se você puder fazê-lo no SQL, mas ...
  • Provavelmente vale a pena considerar o SQL dinâmico, apesar dos problemas mencionados com testes e manutenção

--EDITAR--

Para responder ao lado mais geral da pergunta, existem técnicas para reduzir a repetição no SQL, incluindo:

Mas você não pode trazer idéias de OO diretamente para o mundo do SQL - em muitos casos, a repetição é boa se a consulta for legível e bem escrita, e seria imprudente recorrer ao SQL dinâmico (por exemplo) apenas para evitar a repetição.

A consulta final, incluindo a alteração sugerida por Leigh e uma CTE, em vez de uma exibição, pode ser algo como isto:

with t as ( select id, name, attr#, 
                   decode(attr#,1,attr1,2,attr2,3,attr3,4,attr4,attr5) attr
            from test_attribs
                 cross join (select rownum attr# from dual connect by rownum<=5))
select 'attr'||t1.attr# as different, t1.id as id_1, t2.id as id_2, t1.name, 
       t2.name as name_dup, t1.attr as val1, t2.attr as val2
from t t1 join test_attribs_unpivot t2 
               on( t1.id<>t2.id and 
                   t1.name = replace(t2.name, 'DUP_', '') and 
                   t1.attr#=t2.attr# )
where t1.attr<>t2.attr or (t1.attr is null and t2.attr is not null)
      or (t1.attr is not null and t2.attr is null);
Jack Douglas
fonte
1
+1, parcialmente para UNION ALL. Muitas vezes UNION, sem ALLgeralmente resulta em um carretel de armazenamento temporário para a operação de classificação exigida (como 'união' é efetivamente UNION ALLseguido DISTINCTpelo que implica uma espécie) para que, em alguns casos a diferença de desempenho pode ser enorme.
David Spillett 01/01
7

Aqui está uma alternativa à visualização test_attribs_unpivot fornecida por JackPDouglas (+1) que funciona em versões anteriores ao 11g e faz menos varreduras completas na tabela:

CREATE OR REPLACE VIEW test_attribs_unpivot AS
   SELECT ID, Name, MyRow Attr#, CAST(
      DECODE(MyRow,1,attr1,2,attr2,3,attr3,4,attr4,attr5) AS VARCHAR2(2000)) attr
   FROM TEST_ATTRIBS 
   CROSS JOIN (SELECT level MyRow FROM dual connect by level<=5);

Sua consulta final pode ser usada inalterada com essa visualização.

Leigh Riffel
fonte
Muito melhor! Eu acho que você pode até largar o elenco?
Jack Douglas
Em vez de SELECT rownum MyRow FROM test_attribs where rownum<=5usar select level MyRow from dual connect by level <= 5. Você não deseja que todas essas lógicas sejam apenas para criar 5 linhas.
Štefan Oravec
@ Štefan Oravec - Eu tive assim, mas mudei porque não tinha certeza de quais versões as consultas hierárquicas estavam disponíveis. Como ele está disponível desde pelo menos a versão 8, vou alterá-lo.
Leigh Riffel
4

Costumo encontrar o problema semelhante para comparar duas versões de uma tabela para linhas novas, excluídas ou alteradas. Há um mês, publiquei uma solução para o SQL Server usando o PowerShell aqui .

Para adaptá-lo ao seu problema, primeiro criei duas visualizações para separar o original das linhas duplicadas

CREATE OR REPLACE VIEW V1_TEST_ATTRIBS AS 
select * from TEST_ATTRIBS where SUBSTR(name, 1, 4) <> 'DUP_'; 

CREATE OR REPLACE VIEW V2_TEST_ATTRIBS AS 
select id, REPLACE(name, 'DUP_', '') name, attr1, attr2, attr3, attr4, attr5 from TEST_ATTRIBS where SUBSTR(name, 1, 4) = 'DUP_'; 

e depois verifico as alterações com

SELECT 1 SRC, NAME, ATTR1, ATTR2, ATTR3, ATTR4, ATTR5 FROM V1_TEST_ATTRIBS
MINUS
Select 1 SRC, NAME, ATTR1, ATTR2, ATTR3, ATTR4, ATTR5 from V2_TEST_ATTRIBS
UNION
SELECT 2 SRC, NAME, ATTR1, ATTR2, ATTR3, ATTR4, ATTR5 FROM V2_TEST_ATTRIBS
MINUS
SELECT 2 SRC ,NAME, ATTR1, ATTR2, ATTR3, ATTR4, ATTR5 FROM V1_TEST_ATTRIBS
ORDER BY NAME, SRC;

A partir daqui, posso encontrar seus IDs originais

Select NVL(v1.id, v2.id) id,  t.name, t.attr1, t.attr2, t.attr3, t.attr4, t.attr5 from
(
SELECT 1 SRC, NAME, ATTR1, ATTR2, ATTR3, ATTR4, ATTR5 FROM V1_TEST_ATTRIBS
MINUS
Select 1 SRC, NAME, ATTR1, ATTR2, ATTR3, ATTR4, ATTR5 from V2_TEST_ATTRIBS
UNION
SELECT 2 SRC, NAME, ATTR1, ATTR2, ATTR3, ATTR4, ATTR5 FROM V2_TEST_ATTRIBS
MINUS
Select 2 SRC ,NAME, ATTR1, ATTR2, ATTR3, ATTR4, ATTR5 from V1_TEST_ATTRIBS
) t
LEFT JOIN V1_TEST_ATTRIBS V1 ON T.NAME = V1.NAME AND T.SRC = 1
LEFT JOIN V2_TEST_ATTRIBS V2 ON T.NAME = V2.NAME AND T.SRC = 2
ORDER by NAME, SRC;

BTW: MINUS e UNION e GROUP BY tratam NULL diferentes como iguais. O uso dessas operações torna as consultas mais elegantes.

Dica para usuários do SQL Server: MINUS é nomeado EXCEPT, mas funciona de maneira semelhante.

bernd_k
fonte