Alternatif untuk menyatukan string atau menjadi prosedural untuk mencegah pengulangan kode query SQL?

19

Penafian: Tolong tahan dengan saya sebagai seseorang yang hanya menggunakan database sebagian kecil dari waktu kerjanya. (Sebagian besar waktu saya melakukan pemrograman C ++ dalam pekerjaan saya, tetapi setiap bulan aneh saya perlu mencari / memperbaiki / menambahkan sesuatu dalam database Oracle.)

Saya telah berulang kali diperlukan untuk menulis kueri SQL yang kompleks, baik untuk kueri ad-hoc dan untuk kueri yang dibangun ke dalam aplikasi, di mana sebagian besar kueri hanya mengulangi "kode".

Menulis kekejian seperti itu dalam bahasa pemrograman tradisional akan membuat Anda dalam masalah besar, namun saya ( saya ) belum dapat menemukan teknik yang layak untuk mencegah pengulangan kode kueri SQL.


Sunting: Pertama, saya ingin mengucapkan terima kasih kepada para penjawab yang memberikan peningkatan luar biasa pada contoh asli saya . Namun, pertanyaan ini bukan tentang contoh saya. Ini tentang pengulangan dalam query SQL. Dengan demikian, jawabannya ( JackP , Leigh ) sejauh ini melakukan pekerjaan yang baik untuk menunjukkan bahwa Anda dapat mengurangi pengulangan dengan menulis pertanyaan yang lebih baik . Namun bahkan kemudian Anda menghadapi beberapa pengulangan yang tampaknya tidak dapat dihapus: Ini selalu mengganggu saya dengan SQL. Dalam bahasa pemrograman "tradisional" saya dapat melakukan refactor cukup banyak untuk meminimalkan pengulangan dalam kode, tetapi dengan SQL tampaknya tidak ada alat (?) Yang memungkinkan untuk ini, kecuali untuk menulis pernyataan yang kurang berulang untuk memulai.

Perhatikan bahwa saya telah menghapus tag Oracle lagi, karena saya akan benar-benar tertarik apakah tidak ada database atau bahasa scripting yang memungkinkan untuk sesuatu yang lebih.


Inilah salah satu permata yang saya buat hari ini. Ini pada dasarnya melaporkan perbedaan dalam satu set kolom dari satu tabel. Silakan membaca kode berikut, esp. permintaan besar di akhir. Saya akan lanjutkan di bawah.

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

Seperti yang Anda lihat, permintaan untuk menghasilkan "laporan perbedaan" menggunakan blok SQL SELECT yang sama 5 kali (bisa dengan mudah menjadi 42 kali!). Ini menurut saya benar-benar mati otak (saya diizinkan untuk mengatakan ini, setelah semua saya menulis kode), tetapi saya belum dapat menemukan solusi yang baik untuk ini.

  • Jika ini akan menjadi kueri dalam beberapa kode aplikasi aktual, saya bisa menulis fungsi yang menyatukan kueri ini sebagai string dan kemudian saya akan menjalankan kueri sebagai string.

    • -> Membangun string mengerikan dan mengerikan untuk diuji dan dipelihara. Jika "kode aplikasi" ditulis dalam bahasa seperti PL / SQL rasanya sangat salah.
  • Atau, jika digunakan dari PL / SQL atau sejenisnya, saya kira ada beberapa cara prosedural untuk membuat kueri ini lebih mudah dikelola.

    • -> Membuka gulungan sesuatu yang bisa diekspresikan dalam satu query ke dalam langkah prosedural hanya untuk mencegah pengulangan kode juga terasa salah.
  • Jika kueri ini diperlukan sebagai tampilan dalam database, maka - sejauh yang saya mengerti - tidak akan ada cara lain selain mempertahankan definisi tampilan seperti yang saya posting di atas. (!!?)

    • -> Saya benar-benar harus melakukan pemeliharaan pada definisi tampilan 2 halaman setelah itu tidak jauh dari pernyataan di atas. Jelas, mengubah apa pun dalam tampilan ini memerlukan pencarian teks regexp atas definisi tampilan untuk apakah sub-pernyataan yang sama telah digunakan di baris lain dan apakah perlu diubah di sana.

Jadi, seperti judulnya - teknik apa yang ada untuk mencegah keharusan menulis kekejian seperti itu?

Martin
sumber

Jawaban:

13

Anda terlalu sederhana - SQL Anda ditulis dengan baik dan ringkas mengingat tugas yang Anda lakukan. Beberapa petunjuk:

  • t1.name <> t2.nameselalu benar jika t1.name = REPLACE(t2.name, 'DUP_', '')- Anda dapat menjatuhkan yang pertama
  • biasanya kamu mau union all. unionberarti union allkemudian menjatuhkan duplikat. Mungkin tidak ada bedanya dalam kasus ini tetapi selalu menggunakan union alladalah kebiasaan yang baik kecuali jika Anda secara eksplisit ingin menjatuhkan duplikat.
  • jika Anda bersedia agar perbandingan numerik terjadi setelah casting ke varchar, berikut ini mungkin layak dipertimbangkan:

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

    pandangan kedua adalah semacam unpivotoperasi - jika Anda memiliki setidaknya 11g Anda dapat melakukan ini lebih ringkas dengan unpivotklausa - lihat di sini untuk contoh

  • Saya katakan jangan turun rute prosedural jika Anda bisa melakukannya dalam SQL, tapi ...
  • Dynamic SQL mungkin layak dipertimbangkan meskipun ada masalah yang Anda sebutkan dengan pengujian dan pemeliharaan

EDIT

Untuk menjawab sisi pertanyaan yang lebih umum, ada beberapa teknik untuk mengurangi pengulangan dalam SQL, termasuk:

  • Tampilan - Anda tahu tentang itu :)
  • Ekspresi Tabel Umum (lihat di sini misalnya)
  • fitur individual dari basis data seperti decode(lihat jawaban Leigh untuk bagaimana ini dapat mengurangi pengulangan), fungsi jendela dan kueri hierarki / rekursif untuk memberi nama tetapi beberapa

Tetapi Anda tidak dapat membawa ide-ide OO ke dunia SQL secara langsung - dalam banyak kasus, pengulangan baik-baik saja jika kueri dapat dibaca dan ditulis dengan baik, dan tidak bijaksana untuk menggunakan SQL dinamis (misalnya) hanya untuk menghindari pengulangan.

Kueri terakhir termasuk perubahan yang disarankan Leigh dan CTE sebagai ganti tampilan bisa terlihat seperti ini:

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
sumber
1
+1, sebagian untuk UNION ALL. Seringkali UNIONtanpa ALLbiasanya menghasilkan gulungan ke penyimpanan sementara untuk operasi sortir yang diperlukan (karena 'UNION' secara efektif UNION ALLdiikuti oleh DISTINCTyang menyiratkan sortir) sehingga dalam beberapa kasus perbedaan kinerja bisa sangat besar.
David Spillett
7

Berikut ini adalah alternatif untuk tampilan test_attribs_unpivot yang disediakan oleh JackPDouglas (+1) yang berfungsi dalam versi sebelum 11g dan melakukan lebih sedikit pemindaian tabel penuh:

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

Permintaan terakhirnya dapat digunakan tidak berubah dengan tampilan ini.

Leigh Riffel
sumber
Jauh lebih baik! Saya pikir Anda bahkan dapat menjatuhkan para pemain?
Jack Douglas
Alih-alih SELECT rownum MyRow FROM test_attribs where rownum<=5digunakan select level MyRow from dual connect by level <= 5. Anda tidak ingin semua yang logis hanya untuk membuat 5 baris.
Štefan Oravec
@ Štefan Oravec - Saya sudah seperti itu, tetapi saya mengubahnya karena saya tidak yakin untuk versi apa permintaan hierarkis tersedia. Karena sudah tersedia setidaknya versi 8, saya akan mengubahnya.
Leigh Riffel
4

Saya sering mengalami masalah yang sama untuk membandingkan dua versi tabel untuk baris baru, dihapus atau diubah. Beberapa bulan yang lalu saya menerbitkan solusi untuk SQL Server menggunakan PowerShell di sini .

Untuk menyesuaikannya dengan masalah Anda, pertama-tama saya membuat dua tampilan untuk memisahkan yang asli dari baris duplikat

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

dan kemudian saya memeriksa perubahannya dengan

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;

Dari sini saya dapat menemukan id asli Anda

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, UNION, dan GROUP BY memperlakukan NULL yang berbeda sebagai setara. Menggunakan operasi ini membuat kueri lebih elegan.

Petunjuk untuk pengguna SQL Server: MINUS bernama KECUALI di sana, tetapi berfungsi serupa.

bernd_k
sumber