¿Alternativas para concatenar cadenas o pasar a procedimientos para evitar la repetición del código de consulta SQL?


19

Descargo de responsabilidad: Tenga paciencia conmigo como alguien que solo utiliza bases de datos una pequeña fracción de su tiempo de trabajo. (La mayoría de las veces hago programación C ++ en mi trabajo, pero cada mes extraño necesito buscar / arreglar / agregar algo en una base de datos Oracle).

En repetidas ocasiones he necesitado escribir consultas SQL complejas, tanto para consultas ad-hoc como para consultas integradas en aplicaciones, donde grandes partes de las consultas solo repetían "código".

Escribir tales abominaciones en un lenguaje de programación tradicional lo metería en serios problemas, sin embargo, yo ( I ) aún no he podido encontrar ninguna técnica decente para evitar la repetición del código de consulta SQL.


Editar: primero, quiero agradecer a los que respondieron que proporcionaron excelentes mejoras a mi ejemplo original . Sin embargo, esta pregunta no se trata de mi ejemplo. Se trata de la repetitividad en las consultas SQL. Como tal, las respuestas ( JackP , Leigh ) hasta ahora hacen un gran trabajo al mostrar que puede reducir la repetición escribiendo mejores consultas . Sin embargo, incluso entonces te enfrentas a una repetición que aparentemente no se puede eliminar: esto siempre me fastidiaba con SQL. En los lenguajes de programación "tradicionales" puedo refactorizar mucho para minimizar la repetición en el código, pero con SQL parece que no hay herramientas (?) Que permitan esto, excepto para escribir una declaración menos repetitiva para empezar.

Tenga en cuenta que he eliminado la etiqueta de Oracle nuevamente, ya que estaría realmente interesado si no hay una base de datos o lenguaje de script que permita algo más.


Aquí hay una de esas gemas que improvisé hoy. Básicamente informa la diferencia en un conjunto de columnas de una sola tabla. Lea el siguiente código, especialmente. La consulta grande al final. Continuaré abajo.

--
-- 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 puede ver, la consulta para generar un "informe de diferencia" usa el mismo bloque SQL SELECT 5 veces (¡podría ser 42 veces fácilmente!). Esto me parece absolutamente mortal (se me permite decir esto, después de todo, escribí el código), pero no he podido encontrar ninguna buena solución para esto.

  • Si esto fuera una consulta en algún código de aplicación real, podría escribir una función que combine esta consulta como una cadena y luego ejecutaría la consulta como una cadena.

    • -> Construir cadenas es horrible y horrible de probar y mantener. Si el "código de la aplicación" está escrito en un lenguaje como PL / SQL, se siente tan mal que duele.
  • Alternativamente, si se usa desde PL / SQL o similar, supongo que hay algunos medios de procedimiento para hacer que esta consulta sea más fácil de mantener.

    • -> Desenrollar algo que se puede expresar en una sola consulta en pasos de procedimiento solo para evitar la repetición del código también se siente mal.
  • Si esta consulta fuera necesaria como una vista en la base de datos, entonces, por lo que yo entiendo, no habría otra forma que no sea mantener la definición de la vista tal como la publiqué anteriormente. (!!?)

    • -> Realmente tuve que hacer algo de mantenimiento en una definición de vista de 2 páginas una vez que no estaba muy lejos de la declaración anterior. Obviamente, cambiar cualquier cosa en esta vista requería una búsqueda de texto regexp sobre la definición de vista para ver si la misma subdeclaración se había utilizado en otra línea y si era necesario cambiarla allí.

Entonces, como dice el título, ¿qué técnicas existen para evitar tener que escribir tales abominaciones?

Respuestas:


13

Eres demasiado modesto: tu SQL está bien escrito de forma concisa dada la tarea que estás realizando. Algunos consejos:

  • t1.name <> t2.namesiempre es cierto si t1.name = REPLACE(t2.name, 'DUP_', '')puedes dejar el primero
  • generalmente quieres union all. unionsignifica union allentonces soltar duplicados. Puede que no haga ninguna diferencia en este caso, pero usar siempre union alles un buen hábito a menos que explícitamente desee eliminar cualquier duplicado.
  • Si está dispuesto a que las comparaciones numéricas sucedan después de enviar a varchar, vale la pena considerar lo siguiente:

    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 segunda vista es un tipo de unpivotoperación: si tiene al menos 11 g, puede hacerlo de manera más concisa con la unpivotcláusula; consulte aquí para ver un ejemplo

  • Supongo que no sigas la ruta del procedimiento si puedes hacerlo en SQL, pero ...
  • Probablemente valga la pena considerar el SQL dinámico a pesar de los problemas que menciona con las pruebas y el mantenimiento

--EDITAR--

Para responder al lado más general de la pregunta, existen técnicas para reducir la repetición en SQL, que incluyen:

Pero no puede traer ideas OO al mundo SQL directamente: en muchos casos, la repetición está bien si la consulta es legible y está bien escrita, y no sería prudente recurrir al SQL dinámico (por ejemplo) solo para evitar la repetición.

La consulta final que incluye el cambio sugerido de Leigh y un CTE en lugar de una vista podría verse así:

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, en parte por UNION ALL. A menudo, UNIONsin que ALLnormalmente se produzca un spool para el almacenamiento temporal para la operación de clasificación requerida (ya que 'UNION' es UNION ALLseguido efectivamente por lo DISTINCTque implica una clasificación), en algunos casos la diferencia de rendimiento puede ser enorme.
David Spillett

7

Aquí hay una alternativa a la vista test_attribs_unpivot proporcionada por JackPDouglas (+1) que funciona en versiones anteriores a 11g y realiza menos escaneos de tabla 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);

Su consulta final se puede usar sin cambios con esta vista.


¡Mucho mejor! Creo que incluso puedes dejar el elenco?
Jack Douglas

En lugar de SELECT rownum MyRow FROM test_attribs where rownum<=5usar select level MyRow from dual connect by level <= 5. No querrás todos esos resultados lógicos solo para crear 5 filas.
Štefan Oravec

@ Štefan Oravec: lo tenía así, pero lo cambié porque no estaba seguro de qué versiones de consultas jerárquicas estaban disponibles. Como ha estado disponible desde al menos la versión 8, lo cambiaré.
Leigh Riffel

4

A menudo encuentro el mismo problema al comparar dos versiones de una tabla para filas nuevas, eliminadas o modificadas. Hace un mes publiqué una solución para SQL Server usando PowerShell aquí .

Para adaptarlo a su problema, primero creo dos vistas para separar el original de las filas 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_'; 

y luego verifico los cambios 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;

Desde aquí puedo encontrar tus identificadores originales

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;

Por cierto: MINUS y UNION y GROUP BY tratan diferentes NULL como iguales. El uso de estas operaciones hace que las consultas sean más elegantes.

Sugerencia para usuarios de SQL Server: MINUS se llama EXCEPTO allí, pero funciona de manera similar.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.