Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Dasar-dasar ekspresi tabel, Bagian 5 – CTE, pertimbangan logis

Artikel ini adalah bagian kelima dari seri tentang ekspresi tabel. Di Bagian 1 saya memberikan latar belakang untuk ekspresi tabel. Di Bagian 2, Bagian 3, dan Bagian 4, saya membahas aspek logis dan optimasi dari tabel turunan. Bulan ini saya memulai liputan ekspresi tabel umum (CTE). Seperti tabel turunan, pertama-tama saya akan membahas perlakuan logis CTE, dan di masa mendatang saya akan membahas pertimbangan pengoptimalan.

Dalam contoh saya, saya akan menggunakan database sampel yang disebut TSQLV5. Anda dapat menemukan skrip yang membuat dan mengisinya di sini, dan diagram ER-nya di sini.

CTE

Mari kita mulai dengan istilah ekspresi tabel umum . Baik istilah ini, maupun akronimnya CTE, tidak muncul dalam spesifikasi standar ISO/IEC SQL. Jadi bisa jadi istilah tersebut berasal dari salah satu produk database dan kemudian diadopsi oleh beberapa vendor database lainnya. Anda dapat menemukannya di dokumentasi Microsoft SQL Server dan Azure SQL Database. T-SQL mendukungnya dimulai dengan SQL Server 2005. Standar ini menggunakan istilah ekspresi kueri untuk mewakili ekspresi yang mendefinisikan satu atau lebih CTE, termasuk kueri luar. Ini menggunakan istilah dengan elemen daftar untuk mewakili apa yang disebut T-SQL sebagai CTE. Saya akan segera memberikan sintaks untuk ekspresi kueri.

Selain sumber istilah, ekspresi tabel umum , atau CTE , adalah istilah yang umum digunakan oleh praktisi T-SQL untuk struktur yang menjadi fokus artikel ini. Jadi pertama-tama, mari kita bahas apakah itu istilah yang tepat. Kami telah menyimpulkan bahwa istilah ekspresi tabel sesuai untuk ekspresi yang secara konseptual mengembalikan tabel. Tabel turunan, CTE, tampilan, dan fungsi bernilai tabel sebaris adalah semua jenis ekspresi tabel bernama yang didukung T-SQL. Jadi, ekspresi tabel bagian dari ekspresi tabel umum tentu tampaknya sesuai. Adapun umum bagian dari istilah, itu mungkin ada hubungannya dengan salah satu keunggulan desain CTE dibandingkan tabel turunan. Ingatlah bahwa Anda tidak dapat menggunakan kembali nama tabel turunan (atau lebih tepatnya nama variabel rentang) lebih dari sekali dalam kueri luar. Sebaliknya, nama CTE dapat digunakan beberapa kali dalam kueri luar. Dengan kata lain, nama CTE adalah umum ke kueri luar. Tentu saja, saya akan menunjukkan aspek desain ini di artikel ini.

CTE memberi Anda manfaat serupa dengan tabel turunan, termasuk memungkinkan pengembangan solusi modular, menggunakan kembali alias kolom, berinteraksi secara tidak langsung dengan fungsi jendela dalam klausa yang biasanya tidak mengizinkannya, mendukung modifikasi yang secara tidak langsung mengandalkan TOP atau OFFSET FETCH dengan spesifikasi pesanan, dan lain-lain. Namun ada keunggulan desain tertentu dibandingkan dengan tabel turunan, yang akan saya bahas secara mendetail setelah saya memberikan sintaks untuk strukturnya.

Sintaks

Berikut sintaks standar untuk ekspresi kueri:

7.17


Fungsi
Tentukan tabel.


Format
::=
[ ]
[ ] [ ] [ ]
::=WITH [ RECURSIVE ]
::= [ { }… ]
::=
[ ]
SEBAGAI [ ]
::=
::=

| UNION [ SEMUA | BERBEDA ]
[ ]
| KECUALI [ SEMUA | BERBEDA ]
[ ]
::=

| INTERSECT [ SEMUA | BERBEDA ]
[ ]
::=

|
[ ] [ ] [ ]

::=
| |
::=TABEL
::=
SESUAI [ OLEH ]
::=
::=ORDER BY
::=OFFSET { ROW | BARIS }
::=
FETCH { PERTAMA | BERIKUTNYA } [ ] { BARIS | BARIS } { HANYA | WITH TIES }
::=

|
::=
::=
::= PERCENT


7.18


Fungsi
Menentukan pembuatan informasi pengurutan dan deteksi siklus dalam hasil ekspresi kueri rekursif.


Format
::=
| |
::=
SEARCH SET
::=
KEDALAMAN PERTAMA OLEH | LUAS PERTAMA OLEH
::=
::=
CYCLE SET UNTUK
DEFAULT MENGGUNAKAN
::= [ { }… ]
::=
::=
::=
::=
::=


7.3


Fungsi
Tentukan satu set s yang akan dibangun menjadi sebuah tabel.


Format
::=NILAI
::=
[ { }… ]
::=
VALUES
::=

[ { }… ]

Istilah standar ekspresi kueri mewakili ekspresi yang melibatkan klausa WITH, dengan daftar , yang terbuat dari satu atau lebih dengan elemen daftar , dan kueri luar. T-SQL mengacu pada standar dengan elemen daftar sebagai CTE.

T-SQL tidak mendukung semua elemen sintaks standar. Misalnya, ini tidak mendukung beberapa elemen kueri rekursif lanjutan yang memungkinkan Anda mengontrol arah penelusuran dan menangani siklus dalam struktur grafik. Kueri rekursif adalah fokus artikel bulan depan.

Berikut sintaks T-SQL untuk kueri yang disederhanakan terhadap CTE:

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
SELECT < select list >
FROM < table name >;

Berikut ini contoh kueri sederhana terhadap CTE yang mewakili pelanggan AS:

WITH UC AS
(
  SELECT custid, companyname
  FROM Sales.Customers
  WHERE country = N'USA'
)
SELECT custid, companyname
FROM UC;

Anda akan menemukan tiga bagian yang sama dalam pernyataan terhadap CTE seperti yang Anda lakukan dengan pernyataan terhadap tabel turunan:

  1. Ekspresi tabel (kueri dalam)
  2. Nama yang ditetapkan untuk ekspresi tabel (nama variabel rentang)
  3. Permintaan luar

Apa yang berbeda tentang desain CTE dibandingkan dengan tabel turunan adalah di mana dalam kode ketiga elemen ini berada. Dengan tabel turunan, kueri dalam disarangkan di dalam klausa FROM kueri luar, dan nama ekspresi tabel ditetapkan setelah ekspresi tabel itu sendiri. Elemen-elemennya semacam saling terkait. Sebaliknya, dengan CTE, kode memisahkan tiga elemen:pertama Anda menetapkan nama ekspresi tabel; kedua Anda menentukan ekspresi tabel—dari awal hingga akhir tanpa interupsi; ketiga Anda menentukan kueri luar—dari awal hingga akhir tanpa interupsi. Nanti, di bagian “Pertimbangan desain”, saya akan menjelaskan implikasi dari perbedaan desain ini.

Sepatah kata tentang CTE dan penggunaan titik koma sebagai terminator pernyataan. Sayangnya, tidak seperti SQL standar, T-SQL tidak memaksa Anda untuk mengakhiri semua pernyataan dengan titik koma. Namun, ada sangat sedikit kasus di T-SQL di mana tanpa terminator kodenya ambigu. Dalam kasus tersebut, penghentian adalah wajib. Salah satu kasus tersebut menyangkut fakta bahwa klausa WITH digunakan untuk berbagai tujuan. Salah satunya adalah untuk mendefinisikan CTE, yang lain adalah untuk mendefinisikan petunjuk tabel untuk kueri, dan ada beberapa kasus penggunaan tambahan. Sebagai contoh, dalam pernyataan berikut, klausa WITH digunakan untuk memaksa tingkat isolasi serial dengan petunjuk tabel:

SELECT custid, country FROM Sales.Customers WITH (SERIALIZABLE);

Potensi ambiguitas adalah ketika Anda memiliki pernyataan yang tidak diakhiri sebelum definisi CTE, dalam hal ini pengurai mungkin tidak dapat mengetahui apakah klausa WITH termasuk dalam pernyataan pertama atau kedua. Berikut ini contoh yang menunjukkan hal ini:

SELECT custid, country FROM Sales.Customers
 
WITH UC AS
(
  SELECT custid, companyname
  FROM Sales.Customers
  WHERE country = N'USA'
)
SELECT custid, companyname
FROM UC

Di sini parser tidak dapat menentukan apakah klausa WITH seharusnya digunakan untuk mendefinisikan petunjuk tabel untuk tabel Pelanggan dalam pernyataan pertama, atau memulai definisi CTE. Anda mendapatkan kesalahan berikut:

Msg 336, Level 15, State 1, Line 159
Sintaks salah di dekat 'UC'. Jika ini dimaksudkan sebagai ekspresi tabel yang umum, Anda harus secara eksplisit mengakhiri pernyataan sebelumnya dengan titik koma.

Perbaikannya tentu saja untuk mengakhiri pernyataan sebelum definisi CTE, tetapi sebagai praktik terbaik, Anda benar-benar harus menghentikan semua pernyataan Anda:

SELECT custid, country FROM Sales.Customers;
 
WITH UC AS
(
  SELECT custid, companyname
  FROM Sales.Customers
  WHERE country = N'USA'
)
SELECT custid, companyname
FROM UC;

Anda mungkin telah memperhatikan bahwa beberapa orang memulai definisi CTE mereka dengan titik koma sebagai praktik, seperti:

;WITH UC AS
(
  SELECT custid, companyname
  FROM Sales.Customers
  WHERE country = N'USA'
)
SELECT custid, companyname
FROM UC;

Inti dari praktik ini adalah untuk mengurangi potensi kesalahan di masa mendatang. Bagaimana jika di kemudian hari seseorang menambahkan pernyataan yang tidak diakhiri tepat sebelum definisi CTE Anda dalam skrip, dan tidak repot-repot memeriksa skrip lengkap, melainkan hanya pernyataan mereka? Titik koma Anda tepat sebelum klausa WITH secara efektif menjadi terminator pernyataan mereka. Anda tentu dapat melihat kepraktisan dari latihan ini, tetapi ini agak tidak wajar. Apa yang direkomendasikan, meskipun lebih sulit untuk dicapai, adalah menanamkan praktik pemrograman yang baik dalam organisasi, termasuk penghentian semua pernyataan.

Dalam hal aturan sintaks yang berlaku untuk ekspresi tabel yang digunakan sebagai kueri dalam dalam definisi CTE, aturan tersebut sama dengan aturan yang berlaku untuk ekspresi tabel yang digunakan sebagai kueri dalam dalam definisi tabel turunan. Yaitu:

  • Semua kolom ekspresi tabel harus memiliki nama
  • Semua nama kolom ekspresi tabel harus unik
  • Baris ekspresi tabel tidak memiliki urutan

Untuk detailnya, lihat bagian “Ekspresi tabel adalah tabel” di Bagian 2 seri ini.

Pertimbangan desain

Jika Anda mensurvei pengembang T-SQL yang berpengalaman tentang apakah mereka lebih suka menggunakan tabel turunan atau CTE, tidak semua orang akan setuju mana yang lebih baik. Secara alami, orang yang berbeda memiliki preferensi gaya yang berbeda. Saya terkadang menggunakan tabel turunan dan terkadang CTE. Ada baiknya jika Anda secara sadar mengidentifikasi perbedaan desain bahasa tertentu antara kedua alat tersebut, dan memilih berdasarkan prioritas Anda dalam setiap solusi yang diberikan. Dengan waktu dan pengalaman, Anda membuat pilihan dengan lebih intuitif.

Selain itu, penting untuk tidak membingungkan penggunaan ekspresi tabel dan tabel sementara, tetapi itu adalah diskusi terkait kinerja yang akan saya bahas di artikel mendatang.

CTE memiliki kemampuan kueri rekursif dan tabel turunan tidak. Jadi, jika Anda perlu mengandalkan itu, Anda tentu akan menggunakan CTE. Kueri rekursif adalah fokus artikel bulan depan.

Di Bagian 2 saya menjelaskan bahwa saya melihat penggabungan tabel turunan sebagai penambahan kompleksitas pada kode, karena membuatnya sulit untuk mengikuti logika. Saya memberikan contoh berikut, mengidentifikasi tahun pemesanan di mana lebih dari 70 pelanggan melakukan pemesanan:

SELECT orderyear, numcusts
FROM ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
         FROM ( SELECT YEAR(orderdate) AS orderyear, custid
                FROM Sales.Orders ) AS D1
         GROUP BY orderyear ) AS D2
  WHERE numcusts > 70;

CTE tidak mendukung bersarang. Jadi, saat Anda meninjau atau memecahkan masalah solusi berdasarkan CTE, Anda tidak tersesat dalam logika bersarang. Alih-alih bersarang, Anda membangun lebih banyak solusi modular dengan mendefinisikan beberapa CTE di bawah pernyataan WITH yang sama, dipisahkan dengan koma. Setiap CTE didasarkan pada kueri yang ditulis dari awal hingga akhir tanpa interupsi. Saya melihatnya sebagai hal yang baik dari perspektif kejelasan kode dan pemeliharaan.

Berikut solusi untuk tugas yang disebutkan di atas menggunakan CTE:

WITH C1 AS
(
  SELECT YEAR(orderdate) AS orderyear, custid
  FROM Sales.Orders
),
C2 AS
(
  SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
  FROM C1
  GROUP BY orderyear
)
SELECT orderyear, numcusts
FROM C2
WHERE numcusts > 70;

Saya lebih suka solusi berbasis CTE. Tetapi sekali lagi, tanyakan kepada pengembang berpengalaman yang mana dari dua solusi di atas yang mereka sukai, dan mereka tidak akan semua setuju. Beberapa sebenarnya lebih menyukai logika bersarang, dan dapat melihat semuanya di satu tempat.

Satu keuntungan yang sangat jelas dari CTE dibandingkan tabel turunan, adalah ketika Anda perlu berinteraksi dengan beberapa instance dari ekspresi tabel yang sama dalam solusi Anda. Ingat contoh berikut berdasarkan tabel turunan dari Bagian 2 dalam seri:

SELECT CUR.orderyear, CUR.numorders,
  CUR.numorders - PRV.numorders AS diff
FROM ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders
         FROM Sales.Orders
         GROUP BY YEAR(orderdate) ) AS CUR
    LEFT OUTER JOIN
       ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders
         FROM Sales.Orders
         GROUP BY YEAR(orderdate) ) AS PRV
      ON CUR.orderyear = PRV.orderyear + 1;

Solusi ini mengembalikan tahun pesanan, jumlah pesanan per tahun, dan perbedaan antara jumlah tahun ini dan tahun sebelumnya. Ya, Anda dapat melakukannya dengan lebih mudah dengan fungsi LAG, tetapi fokus saya di sini bukanlah menemukan cara terbaik untuk mencapai tugas yang sangat spesifik ini. Saya menggunakan contoh ini untuk mengilustrasikan aspek desain bahasa tertentu dari ekspresi tabel bernama.

Masalah dengan solusi ini adalah Anda tidak dapat menetapkan nama ke ekspresi tabel dan menggunakannya kembali dalam langkah pemrosesan kueri logis yang sama. Anda memberi nama tabel turunan setelah ekspresi tabel itu sendiri dalam klausa FROM. Jika Anda mendefinisikan dan memberi nama tabel turunan sebagai input pertama dari gabungan, Anda juga tidak dapat menggunakan kembali nama tabel turunan itu sebagai input kedua dari gabungan yang sama. Jika Anda perlu menggabungkan sendiri dua instance dari ekspresi tabel yang sama, dengan tabel turunan Anda tidak punya pilihan selain menduplikasi kode. Itulah yang Anda lakukan dalam contoh di atas. Sebaliknya, nama CTE ditetapkan sebagai elemen pertama dari kode di antara tiga yang disebutkan di atas (nama CTE, kueri dalam, kueri luar). Dalam istilah pemrosesan kueri logis, pada saat Anda masuk ke kueri luar, nama CTE sudah ditentukan dan tersedia. Ini berarti Anda dapat berinteraksi dengan beberapa contoh nama CTE di kueri luar, seperti:

WITH OrdCount AS
(
  SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders
  FROM Sales.Orders
  GROUP BY YEAR(orderdate)
)
SELECT CUR.orderyear, CUR.numorders,
  CUR.numorders - PRV.numorders AS diff
FROM OrdCount AS CUR
  LEFT OUTER JOIN OrdCount AS PRV
    ON CUR.orderyear = PRV.orderyear + 1;

Solusi ini memiliki keunggulan programabilitas yang jelas dibandingkan solusi yang didasarkan pada tabel turunan karena Anda tidak perlu mempertahankan dua salinan dari ekspresi tabel yang sama. Masih banyak yang bisa dikatakan tentangnya dari perspektif pemrosesan fisik, dan membandingkannya dengan penggunaan tabel sementara, tetapi saya akan melakukannya di artikel mendatang yang berfokus pada kinerja.

Satu keuntungan bahwa kode berdasarkan tabel turunan dibandingkan dengan kode berdasarkan CTE berkaitan dengan properti penutupan yang seharusnya dimiliki oleh ekspresi tabel. Ingat bahwa properti penutupan dari ekspresi relasional mengatakan bahwa baik input maupun output adalah relasi, dan oleh karena itu ekspresi relasional dapat digunakan di mana suatu relasi diharapkan, sebagai input ke ekspresi relasional lainnya. Demikian pula, ekspresi tabel mengembalikan tabel dan seharusnya tersedia sebagai tabel input untuk ekspresi tabel lain. Ini berlaku untuk kueri yang didasarkan pada tabel turunan—Anda bisa menggunakannya di tempat yang diharapkan memiliki tabel. Misalnya, Anda bisa menggunakan kueri yang didasarkan pada tabel turunan sebagai kueri dalam dari definisi CTE, seperti dalam contoh berikut:

WITH C AS
(
  SELECT orderyear, numcusts
  FROM ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
         FROM ( SELECT YEAR(orderdate) AS orderyear, custid
                FROM Sales.Orders ) AS D1
         GROUP BY orderyear ) AS D2
  WHERE numcusts > 70
)
SELECT orderyear, numcusts
FROM C;

Namun, hal yang sama tidak berlaku untuk kueri yang didasarkan pada CTE. Meskipun secara konseptual dianggap sebagai ekspresi tabel, Anda tidak dapat menggunakannya sebagai kueri dalam dalam definisi tabel turunan, subkueri, dan CTE itu sendiri. Misalnya, kode berikut ini tidak valid di T-SQL:

SELECT orderyear, custid
FROM (WITH C1 AS
      (
        SELECT YEAR(orderdate) AS orderyear, custid
        FROM Sales.Orders
      ),
      C2 AS
      (
        SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
        FROM C1
        GROUP BY orderyear
      )
      SELECT orderyear, numcusts
      FROM C2
      WHERE numcusts > 70) AS D;

Kabar baiknya adalah Anda dapat menggunakan kueri yang didasarkan pada CTE sebagai kueri dalam dalam tampilan dan fungsi bernilai tabel sebaris, yang akan saya bahas di artikel mendatang.

Juga, ingat, Anda selalu dapat menentukan CTE lain berdasarkan kueri terakhir, dan kemudian meminta kueri terluar berinteraksi dengan CTE itu:

WITH C1 AS
(
  SELECT YEAR(orderdate) AS orderyear, custid
  FROM Sales.Orders
),
C2 AS
(
  SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
  FROM C1
  GROUP BY orderyear
),
C3 AS
(
  SELECT orderyear, numcusts
  FROM C2
  WHERE numcusts &gt; 70
)
SELECT orderyear, numcusts
FROM C3;

Dari sudut pandang pemecahan masalah, seperti yang disebutkan, saya biasanya lebih mudah mengikuti logika kode yang didasarkan pada CTE, dibandingkan dengan kode yang didasarkan pada tabel turunan. Namun, solusi berdasarkan tabel turunan memiliki keuntungan karena Anda dapat menyorot setiap level bersarang dan menjalankannya secara independen, seperti yang ditunjukkan pada Gambar 1.

Gambar 1:Dapat menyorot dan menjalankan bagian kode dengan tabel turunan

Dengan CTE, segalanya menjadi lebih rumit. Agar kode yang melibatkan CTE dapat dijalankan, kode tersebut harus dimulai dengan klausa WITH, diikuti oleh satu atau lebih ekspresi tabel yang diberi tanda kurung yang dipisahkan dengan koma, diikuti oleh kueri yang tidak diberi tanda kurung tanpa koma sebelumnya. Anda dapat menyorot dan menjalankan salah satu kueri dalam yang benar-benar mandiri, serta kode solusi lengkapnya; namun, Anda tidak dapat menyorot dan berhasil menjalankan bagian perantara lainnya dari solusi. Misalnya, Gambar 2 menunjukkan upaya yang gagal untuk menjalankan kode yang mewakili C2.

Gambar 2:Tidak dapat menyorot dan menjalankan bagian kode dengan CTE

Jadi dengan CTE, Anda harus menggunakan cara yang agak canggung agar dapat memecahkan masalah langkah menengah dari solusi. Misalnya, satu solusi umum adalah untuk sementara menyuntikkan kueri SELECT * FROM your_cte tepat di bawah CTE yang relevan. Anda kemudian menyorot dan menjalankan kode termasuk kueri yang disuntikkan, dan setelah selesai, Anda menghapus kueri yang disuntikkan. Gambar 3 menunjukkan teknik ini.

Gambar 3:Inject SELECT * di bawah CTE yang relevan

Masalahnya adalah setiap kali Anda membuat perubahan pada kode—bahkan perubahan kecil sementara seperti di atas—ada kemungkinan ketika Anda mencoba untuk kembali ke kode asli, Anda akan menemukan bug baru.

Opsi lainnya adalah menata kode Anda sedikit berbeda, sehingga setiap definisi CTE bukan pertama dimulai dengan baris kode terpisah yang terlihat seperti ini:

, cte_name AS (

Kemudian, kapan pun Anda ingin menjalankan bagian perantara dari kode ke CTE tertentu, Anda dapat melakukannya dengan sedikit perubahan pada kode Anda. Dengan menggunakan komentar baris, Anda hanya mengomentari satu baris kode yang sesuai dengan CTE itu. Anda kemudian menyorot dan menjalankan kode ke dan memasukkan kueri dalam CTE itu, yang sekarang dianggap sebagai kueri terluar, seperti yang diilustrasikan pada Gambar 4.

Gambar 4:Atur ulang sintaks untuk mengaktifkan komentar satu baris kode

Jika Anda tidak puas dengan gaya ini, Anda masih memiliki pilihan lain. Anda dapat menggunakan komentar blok yang dimulai tepat sebelum koma yang mendahului CTE yang diinginkan dan berakhir setelah kurung buka, seperti yang diilustrasikan pada Gambar 5.

Gambar 5:Gunakan komentar blokir

Itu bermuara pada preferensi pribadi. Saya biasanya menggunakan teknik kueri SELECT * yang disuntikkan sementara.

Konstruktor nilai tabel

Ada batasan tertentu dalam dukungan T-SQL untuk konstruktor nilai tabel dibandingkan dengan standar. Jika Anda tidak terbiasa dengan konstruksinya, pastikan untuk memeriksa Bagian 2 dalam seri ini terlebih dahulu, di mana saya menjelaskannya secara rinci. Sementara T-SQL memungkinkan Anda untuk mendefinisikan tabel turunan berdasarkan konstruktor nilai tabel, T-SQL tidak memungkinkan Anda untuk mendefinisikan CTE berdasarkan konstruktor nilai tabel.

Berikut adalah contoh yang didukung yang menggunakan tabel turunan:

SELECT custid, companyname, contractdate
FROM ( VALUES( 2, 'Cust 2', '20200212' ),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);

Sayangnya, kode serupa yang menggunakan CTE tidak didukung:

WITH MyCusts(custid, companyname, contractdate) AS
(
  VALUES( 2, 'Cust 2', '20200212' ),
        ( 3, 'Cust 3', '20200118' ),
        ( 5, 'Cust 5', '20200401' )
)
SELECT custid, companyname, contractdate
FROM MyCusts;

Kode ini menghasilkan kesalahan berikut:

Msg 156, Level 15, State 1, Line 337
Sintaks salah di dekat kata kunci 'VALUES'.

Ada beberapa solusi, meskipun. Salah satunya adalah dengan menggunakan kueri terhadap tabel turunan, yang pada gilirannya didasarkan pada konstruktor nilai tabel, sebagai kueri dalam CTE, seperti:

WITH MyCusts AS
(
  SELECT *
  FROM ( VALUES( 2, 'Cust 2', '20200212' ),
               ( 3, 'Cust 3', '20200118' ),
               ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate)
)
SELECT custid, companyname, contractdate
FROM MyCusts;

Cara lainnya adalah menggunakan teknik yang digunakan orang sebelum konstruktor bernilai tabel diperkenalkan ke T-SQL—menggunakan serangkaian kueri FROMless yang dipisahkan oleh operator UNION ALL, seperti:

WITH MyCusts(custid, companyname, contractdate) AS
(
            SELECT 2, 'Cust 2', '20200212'
  UNION ALL SELECT 3, 'Cust 3', '20200118'
  UNION ALL SELECT 5, 'Cust 5', '20200401'
)
SELECT custid, companyname, contractdate
FROM MyCusts;

Perhatikan bahwa alias kolom diberikan tepat setelah nama CTE.

Kedua metode ini dialjabar dan dioptimalkan sama, jadi gunakan mana saja yang lebih nyaman bagi Anda.

Membuat urutan angka

Alat yang cukup sering saya gunakan dalam solusi saya adalah tabel angka tambahan. Salah satu opsi adalah membuat tabel angka aktual di database Anda dan mengisinya dengan urutan berukuran wajar. Lain adalah untuk mengembangkan solusi yang menghasilkan urutan angka dengan cepat. Untuk opsi terakhir, Anda ingin input menjadi pembatas dari rentang yang diinginkan (kami akan menyebutnya @low dan @high ). Anda ingin solusi Anda mendukung rentang yang berpotensi besar. Inilah solusi saya untuk tujuan ini, menggunakan CTE, dengan permintaan untuk rentang 1001 hingga 1010 dalam contoh khusus ini:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
WITH
  L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1)) AS D(c) ),
  L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
  L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
  L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
  L4 AS ( SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B ),
  L5 AS ( SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B ),
  Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
            FROM L5 )
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM Nums
ORDER BY rownum;

Kode ini menghasilkan output berikut:

n
-----
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010

CTE pertama yang disebut L0 didasarkan pada konstruktor nilai tabel dengan dua baris. Nilai aktual di sana tidak signifikan; yang penting ada dua baris. Kemudian, ada urutan lima CTE tambahan bernama L1 hingga L5, masing-masing menerapkan gabungan silang antara dua contoh CTE sebelumnya. Kode berikut menghitung jumlah baris yang berpotensi dihasilkan oleh masing-masing CTE, di mana @L adalah nomor level CTE:

DECLARE @L AS INT = 5;
 
SELECT POWER(2., POWER(2., @L));

Berikut adalah angka yang Anda dapatkan untuk setiap CTE:

CTE Kardinalitas
L0 2
L1 4
L2 16
L3 256
L4 65.536
L5 4.294.967.296

Naik ke level 5 memberi Anda lebih dari empat miliar baris. Ini seharusnya cukup untuk kasus penggunaan praktis apa pun yang dapat saya pikirkan. Langkah selanjutnya terjadi di CTE yang disebut Nums. Anda menggunakan fungsi ROW_NUMBER untuk menghasilkan urutan bilangan bulat yang dimulai dengan 1 berdasarkan urutan yang tidak ditentukan (ORDER BY (SELECT NULL)), dan beri nama kolom hasil rownum. Terakhir, kueri luar menggunakan filter TOP berdasarkan urutan rownum untuk memfilter sebanyak mungkin bilangan sesuai dengan kardinalitas urutan yang diinginkan (@tinggi – @rendah + 1), dan menghitung nomor hasil n sebagai @rendah + rownum – 1.

Di sini Anda dapat benar-benar menghargai keindahan dalam desain CTE dan penghematan yang dimungkinkan saat Anda membangun solusi dengan cara modular. Pada akhirnya, proses unnesting membongkar 32 tabel, masing-masing terdiri dari dua baris berdasarkan konstanta. Hal ini dapat dilihat dengan jelas pada rencana eksekusi untuk kode ini, seperti yang ditunjukkan pada Gambar 6 menggunakan SentryOne Plan Explorer.

Gambar 6:Rencana kueri yang menghasilkan urutan angka

Setiap operator Pemindaian Konstan mewakili tabel konstanta dengan dua baris. Masalahnya, operator Top adalah yang meminta baris-baris itu, dan korsleting setelah mendapat nomor yang diinginkan. Perhatikan 10 baris yang ditunjukkan di atas panah yang mengalir ke operator Top.

Saya tahu bahwa fokus artikel ini adalah perlakuan konseptual CTE dan bukan pertimbangan fisik/kinerja, tetapi dengan melihat rencananya, Anda dapat benar-benar menghargai singkatnya kode dibandingkan dengan bertele-tele dari apa yang diterjemahkan ke belakang layar.

Dengan menggunakan tabel turunan, Anda sebenarnya dapat menulis solusi yang menggantikan setiap referensi CTE dengan kueri dasar yang diwakilinya. Apa yang Anda dapatkan cukup menakutkan:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
       FROM ( SELECT 1 AS C
              FROM ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
 
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D7
                CROSS JOIN
                   ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D8 ) AS D9
         CROSS JOIN
            ( SELECT 1 AS C
              FROM ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D7
                CROSS JOIN
                   ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D8 ) AS D10 ) AS Nums
ORDER BY rownum;

Obviously, you don’t want to write a solution like this, but it’s a good way to illustrate what SQL Server does behind the scenes with your CTE code.

If you were really planning to write a solution based on derived tables, instead of using the above nested approach, you’d be better off simplifying the logic to a single query with 31 cross joins between 32 table value constructors, each based on two rows, like so:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
       FROM         (VALUES(1),(1)) AS D01(c)
         CROSS JOIN (VALUES(1),(1)) AS D02(c)
         CROSS JOIN (VALUES(1),(1)) AS D03(c)
         CROSS JOIN (VALUES(1),(1)) AS D04(c)
         CROSS JOIN (VALUES(1),(1)) AS D05(c)
         CROSS JOIN (VALUES(1),(1)) AS D06(c)
         CROSS JOIN (VALUES(1),(1)) AS D07(c)
         CROSS JOIN (VALUES(1),(1)) AS D08(c)
         CROSS JOIN (VALUES(1),(1)) AS D09(c)
         CROSS JOIN (VALUES(1),(1)) AS D10(c)
         CROSS JOIN (VALUES(1),(1)) AS D11(c)
         CROSS JOIN (VALUES(1),(1)) AS D12(c)
         CROSS JOIN (VALUES(1),(1)) AS D13(c)
         CROSS JOIN (VALUES(1),(1)) AS D14(c)
         CROSS JOIN (VALUES(1),(1)) AS D15(c)
         CROSS JOIN (VALUES(1),(1)) AS D16(c)
         CROSS JOIN (VALUES(1),(1)) AS D17(c)
         CROSS JOIN (VALUES(1),(1)) AS D18(c)
         CROSS JOIN (VALUES(1),(1)) AS D19(c)
         CROSS JOIN (VALUES(1),(1)) AS D20(c)
         CROSS JOIN (VALUES(1),(1)) AS D21(c)
         CROSS JOIN (VALUES(1),(1)) AS D22(c)
         CROSS JOIN (VALUES(1),(1)) AS D23(c)
         CROSS JOIN (VALUES(1),(1)) AS D24(c)
         CROSS JOIN (VALUES(1),(1)) AS D25(c)
         CROSS JOIN (VALUES(1),(1)) AS D26(c)
         CROSS JOIN (VALUES(1),(1)) AS D27(c)
         CROSS JOIN (VALUES(1),(1)) AS D28(c)
         CROSS JOIN (VALUES(1),(1)) AS D29(c)
         CROSS JOIN (VALUES(1),(1)) AS D30(c)
         CROSS JOIN (VALUES(1),(1)) AS D31(c)
         CROSS JOIN (VALUES(1),(1)) AS D32(c) ) AS Nums
ORDER BY rownum;

Still, the solution based on CTEs is obviously significantly simpler. The plans are identical.

Used in modification statements

CTEs can be used as the source and target tables in INSERT, UPDATE, DELETE and MERGE statements. They cannot be used in the TRUNCATE statement.

The syntax is pretty straightforward. You start the statement as usual with a WITH clause, followed by one or more CTEs separated by commas. Then you specify the outer modification statement, which interacts with the CTEs that were defined under the WITH clause as the source tables, target table, or both. Just like I explained in Part 2 about derived tables, also with CTEs what really gets modified is the underlying base table that the table expression uses. I’ll show a couple of examples using DELETE and UPDATE statements, but remember that you can use CTEs in MERGE and INSERT statements as well.

Here’s the general syntax of a DELETE statement against a CTE:

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
DELETE [ FROM ] <table name>
[ WHERE <filter predicate> ];

As an example (don’t actually run it), the following code deletes the 10 oldest orders:

WITH OldestOrders AS
(
  SELECT TOP (10) *
  FROM Sales.Orders
  ORDER BY orderdate, orderid
)
DELETE FROM OldestOrders;

Here’s the general syntax of an UPDATE statement against a CTE:

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
UPDATE <table name>
  SET <assignments>
[ WHERE <filter predicate> ];

As an example, the following code updates the 10 oldest unshipped orders that have an overdue required date, increasing the required date to 10 days from today:

BEGIN TRAN;
 
WITH OldestUnshippedOrders AS
(
  SELECT TOP (10) orderid, requireddate,
    DATEADD(day, 10, CAST(SYSDATETIME() AS DATE)) AS newrequireddate
  FROM Sales.Orders
  WHERE shippeddate IS NULL
    AND requireddate &lt; CAST(SYSDATETIME() AS DATE)
  ORDER BY orderdate, orderid
)
UPDATE OldestUnshippedOrders
  SET requireddate = newrequireddate
    OUTPUT
      inserted.orderid,
      deleted.requireddate AS oldrequireddate,
      inserted.requireddate AS newrequireddate;
 
ROLLBACK TRAN;

The code applies the update in a transaction that it then rolls back so that the change won’t stick.

This code generates the following output, showing both the old and the new required dates:

orderid     oldrequireddate newrequireddate
----------- --------------- ---------------
11008       2019-05-06      2020-07-16
11019       2019-05-11      2020-07-16
11039       2019-05-19      2020-07-16
11040       2019-05-20      2020-07-16
11045       2019-05-21      2020-07-16
11051       2019-05-25      2020-07-16
11054       2019-05-26      2020-07-16
11058       2019-05-27      2020-07-16
11059       2019-06-10      2020-07-16
11061       2019-06-11      2020-07-16

(10 rows affected)

Of course you will get a different new required date based on when you run this code.

Ringkasan

I like CTEs. They have a few advantages compared to derived tables. Instead of nesting the code, you define multiple CTEs separated by commas, typically leading to a more modular solution that is easier to review and maintain. Also, you can have multiple references to the same CTE name in the outer statement, so you don’t need to repeat the inner table expression’s code. However, unlike derived tables, CTEs cannot be defined directly based on a table value constructor, and you cannot highlight and execute some of the intermediate parts of the code. The following table summarizes the differences between derived tables and CTEs:

Item Derived table CTE Supports nesting Yes No Supports multiple references No Yes Supports table value constructor Yes No Can highlight and run part of code Yes No Supports recursion No Yes

As the last item says, derived tables do not support recursive capabilities, whereas CTEs do. Recursive queries are the focus of next month’s article.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Mitos bahwa DROP dan TRUNCATE TABLE adalah Non-Logged

  2. Bukan Anda, ini saya (pemecahan masalah I/O)

  3. Cara menggunakan Prisma

  4. Model Data Aplikasi Pelatihan Marathon

  5. Lembar Cheat SQL:Apa itu SQL, Perintah SQL, dan Injeksi SQL