Alternative alla concatenazione di stringhe o alla procedura per prevenire la ripetizione del codice di query SQL?


19

Disclaimer: per favore, abbi pazienza con me come qualcuno che utilizza database solo una piccola parte del suo tempo di lavoro. (La maggior parte delle volte eseguo la programmazione C ++ nel mio lavoro, ma ogni mese dispari ho bisogno di cercare / correggere / aggiungere qualcosa in un database Oracle.)

Ho ripetutamente avuto bisogno di scrivere complesse query SQL, sia per query ad-hoc sia per query integrate in applicazioni, in cui gran parte delle query in cui si ripeteva semplicemente "codice".

Scrivere tali abominazioni in un linguaggio di programmazione tradizionale ti metterebbe nei guai profondi, eppure io ( I ) non sono stato ancora in grado di trovare alcuna tecnica decente per prevenire la ripetizione del codice di query SQL.


Modifica: 1 °, voglio ringraziare i rispondenti che hanno fornito eccellenti miglioramenti al mio esempio originale . Tuttavia, questa domanda non riguarda il mio esempio. Riguarda la ripetitività nelle query SQL. Pertanto, le risposte ( JackP , Leigh ) finora fanno un ottimo lavoro nel dimostrare che è possibile ridurre la ripetitività scrivendo query migliori . Tuttavia anche in questo caso si deve affrontare una certa ripetitività che apparentemente non può essere rimossa: questo mi ha sempre infastidito con SQL. Nei linguaggi di programmazione "tradizionali" posso refactificare parecchio per minimizzare la ripetitività nel codice, ma con SQL sembra che non ci siano (?) Strumenti che lo consentano, tranne per la scrittura di un'istruzione meno ripetitiva per cominciare.

Nota che ho rimosso di nuovo il tag Oracle, poiché sarei davvero interessato se non ci sono database o linguaggi di scripting che consentano qualcosa di più.


Ecco una gemma del genere che ho messo insieme oggi. Riporta sostanzialmente la differenza in un insieme di colonne di una singola tabella. Si prega di scorrere il seguente codice, esp. la query di grandi dimensioni alla fine. Continuerò di seguito.

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

Come puoi vedere, la query per generare un "rapporto sulle differenze" utilizza lo stesso blocco SQL SELECT 5 volte (potrebbe essere facilmente 42 volte!). Questo mi sembra assolutamente mortale (posso dire questo, dopo tutto quello che ho scritto il codice), ma non sono riuscito a trovare una buona soluzione a questo.

  • Se questa fosse una query in un vero codice applicativo, potrei scrivere una funzione che mette insieme questa query come una stringa e quindi eseguirò una query come una stringa.

    • -> Costruire stringhe è orribile e orribile da testare e mantenere. Se il "codice dell'applicazione" è scritto in un linguaggio come PL / SQL, sembra così sbagliato che fa male.
  • In alternativa, se utilizzato da PL / SQL o simili, suppongo ci siano alcuni mezzi procedurali per rendere questa query più gestibile.

    • -> Srotolare qualcosa che può essere espresso in una singola query in passaggi procedurali solo per prevenire la ripetizione del codice sembra sbagliato.
  • Se questa query fosse necessaria come vista nel database, allora - per quanto ho capito - non ci sarebbe altro che mantenere la definizione della vista come ho postato sopra. (!!?)

    • -> In realtà ho dovuto fare un po 'di manutenzione su una definizione di vista di 2 pagine una volta che non era molto lontana dall'affermazione precedente. Ovviamente, cambiare qualcosa in questa vista richiedeva una ricerca di testo regexp sulla definizione della vista per sapere se la stessa sotto-istruzione fosse stata usata in un'altra riga e se fosse necessario cambiarla.

Quindi, come dice il titolo: quali tecniche ci sono per evitare di dover scrivere simili abominazioni?

Risposte:


13

Sei troppo modesto - il tuo SQL è ben scritto e conciso, dato il compito che stai intraprendendo. Alcuni suggerimenti:

  • t1.name <> t2.nameè sempre vero se t1.name = REPLACE(t2.name, 'DUP_', '')- puoi eliminare il primo
  • di solito vuoi union all. unionsignifica union allquindi eliminare i duplicati. In questo caso potrebbe non fare alcuna differenza, ma l'utilizzo union allè sempre una buona abitudine a meno che non si desideri eliminare esplicitamente duplicati.
  • se si desidera che i confronti numerici si verifichino dopo il cast su varchar, vale la pena considerare quanto segue:

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

    la seconda vista è una specie di unpivotoperazione - se hai almeno 11 g puoi farlo in modo più conciso con la unpivotclausola - vedi qui per un esempio

  • Dico che non seguire la procedura se puoi farlo in SQL, ma ...
  • Probabilmente vale la pena prendere in considerazione l'SQL dinamico, nonostante i problemi menzionati durante i test e la manutenzione

--MODIFICARE--

Per rispondere al lato più generale della domanda, esistono tecniche per ridurre la ripetizione in SQL, tra cui:

Ma non puoi portare direttamente le idee OO nel mondo SQL: in molti casi la ripetizione va bene se la query è leggibile e ben scritta, e non sarebbe saggio ricorrere a SQL dinamico (ad esempio) solo per evitare la ripetizione.

La query finale che include la modifica suggerita da Leigh e un CTE anziché una vista potrebbe essere simile a questa:

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

1
+1, in parte per UNION ALL. Spesso UNIONsenza di ALLsolito si traduce in uno spool in memoria temporanea per l'operazione di ordinamento richiesta (poiché "UNION" è effettivamente UNION ALLseguito da ciò DISTINCTche implica un ordinamento), quindi in alcuni casi la differenza di prestazioni può essere enorme.
David Spillett,

7

Ecco un'alternativa alla vista test_attribs_unpivot fornita da JackPDouglas (+1) che funziona nelle versioni precedenti a 11g e esegue meno scansioni della tabella completa:

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

La sua query finale può essere utilizzata invariata con questa vista.


Molto meglio! Penso che puoi anche far cadere il cast?
Jack Douglas,

Invece di SELECT rownum MyRow FROM test_attribs where rownum<=5usare select level MyRow from dual connect by level <= 5. Non vuoi che tutti questi logici ottengano solo per la creazione di 5 righe.
Štefan Oravec,

@ Štefan Oravec - L'ho avuto in quel modo, ma l'ho cambiato perché non ero sicuro di quali versioni gerarchiche fossero disponibili. Dal momento che è stato disponibile almeno dalla versione 8, lo cambierò.
Leigh Riffel,

4

Incontro spesso il problema simile per confrontare due versioni di una tabella per righe nuove, cancellate o modificate. Alcuni mesi fa ho pubblicato una soluzione per SQL Server usando PowerShell qui .

Per adattarlo al tuo problema, per prima cosa creo due viste per separare l'originale dalle righe duplicate

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 poi controllo le modifiche con

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;

Da qui posso trovare i tuoi ID originali

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;

A proposito: MINUS e UNION e GROUP BY considerano NULL diversi come uguali. L'uso di queste operazioni rende le query più eleganti.

Suggerimento per gli utenti di SQL Server: MINUS è denominato EXCEPT lì, ma funziona in modo simile.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.