Kita sering melihat kueri SQL kompleks yang ditulis dengan buruk berjalan melawan tabel atau tabel dalam database. Kueri tersebut membuat waktu eksekusi menjadi sangat lama dan memakan banyak CPU dan sumber daya lainnya. Namun, kueri kompleks memberikan informasi berharga kepada aplikasi/orang yang menjalankannya dalam banyak kasus. Oleh karena itu, mereka adalah aset yang berguna di semua jenis aplikasi.
Kueri kompleks sulit untuk di-debug
Jika kita melihat lebih dekat pada kueri yang bermasalah, banyak di antaranya yang rumit, terutama yang spesifik yang digunakan dalam laporan.
Kueri kompleks sering kali terdiri dari lima atau lebih tabel besar dan digabungkan bersama oleh banyak subkueri. Setiap sub-kueri memiliki klausa WHERE yang melakukan kalkulasi sederhana hingga kompleks dan/atau transformasi data sambil menggabungkan kolom tabel yang relevan.
Kueri semacam itu mungkin sulit untuk di-debug tanpa menghabiskan banyak sumber daya. Alasannya, sulit untuk menentukan apakah setiap sub-query dan/atau sub-query yang digabungkan menghasilkan hasil yang benar.
Skenario tipikal adalah:mereka menelepon Anda larut malam untuk memecahkan masalah pada server database yang sibuk dengan kueri kompleks yang terlibat, dan Anda perlu memperbaikinya dengan cepat. Sebagai Pengembang atau DBA, Anda mungkin memiliki waktu dan sumber daya sistem yang sangat terbatas yang tersedia pada jam yang terlambat. Jadi, hal pertama yang Anda butuhkan adalah rencana tentang cara men-debug kueri yang bermasalah.
Terkadang, prosedur debugging berjalan dengan baik. Terkadang, dibutuhkan banyak waktu dan usaha sebelum Anda mencapai tujuan dan menyelesaikan masalah.
Menulis Kueri dalam struktur CTE
Tetapi bagaimana jika ada cara untuk menulis kueri yang rumit sehingga orang dapat men-debugnya dengan cepat, sepotong demi sepotong?
Ada cara seperti itu. Ini disebut Ekspresi Tabel Umum atau CTE.
Common Table Expression adalah fitur standar di sebagian besar database modern seperti SQLServer, MySQL (pada versi 8.0), MariaDB (versi 10.2.1), Db2, dan Oracle. Ini memiliki struktur sederhana yang merangkum satu atau banyak sub-kueri ke dalam kumpulan hasil bernama sementara. Anda dapat menggunakan kumpulan hasil ini lebih lanjut di CTE atau sub-kueri bernama lainnya.
Ekspresi Tabel Umum, sampai batas tertentu, adalah VIEW yang hanya ada dan direferensikan oleh kueri pada saat eksekusi.
Mengubah kueri kompleks menjadi kueri gaya CTE memerlukan pemikiran terstruktur. Hal yang sama berlaku untuk OOP dengan enkapsulasi saat menulis ulang kueri kompleks ke dalam struktur CTE.
Anda perlu memikirkan:
- Setiap kumpulan data yang Anda ambil dari setiap tabel.
- Bagaimana mereka digabungkan untuk merangkum subkueri terdekat menjadi satu set hasil bernama sementara.
Ulangi untuk setiap sub-kueri dan kumpulan data yang tersisa hingga Anda mencapai hasil akhir kueri. Perhatikan bahwa setiap kumpulan hasil bernama sementara juga merupakan sub-kueri.
Bagian akhir dari kueri harus berupa pemilihan yang sangat "sederhana", mengembalikan hasil akhir ke aplikasi. Setelah Anda mencapai bagian akhir ini, Anda dapat menukarnya dengan kueri yang memilih data dari kumpulan hasil sementara yang diberi nama satu per satu.
Dengan cara ini, debug dari setiap kumpulan hasil sementara menjadi pekerjaan yang mudah.
Untuk memahami bagaimana kita dapat membangun kueri kita dari yang sederhana hingga yang kompleks, mari kita lihat struktur CTE. Bentuk paling sederhana adalah sebagai berikut:
WITH CTE_1 as (
select .... from some_table where ...
)
select ... from CTE_1
where ...
Di sini CTE_1 adalah nama unik yang Anda berikan ke kumpulan hasil bernama sementara. Dapat ada set hasil sebanyak yang diperlukan. Dengan itu, formulir meluas ke, seperti yang ditunjukkan di bawah ini:
WITH CTE_1 as (
select .... from some_table where ...
), CTE_2 as (
select .... from some_other_table where ...
)
select ... from CTE_1 c1,CTE_2 c2
where c1.col1 = c2.col1
....
Pada awalnya, setiap bagian CTE dibuat secara terpisah. Kemudian berlanjut, saat CTE ditautkan bersama untuk membangun kumpulan hasil akhir kueri.
Sekarang, mari kita periksa kasus lain, menanyakan database Penjualan fiktif. Kami ingin mengetahui produk apa saja, termasuk kuantitas dan total penjualan, yang terjual di setiap kategori pada bulan sebelumnya, dan produk mana yang mendapatkan total penjualan lebih banyak dari bulan sebelumnya.
Kami menyusun kueri kami menjadi beberapa bagian CTE, di mana setiap bagian merujuk ke bagian sebelumnya. Pertama, kami membuat kumpulan hasil untuk mencantumkan data terperinci yang kami butuhkan dari tabel kami untuk membentuk kueri lainnya:
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
)
select dt.*
from detailed_data dt.
order by dt.order_date desc, dt.category_name, dt.product_name
Langkah selanjutnya adalah meringkas data kuantitas dan total penjualan menurut setiap kategori dan nama produk:
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
), product_sales as (
select year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name,sum(dt.quantity) total_quantity, sum(dt.listprice * (1 - dt.discount)) total_product_sales
from detailed_data dt
group by year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name
)
select ps.*
from product_sales ps
order by ps.year desc, ps.month desc, ps.category_name,ps.product_name
Langkah terakhir adalah membuat dua set hasil sementara yang mewakili data bulan lalu dan bulan sebelumnya. Setelah itu, saring data yang akan dikembalikan sebagai hasil akhir yang ditetapkan:
WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
), product_sales as (
select year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name,sum(dt.quantity) total_quantity, sum(dt.listprice * (1 - dt.discount)) total_product_sales
from detailed_data dt
group by year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name
), last_month_data (
select ps.*
from product_sales ps.
where ps.year = year(CURRENT_DATE) -1
and ps.month = month(CURRENT_DATE) -1
), prev_month_data (
select ps.*
from product_sales ps.
where ps.year = year(CURRENT_DATE) -2
and ps.month = month(CURRENT_DATE) -2
)
select lmd.*
from last_month_data lmd, prev_month_data pmd
where lmd.category_name = pmd.category_name
and lmd.product_name = pmd.product_name
and ( lmd.total_quantity > pmd.total_quantity
or lmd.total_product_sales > pmd.total_product_sales )
order by lmd.year desc, lmd.month desc, lmd.category_name,lmd.product_name, lmd.total_product_sales desc, lmd.total_quantity desc
Perhatikan bahwa di SQLServer Anda menyetel getdate() alih-alih CURRENT_DATE.
Dengan cara ini, kita dapat menukar bagian terakhir dengan pilihan yang menanyakan bagian CTE individu untuk melihat hasil dari bagian yang dipilih. Hasilnya, kami dapat dengan cepat men-debug masalah.
Selain itu, dengan mengeksekusi penjelasan pada setiap bagian CTE (dan seluruh kueri), kami memperkirakan seberapa baik kinerja setiap bagian dan/atau seluruh kueri pada tabel dan data.
Sejalan dengan itu, Anda dapat mengoptimalkan setiap bagian dengan menulis ulang dan/atau menambahkan indeks yang tepat ke tabel yang terlibat. Kemudian Anda menjelaskan seluruh kueri untuk melihat rencana kueri akhir dan melanjutkan dengan pengoptimalan jika diperlukan.
Kueri rekursif menggunakan struktur CTE
Fitur lain yang berguna dari CTE adalah membuat kueri rekursif.
Kueri SQL rekursif memungkinkan Anda mencapai hal-hal yang tidak mungkin Anda bayangkan dengan jenis SQL ini dan kecepatannya. Anda dapat memecahkan banyak masalah bisnis dan bahkan menulis ulang beberapa logika SQL/aplikasi yang kompleks menjadi panggilan SQL rekursif sederhana ke database.
Ada sedikit variasi dalam membuat kueri rekursif antara sistem database. Namun, tujuannya sama.
Beberapa contoh kegunaan CTE rekursif:
- Anda dapat menggunakannya untuk menemukan celah dalam data.
- Anda dapat membuat bagan organisasi.
- Anda dapat membuat data yang telah dihitung sebelumnya untuk digunakan lebih lanjut di bagian CTE lainnya
- Akhirnya, Anda dapat membuat data pengujian.
Kata rekursif mengatakan itu semua. Anda memiliki kueri yang berulang kali menyebut dirinya sendiri dengan beberapa titik awal, dan, SANGAT PENTING, sebuah titik akhir (keluar dari kegagalan-aman begitu saya menyebutnya).
Jika Anda tidak memiliki jalan keluar yang aman dari kegagalan, atau formula rekursif Anda melampauinya, Anda berada dalam masalah besar. Kueri akan masuk ke loop tak terbatas menghasilkan CPU yang sangat tinggi dan penggunaan LOG yang sangat tinggi. Ini akan menyebabkan kehabisan memori dan/atau penyimpanan.
Jika kueri Anda rusak, Anda harus berpikir sangat cepat untuk menonaktifkannya. Jika Anda tidak dapat melakukannya, segera beri tahu DBA Anda, sehingga mereka mencegah sistem database tersedak, mematikan utas pelarian.
Lihat contohnya:
with RECURSIVE mydates (level,nextdate) as (
select 1 level, FROM_UNIXTIME(RAND()*2147483647) nextdate from DUAL
union all
select level+1, FROM_UNIXTIME(RAND()*2147483647) nextdate
from mydates
where level < 1000
)
SELECT nextdate from mydates
);
Contoh ini adalah sintaks CTE rekursif MySQL/MariaDB. Dengan itu, kami menghasilkan seribu tanggal acak. Levelnya adalah penghitung dan pintu keluar gagal-aman kami untuk keluar dari kueri rekursif dengan aman.
Seperti yang ditunjukkan, baris 2 adalah titik awal kita, sedangkan baris 4-5 adalah panggilan rekursif dengan titik akhir dalam klausa WHERE (baris 6). Baris 8 dan 9 adalah panggilan dalam mengeksekusi query rekursif dan mengambil data.
Contoh lain:
DECLARE @today as date;
DECLARE @1stjanprevyear as date;
select @today = DATEADD(DAY, 0, DATEDIFF(DAY, 0, getdate())),
@1stjanprevyear = DATEFROMPARTS(YEAR(GETDATE())-1, 1, 1) ;
WITH DatesCTE as (
SELECT @1stjanprevyear as CalendarDate
UNION ALL
SELECT dateadd(day , 1, CalendarDate) AS CalendarDate FROM DatesCTE
WHERE dateadd (day, 1, CalendarDate) < @today
), MaxMinDates as (
SELECT Max(CalendarDate) MaxDate,Min(CalendarDate) MinDate
FROM DatesCTE
)
SELECT i.*
FROM InvoiceTable i, MaxMinDates t
where i.INVOICE_DATE between t.MinDate and t.MaxDate
OPTION (MAXRECURSION 1000);
Contoh ini adalah sintaks SQLServer. Di sini, kami membiarkan bagian DatesCTE menghasilkan semua tanggal antara hari ini dan 1 Januari tahun sebelumnya. Kami menggunakannya untuk mengembalikan semua Faktur milik tanggal tersebut.
Titik awalnya adalah @1stjanprevyear variabel dan pintu keluar gagal-aman @hari ini . Maksimal 730 hari dimungkinkan. Jadi, opsi rekursi maksimum diatur ke 1000 untuk memastikannya berhenti.
Kami bahkan dapat melewati MaxMinDates bagian dan menulis bagian akhir, seperti yang ditunjukkan di bawah ini. Ini bisa menjadi pendekatan yang lebih cepat, karena kami memiliki klausa WHERE yang cocok.
....
SELECT i.*
FROM InvoiceTable i, DatesCTE t
where i.INVOICE_DATE = t.CalendarDate
OPTION (MAXRECURSION 1000);
Kesimpulan
Secara keseluruhan, kami telah membahas secara singkat dan menunjukkan cara mengubah kueri kompleks menjadi kueri terstruktur CTE. Saat kueri dipecah menjadi bagian CTE yang berbeda, Anda dapat menggunakannya di bagian lain dan memanggil secara independen dalam kueri SQL akhir untuk tujuan debugging.
Poin penting lainnya adalah bahwa menggunakan CTE membuatnya lebih mudah untuk men-debug kueri kompleks ketika dipecah menjadi bagian-bagian yang dapat dikelola, untuk mengembalikan kumpulan hasil yang benar dan diharapkan. Penting untuk disadari bahwa menjalankan penjelasan pada setiap bagian kueri dan seluruh kueri sangat penting untuk memastikan bahwa kueri dan DBMS berjalan seoptimal mungkin.
Saya juga telah mengilustrasikan penulisan kueri/bagian CTE rekursif yang kuat dalam menghasilkan data dengan cepat untuk digunakan lebih lanjut dalam kueri.
Khususnya, saat menulis kueri rekursif, berhati-hatilah untuk TIDAK melupakan jalan keluar yang aman dari kegagalan . Pastikan untuk memeriksa ulang perhitungan yang digunakan dalam exit fail-safe untuk menghasilkan sinyal berhenti dan/atau gunakan maxrecursion opsi yang disediakan SQLServer.
Demikian pula, DBMS lain dapat menggunakan cte_max_recursion_depth (MySQL 8.0) atau max_recursive_iterations (MariaDB 10.3) sebagai pintu keluar tambahan yang aman dari kegagalan.
Baca Juga
Semua yang Perlu Anda Ketahui Tentang SQL CTE di Satu Tempat