この記事は、テーブル式に関するシリーズの第11部です。これまで、派生テーブルとCTEについて説明してきましたが、最近、ビューの説明を開始しました。パート9では、ビューを派生テーブルおよびCTEと比較し、パート10では、DDLの変更と、ビューの内部クエリでSELECT*を使用することの意味について説明しました。この記事では、変更に関する考慮事項に焦点を当てます。
ご存知かもしれませんが、ビューなどの名前付きテーブル式を使用して、ベーステーブルのデータを間接的に変更できます。ビューに対する変更権限を制御できます。実際、基になるテーブルを直接変更する権限をユーザーに付与せずに、ビューを介してデータを変更する権限をユーザーに付与できます。
ビューを介した変更に適用される特定の複雑さと制限に注意する必要があります。興味深いことに、サポートされている変更の一部は、特にデータを変更しているユーザーがビューを操作していることに気付いていない場合に、驚くべき結果をもたらす可能性があります。この記事で説明するCHECKOPTIONというオプションを使用すると、ビューを介した変更にさらに制限を課すことができます。カバレッジの一部として、ビューのCHECK OPTIONとテーブルのCHECK制約が変更、特にNULLを含む変更を処理する方法の間の奇妙な矛盾について説明します。
サンプルデータ
この記事のサンプルデータとして、OrdersおよびOrderDetailsというテーブルを使用します。次のコードを使用して、これらのテーブルをtempdbに作成し、初期サンプルデータを入力します。
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);
Ordersテーブルには注文ヘッダーが含まれ、OrderDetailsテーブルには注文行が含まれます。未発送の注文の発送日列はNULLです。 NULLを使用しないデザインを好む場合は、「99991231」など、未発送の注文に特定の将来の日付を使用できます。
オプションを確認
ビューの定義の一部としてチェックオプションを使用する状況を理解するために、まず、ビューを使用しない場合に何が起こり得るかを調べます。
次のコードは、注文から7日以内に発送された注文を表すFastOrdersというビューを作成します。
CREATE OR ALTER VIEW dbo.FastOrders AS SELECT orderid, orderdate, shippeddate FROM dbo.Orders WHERE DATEDIFF(day, orderdate, shippeddate) <= 7; GO
次のコードを使用して、発注から2日後に発送された注文をビューに挿入します。
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(6, '20210805', '20210807');
ビューをクエリします:
SELECT * FROM dbo.FastOrders;
新しい注文を含む次の出力が表示されます。
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
基になるテーブルをクエリします:
SELECT * FROM dbo.Orders;
新しい注文を含む次の出力が表示されます。
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
行は、ビューを介して基になるベーステーブルに挿入されました。
次に、ビューの内部クエリフィルタと矛盾して、配置されてから10日後に出荷された行をビューに挿入します。
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(7, '20210805', '20210815');
ステートメントは正常に完了し、影響を受けた1つの行が報告されます。
ビューをクエリします:
SELECT * FROM dbo.FastOrders;
次の出力が表示されますが、新しい注文は除外されています。
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
FastOrdersがビューであることがわかっている場合、これはすべて賢明に思えるかもしれません。結局のところ、行は基になるテーブルに挿入されており、ビューの内部クエリフィルターを満たしていません。ただし、FastOrdersがビューであり、ベーステーブルではないことに気付いていない場合、この動作は意外に思われるでしょう。
基になるOrdersテーブルをクエリします:
SELECT * FROM dbo.Orders;
新しい注文を含む次の出力が表示されます。
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
現在ビューの一部である行のshippeddate値を、ビューの一部としての資格を失った日付に更新すると、同様の驚くべき動作が発生する可能性があります。このような更新は通常許可されていますが、この場合も、基になるベーステーブルで行われます。このような更新後にビューをクエリすると、変更された行がなくなったように見えます。実際には、基になるテーブルにまだ存在しているため、ビューの一部とは見なされなくなりました。
次のコードを実行して、前に追加した行を削除します。
DELETE FROM dbo.Orders WHERE orderid >= 6;
ビューの内部クエリフィルターと競合する変更を防ぎたい場合は、次のように、ビュー定義の一部として内部クエリの最後にWITHCHECKOPTIONを追加します。
CREATE OR ALTER VIEW dbo.FastOrders AS SELECT orderid, orderdate, shippeddate FROM dbo.Orders WHERE DATEDIFF(day, orderdate, shippeddate) <= 7 WITH CHECK OPTION; GO
ビューを介した挿入と更新は、内部クエリのフィルターに準拠している限り許可されます。それ以外の場合は拒否されます。
たとえば、次のコードを使用して、内部クエリフィルタと競合しない行をビューに挿入します。
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(6, '20210805', '20210807');
行が正常に追加されました。
フィルタと競合する行を挿入してみてください:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(7, '20210805', '20210815');
今回は、次のエラーで行が拒否されます:
レベル16、状態1、行135ターゲット・ビューがWITH CHECK OPTIONを指定しているか、WITH CHECK OPTIONを指定するビューにまたがっており、操作の結果の1つ以上の行が対象外であったため、挿入または更新の試行が失敗しました。 OPTION制約を確認してください。
NULLの不整合
しばらくの間T-SQLを使用している場合は、前述の変更の複雑さとCHECKOPTIONの機能をよく知っているはずです。多くの場合、経験豊富な人々でさえ、CHECKOPTIONのNULL処理は驚くべきものだと感じています。何年もの間、私はビュー内のCHECK OPTIONを、ベーステーブルの定義内のCHECK制約と同じ機能を果たすものと考えていました。それはまた、私がそれについて書いたり教えたりするときにこのオプションを説明するために使用した方法です。実際、フィルター述語にNULLが含まれていない限り、2つを同様の用語で考えると便利です。このような場合、これらは一貫して動作します。つまり、述語に一致する行を受け入れ、述語と競合する行を拒否します。ただし、2つは一貫性のないNULLを処理します。
CHECK OPTIONを使用する場合、述語がtrueと評価される限り、ビューを介して変更が許可されます。それ以外の場合は拒否されます。これは、ビューの述語がfalseまたは不明と評価された場合(NULLが含まれている場合)に拒否されることを意味します。 CHECK制約を使用すると、制約の述語がtrueまたはunknownと評価された場合に変更が許可され、述語がfalseと評価された場合に変更が拒否されます。それは興味深い違いです!まず、これが実際に動作していることを確認してから、この不整合の背後にある論理を理解してみましょう。
出荷日がNULLの行をビューに挿入してみてください:
INSERT INTO dbo.FastOrders(orderid, orderdate, shippeddate) VALUES(8, '20210828', NULL);
ビューの述語は不明と評価され、行は次のエラーで拒否されます:
メッセージ550、レベル16、状態1、行147ターゲットビューがWITHCHECK OPTIONを指定しているか、WITH CHECK OPTIONを指定するビューにまたがっており、操作の結果の1つ以上の行がそうではなかったため、挿入または更新の試行が失敗しましたCHECKOPTION制約の下で適格です。
CHECK制約を使用してベーステーブルに対して同様の挿入を試してみましょう。次のコードを使用して、このような制約をOrderのテーブル定義に追加します。
ALTER TABLE dbo.Orders ADD CONSTRAINT CHK_Orders_FastOrder CHECK(DATEDIFF(day, orderdate, shippeddate) <= 7);
まず、NULLが含まれていないときに制約が機能することを確認するには、注文日から10日離れた出荷日で次の注文を挿入してみてください。
INSERT INTO dbo.Orders(orderid, orderdate, shippeddate) VALUES(7, '20210805', '20210815');
この試みられた挿入は、次のエラーで拒否されます:
メッセージ547、レベル16、状態0、行159INSERTステートメントがCHECK制約「CHK_Orders_FastOrder」と競合していました。データベース「tempdb」、テーブル「dbo.Orders」で競合が発生しました。
次のコードを使用して、出荷日がNULLの行を挿入します。
INSERT INTO dbo.Orders(orderid, orderdate, shippeddate) VALUES(8, '20210828', NULL);
CHECK制約は誤ったケースを拒否することになっていますが、この場合、述語は不明と評価されるため、行は正常に追加されます。
Ordersテーブルをクエリします:
SELECT * FROM dbo.Orders;
出力に新しい順序が表示されます:
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>
この矛盾の背後にある論理は何ですか? CHECK制約は、制約の述語が明らかに違反している場合、つまりfalseと評価された場合にのみ適用する必要があると主張できます。このように、問題の列でNULLを許可することを選択した場合、制約の述語が不明と評価されても、列にNULLが含まれる行が許可されます。この例では、shippeddate列にNULLが含まれる未出荷の注文を表し、出荷済みの注文に対してのみ「高速注文」ルールを適用しながら、テーブルで未出荷の注文を許可します。
ビューで異なるロジックを使用するという議論は、結果の行がビューの有効な部分である場合にのみ、ビューを介して変更を許可する必要があるというものです。ビューの述語が不明と評価された場合、たとえば、出荷日がNULLの場合、結果の行はビューの有効な部分ではないため、拒否されます。述語がtrueと評価される行のみがビューの有効な部分であるため、許可されます。
NULLは、言語に多くの複雑さを追加します。好むと好まざるとにかかわらず、データがそれらをサポートしている場合は、T-SQLがそれらをどのように処理するかを確実に理解する必要があります。
この時点で、OrdersテーブルからCHECK制約を削除し、クリーンアップのためにFastOrdersビューを削除することもできます。
ALTER TABLE dbo.Orders DROP CONSTRAINT CHK_Orders_FastOrder; DROP VIEW IF EXISTS dbo.FastOrders;
TOP/OFFSET-FETCHの制限
TOPおよびOFFSET-FETCHフィルターを含むビューによる変更は通常許可されます。ただし、チェックオプションなしで定義されたビューに関する以前の説明と同様に、ユーザーがビューを操作していることに気付いていない場合、そのような変更の結果はユーザーには奇妙に見える可能性があります。
例として、最近の注文を表す次のビューについて考えてみます。
CREATE OR ALTER VIEW dbo.RecentOrders AS SELECT TOP (5) orderid, orderdate, shippeddate FROM dbo.Orders ORDER BY orderdate DESC, orderid DESC; GO
次のコードを使用して、RecentOrdersビューから6つの注文を挿入します。
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');
ビューをクエリします:
SELECT * FROM dbo.RecentOrders;
次の出力が得られます:
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
挿入された6つの注文のうち、ビューの一部は4つだけです。 TOPフィルターを使用したクエリに基づくビューをクエリしていることに気付いている場合、これは完全に賢明なようです。ただし、ベーステーブルをクエリしていると考えている場合は、奇妙に思えるかもしれません。
基になるOrdersテーブルを直接クエリします:
SELECT * FROM dbo.Orders;
追加されたすべての注文を示す次の出力が表示されます。
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
ビュー定義にCHECKOPTIONを追加すると、ビューに対するINSERTおよびUPDATEステートメントは拒否されます。この変更を適用するには、次のコードを使用します。
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
ビューから注文を追加してみてください:
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate) VALUES(15, '20210801', '20210805');
次のエラーが発生します:
メッセージ4427、レベル16、状態1、行247ビュー「dbo.RecentOrders」は、ビュー「dbo.RecentOrders」またはそれが参照するビューがWITH CHECK OPTIONで作成され、その定義にTOPまたはOFFSET句が含まれているため、更新できません。
SQL Serverは、ここではあまり賢くしようとはしていません。挿入しようとした行がその時点でビューの有効な部分になったとしても、変更は拒否されます。たとえば、この時点で上位5位に入る、より新しい日付の注文を追加してみてください。
INSERT INTO dbo.RecentOrders(orderid, orderdate, shippeddate) VALUES(15, '20210904', '20210906');
試みられた挿入は、次のエラーで拒否されます:
メッセージ4427、レベル16、状態1、行254ビュー「dbo.RecentOrders」またはそれが参照するビューがWITHCHECKOPTIONで作成され、その定義にTOPまたはOFFSET句が含まれているため、更新できません。
ビューを介して行を更新してみてください:
UPDATE dbo.RecentOrders SET shippeddate = DATEADD(day, 2, orderdate);
この場合、試行された変更も次のエラーで拒否されます:
メッセージ4427、レベル16、状態1、行260ビュー「dbo.RecentOrders」は、ビュー「dbo.RecentOrders」またはそれが参照するビューがWITH CHECK OPTIONで作成され、その定義にTOPまたはOFFSET句が含まれているため、更新できません。
TOPまたはOFFSET-FETCHおよびCHECKOPTIONを使用したクエリに基づいてビューを定義すると、ビューを介したINSERTおよびUPDATEステートメントがサポートされなくなることに注意してください。
このようなビューによる削除がサポートされています。次のコードを実行して、現在の5つの最新の注文をすべて削除します。
DELETE FROM dbo.RecentOrders;
コマンドは正常に完了します。
テーブルをクエリします:
SELECT * FROM dbo.Orders;
ID 8、11、12、13、および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
この時点で、次のセクションの例を実行する前に、クリーンアップのために次のコードを実行します。
DELETE FROM dbo.Orders WHERE orderid > 5; DROP VIEW IF EXISTS dbo.RecentOrders;
参加
基になるベーステーブルの1つだけが変更の影響を受ける限り、複数のテーブルを結合するビューの更新がサポートされます。
例として、OrdersとOrderDetailsを結合する次のビューについて考えてみます。
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
ビューに行を挿入してみてください。そうすれば、基礎となる両方のベーステーブルが影響を受けます。
INSERT INTO dbo.OrdersOrderDetails(orderid, orderdate, shippeddate, productid, qty, unitprice, discount) VALUES(6, '20210828', NULL, 1001, 5, 10.50, 0.05);
次のエラーが発生します:
メッセージ4405、レベル16、状態1、行306変更が複数のベーステーブルに影響するため、ビューまたは関数'dbo.OrdersOrderDetails'は更新できません。
ビューに行を挿入して、Ordersテーブルのみが影響を受けるようにしてください:
INSERT INTO dbo.OrdersOrderDetails(orderid, orderdate, shippeddate) VALUES(6, '20210828', NULL);
このコマンドは正常に完了し、行は基になるOrdersテーブルに挿入されます。
しかし、ビューを介して行をOrderDetailsテーブルに挿入できるようにしたい場合はどうでしょうか。現在のビュー定義では、ビューがOrderDetailsテーブルからではなくOrdersテーブルからorderid列を返すため、これは(トリガーは別として)不可能です。どういうわけか自動的に値を取得できないOrderDetailsテーブルの1つの列がビューの一部ではなく、ビューを介したOrderDetailsへの挿入を防ぐことができます。もちろん、ビューにOrdersのorderidとOrderDetailsのorderidの両方を含めるようにいつでも決定できます。このような場合、ビューで表されるテーブルの見出しには一意の列名が必要であるため、2つの列に異なるエイリアスを割り当てる必要があります。
次のコードを使用して、両方の列を含むようにビュー定義を変更し、Ordersからの1つをO_orderidとして、OrderDetailsからの1つを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
これで、ターゲット列リストがどのテーブルからのものであるかに応じて、ビューを介してOrdersまたはOrderDetailsのいずれかに行を挿入できます。ビューを介して注文6に関連付けられたいくつかの注文明細を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);
行が正常に追加されました。
ビューをクエリします:
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;
次の出力が得られます:
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
同様の制限が、ビューを介したUPDATEステートメントに適用されます。基になるベーステーブルが1つだけ影響を受ける限り、更新は許可されます。ただし、片側だけが変更される限り、ステートメントの両側から列を参照できます。
例として、ビューを介した次のUPDATEステートメントは、注文明細の注文IDが6で、製品IDが1001である行の注文日を「20210901」に設定します。
UPDATE dbo.OrdersOrderDetails SET orderdate = '20210901' WHERE OD_orderid = 6 AND productid = 1001;
このステートメントをUpdateステートメント1と呼びます。
更新は次のメッセージで正常に完了します:
(1 row affected)
ここで重要なのは、ステートメントがOrderDetailsテーブルの要素でフィルタリングされていることですが、変更された列のorderdateはOrdersテーブルからのものです。したがって、SQL Serverがこのステートメント用に構築する計画では、Ordersテーブルでどの注文を変更する必要があるかを把握する必要があります。このステートメントの計画を図1に示します。
図1:更新ステートメント1の計画
OrderDetails側をorderid=6とproductid=1001の両方でフィルタリングし、Orders側をorderid =6でフィルタリングすることで、計画がどのように開始されるかを確認できます。結果は1行だけです。このアクティビティから守るべき唯一の関連部分は、Ordersテーブルのどの注文IDが更新が必要な行を表すかです。この例では、注文IDが6の注文です。さらに、Compute Scalarオペレーターは、ステートメントがターゲット注文のorderdate列に割り当てる値を使用してExpr1002というメンバーを準備します。 Clustered Index Updateオペレーターを使用したプランの最後の部分は、実際の更新を注文ID 6の注文の行に適用し、その注文日値をExpr1002に設定します。
ここで強調する重要なポイントは、Ordersテーブルのorderid6が更新された1行のみです。ただし、この行には、OrderDetailsテーブルとの結合の結果で2つの一致があります。1つは製品ID 1001(元の更新でフィルタリングされた)で、もう1つは製品ID 1002(元の更新ではフィルタリングされなかった)です。この時点でビューをクエリし、注文ID6のすべての行をフィルタリングします。
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;
次の出力が得られます:
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
元の更新では製品ID1001の行のみがフィルタリングされましたが、両方の行に新しい注文日が表示されます。これも、カバーの下にある2つのベーステーブルを結合するビューを操作していることがわかっている場合は、完全に理にかなっているように見えますが、これに気づかないと、とても奇妙に見えるかもしれません。
不思議なことに、SQL Serverは、複数のソース行(この場合はOrderDetailsから)が単一のターゲット行(この場合はOrders)と一致する非決定的な更新もサポートしています。理論的には、このようなケースを処理する1つの方法は、それを拒否することです。実際、複数のソース行が1つのターゲット行と一致するMERGEステートメントでは、SQLServerはその試行を拒否します。ただし、ビューのような名前付きテーブル式を介して直接または間接的に、結合に基づくUPDATEを使用することはできません。 SQL Serverは、それを非決定論的な更新として処理するだけです。
次の例を考えてみましょう。これをステートメント2と呼びます。
UPDATE dbo.OrdersOrderDetails SET orderdate = CASE WHEN unitprice >= 20.00 THEN '20210902' ELSE '20210903' END WHERE OD_orderid = 6;
不自然な例であることをお許しいただければ幸いですが、それは要点を示しています。
ビューには2つの修飾行があり、基になるOrderDetailsテーブルの2つの修飾ソース注文行を表します。ただし、基になるOrdersテーブルには対象となるターゲット行が1つだけあります。さらに、1つのソースOrderDetails行では、割り当てられたCASE式が1つの値( '20210902')を返し、他のソースOrderDetails行では別の値( '20210903')を返します。この場合、SQL Serverは何をすべきですか?前述のように、MERGEステートメントで同様の状況が発生すると、エラーが発生し、試行された変更が拒否されます。しかし、UPDATEステートメントを使用すると、SQLServerは単にコインを投げます。技術的には、これはANYと呼ばれる内部集計関数を使用して行われます。
したがって、更新は正常に完了し、影響を受けた1行が報告されます。このステートメントの計画を図2に示します。
図2:更新ステートメント2の計画
結合の結果には2つの行があります。これらの2つの行は、更新のソース行になります。ただし、ANY関数を適用する集計演算子は、これらのソース行から1つの(任意の)orderid値と1つの(任意の)unitprice値を選択します。両方のソース行のorderid値が同じであるため、正しい順序が変更されます。ただし、ANY集計が最終的に選択するソース単価値に応じて、CASE式が返す値が決まり、ターゲット注文の更新された注文日値として使用されます。このような更新をサポートすることに反対する議論は確かにわかりますが、SQLServerでは完全にサポートされています。
ビューにクエリを実行して、この変更の結果を確認しましょう(今が結果として賭けをするときです):
SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;
次の出力が得られました:
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
2つのソース単価値の1つだけが選択され、単一のターゲット注文の注文日を決定するために使用されましたが、ビューをクエリすると、一致する両方の注文行に対して注文日値が繰り返されます。ご存知のように、単価値の選択は非決定的であったため、結果は他の日付(2021-09-02)でも同様であった可能性があります。奇抜なもの!
したがって、特定の条件下では、INSERTステートメントとUPDATEステートメントは、複数の基になるテーブルを結合するビューを介して許可されます。ただし、そのようなビューに対して削除は許可されていません。 SQL Serverは、どちらの側が削除のターゲットになると思われるかをどのように判断できますか?
ビューを介してそのような削除を適用する試みは次のとおりです。
DELETE FROM dbo.OrdersOrderDetails WHERE O_orderid = 6;
この試行は次のエラーで拒否されます:
メッセージ4405、レベル16、状態1、行377変更が複数のベーステーブルに影響するため、ビューまたは関数'dbo.OrdersOrderDetails'は更新できません。
この時点で、クリーンアップのために次のコードを実行します。
DELETE FROM dbo.OrderDetails WHERE orderid = 6; DELETE FROM dbo.Orders WHERE orderid = 6; DROP VIEW IF EXISTS dbo.OrdersOrderDetails;
派生列
ビューを介した変更に対するもう1つの制限は、派生列に関係しています。ビュー列が計算の結果である場合、SQL Serverは、ビューを介してデータを挿入または更新しようとしたときに、数式をリバースエンジニアリングしようとはせず、そのような変更を拒否します。
例として次のビューを検討してください。
CREATE OR ALTER VIEW dbo.OrderDetailsNetPrice AS SELECT orderid, productid, qty, unitprice * (1.0 - discount) AS netunitprice, discount FROM dbo.OrderDetails; GO
ビューは、基になるOrderDetailsテーブルの列unitpriceとdiscountに基づいてnetunitprice列を計算します。
ビューをクエリします:
SELECT * FROM dbo.OrderDetailsNetPrice;
次の出力が得られます:
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
ビューに行を挿入してみてください:
INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, netunitprice, discount) VALUES(1, 1005, 1, 28.595, 0.05);
理論的には、ビューのnetunitpriceとdiscount値からベーステーブルのunitprice値をリバースエンジニアリングすることにより、基になるOrderDetailsテーブルに挿入する必要のある行を把握できます。 SQL Serverはそのようなリバースエンジニアリングを試みませんが、次のエラーで挿入の試みを拒否します:
メッセージ4406、レベル16、状態1、行412ビューまたは関数'dbo.OrderDetailsNetPrice'の更新または挿入は、派生フィールドまたは定数フィールドが含まれているため失敗しました。
挿入から計算列を省略してみてください:
INSERT INTO dbo.OrderDetailsNetPrice(orderid, productid, qty, discount) VALUES(1, 1005, 1, 0.05);
これで、基になるテーブルの値を自動的に取得しないすべての列を挿入の一部にする必要があるという要件に戻りました。ここでは、unitprice列がありません。この挿入は次のエラーで失敗します:
メッセージ515、レベル16、状態2、行421値NULLを列'unitprice'、テーブル'tempdb.dbo.OrderDetails'に挿入できません。列はnullを許可しません。 INSERTは失敗します。
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;
概要
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.