Альтернативы объединению строк или процедурному предотвращению повторения кода SQL-запроса?

19

Отказ от ответственности: Пожалуйста, имейте меня как человека, который использует базы данных лишь малую часть своего рабочего времени. (Большую часть времени я занимаюсь программированием на С ++ на своей работе, но каждый нечетный месяц мне нужно искать / исправлять / добавлять что-то в базе данных Oracle.)

Мне неоднократно приходилось писать сложные запросы SQL, как для специальных запросов, так и для запросов, встроенных в приложения, где большие части запросов просто повторяли «код».

Написание таких мерзостей на традиционном языке программирования доставит вам большие неприятности, однако я ( I ) пока не смог найти какой-либо достойной техники, предотвращающей повторение кода SQL-запроса.


Изменить: во-первых, я хочу поблагодарить тех, кто предоставил отличные улучшения в моем первоначальном примере . Однако этот вопрос не о моем примере. Речь идет о повторяемости в запросах SQL. Таким образом, ответы ( JackP , Leigh ) до сих пор отлично показывают, что вы можете уменьшить повторяемость, написав лучшие запросы . Однако даже тогда вы сталкиваетесь с некоторой повторяемостью, которую, очевидно, невозможно устранить: это всегда раздражало меня SQL. В «традиционных» языках программирования я могу довольно много реорганизовать, чтобы минимизировать повторяемость кода, но с SQL кажется, что нет (?) Инструментов, позволяющих это сделать, за исключением написания менее повторяющегося выражения для начала.

Обратите внимание, что я снова удалил тег Oracle, так как мне было бы искренне интересно, нет ли базы данных или языка сценариев, который допускает нечто большее.


Вот одна из таких жемчужин, которую я сегодня собрал вместе. Это в основном сообщает разницу в наборе столбцов одной таблицы. Пожалуйста, просмотрите следующий код, особенно большой запрос в конце. Я продолжу ниже.

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

Как вы можете видеть, запрос для генерации «отчета о различиях» использует один и тот же блок SQL SELECT 5 раз (может быть легко 42 раза!). Это кажется мне совершенно мертвым (я могу сказать это после того, как написал код), но я не смог найти хорошего решения для этого.

  • Если бы это был запрос в каком-то реальном коде приложения, я мог бы написать функцию, которая объединяет этот запрос в виде строки, а затем выполнить запрос в виде строки.

    • -> Строить струны - это ужасно и ужасно проверять и поддерживать. Если «код приложения» написан на языке, таком как PL / SQL, он чувствует себя настолько неправильно, что причиняет боль.
  • В качестве альтернативы, если бы он использовался из PL / SQL или тому подобного, я бы предположил, что существуют некоторые процедурные средства, чтобы сделать этот запрос более понятным.

    • -> Развертывание чего-либо, что может быть выражено в одном запросе, в процедурных шагах, просто чтобы предотвратить повторение кода, тоже кажется неправильным.
  • Если этот запрос понадобится как представление в базе данных, то, насколько я понимаю, не будет другого способа, кроме как сохранить определение представления, как я опубликовал выше. (!!?)

    • -> Я действительно должен был провести некоторое обслуживание определения двухстраничного представления, как только оно вышло далеко за рамки заявления. Очевидно, что для изменения чего-либо в этом представлении требовался текстовый поиск по регулярному выражению по определению представления для того, использовался ли тот же вложенный оператор в другой строке и нужно ли его менять там.

Итак, как гласит заголовок - какие существуют методы, позволяющие избежать написания таких мерзостей?

Мартин
источник

Ответы:

13

Вы слишком скромны - ваш SQL хорошо и сжато написан, учитывая задачу, которую вы выполняете. Несколько указателей:

  • t1.name <> t2.nameвсегда верно, если t1.name = REPLACE(t2.name, 'DUP_', '')- вы можете отказаться от первого
  • обычно ты хочешь union all. unionзначит, union allтогда отбросьте дубликаты. В этом случае это может не иметь значения, но всегда union allполезно использовать привычку, если вы явно не хотите удалять дубликаты.
  • если вы хотите, чтобы числовые сравнения происходили после приведения к varchar, стоит подумать о следующем:

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

    второе представление - это своего рода unpivotоперация - если вы набрали хотя бы 11g, вы можете сделать это более кратко с unpivotпредложением - см. здесь пример

  • Я говорю, не ходите по процедурному маршруту, если вы можете сделать это в SQL, но ...
  • Динамический SQL, вероятно, стоит рассмотреть, несмотря на проблемы, которые вы упоминаете при тестировании и обслуживании.

--РЕДАКТИРОВАТЬ--

Чтобы ответить на более общую сторону вопроса, существуют методы для уменьшения повторения в SQL, в том числе:

Но вы не можете напрямую внедрять идеи ОО в мир SQL - во многих случаях повторение хорошо, если запрос читабелен и хорошо написан, и было бы неразумно прибегать к динамическому SQL (например) просто для того, чтобы избежать повторения.

Последний запрос, включающий предложенное Ли изменение и CTE вместо представления, может выглядеть примерно так:

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, частично за UNION ALL. Часто UNIONбез ALLобычно приводит к катушке для временного хранения требуемой операции сортировки (как «UNION» эффективно UNION ALLследует DISTINCTчто подразумевает свой род) , так что в некоторых случаях разница в производительности может быть огромной.
Дэвид Спиллетт
7

Вот альтернатива представлению test_attribs_unpivot, предоставленному JackPDouglas (+1), которое работает в версиях до 11g и делает меньше полных сканирований таблицы:

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

Его последний запрос может быть использован без изменений с этим представлением.

Ли Риффель
источник
Намного лучше! Я думаю, что вы можете даже бросить актерский состав?
Джек Дуглас
Вместо SELECT rownum MyRow FROM test_attribs where rownum<=5использования select level MyRow from dual connect by level <= 5. Вы не хотите, чтобы все эти логические выгоды только для создания 5 строк.
Штефан Оравец
@ Штефан Оравец - у меня было так, но я изменил это, потому что я не был уверен, для каких версий были доступны иерархические запросы. Так как он был доступен по крайней мере с версии 8, я изменю его.
Ли Риффель
4

Я часто сталкиваюсь с подобной проблемой, сравнивая две версии таблицы для новых, удаленных или измененных строк. Несколько месяцев назад я опубликовал решение для SQL Server с использованием PowerShell здесь .

Чтобы приспособить его к вашей проблеме, я сначала создаю два представления, чтобы отделить оригинал от дублирующихся строк.

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

а затем я проверяю изменения с

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;

Отсюда я могу найти ваши оригинальные идентификаторы

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;

Кстати: MINUS, UNION и GROUP BY рассматривают разные NULL как равные. Использование этих операций делает запросы более элегантными.

Подсказка для пользователей SQL Server: MINUS называется EXCEPT, но работает аналогично.

bernd_k
источник