it-swarm-fr.com

Alternatives à la concaténation des chaînes ou de traverser la procédure pour prévenir la répétition de code de requête SQL?

Disclaimer: Veuillez supporter avec moi une personne qui utilise uniquement des bases de données une infime fraction de son temps de travail. (La plupart du temps, je fais une programmation C++ dans mon travail, mais chaque mois impaire, je dois rechercher/corriger/ajouter quelque chose dans une base de données Oracle.)

J'ai besoin à plusieurs reprises d'écrire des requêtes SQL complexes, à la fois pour les requêtes ad hoc et pour les requêtes intégrées aux applications, dans lesquelles de grandes parties des requêtes où vient de répéter "code".

Écrire de telles abominations dans un langage de programmation traditionnel vous donnerait des problèmes profonds, mais je ([~ # ~] i [~ # ~ ~]) n'a pas encore été incapable de trouver une technique décente pour empêcher la requête SQL Répétition de code.


EDIT: 1st, je tiens à remercier les répondeurs qui ont fourni d'excellentes améliorations à mon fichier original exemple. Cependant, cette question ne concerne pas mon exemple. Il s'agit de répétitivité dans les requêtes SQL. En tant que telle, les réponses ( jackp , Leigh ) jusqu'à présent, faites un excellent travail de montrer que vous peut réduire la répétitivité en écrivant de meilleures requêtes . Cependant, même Alors Vous faites face à une répétitivité qui ne peut apparemment pas être supprimée: cela m'a toujours nagré avec SQL. Dans les langages de programmation "traditionnels", je peux refroidir beaucoup pour minimiser la répétitivité dans le code, mais avec SQL, il semble qu'il n'y ait pas de (?) Des outils qui le permettent, à l'exception de la rédaction d'une déclaration moins répétitive pour commencer.

Notez que j'ai retiré la balise Oracle à nouveau, car je serais véritablement intéressé s'il n'y a pas de langue ou langue de script qui permet quelque chose de plus.


Voici un de ces gemmes que j'ai piété ensemble aujourd'hui. Il rapporte essentiellement la différence dans un ensemble de colonnes d'une seule table. S'il vous plaît écrémer dans le code suivant, esp. la grande requête à la fin. Je vais continuer ci-dessous.

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

Comme vous pouvez le constater, la requête pour générer un "rapport de différence" utilise le même bloc de sélection SQL 5 fois (pourrait facilement être 42 fois!). Cela me frappe comme absolument cérébral mort (je suis autorisé à dire cela, après tout, j'ai écrit le code), mais je n'ai pas pu trouver de bonne solution à cela.

  • Si ce serait une requête dans un code d'application réel, je pourrait écrire une fonction qui gère ensemble cette requête en tant que chaîne, puis j'exécuterais la requête en tant que chaîne de caractères.

    • -> Les cordes de construction sont horribles et horribles de tester et de maintenir. Si le "code d'application" est écrit dans une langue telle que PL/SQL, il se sent si mal qu'il fait mal.
  • Alternativement, s'il est utilisé de PL/SQL ou similaire, je suppose que des moyens procéduraux de rendre cette requête plus maintenable.

    • -> dérouler quelque chose qui peut être exprimé dans une seule requête dans des étapes de procédure pour empêcher que la répétition de code ne soit pas aussi fatile.
  • Si cette requête serait nécessaire comme vue dans la base de données, alors - aussi loin que je comprends - il n'y aurait pas de moyen autre que de maintenir la définition de vue comme je l'ai posté ci-dessus. (!!?)

    • -> Je devais en fait faire un peu de maintenance sur une définition de vue de 2 pages une fois que cela n'était pas loin de la déclaration ci-dessus. Évidemment, changer tout ce qui est de cette vue nécessitait une recherche de texte Regexp sur la définition d'affichage pour savoir si la même sous-déclaration avait été utilisée dans une autre ligne et si elle avait besoin de changer de temps.

Ainsi, comme le titre passe - Quelles techniques sont là pour empêcher d'écrire de telles abominations?

19
Martin

Vous êtes trop modeste - votre SQL est bien et reconnu de manière concise compte tenu de la tâche que vous entreprenez. Quelques indicateurs:

  • t1.name <> t2.name Est toujours vrai si t1.name = REPLACE(t2.name, 'DUP_', '') - vous pouvez laisser tomber le premier
  • habituellement, vous voulez union all. union signifie union all Puis déposez des doublons. Cela pourrait ne faire aucune différence dans ce cas, mais toujours en utilisant union all Est une bonne habitude à moins que vous ne vouliez explicitement abandonner les doublons.
  • si vous êtes prêt pour que les comparaisons numériques se produisent après coulée à Varchar, ce qui suit pourrait valoir la peine d'être envisagé:

    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 deuxième vue est une sorte de fonctionnement unpivot - si vous êtes sur au moins 11g, vous pouvez le faire plus concis avec le unpivot Clause - voir aut pour un exemple

  • Je dis ne descends pas de la voie de procédure si vous pouvez le faire dans SQL, mais ...
  • Dynamic SQL vaut probablement la peine d'être envisagé malgré les problèmes que vous mentionnez avec les tests et la maintenance

--ÉDITER--

Pour répondre au côté le plus général de la question, il existe des techniques pour réduire la répétition dans SQL, notamment:

Mais vous ne pouvez pas apporter OO Idées dans le monde SQL directement - dans de nombreux cas, la répétition est bonne si la requête est lisible et bien écrite, et il serait imprudé de recourir à Dynamic SQL ( Par exemple) Juste pour éviter la répétition.

La requête finale, y compris le changement suggéré par Leigh's, et une CTE au lieu d'une vue pourrait ressembler à ceci:

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

Voici une alternative à la vue Test_attribs_unpivot fourni par Jackpdouglas (+1) Cela fonctionne dans les versions avant 11g et fait moins de table complète:

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

Sa requête finale peut être utilisée inchangée avec cette vue.

7
Leigh Riffel

Je rencontre souvent le problème similaire pour comparer deux versions d'une table pour les nouvelles lignes nouvelles, supprimées ou modifiées. Il y a quelques mois, j'ai publié une solution pour SQL Server à l'aide de PowerShell ici .

Pour l'adapter à votre problème, je crée d'abord deux vues pour séparer l'original des lignes en double.

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_'; 

et puis je vérifie les changements avec

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;

D'ici je peux trouver vos identifiants d'origine

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: moins et un syndicat et un groupe en traitent différents NULL comme étant égales. L'utilisation de ces opérations rend les requêtes plus élégantes.

Indice pour les utilisateurs SQL Server: Minus est nommé sauf là-bas, mais fonctionne comme similaire.

4
bernd_k