Artikel ini adalah bagian kesebelas dalam seri tentang ekspresi tabel. Sejauh ini, saya telah membahas tabel turunan dan CTE, dan baru-baru ini memulai cakupan tampilan. Di Bagian 9 saya membandingkan tampilan dengan tabel turunan dan CTE, dan di Bagian 10 saya membahas perubahan DDL dan implikasi penggunaan SELECT * dalam kueri dalam tampilan. Dalam artikel ini, saya fokus pada pertimbangan modifikasi.
Seperti yang mungkin Anda ketahui, Anda diizinkan untuk mengubah data dalam tabel dasar secara tidak langsung melalui ekspresi tabel bernama seperti tampilan. Anda dapat mengontrol izin modifikasi terhadap tampilan. Bahkan, Anda dapat memberikan izin kepada pengguna untuk mengubah data melalui tampilan tanpa memberi mereka izin untuk mengubah tabel pokok secara langsung.
Anda perlu menyadari kerumitan dan batasan tertentu yang berlaku untuk modifikasi melalui tampilan. Menariknya, beberapa modifikasi yang didukung dapat berakhir dengan hasil yang mengejutkan, terutama jika pengguna yang memodifikasi data tidak menyadari bahwa mereka sedang berinteraksi dengan tampilan. Anda dapat menerapkan batasan lebih lanjut untuk modifikasi melalui tampilan dengan menggunakan opsi yang disebut OPSI PERIKSA, yang akan saya bahas di artikel ini. Sebagai bagian dari cakupan, saya akan menjelaskan ketidakkonsistenan yang aneh antara cara CHECK OPTION dalam tampilan dan batasan CHECK dalam tabel menangani modifikasi—khususnya yang melibatkan NULL.
Contoh Data
Sebagai contoh data untuk artikel ini, saya akan menggunakan tabel yang disebut Orders dan OrderDetails. Gunakan kode berikut untuk membuat tabel ini di tempdb dan mengisinya dengan beberapa data sampel awal:
USE tempdb; GO DROP TABLE IF EXISTS dbo.OrderDetails, dbo.Orders; GO CREATE TABLE dbo.Orders ( orderid INT NOT NULL CONSTRAINT PK_Orders PRIMARY KEY, orderdate DATE NOT NULL, shippeddate DATE NULL ); INSERT INTO dbo.Orders(orderid, orderdate, shippeddate) VALUES(1, '20210802', '20210804'), (2, '20210802', '20210805'), (3, '20210804', '20210806'), (4, '20210826', NULL), (5, '20210827', NULL); CREATE TABLE dbo.OrderDetails ( orderid INT NOT NULL CONSTRAINT FK_OrderDetails_Orders REFERENCES dbo.Orders, productid INT NOT NULL, qty INT NOT NULL, unitprice NUMERIC(12, 2) NOT NULL, discount NUMERIC(5, 4) NOT NULL, CONSTRAINT PK_OrderDetails PRIMARY KEY(orderid, productid) ); INSERT INTO dbo.OrderDetails(orderid, productid, qty, unitprice, discount) VALUES(1, 1001, 5, 10.50, 0.05), (1, 1004, 2, 20.00, 0.00), (2, 1003, 1, 52.99, 0.10), (3, 1001, 1, 10.50, 0.05), (3, 1003, 2, 54.99, 0.10), (4, 1001, 2, 10.50, 0.05), (4, 1004, 1, 20.30, 0.00), (4, 1005, 1, 30.10, 0.05), (5, 1003, 5, 54.99, 0.00), (5, 1006, 2, 12.30, 0.08);
Tabel Pesanan berisi header pesanan, dan tabel OrderDetails berisi baris pesanan. Pesanan yang belum terkirim memiliki NULL di kolom tanggal pengiriman. Jika Anda lebih suka desain yang tidak menggunakan NULL, Anda dapat menggunakan tanggal tertentu di masa mendatang untuk pesanan yang belum terkirim, seperti “99991231”.
PERIKSA OPSI
Untuk memahami keadaan di mana Anda ingin menggunakan OPSI PERIKSA sebagai bagian dari definisi tampilan, pertama-tama kami akan memeriksa apa yang dapat terjadi jika Anda tidak menggunakannya.
Kode berikut membuat tampilan yang disebut FastOrders yang mewakili pesanan yang dikirim dalam waktu tujuh hari sejak dibuat:
CREATE OR ALTER VIEW dbo.FastOrders AS SELECT orderid, orderdate, shippeddate FROM dbo.Orders WHERE DATEDIFF(day, orderdate, shippeddate) <= 7; GO
Gunakan kode berikut untuk menyisipkan melalui tampilan pesanan dikirim dua hari setelah ditempatkan:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(6, '20210805', '20210807');
Kueri tampilan:
SELECT * FROM dbo.FastOrders;
Anda mendapatkan output berikut, yang mencakup pesanan baru:
orderid orderdate shippeddate ----------- ---------- ----------- 1 2021-08-02 2021-08-04 2 2021-08-02 2021-08-05 3 2021-08-04 2021-08-06 6 2021-08-05 2021-08-07
Buat kueri tabel yang mendasarinya:
SELECT * FROM dbo.Orders;
Anda mendapatkan output berikut, yang mencakup pesanan baru:
orderid orderdate shippeddate ----------- ---------- ----------- 1 2021-08-02 2021-08-04 2 2021-08-02 2021-08-05 3 2021-08-04 2021-08-06 4 2021-08-26 NULL 5 2021-08-27 NULL 6 2021-08-05 2021-08-07
Baris dimasukkan ke dalam tabel dasar yang mendasarinya melalui tampilan.
Selanjutnya, masukkan melalui tampilan baris yang dikirimkan 10 hari setelah ditempatkan, yang bertentangan dengan filter kueri dalam tampilan:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(7, '20210805', '20210815');
Pernyataan berhasil diselesaikan, melaporkan satu baris terpengaruh.
Kueri tampilan:
SELECT * FROM dbo.FastOrders;
Anda mendapatkan output berikut, yang tidak termasuk pesanan baru:
orderid orderdate shippeddate ----------- ---------- ----------- 1 2021-08-02 2021-08-04 2 2021-08-02 2021-08-05 3 2021-08-04 2021-08-06 6 2021-08-05 2021-08-07
Jika Anda tahu FastOrders adalah tampilan, ini semua mungkin tampak masuk akal. Bagaimanapun, baris dimasukkan ke dalam tabel yang mendasarinya, dan itu tidak memenuhi filter kueri dalam tampilan. Tetapi jika Anda tidak menyadari bahwa FastOrders adalah tampilan dan bukan tabel dasar, perilaku ini akan tampak mengejutkan.
Buat kueri tabel Pesanan yang mendasarinya:
SELECT * FROM dbo.Orders;
Anda mendapatkan output berikut, yang mencakup pesanan baru:
orderid orderdate shippeddate ----------- ---------- ----------- 1 2021-08-02 2021-08-04 2 2021-08-02 2021-08-05 3 2021-08-04 2021-08-06 4 2021-08-26 NULL 5 2021-08-27 NULL 6 2021-08-05 2021-08-07 7 2021-08-05 2021-08-15
Anda bisa mengalami perilaku mengejutkan serupa jika Anda memperbarui melalui tampilan nilai tanggal pengiriman dalam baris yang saat ini menjadi bagian dari tampilan ke tanggal yang membuatnya tidak memenuhi syarat sebagai bagian dari tampilan lagi. Pembaruan seperti itu biasanya diperbolehkan, tetapi sekali lagi, itu terjadi di tabel dasar yang mendasarinya. Jika Anda menanyakan tampilan setelah pembaruan semacam itu, baris yang dimodifikasi tampaknya hilang. Dalam praktiknya, itu masih ada di tabel yang mendasarinya, hanya saja tidak dianggap sebagai bagian dari tampilan lagi.
Jalankan kode berikut untuk menghapus baris yang Anda tambahkan sebelumnya:
DELETE FROM dbo.Orders WHERE orderid >= 6;
Jika Anda ingin mencegah modifikasi yang bertentangan dengan filter kueri dalam tampilan, tambahkan WITH CHECK OPTION di akhir kueri dalam sebagai bagian dari definisi tampilan, seperti:
CREATE OR ALTER VIEW dbo.FastOrders AS SELECT orderid, orderdate, shippeddate FROM dbo.Orders WHERE DATEDIFF(day, orderdate, shippeddate) <= 7 WITH CHECK OPTION; GO
Sisipan dan pembaruan melalui tampilan diizinkan selama sesuai dengan filter kueri dalam. Jika tidak, mereka akan ditolak.
Misalnya, gunakan kode berikut untuk menyisipkan melalui tampilan baris yang tidak bertentangan dengan filter kueri dalam:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(6, '20210805', '20210807');
Baris berhasil ditambahkan.
Mencoba menyisipkan baris yang bertentangan dengan filter:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(7, '20210805', '20210815');
Kali ini baris ditolak dengan kesalahan berikut:
Level 16, State 1, Line 135Upaya penyisipan atau pembaruan gagal karena tampilan target menentukan WITH CHECK OPTION atau mencakup tampilan yang menetapkan WITH CHECK OPTION dan satu atau lebih baris yang dihasilkan dari operasi tidak memenuhi syarat di bawah PERIKSA batasan OPSI.
Inkonsistensi NULL
Jika Anda telah bekerja dengan T-SQL untuk beberapa waktu, Anda mungkin sangat menyadari kompleksitas modifikasi yang disebutkan di atas dan fungsi CHECK OPTION berfungsi. Seringkali, bahkan orang yang berpengalaman menganggap penanganan NULL dari PILIHAN PERIKSA mengejutkan. Selama bertahun-tahun saya biasa memikirkan OPSI PERIKSA dalam pandangan sebagai melayani fungsi yang sama sebagai batasan PERIKSA dalam definisi tabel dasar. Itu juga cara saya menggambarkan opsi ini saat menulis atau mengajar tentangnya. Memang, selama tidak ada NULL yang terlibat dalam predikat filter, lebih mudah untuk memikirkan keduanya dalam istilah yang sama. Mereka berperilaku konsisten dalam kasus seperti itu—menerima baris yang sesuai dengan predikat dan menolak baris yang bertentangan dengannya. Namun, keduanya menangani NULL secara tidak konsisten.
Saat menggunakan OPSI PERIKSA, modifikasi diperbolehkan melalui tampilan selama predikat bernilai benar, jika tidak maka akan ditolak. Ini berarti itu ditolak ketika predikat tampilan bernilai salah atau tidak diketahui (bila NULL terlibat). Dengan batasan CHECK, modifikasi diperbolehkan ketika predikat batasan bernilai benar atau tidak diketahui, dan ditolak ketika predikat bernilai salah. Itu perbedaan yang menarik! Pertama, mari kita lihat tindakannya, lalu kita akan mencoba dan mencari tahu logika di balik ketidakkonsistenan ini.
Mencoba menyisipkan melalui tampilan baris dengan tanggal pengiriman NULL:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(8, '20210828', NULL);
Predikat tampilan dievaluasi menjadi tidak diketahui, dan baris ditolak dengan kesalahan berikut:
Msg 550, Level 16, State 1, Line 147Upaya penyisipan atau pembaruan gagal karena tampilan target menentukan WITH CHECK OPTION atau mencakup tampilan yang menetapkan WITH CHECK OPTION dan satu atau lebih baris yang dihasilkan dari operasi tidak memenuhi syarat di bawah batasan OPSI PERIKSA.
Mari kita coba penyisipan serupa terhadap tabel dasar dengan batasan CHECK. Gunakan kode berikut untuk menambahkan batasan seperti itu ke definisi tabel Pesanan kami:
ALTER TABLE dbo.Orders ADD CONSTRAINT CHK_Orders_FastOrder CHECK(DATEDIFF(day, orderdate, shippeddate) <= 7);
Pertama, untuk memastikan batasan berfungsi saat tidak ada NULL yang terlibat, coba masukkan pesanan berikut dengan tanggal pengiriman 10 hari dari tanggal pesanan:
INSERT INTO dbo.Orders(orderid, orderdate, shippeddate) VALUES(7, '20210805', '20210815');
Upaya penyisipan ini ditolak dengan kesalahan berikut:
Msg 547, Level 16, State 0, Line 159Pernyataan INSERT bertentangan dengan batasan CHECK "CHK_Orders_FastOrder". Konflik terjadi di database "tempdb", tabel "dbo.Orders".
Gunakan kode berikut untuk menyisipkan baris dengan tanggal pengiriman NULL:
INSERT INTO dbo.Orders(orderid, orderdate, shippeddate) VALUES(8, '20210828', NULL);
Batasan CHECK seharusnya menolak kasus palsu, tetapi dalam kasus kami, predikat dievaluasi menjadi tidak diketahui, sehingga baris berhasil ditambahkan.
Kueri tabel Pesanan:
SELECT * FROM dbo.Orders;
Anda dapat melihat pesanan baru di output:
orderid orderdate shippeddate ----------- ---------- ----------- 1 2021-08-02 2021-08-04 2 2021-08-02 2021-08-05 3 2021-08-04 2021-08-06 4 2021-08-26 NULL 5 2021-08-27 NULL 6 2021-08-05 2021-08-07 8 2021-08-28 NULL
Apa logika di balik ketidakkonsistenan ini? Anda dapat berargumen bahwa batasan CHECK hanya boleh diterapkan ketika predikat batasan jelas dilanggar, artinya ketika dievaluasi menjadi salah. Dengan cara ini, jika Anda memilih untuk mengizinkan NULL di kolom yang dimaksud, baris dengan NULL di kolom diizinkan meskipun predikat batasan dievaluasi menjadi tidak diketahui. Dalam kasus kami, kami mewakili pesanan yang belum terkirim dengan NULL di kolom tanggal pengiriman, dan kami mengizinkan pesanan yang belum terkirim di tabel sambil menerapkan aturan "pesanan cepat" hanya untuk pesanan yang dikirim.
Argumen untuk menggunakan logika yang berbeda dengan tampilan adalah bahwa modifikasi harus diizinkan melalui tampilan hanya jika baris hasil adalah bagian tampilan yang valid. Jika predikat tampilan dievaluasi menjadi tidak diketahui, misalnya, ketika tanggal pengiriman adalah NULL, baris hasil bukan bagian yang valid dari tampilan, oleh karena itu ditolak. Hanya baris yang predikatnya bernilai benar yang merupakan bagian valid dari tampilan dan karenanya diizinkan.
NULL menambah banyak kerumitan pada bahasa. Suka atau tidak, jika data Anda mendukungnya, pastikan Anda memahami cara T-SQL menanganinya.
Pada titik ini Anda dapat menghapus batasan CHECK dari tabel Pesanan dan juga menghapus tampilan FastOrders untuk pembersihan:
ALTER TABLE dbo.Orders DROP CONSTRAINT CHK_Orders_FastOrder; DROP VIEW IF EXISTS dbo.FastOrders;
Batasan TOP/OFFSET-FETCH
Modifikasi melalui tampilan yang melibatkan filter TOP dan OFFSET-FETCH biasanya diperbolehkan. Namun, seperti diskusi kami sebelumnya tentang tampilan yang ditentukan tanpa OPSI PERIKSA, hasil modifikasi tersebut mungkin tampak aneh bagi pengguna jika mereka tidak menyadari bahwa mereka sedang berinteraksi dengan tampilan.
Pertimbangkan tampilan berikut yang mewakili pesanan terbaru sebagai contoh:
CREATE OR ALTER VIEW dbo.RecentOrders AS SELECT TOP (5) orderid, orderdate, shippeddate FROM dbo.Orders ORDER BY orderdate DESC, orderid DESC; GO
Gunakan kode berikut untuk menyisipkan enam pesanan melalui tampilan RecentOrders:
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate) VALUES(9, '20210801', '20210803'), (10, '20210802', '20210804'), (11, '20210829', '20210831'), (12, '20210830', '20210902'), (13, '20210830', '20210903'), (14, '20210831', '20210903');
Kueri tampilan:
SELECT * FROM dbo.RecentOrders;
Anda mendapatkan output berikut:
orderid orderdate shippeddate ----------- ---------- ----------- 14 2021-08-31 2021-09-03 13 2021-08-30 2021-09-03 12 2021-08-30 2021-09-02 11 2021-08-29 2021-08-31 8 2021-08-28 NULL
Dari enam pesanan yang dimasukkan, hanya empat yang merupakan bagian dari tampilan. Ini tampaknya sangat masuk akal jika Anda menyadari bahwa Anda menanyakan tampilan yang didasarkan pada kueri dengan filter TOP. Tetapi mungkin tampak aneh jika Anda berpikir bahwa Anda menanyakan tabel dasar.
Kueri tabel Pesanan yang mendasarinya secara langsung:
SELECT * FROM dbo.Orders;
Anda mendapatkan output berikut yang menunjukkan semua pesanan yang ditambahkan:
orderid orderdate shippeddate ----------- ---------- ----------- 1 2021-08-02 2021-08-04 2 2021-08-02 2021-08-05 3 2021-08-04 2021-08-06 4 2021-08-26 NULL 5 2021-08-27 NULL 6 2021-08-05 2021-08-07 8 2021-08-28 NULL 9 2021-08-01 2021-08-03 10 2021-08-02 2021-08-04 11 2021-08-29 2021-08-31 12 2021-08-30 2021-09-02 13 2021-08-30 2021-09-03 14 2021-08-31 2021-09-03
Jika Anda menambahkan OPSI PERIKSA ke definisi tampilan, pernyataan INSERT dan UPDATE terhadap tampilan akan ditolak. Gunakan kode berikut untuk menerapkan perubahan ini:
CREATE OR ALTER VIEW dbo.RecentOrders AS SELECT TOP (5) orderid, orderdate, shippeddate FROM dbo.Orders ORDER BY orderdate DESC, orderid DESC WITH CHECK OPTION; GO
Coba tambahkan pesanan melalui tampilan:
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate) VALUES(15, '20210801', '20210805');
Anda mendapatkan kesalahan berikut:
Msg 4427, Level 16, State 1, Line 247Tidak dapat memperbarui tampilan "dbo.RecentOrders" karena itu atau tampilan yang direferensikannya dibuat dengan WITH CHECK OPTION dan definisinya berisi klausa TOP atau OFFSET.
SQL Server tidak mencoba menjadi terlalu pintar di sini. Itu akan menolak perubahan bahkan jika baris yang Anda coba masukkan akan menjadi bagian tampilan yang valid pada saat itu. Misalnya, coba tambahkan pesanan dengan tanggal yang lebih baru yang akan masuk dalam 5 teratas pada saat ini:
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate) VALUES(15, '20210904', '20210906');
Upaya penyisipan masih ditolak dengan kesalahan berikut:
Msg 4427, Level 16, State 1, Line 254Tidak dapat memperbarui tampilan "dbo.RecentOrders" karena itu atau referensi tampilannya dibuat dengan WITH CHECK OPTION dan definisinya berisi klausa TOP atau OFFSET.
Coba perbarui baris melalui tampilan:
UPDATE dbo.RecentOrders SET shippeddate = DATEADD(day, 2, orderdate);
Dalam hal ini, upaya perubahan juga ditolak dengan kesalahan berikut:
Msg 4427, Level 16, State 1, Line 260Tidak dapat memperbarui tampilan "dbo.RecentOrders" karena itu atau tampilan yang dirujuknya dibuat dengan WITH CHECK OPTION dan definisinya berisi klausa TOP atau OFFSET.
Ketahuilah bahwa mendefinisikan tampilan berdasarkan kueri dengan TOP atau OFFSET-FETCH dan OPSI PERIKSA akan mengakibatkan kurangnya dukungan untuk pernyataan INSERT dan UPDATE melalui tampilan.
Penghapusan melalui tampilan seperti itu didukung. Jalankan kode berikut untuk menghapus semua lima pesanan terbaru saat ini:
DELETE FROM dbo.RecentOrders;
Perintah berhasil diselesaikan.
Buat kueri tabel:
SELECT * FROM dbo.Orders;
Anda mendapatkan output berikut setelah penghapusan pesanan dengan ID 8, 11, 12, 13, dan 14.
orderid orderdate shippeddate ----------- ---------- ----------- 1 2021-08-02 2021-08-04 2 2021-08-02 2021-08-05 3 2021-08-04 2021-08-06 4 2021-08-26 NULL 5 2021-08-27 NULL 6 2021-08-05 2021-08-07 9 2021-08-01 2021-08-03 10 2021-08-02 2021-08-04
Pada titik ini, jalankan kode berikut untuk pembersihan sebelum menjalankan contoh di bagian berikutnya:
DELETE FROM dbo.Orders WHERE orderid > 5; DROP VIEW IF EXISTS dbo.RecentOrders;
Bergabung
Memperbarui tampilan yang menggabungkan beberapa tabel didukung, selama hanya satu dari tabel dasar yang terpengaruh oleh perubahan.
Pertimbangkan tampilan berikut yang menggabungkan Pesanan dan Detail Pesanan sebagai contoh:
CREATE OR ALTER VIEW dbo.OrdersOrderDetails AS SELECT O.orderid, O.orderdate, O.shippeddate, OD.productid, OD.qty, OD.unitprice, OD.discount FROM dbo.Orders AS O INNER JOIN dbo.OrderDetails AS OD ON O.orderid = OD.orderid; GO
Coba sisipkan baris melalui tampilan, sehingga kedua tabel dasar yang mendasarinya akan terpengaruh:
INSERT INTO dbo.OrdersOrderDetails(orderid, orderdate, shippeddate, productid, qty, unitprice, discount) VALUES(6, '20210828', NULL, 1001, 5, 10.50, 0.05);
Anda mendapatkan kesalahan berikut:
Msg 4405, Level 16, Status 1, Baris 306Tampilan atau fungsi 'dbo.OrdersOrderDetails' tidak dapat diperbarui karena modifikasi memengaruhi beberapa tabel dasar.
Coba masukkan baris melalui tampilan, sehingga hanya tabel Pesanan yang akan terpengaruh:
INSERT INTO dbo.OrdersOrderDetails(orderid, orderdate, shippeddate) VALUES(6, '20210828', NULL);
Perintah ini berhasil diselesaikan, dan baris tersebut dimasukkan ke dalam tabel Pesanan yang mendasarinya.
Tetapi bagaimana jika Anda juga ingin dapat menyisipkan baris melalui tampilan ke dalam tabel OrderDetails? Dengan definisi tampilan saat ini, ini tidak mungkin (bukan memicu selain) karena tampilan mengembalikan kolom orderid dari tabel Pesanan dan bukan dari tabel OrderDetails. Cukuplah bahwa satu kolom dari tabel OrderDetails yang entah bagaimana tidak bisa mendapatkan nilainya secara otomatis bukan bagian dari tampilan untuk mencegah penyisipan ke OrderDetails melalui tampilan. Tentu saja, Anda selalu dapat memutuskan tampilan akan menyertakan orderid dari Orders dan orderid dari OrderDetails. Dalam kasus seperti itu, Anda harus menetapkan dua kolom dengan alias berbeda karena judul tabel yang diwakili oleh tampilan harus memiliki nama kolom yang unik.
Gunakan kode berikut untuk mengubah definisi tampilan untuk menyertakan kedua kolom, alias satu dari Pesanan sebagai O_orderid dan satu dari OrderDetails sebagai OD_orderid:
CREATE OR ALTER VIEW dbo.OrdersOrderDetails AS SELECT O.orderid AS O_orderid, O.orderdate, O.shippeddate, OD.orderid AS OD_orderid,OD.productid, OD.qty, OD.unitprice, OD.discount FROM dbo.Orders AS O INNER JOIN dbo.OrderDetails AS OD ON O.orderid = OD.orderid; GO
Sekarang Anda dapat menyisipkan baris melalui tampilan baik ke Pesanan atau ke OrderDetails, tergantung dari tabel mana daftar kolom target berasal. Berikut adalah contoh untuk menyisipkan beberapa baris pesanan yang terkait dengan pesanan 6 melalui tampilan ke dalam OrderDetails:
INSERT INTO dbo.OrdersOrderDetails(OD_orderid, productid, qty, unitprice, discount) VALUES(6, 1001, 5, 10.50, 0.05), (6, 1002, 5, 20.00, 0.05);
Baris berhasil ditambahkan.
Kueri tampilan:
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;
Anda mendapatkan output berikut:
O_orderid orderdate shippeddate OD_orderid productid qty unitprice discount ----------- ---------- ----------- ----------- ----------- ---- ---------- --------- 6 2021-08-28 NULL 6 1001 5 10.50 0.0500 6 2021-08-28 NULL 6 1002 5 20.00 0.0500
Pembatasan serupa berlaku untuk pernyataan UPDATE melalui tampilan. Pembaruan diperbolehkan selama hanya satu tabel dasar yang terpengaruh. Tetapi Anda diperbolehkan untuk mereferensikan kolom dari kedua sisi dalam pernyataan selama hanya satu sisi yang diubah.
Sebagai contoh, pernyataan UPDATE berikut melalui tampilan menetapkan tanggal pesanan dari baris di mana ID pesanan baris pesanan adalah 6 dan ID produk adalah 1001 menjadi “20210901:”
UPDATE dbo.OrdersOrderDetails SET orderdate = '20210901' WHERE OD_orderid = 6 AND productid = 1001;
Kami akan menyebut pernyataan ini Perbarui pernyataan 1.
Pembaruan berhasil diselesaikan dengan pesan berikut:
(1 row affected)
Yang penting untuk diperhatikan di sini adalah filter pernyataan berdasarkan elemen dari tabel OrderDetails, namun tanggal pemesanan kolom yang dimodifikasi berasal dari tabel Pesanan. Jadi, dalam rencana yang dibuat SQL Server untuk pernyataan ini, ia harus mencari tahu pesanan mana yang perlu diubah dalam tabel Pesanan. Rencana untuk pernyataan ini ditunjukkan pada Gambar 1.
Gambar 1:Pernyataan Rencana Pembaruan 1
Anda dapat melihat bagaimana rencana dimulai dengan memfilter sisi OrderDetails dengan orderid =6 dan productid =1001, dan Orders side by orderid =6, menggabungkan keduanya. Hasilnya hanya satu baris. Satu-satunya bagian relevan yang harus disimpan dari aktivitas ini adalah ID pesanan mana dalam tabel Pesanan yang mewakili baris yang perlu diperbarui. Dalam kasus kami, ini adalah pesanan dengan ID pesanan 6. Selain itu, operator Hitung Skalar menyiapkan anggota yang disebut Expr1002 dengan nilai yang akan diberikan pernyataan ke kolom tanggal pesanan dari pesanan target. Bagian terakhir dari rencana dengan operator Pembaruan Indeks Cluster menerapkan pembaruan aktual ke baris dalam Pesanan dengan ID pesanan 6, menyetel nilai tanggal pesanannya ke Expr1002.
Poin kunci untuk ditekankan di sini adalah hanya satu baris dengan orderid 6 di tabel Pesanan yang diperbarui. Namun baris ini memiliki dua kecocokan dalam hasil penggabungan dengan tabel OrderDetails—satu dengan ID produk 1001 (yang difilter oleh pembaruan asli) dan satu lagi dengan ID produk 1002 (yang tidak difilter oleh pembaruan asli). Kueri tampilan pada titik ini, filter semua baris dengan ID pesanan 6:
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;
Anda mendapatkan output berikut:
O_orderid orderdate shippeddate OD_orderid productid qty unitprice discount ----------- ---------- ----------- ----------- ----------- ---- ---------- --------- 6 2021-09-01 NULL 6 1001 5 10.50 0.0500 6 2021-09-01 NULL 6 1002 5 20.00 0.0500
Kedua baris menunjukkan tanggal pesanan baru, meskipun pembaruan asli hanya memfilter baris dengan ID produk 1001. Sekali lagi, ini tampaknya sangat masuk akal jika Anda tahu Anda berinteraksi dengan tampilan yang menggabungkan dua tabel dasar di bawah sampul, tetapi bisa terasa sangat aneh jika Anda tidak menyadarinya.
Anehnya, SQL Server bahkan mendukung pembaruan nondeterministik di mana beberapa baris sumber (dari OrderDetails dalam kasus kami) cocok dengan satu baris target (dalam Pesanan dalam kasus kami). Secara teoritis, salah satu cara untuk menangani kasus seperti itu adalah dengan menolaknya. Memang, dengan pernyataan MERGE di mana beberapa baris sumber cocok dengan satu baris target, SQL Server menolak upaya tersebut. Tetapi tidak dengan UPDATE berdasarkan gabungan, baik secara langsung maupun tidak langsung melalui ekspresi tabel bernama seperti tampilan. SQL Server hanya menanganinya sebagai pembaruan nondeterministik.
Perhatikan contoh berikut, yang akan kita sebut sebagai Pernyataan 2:
UPDATE dbo.OrdersOrderDetails SET orderdate = CASE WHEN unitprice >= 20.00 THEN '20210902' ELSE '20210903' END WHERE OD_orderid = 6;
Mudah-mudahan Anda akan memaafkan saya bahwa ini adalah contoh yang dibuat-buat, tetapi ini menggambarkan intinya.
Ada dua baris yang memenuhi syarat dalam tampilan, yang mewakili dua baris baris pesanan sumber yang memenuhi syarat dari tabel DetailPesanan yang mendasarinya. Namun hanya ada satu baris target yang memenuhi syarat di tabel Pesanan yang mendasarinya. Selain itu, dalam satu baris OrderDetails sumber, ekspresi CASE yang ditetapkan mengembalikan satu nilai ('20210902') dan di baris OrderDetails sumber lain mengembalikan nilai lain ('20210903'). Apa yang harus dilakukan SQL Server dalam kasus ini? Seperti disebutkan, situasi serupa dengan pernyataan MERGE akan menghasilkan kesalahan, menolak upaya perubahan. Namun dengan pernyataan UPDATE, SQL Server hanya membalik koin. Secara teknis, ini dilakukan dengan menggunakan fungsi agregat internal yang disebut ANY.
Jadi, pembaruan kami berhasil diselesaikan, melaporkan 1 baris terpengaruh. Rencana untuk pernyataan ini ditunjukkan pada Gambar 2.
Gambar 2:Pernyataan Rencana Pembaruan 2
Ada dua baris hasil join. Kedua baris ini menjadi baris sumber untuk pembaruan. Tetapi kemudian operator agregat yang menerapkan fungsi APA PUN mengambil satu (apa saja) nilai orderid dan satu (apa saja) nilai harga unit dari baris sumber ini. Kedua baris sumber memiliki nilai orderid yang sama, sehingga urutan yang benar akan diubah. Tetapi tergantung pada nilai unitprice sumber mana yang akhirnya dipilih agregat APAPUN, ini akan menentukan nilai mana yang akan dikembalikan oleh ekspresi CASE, untuk kemudian digunakan sebagai nilai tanggal pesanan yang diperbarui dalam pesanan target. Anda tentu dapat melihat argumen yang menentang dukungan pembaruan semacam itu, tetapi itu didukung sepenuhnya di SQL Server.
Mari kita query tampilan untuk melihat hasil dari perubahan ini (sekarang saatnya untuk bertaruh untuk hasilnya):
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;
Saya mendapatkan output berikut:
O_orderid orderdate shippeddate OD_orderid productid qty unitprice discount ----------- ---------- ----------- ----------- ----------- ---- ---------- --------- 6 2021-09-03 NULL 6 1001 5 10.50 0.0500 6 2021-09-03 NULL 6 1002 5 20.00 0.0500
Hanya satu dari dua nilai harga satuan sumber yang diambil dan digunakan untuk menentukan tanggal pemesanan dari pesanan target tunggal, namun saat menanyakan tampilan, nilai tanggal pesanan diulang untuk kedua baris pesanan yang cocok. Seperti yang dapat Anda sadari, hasilnya bisa saja terjadi pada tanggal lain (2021-09-02) karena pilihan nilai harga satuan tidak dapat ditentukan. Hal-hal aneh!
Jadi, dalam kondisi tertentu, pernyataan INSERT dan UPDATE diizinkan melalui tampilan yang menggabungkan beberapa tabel yang mendasarinya. Penghapusan, bagaimanapun, tidak diperbolehkan terhadap pandangan tersebut. Bagaimana SQL Server dapat mengetahui sisi mana yang seharusnya menjadi target untuk dihapus?
Berikut adalah upaya untuk menerapkan penghapusan tersebut melalui tampilan:
DELETE FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;
Upaya ini ditolak dengan kesalahan berikut:
Msg 4405, Level 16, Status 1, Baris 377Tampilan atau fungsi 'dbo.OrdersOrderDetails' tidak dapat diperbarui karena modifikasi memengaruhi beberapa tabel dasar.
Pada titik ini, jalankan kode berikut untuk pembersihan:
DELETE FROM dbo.OrderDetails WHERE orderid = 6; DELETE FROM dbo.Orders WHERE orderid = 6; DROP VIEW IF EXISTS dbo.OrdersOrderDetails;
Kolom Turunan
Pembatasan lain untuk modifikasi melalui tampilan berkaitan dengan kolom turunan. Jika kolom tampilan adalah hasil dari komputasi, SQL Server tidak akan mencoba merekayasa balik rumusnya saat Anda mencoba memasukkan atau memperbarui data melalui tampilan—sebaliknya, itu akan menolak modifikasi tersebut.
Perhatikan tampilan berikut sebagai contoh:
CREATE OR ALTER VIEW dbo.OrderDetailsNetPrice AS SELECT orderid, productid, qty, unitprice * (1.0 - discount) AS netunitprice, discount FROM dbo.OrderDetails; GO
Tampilan menghitung kolom netunitprice berdasarkan kolom tabel OrderDetails yang mendasari harga unit dan diskon.
Kueri tampilan:
SELECT * FROM dbo.OrderDetailsNetPrice;
Anda mendapatkan output berikut:
orderid productid qty netunitprice discount ----------- ----------- ----------- ------------- --------- 1 1001 5 9.975000 0.0500 1 1004 2 20.000000 0.0000 2 1003 1 47.691000 0.1000 3 1001 1 9.975000 0.0500 3 1003 2 49.491000 0.1000 4 1001 2 9.975000 0.0500 4 1004 1 20.300000 0.0000 4 1005 1 28.595000 0.0500 5 1003 5 54.990000 0.0000 5 1006 2 11.316000 0.0800
Coba masukkan baris melalui tampilan:
INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, netunitprice, discount) VALUES(1, 1005, 1, 28.595, 0.05);
Secara teoritis, Anda dapat mengetahui baris apa yang perlu dimasukkan ke dalam tabel DetailPesanan yang mendasarinya dengan merekayasa balik nilai harga unit tabel dasar dari nilai harga neto dan nilai diskon tampilan. SQL Server tidak mencoba rekayasa balik seperti itu, tetapi menolak upaya penyisipan dengan kesalahan berikut:
Msg 4406, Level 16, Status 1, Baris 412Pembaruan atau penyisipan tampilan atau fungsi 'dbo.OrderDetailsNetPrice' gagal karena berisi bidang turunan atau konstan.
Cobalah untuk menghilangkan kolom yang dihitung dari penyisipan:
INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, discount) VALUES(1, 1005, 1, 0.05);
Sekarang kita kembali ke persyaratan bahwa semua kolom dari tabel dasar yang entah bagaimana tidak mendapatkan nilainya secara otomatis harus menjadi bagian dari penyisipan, dan di sini kita kehilangan kolom harga satuan. Penyisipan ini gagal dengan kesalahan berikut:
Msg 515, Level 16, State 2, Line 421Tidak dapat memasukkan nilai NULL ke kolom 'unitprice', tabel 'tempdb.dbo.OrderDetails'; kolom tidak mengizinkan nol. INSERT gagal.
If you want to support insertions through the view, you basically have two options. One is to include the unitprice column in the view definition. Another is to create an instead of trigger on the view where you handle the reverse engineering logic yourself.
At this point, run the following code for cleanup:
DROP VIEW IF EXISTS dbo.OrderDetailsNetPrice;
Set Operators
As mentioned in the last section, you’re not allowed to modify a column in a view if the column is a result of a computation. The columns modified in the view using INSERT and UPDATE statements have to map directly to the underlying base table’s columns with no manipulation. In the list of restrictions to modifications through views, T-SQL’s documentation specifies that columns formed by using the set operators UNION, UNION ALL, EXCEPT, and INTERSECT amount to a computation and therefore are also not updatable.
One exception to this restriction is when using the UNION ALL operator to combine rows from different tables to form an updatable partitioned view. That’s a big topic in its own right. I’ll cover it briefly here to give you a sense, and you can investigate it further if you like in the product’s documentation.
Partitioned views predates table and index partitioning in SQL Server. The basic idea is that you can store disjoint subsets of rows in different base tables and have a view that unifies the rows from the different tables using a UNION ALL operator. If certain requirements are met, you can not only read the data through the view but also modify it through the view. SQL Server will figure out how to direct the modifications through the view to the right underlying tables.
The requirements for supporting modifications through such a view include having a partitioning column. Each of the underlying tables needs to have a CHECK constraint based on the partitioning column that defines a disjoint subset of rows. Also, the partitioning column needs to be part of the table’s primary key, meaning it cannot allow NULLs.
Consider the Orders table you used earlier in this article. Suppose that instead of holding all orders in one table, you want to store unshipped orders in one table (called UnshippedOrders) and shipped orders in another table (called ShippedOrders). You also want to create a view called Orders combining the rows from both tables. You want the view to be updatable.
Let’s start by removing any existing objects before creating the new ones:
DROP VIEW IF EXISTS dbo.Orders; DROP TABLE IF EXISTS dbo.OrderDetails, dbo.Orders; DROP TABLE IF EXISTS dbo.ShippedOrders, dbo.UnshippedOrders;
The partitioning column in our example is the shippeddate column. Our first obstacle is that we want to represent unshipped orders with a NULL shippeddate, but the partitioning column cannot allow NULLs. One possible workaround is to decide on some specific future date to represent unshipped orders. For example, the maximum supported date December 31st, 9999. Then you could have a CHECK constraint in the UnshippedOrders table checking that the shipped date is this specific one, and a CHECK constraint in the ShippedOrders table checking that the shipped date is before this one. This will meet the requirement for disjoint sets of rows.
Another obstacle is that the partitioning column needs to be part of the primary key. Originally the primary key was based on the orderid column alone. Now it will need to be extended to be based on (orderid, shippeddate). You will probably still want to enforce uniqueness based on orderid alone. To achieve this, you’ll need to add a unique constraint based on orderid.
With all this in mind, here are the definitions of the ShippedOrders and UnshippedOrders tables:
CREATE TABLE dbo.ShippedOrders ( orderid INT NOT NULL, orderdate DATE NOT NULL, shippeddate DATE NOT NULL, CONSTRAINT PK_ShippedOrders PRIMARY KEY(orderid, shippeddate), CONSTRAINT UNQ_ShippedOrders_orderid UNIQUE(orderid), CONSTRAINT CHK_ShippedOrders_shippeddate CHECK(shippeddate < '99991231') ); CREATE TABLE dbo.UnshippedOrders ( orderid INT NOT NULL, orderdate DATE NOT NULL, shippeddate DATE NOT NULL DEFAULT('99991231'), CONSTRAINT PK_UnshippedOrders PRIMARY KEY(orderid, shippeddate), CONSTRAINT UNQ_UnshippedOrders_orderid UNIQUE(orderid), CONSTRAINT CHK_UnshippedOrders_shippeddate CHECK(shippeddate = '99991231') );
You then create the Orders view, unifying the rows from the two tables using the UNION ALL operator, like so:
CREATE OR ALTER VIEW dbo.Orders AS SELECT orderid, orderdate, shippeddate FROM dbo.ShippedOrders UNION ALL SELECT orderid, orderdate, shippeddate FROM dbo.UnshippedOrders; GO
Since this view meets all requirements for updatability, you can insert, update, and delete rows through the view. SQL Server will direct the changes to the right underlying tables. As an example, the following statement inserts a few rows, including both shipped and unshipped orders:
INSERT INTO dbo.Orders(orderid, orderdate, shippeddate) VALUES(1, '20210802', '20210804'), (2, '20210802', '20210805'), (3, '20210804', '20210806'), (4, '20210826', '99991231'), (5, '20210827', '99991231');
The plan for this code is shown in Figure 3.
Figure 3:Plan for INSERT statement against partitioned view
As you can see, a Compute Scalar operator computes for each source row a member called Ptn1018. This member is set to 0 for shipped orders (shippeddate <'9999-12-31') and 1 for unshipped orders (shippeddate ='9999-12-31'). The rows are spooled along with the member Ptn1018, and then the spool is read twice. Once filtering the rows where Ptn1018 =0, inserting those into the underlying ShippedOrders table, and another time filtering the rows where Ptn1018 =1, inserting those into the underlying UnshippedOrders table.If this seems like an attractive option, consider it very carefully. Remember this is an old feature, predating table and index partitioning. There are many requirements, restrictions, and complications, including optimization complications, integrity enforcement complications, and others. As mentioned, here I just wanted to cover it briefly to describe the exception to the modification restriction involving set operators.When you’re done, run the following code for cleanup:
DROP VIEW IF EXISTS dbo.Orders; DROP TABLE IF EXISTS dbo.OrderDetails, dbo.Orders; DROP TABLE IF EXISTS dbo.ShippedOrders, dbo.UnshippedOrders;
Ringkasan
When I started the coverage of views, one of the first things I explained was that a view is a table. You can read data from a view and you can modify data through a view. But you need to understand that modifications through the view are restricted in a few ways, and the outcome of such modifications could be surprising in some cases.
Using the CHECK OPTION, you’re only allowed to update and insert rows through the view as long as the result rows are considered a valid part of the view. This means unlike a CHECK constraint in a table, the CHECK OPTION rejects changes where the inner query’s filter evaluates to unknown (when a NULL is involved). You’re not allowed to insert or update rows through a view if it’s defined with the CHECK OPTION and uses the TOP or OFFSET-FETCH filters. But you’re allowed to delete rows through such a view.
If a view joins multiple base tables, inserts and updates through the view are allowed provided that only one underlying base table is affected. Oddly, if a modification of a single target row involves multiple related source rows, the modification is allowed but is processed as a nondeterministic one. In such a case, SQL Server uses the internal ANY aggregate the pick a single value from the source rows.
You cannot update or insert rows through a view where at least one of the updated columns is a derived one resulting from a computation. The same applies when using a set operator, with an exception when using the UNION ALL operator to create an updatable partitioned view.