sql >> Databáze >  >> RDS >> Database

Základy tabulkových výrazů, Část 11 – Pohledy, úvahy o úpravách

Tento článek je jedenáctou částí série o tabulkových výrazech. Doposud jsem pokrýval odvozené tabulky a CTE a nedávno jsem začal pokrývat zobrazení. V části 9 jsem porovnával pohledy s odvozenými tabulkami a CTE a v části 10 jsem diskutoval o změnách DDL a důsledcích použití SELECT * ve vnitřním dotazu pohledu. V tomto článku se zaměřím na úvahy o úpravách.

Jak pravděpodobně víte, můžete upravovat data v základních tabulkách nepřímo prostřednictvím pojmenovaných tabulkových výrazů, jako jsou pohledy. Oprávnění k úpravám můžete ovládat vůči pohledům. Ve skutečnosti můžete uživatelům udělit oprávnění upravovat data prostřednictvím pohledů, aniž byste jim udělovali oprávnění přímo upravovat podkladové tabulky.

Musíte si být vědomi určitých složitostí a omezení, která se vztahují na úpravy prostřednictvím pohledů. Zajímavé je, že některé z podporovaných úprav mohou skončit s překvapivými výsledky, zvláště pokud si uživatel upravující data neuvědomuje, že interaguje s pohledem. Další omezení úprav prostřednictvím zobrazení můžete zavést pomocí možnosti nazvané CHECK OPTION, kterou popíšu v tomto článku. V rámci pokrytí popíšu zvláštní nekonzistenci mezi tím, jak CHECK OPTION v pohledu a omezení CHECK v tabulce zpracovávají úpravy – konkrétně ty, které zahrnují hodnoty NULL.

Ukázková data

Jako vzorová data pro tento článek použiji tabulky nazvané Orders a OrderDetails. Pomocí následujícího kódu vytvořte tyto tabulky v databázi tempdb a naplňte je některými počátečními ukázkovými daty:

POUŽÍVEJTE tempdb;GO DROP TABLE IF EXISTS dbo.OrderDetails, dbo.Orders;GO CREATE TABLE dbo.Orders( orderid INT NOT NULL CONSTRAINT PK_Orders PRIMÁRNÍ KLÍČ, datum objednávky DATE NOT NULL, shippeddate DATE NULL); INSERT INTO dbo.Orders(číslo objednávky, datum objednávky, datum odeslání) VALUES(1, '20210802', '20210804'), (2, '20210802', '20210805'), (3, '202108024', 08 4, '20210826', NULL), (5, '20210827', NULL); CREATE TABLE dbo.OrderDetails( orderid INT NOT NULL CONSTRAINT FK_OrderDetails_Orders REFERENCE dbo.Orders, productid INT NOT NULL, mnozstvi INT NOT NULL, jednotková cena NUMERIC(12, 2) NUMERIC(12, 2) STRANA NUMERIC, sleva NUMERRYMCONIC(tail)PKRYLL_CONIC KEY(id objednávky, produktid)); INSERT INTO dbo.OrderDetails(id objednávky, id produktu, množství, jednotková cena, sleva) VALUES(1, 1001, 5, 10,50, 0,05), (1, 1004, 2, 20,00, 0,00), (2, 1052, 9, 1 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,00 , (4, 1005, 1, 30,10, 0,05), (5, 1003, 5, 54,99, 0,00), (5, 1006, 2, 12,30, 0,08);

Tabulka Orders obsahuje záhlaví objednávky a tabulka OrderDetails obsahuje řádky objednávky. Neodeslané objednávky mají ve sloupci datum odeslání hodnotu NULL. Pokud dáváte přednost designu, který nepoužívá hodnoty NULL, můžete pro neodeslané objednávky použít konkrétní budoucí datum, například „99991231“.

ZKONTROLUJTE MOŽNOST

Abychom porozuměli okolnostem, kdy byste chtěli použít CHECK OPTION jako součást definice pohledu, nejprve prozkoumáme, co se může stát, když jej nepoužíváte.

Následující kód vytvoří pohled nazvaný FastOrders představující objednávky odeslané do sedmi dnů od jejich zadání:

VYTVOŘIT NEBO ZMĚNIT ZOBRAZENÍ dbo.FastOrdersAS SELECT orderid, orderdate, shippeddate FROM dbo.Orders WHERE DATEDIFF(den, orderdate, shippeddate) <=7;GO

Pomocí následujícího kódu vložte do zobrazení objednávku odeslanou dva dny po zadání:

INSERT INTO dbo.FastOrders(id objednávky, datum objednávky, datum odeslání) VALUES(6, '20210805', '20210807');

Dotaz na zobrazení:

SELECT * FROM dbo.FastOrders;

Získáte následující výstup, který obsahuje novou objednávku:

id objednávky datum odeslání----------- ---------- ------------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-066 2021-08-05 2021-08-07

Dotaz na podkladovou tabulku:

SELECT * FROM dbo.Orders;

Získáte následující výstup, který obsahuje novou objednávku:

id objednávky datum odeslání----------- ---------- ------------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-064 2021-08-26 NULL5 2021-08-27 NULL6 2021-08-05 2021-08-07

Řádek byl vložen do podkladové základní tabulky prostřednictvím pohledu.

Dále do zobrazení vložte řádek odeslaný 10 dní po umístění, což je v rozporu s vnitřním filtrem dotazů zobrazení:

INSERT INTO dbo.FastOrders(id objednávky, datum objednávky, datum odeslání) VALUES(7, '20210805', '20210815');

Příkaz je úspěšně dokončen a hlásí jeden dotčený řádek.

Dotaz na zobrazení:

SELECT * FROM dbo.FastOrders;

Získáte následující výstup, který vylučuje novou objednávku:

id objednávky datum odeslání----------- ---------- ------------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-066 2021-08-05 2021-08-07

Pokud víte, že FastOrders je pohled, může se to zdát rozumné. Řádek byl koneckonců vložen do podkladové tabulky a nevyhovuje vnitřnímu filtru dotazu zobrazení. Pokud si ale nejste vědomi toho, že FastOrders je pohled a nikoli základní tabulka, toto chování by se zdálo překvapivé.

Dotaz na podkladovou tabulku Objednávky:

SELECT * FROM dbo.Orders;

Získáte následující výstup, který obsahuje novou objednávku:

id objednávky datum odeslání----------- ---------- ------------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-064 2021-08-26 NULL5 2021-08-27 NULL6 2021-08-05 2021-08-0177 2021-08-0177 2021-08-077 2021-08-26 15

K podobnému překvapivému chování byste mohli zaznamenat, pokud prostřednictvím zobrazení aktualizujete hodnotu shippeddate v řádku, který je aktuálně součástí zobrazení, na datum, kvůli kterému se již nebude kvalifikovat jako součást zobrazení. Taková aktualizace je normálně povolena, ale opět se odehrává v podkladové základní tabulce. Pokud se dotazujete na zobrazení po takové aktualizaci, upravený řádek se zdá být pryč. V praxi je stále v podkladové tabulce, jen už to není považováno za součást zobrazení.

Chcete-li odstranit řádky, které jste přidali dříve, spusťte následující kód:

DELETE FROM dbo.Orders WHERE orderid>=6;

Pokud chcete zabránit úpravám, které jsou v konfliktu s vnitřním filtrem dotazu zobrazení, přidejte WITH CHECK OPTION na konec vnitřního dotazu jako součást definice zobrazení, například takto:

VYTVOŘIT NEBO ZMĚNIT ZOBRAZENÍ dbo.FastOrdersAS SELECT orderid, orderdate, shippeddate FROM dbo.Orders WHERE DATEDIFF(den, orderdate, shippeddate) <=7 WITH CHECK OPTION;GO

Vkládání a aktualizace prostřednictvím zobrazení jsou povoleny, pokud vyhovují filtru vnitřního dotazu. V opačném případě budou zamítnuty.

Pomocí následujícího kódu můžete například do zobrazení vložit řádek, který není v konfliktu s vnitřním filtrem dotazů:

INSERT INTO dbo.FastOrders(id objednávky, datum objednávky, datum odeslání) VALUES(6, '20210805', '20210807');

Řádek byl úspěšně přidán.

Pokuste se vložit řádek, který je v konfliktu s filtrem:

INSERT INTO dbo.FastOrders(id objednávky, datum objednávky, datum odeslání) VALUES(7, '20210805', '20210815');

Tentokrát je řádek odmítnut s následující chybou:

Úroveň 16, stav 1, řádek 135
Pokus o vložení nebo aktualizaci selhal, protože cílové zobrazení buď uvádí WITH CHECK OPTION, nebo zahrnuje pohled, který uvádí WITH CHECK OPTION a jeden nebo více řádků vyplývajících z operace nesplňuje podmínky omezení CHECK OPTION.

Nekonzistence NULL

Pokud už nějakou dobu pracujete s T-SQL, pravděpodobně si dobře uvědomujete výše uvedené složitosti úprav a slouží funkce CHECK OPTION. Často i zkušení lidé považují NULL zacházení s MOŽNOSTÍ KONTROLY za překvapivé. Po léta jsem si myslel, že CHECK OPTION v pohledu slouží stejné funkci jako omezení CHECK v definici základní tabulky. Tak jsem také popisoval tuto možnost, když jsem o ní psal nebo učil. Pokud predikát filtru neobsahuje žádné hodnoty NULL, je vhodné o nich přemýšlet podobně. V takovém případě se chovají konzistentně – přijímají řádky, které souhlasí s predikátem a odmítají ty, které jsou s ním v rozporu. Tyto dva však zpracovávají hodnoty NULL nekonzistentně.

Při použití CHECK OPTION je modifikace povolena prostřednictvím pohledu, pokud je predikát vyhodnocen jako pravdivý, jinak je zamítnut. To znamená, že je odmítnut, když je predikát pohledu vyhodnocen jako nepravdivý nebo neznámý (když se jedná o hodnotu NULL). S omezením CHECK je modifikace povolena, když je predikát podmínky vyhodnocen jako pravdivý nebo neznámý, a zamítnuta, když je predikát vyhodnocen jako nepravdivý. To je zajímavý rozdíl! Nejprve se na to podívejme v praxi a poté se pokusíme zjistit logiku této nekonzistence.

Pokuste se vložit do zobrazení řádek s datem odeslání NULL:

INSERT INTO dbo.FastOrders(id objednávky, datum objednávky, datum odeslání) VALUES(8, '20210828', NULL);

Predikát pohledu je vyhodnocen jako neznámý a řádek je odmítnut s následující chybou:

Zpráva 550, Úroveň 16, Stav 1, Řádek 147
Pokus o vložení nebo aktualizaci selhal, protože cílové zobrazení buď specifikuje WITH CHECK OPTION, nebo zahrnuje pohled, který specifikuje WITH CHECK OPTION a jeden nebo více řádků vyplývajících z operace neproběhlo splňují podmínku CHECK OPTION.

Zkusme podobné vložení proti základní tabulce s omezením CHECK. K přidání takového omezení do naší definice tabulky objednávky použijte následující kód:

ALTER TABLE dbo.Orders ADD CONSTRAINT CHK_Orders_FastOrder CHECK(DATEDIFF(den, datum objednávky, datum odeslání) <=7);

Nejprve, abyste se ujistili, že omezení funguje, když nejsou zapojeny žádné hodnoty NULL, zkuste vložit následující objednávku s datem odeslání 10 dní od data objednávky:

INSERT INTO dbo.Orders(ID objednávky, datum objednávky, datum odeslání) VALUES(7, '20210805', '20210815');

Tento pokus o vložení byl odmítnut s následující chybou:

Zpráva 547, úroveň 16, stav 0, řádek 159
Příkaz INSERT byl v konfliktu s omezením CHECK "CHK_Orders_FastOrder". Ke konfliktu došlo v databázi "tempdb", tabulce "dbo.Orders".

K vložení řádku s datem odeslání NULL použijte následující kód:

INSERT INTO dbo.Orders(ID objednávky, datum objednávky, datum odeslání) VALUES(8, '20210828', NULL);

Omezení CHECK má odmítnout falešné případy, ale v našem případě je predikát vyhodnocen jako neznámý, takže řádek je úspěšně přidán.

Dotaz na tabulku Objednávky:

SELECT * FROM dbo.Orders;

Novou objednávku můžete vidět ve výstupu:

id objednávky datum odeslání----------- ---------- ------------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-064 2021-08-26 NULL5 2021-08-27 NULL6 2021-08-05 2021-08-0178 2021-08-0178 2021-08-2028> 

Jaká je logika této nekonzistence? Můžete namítnout, že omezení CHECK by mělo být vynuceno pouze tehdy, když je predikát omezení jasně porušen, to znamená, když je vyhodnocen jako nepravdivý. Tímto způsobem, pokud se rozhodnete povolit hodnoty NULL v příslušném sloupci, budou povoleny řádky s hodnotami NULL ve sloupci, i když je predikát omezení vyhodnocen jako neznámý. V našem případě představujeme neodeslané objednávky s NULL ve sloupci expediční datum a v tabulce povolujeme neodeslané objednávky, přičemž prosazujeme pravidlo „rychlých objednávek“ pouze pro odeslané objednávky.

Argumentem pro použití jiné logiky s pohledem je, že úprava by měla být povolena prostřednictvím pohledu pouze v případě, že je výsledný řádek platnou součástí pohledu. Pokud je predikát pohledu vyhodnocen jako neznámý, např. když je datum odeslání NULL, řádek výsledku není platnou součástí pohledu, a proto je zamítnut. Pouze řádky, pro které je predikát vyhodnocen jako true, jsou platnou součástí zobrazení, a proto jsou povoleny.

Hodnoty NULL dodávají jazyku mnoho složitosti. Ať se vám líbí nebo ne, pokud je vaše data podporují, chcete se ujistit, že rozumíte tomu, jak s nimi T-SQL zachází.

V tomto okamžiku můžete vypustit omezení CHECK z tabulky Objednávky a také zrušit zobrazení FastOrders pro vyčištění:

ALTER TABLE dbo.Orders DROP CONSTRAINT CHK_Orders_FastOrder;DROP VIEW, POKUD EXISTUJE dbo.FastOrders;

Omezení TOP/OFFSET-FETCH

Úpravy prostřednictvím pohledů zahrnujících filtry TOP a OFFSET-FETCH jsou normálně povoleny. Nicméně, stejně jako v naší dřívější diskusi o pohledech definovaných bez CHECK OPTION, může se výsledek takové úpravy zdát uživateli divný, pokud si není vědom, že interaguje s pohledem.

Zvažte následující pohled představující nedávné objednávky jako příklad:

VYTVOŘIT NEBO ZMĚNIT ZOBRAZENÍ dbo.RecentOrdersAS SELECT TOP (5) orderid, orderdate, shippeddate FROM dbo.Orders ORDER BY orderdate DESC, orderid DESC;GO

Pomocí následujícího kódu vložíte do zobrazení RecentOrders šest objednávek:

INSERT INTO dbo.RecentOrders(id objednávky, datum objednávky, datum odeslání) VALUES(9, '20210801', '20210803'), (10, '20210802', '20210804'), (11, '291'02'11 ), (12, '20210830', '20210902'), (13, '20210830', '20210903'), (14, '20210831', '20210903');

Dotaz na zobrazení:

SELECT * FROM dbo.RecentOrders;

Získáte následující výstup:

id objednávky datum odeslání----------- ---------- ------------14 2021-08-31 2021-09-0313 2021 -08-30 2021-09-0312 2021-08-30 2021-09-0211 2021-08-29 2021-08-318 2021-08-28 NULL

Ze šesti vložených objednávek jsou součástí pohledu pouze čtyři. To se zdá být naprosto rozumné, pokud víte, že se dotazujete na zobrazení, které je založeno na dotazu s filtrem TOP. Ale může se to zdát divné, pokud si myslíte, že se dotazujete na základní tabulku.

Dotazujte se přímo na podkladovou tabulku objednávek:

SELECT * FROM dbo.Orders;

Získáte následující výstup zobrazující všechny přidané objednávky:

id objednávky datum odeslání----------- ---------- ------------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-064 2021-08-26 NULL5 2021-08-27 NULL6 2021-08-05 2021-08-0178 2021-NULL-0178 2021-08-0178 2021-08-0178 -01 2021-08-0310 2021-08-02 2021-08-0411 2021-08-29 2021-08-3112 2021-08-30 2021-09-0213 2031-08-1 2021-08-1 -31 2021-09-03

Pokud do definice pohledu přidáte CHECK OPTION, příkazy INSERT a UPDATE proti pohledu budou odmítnuty. K použití této změny použijte následující kód:

VYTVOŘIT NEBO ZMĚNIT ZOBRAZENÍ dbo.RecentOrdersAS SELECT TOP (5) orderid, orderdate, shippeddate FROM dbo.Orders ORDER BY orderdate DESC, orderid DESC WITH CHECK OPTION;GO

Zkuste přidat objednávku prostřednictvím zobrazení:

INSERT INTO dbo.RecentOrders(id objednávky, datum objednávky, datum odeslání) VALUES(15, '20210801', '20210805');

Zobrazí se následující chyba:

Msg 4427, Level 16, State 1, Line 247
Nelze aktualizovat pohled "dbo.RecentOrders", protože on nebo pohled, na který odkazuje, byl vytvořen pomocí WITH CHECK OPTION a jeho definice obsahuje klauzuli TOP nebo OFFSET.

SQL Server se zde nesnaží být příliš chytrý. Změnu odmítne, i když se řádek, který se pokoušíte vložit, stane v tomto okamžiku platnou součástí pohledu. Zkuste například přidat objednávku s novějším datem, která by v tuto chvíli spadala mezi 5 nejlepších:

INSERT INTO dbo.RecentOrders(id objednávky, datum objednávky, datum odeslání) VALUES(15, '20210904', '20210906');

Pokus o vložení je stále odmítnut s následující chybou:

Msg 4427, Level 16, State 1, Line 254
Nelze aktualizovat pohled "dbo.RecentOrders", protože on nebo pohled, na který odkazuje, byl vytvořen pomocí WITH CHECK OPTION a jeho definice obsahuje klauzuli TOP nebo OFFSET.

Zkuste aktualizovat řádek prostřednictvím zobrazení:

UPDATE dbo.RecentOrders SET shippeddate =DATEADD(den, 2, datum objednávky);

V tomto případě je pokus o změnu také odmítnut s následující chybou:

Msg 4427, Level 16, State 1, Line 260
Nelze aktualizovat pohled "dbo.RecentOrders", protože on nebo pohled, na který odkazuje, byl vytvořen pomocí WITH CHECK OPTION a jeho definice obsahuje klauzuli TOP nebo OFFSET.

Uvědomte si, že definování pohledu na základě dotazu pomocí TOP nebo OFFSET-FETCH a CHECK OPTION povede k nedostatečné podpoře příkazů INSERT a UPDATE prostřednictvím pohledu.

Odstranění prostřednictvím takového zobrazení je podporováno. Spuštěním následujícího kódu odstraníte všech pět posledních objednávek:

DELETE FROM dbo.RecentOrders;

Příkaz byl úspěšně dokončen.

Dotaz na tabulku:

SELECT * FROM dbo.Orders;

Po smazání objednávek s ID 8, 11, 12, 13 a 14 získáte následující výstup.

id objednávky datum odeslání----------- ---------- ------------1 2021-08-02 2021-08-042 2021 -08-02 2021-08-053 2021-08-04 2021-08-064 2021-08-26 NULL5 2021-08-27 NULL6 2021-08-05 2021-08-0179 2021-08-0179 2021-0179 2021-08 0310 2021-08-02 2021-08-04

V tomto okamžiku spusťte následující kód pro vyčištění před spuštěním příkladů v další části:

DELETE FROM dbo.Orders WHERE orderid> 5; ZRUŠIT ZOBRAZENÍ, POKUD EXISTUJE dbo.RecentOrders;

Připojuje se

Aktualizace pohledu, který spojuje více tabulek, je podporována, pokud je změnou ovlivněna pouze jedna z podkladových základních tabulek.

Zvažte následující pohled spojující Orders a OrderDetails jako příklad:

VYTVOŘIT NEBO ZMĚNIT ZOBRAZENÍ dbo.OrdersOrderDetailsJAK VYBERTE ID objednávky, datum objednávky, datum odeslání, ID produktu, OD.množství, OD.jednotková cena, sleva OD dbo.Objednávky JAKO VNITŘNÍ PŘIPOJENÍ dbo ASObjednávkaDetail OD ON O.orderid =OD.orderid;GO

Pokuste se vložit řádek skrz pohled, aby byly ovlivněny obě základní základní tabulky:

INSERT INTO dbo.OrdersOrderDetails(id objednávky, datum objednávky, datum odeslání, id produktu, množství, jednotková cena, sleva) VALUES(6, '20210828', NULL, 1001, 5, 10,50, 0,05);

Zobrazí se následující chyba:

Msg 4405, Level 16, State 1, Line 306
Zobrazení nebo funkce 'dbo.OrdersOrderDetails' nelze aktualizovat, protože úprava ovlivňuje více základních tabulek.

Pokuste se vložit řádek skrz zobrazení, takže bude ovlivněna pouze tabulka Objednávky:

INSERT INTO dbo.OrdersOrderDetails(id objednávky, datum objednávky, datum odeslání) VALUES(6, '20210828', NULL);

Tento příkaz se úspěšně dokončí a řádek se vloží do podkladové tabulky Objednávky.

Ale co když chcete mít také možnost vložit řádek skrz pohled do tabulky OrderDetails? S aktuální definicí pohledu to není možné (místo spouštěčů stranou), protože pohled vrací sloupec orderid z tabulky Orders a ne z tabulky OrderDetails. Stačí, že jeden sloupec z tabulky OrderDetails, který nemůže nějak automaticky získat svou hodnotu, není součástí zobrazení, aby se zabránilo vkládání do OrderDetails prostřednictvím zobrazení. Samozřejmě se můžete vždy rozhodnout, že zobrazení bude zahrnovat jak orderid z Orders, tak orderid z OrderDetails. V takovém případě budete muset dvěma sloupcům přiřadit různé aliasy, protože záhlaví tabulky reprezentované pohledem musí mít jedinečné názvy sloupců.

Pomocí následujícího kódu upravte definici pohledu tak, aby zahrnovala oba sloupce, aliasem jeden z Orders jako O_orderid a jeden z OrderDetails jako OD_orderid:

VYTVOŘIT NEBO ZMĚNIT ZOBRAZENÍ dbo.OrdersOrderDetailsAS SELECT O.orderid AS O_orderid, O.orderdate, O.shippeddate, OD.orderid AS OD_orderid,OD.productid, OD.qty, OD.jednotková cena, OD.sleva OD dbo. AS O INNER JOIN dbo.OrderDetails AS OD ON O.orderid =OD.orderid;GO

Nyní můžete v zobrazení vkládat řádky buď do Orders nebo OrderDetails, podle toho, ze které tabulky pochází seznam cílových sloupců. Zde je příklad vložení několika řádků objednávky spojených s objednávkou 6 prostřednictvím zobrazení do OrderDetails:

INSERT INTO dbo.OrdersOrderDetails(OD_orderid, productid, množství, jednotková cena, sleva) VALUES(6, 1001, 5, 10,50, 0,05), (6, 1002, 5, 20,00, 0,05);

Řádky byly úspěšně přidány.

Dotaz na zobrazení:

SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid =6;

Získáte následující výstup:

O_orderid datum objednávky datum odeslání OD_orderid productid množství jednotková cena sleva----------- ---------- ----------- ------- ---- ----------- ---- ---------- ---------6 2021-08-28 NULL 6 1001 5 10,50 0,05006 2021-08-28 NULL 6 1002 5 20,00 0,0500

Podobné omezení platí pro příkazy UPDATE prostřednictvím pohledu. Aktualizace jsou povoleny, pokud je ovlivněna pouze jedna podkladová základní tabulka. Ale můžete odkazovat na sloupce z obou stran v příkazu, pokud se upraví pouze jedna strana.

Jako příklad, následující příkaz UPDATE prostřednictvím zobrazení nastavuje datum objednávky na řádku, kde je ID objednávky řádku objednávky 6 a ID produktu je 1001 až „20210901:“

UPDATE dbo.OrdersOrderDetails SET orderdate ='20210901' WHERE OD_orderid =6 AND productid =1001;

Tento příkaz budeme nazývat Update statement 1.

Aktualizace se úspěšně dokončí s následující zprávou:

(dotčen 1 řádek)

Zde je důležité poznamenat, že filtry příkazů podle prvků z tabulky OrderDetails, ale upravený sloupec orderdate je z tabulky Orders. Takže v plánu, který SQL Server sestaví pro tento příkaz, musí zjistit, které objednávky je třeba upravit v tabulce Objednávky. Plán tohoto prohlášení je znázorněn na obrázku 1.

Obrázek 1:Plán pro prohlášení aktualizace 1

Jak plán začíná, můžete vidět filtrováním OrderDetails podle orderid =6 a productid =1001 a Orders side by orderid =6, čímž se oba spojí. Výsledkem je pouze jeden řádek. Jedinou relevantní částí této aktivity je zachovat, která ID objednávek v tabulce Objednávky představují řádky, které je třeba aktualizovat. V našem případě je to objednávka s ID objednávky 6. Operátor Compute Scalar navíc připraví člen s názvem Expr1002 s hodnotou, kterou příkaz přiřadí sloupci orderdate cílové objednávky. Poslední část plánu s operátorem Clustered Index Update aplikuje aktuální aktualizaci na řádek v Orders with order ID 6, přičemž jeho hodnotu orderdate nastaví na Expr1002.

Klíčovým bodem, který je zde třeba zdůraznit, je aktualizace pouze jednoho řádku s orderid 6 v tabulce Orders. Přesto má tento řádek dvě shody ve výsledku spojení s tabulkou OrderDetails – jednu s ID produktu 1001 (které původní aktualizace filtrovala) a druhou s ID produktu 1002 (které původní aktualizace nefiltrovala). V tomto okamžiku se dotazujte na zobrazení a filtrujte všechny řádky s ID objednávky 6:

SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid =6;

Získáte následující výstup:

O_orderid datum objednávky datum odeslání OD_orderid productid množství jednotková cena sleva----------- ---------- ----------- ------- ---- ----------- ---- ---------- ---------6 2021-09-01 NULL 6 1001 5 10,50 0,05006 2021-09-01 NULL 6 1002 5 20,00 0,0500

Oba řádky zobrazují nové datum objednávky, i když původní aktualizace filtrovala pouze řádek s ID produktu 1001. Opět by se to mělo zdát naprosto rozumné, pokud víte, že komunikujete s pohledem, který spojuje dvě základní tabulky pod kryty, ale může vypadat velmi divně, pokud si to neuvědomujete.

Je zajímavé, že SQL Server dokonce podporuje nedeterministické aktualizace, kde více zdrojových řádků (v našem případě z OrderDetails) odpovídá jednomu cílovému řádku (v našem případě v Orders). Teoreticky by jedním ze způsobů řešení takového případu bylo jeho odmítnutí. S příkazem MERGE, kde více zdrojových řádků odpovídá jednomu cílovému řádku, SQL Server skutečně odmítne pokus. Ale ne s AKTUALIZACÍ založenou na spojení, ať už přímo nebo nepřímo prostřednictvím pojmenovaného tabulkového výrazu, jako je pohled. SQL Server to jednoduše zpracuje jako nedeterministickou aktualizaci.

Zvažte následující příklad, který budeme označovat jako Příkaz 2:

UPDATE dbo.OrdersOrderDetails SET orderdate =CASE WHEN unitprice>=20,00 THEN '20210902' ELSE '20210903' END WHERE OD_orderid =6;

Snad mi odpustíte, že je to vymyšlený příklad, ale ilustruje podstatu věci.

V pohledu jsou dva kvalifikující řádky, které představují dva kvalifikující řádky řádků zdrojové objednávky ze základní tabulky OrderDetails. V základní tabulce objednávek je však pouze jeden kvalifikující cílový řádek. Navíc v jednom zdrojovém řádku OrderDetails vrací přiřazený výraz CASE jednu hodnotu ('20210902') a ve druhém zdrojovém řádku OrderDetails vrací jinou hodnotu ('20210903'). Co by měl SQL Server v tomto případě dělat? Jak již bylo zmíněno, podobná situace s příkazem MERGE by vedla k chybě a odmítnutí pokusu o změnu. S příkazem UPDATE si SQL Server jednoduše hodí mincí. Technicky se to provádí pomocí interní agregační funkce nazvané ANY.

Naše aktualizace byla úspěšně dokončena a hlásíme 1 dotčený řádek. Plán tohoto prohlášení je znázorněn na obrázku 2.


Obrázek 2:Plán pro prohlášení aktualizace 2

Ve výsledku spojení jsou dva řádky. Tyto dva řádky se stanou zdrojovými řádky pro aktualizaci. Ale pak agregační operátor používající funkci ANY vybere jednu (libovolnou) hodnotu orderid a jednu (libovolnou) hodnotu jednotkové ceny z těchto zdrojových řádků. Oba zdrojové řádky mají stejnou hodnotu orderid, takže správné pořadí bude upraveno. Ale v závislosti na tom, kterou ze zdrojových hodnot jednotkové ceny nakonec vybere JAKÝKOLI agregát, určí, jakou hodnotu vrátí výraz CASE, aby se pak použil jako aktualizovaná hodnota orderdate v cílové objednávce. Určitě můžete vidět argument proti podpoře takové aktualizace, ale je plně podporována v SQL Server.

Zeptejme se zobrazení, abychom viděli výsledek této změny (nyní je čas vsadit na výsledek):

SELECT * FROM dbo.OrdersOrderDetails WHERE O_orderid =6;

Mám následující výstup:

O_orderid datum objednávky datum odeslání OD_orderid productid množství jednotková cena sleva----------- ---------- ----------- ------- ---- ----------- ---- ---------- ---------6 2021-09-03 NULL 6 1001 5 10,50 0,05006 2021-09-03 NULL 6 1002 5 20,00 0,0500

Pouze jedna ze dvou hodnot zdrojové jednotkové ceny byla vybrána a použita k určení data objednávky jediné cílové objednávky, přesto se při dotazu na pohled hodnota orderdate opakuje pro oba odpovídající řádky objednávky. Jak si můžete uvědomit, výsledkem mohlo být stejně dobře jiné datum (2021-09-02), protože volba hodnoty jednotkové ceny byla nedeterministická. Šílené věci!

Za určitých podmínek jsou tedy povoleny příkazy INSERT a UPDATE prostřednictvím pohledů, které spojují více podkladových tabulek. Smazání však proti takovýmto pohledům není povoleno. Jak může SQL Server zjistit, která ze stran má být cílem pro odstranění?

Zde je pokus o použití takového odstranění prostřednictvím zobrazení:

DELETE FROM dbo.OrdersOrderDetails WHERE O_orderid =6;

Tento pokus byl odmítnut s následující chybou:

Msg 4405, Level 16, State 1, Line 377
Zobrazení nebo funkce 'dbo.OrdersOrderDetails' nelze aktualizovat, protože úprava ovlivňuje více základních tabulek.

V tomto okamžiku spusťte následující kód pro vyčištění:

DELETE FROM dbo.OrderDetails WHERE orderid =6;DELETE FROM dbo.Orders WHERE orderid =6;DOP ZOBRAZENÍ, POKUD EXISTUJE dbo.OrdersOrderDetails;

Odvozené sloupce

Další omezení úprav prostřednictvím pohledů souvisí s odvozenými sloupci. Pokud je sloupec zobrazení výsledkem výpočtu, SQL Server se nepokusí zpětně analyzovat jeho vzorec, když se pokusíte vložit nebo aktualizovat data prostřednictvím zobrazení – spíše takové úpravy odmítne.

Zvažte následující pohled jako příklad:

VYTVOŘIT NEBO ZMĚNIT ZOBRAZENÍ dbo.OrderDetailsNetPriceAS SELECT orderid, productid, množstevní, jednotková cena * (1,0 – sleva) JAKO čistá jednotková cena, sleva OD dbo.OrderDetails;GO

Zobrazení vypočítá sloupec netunitprice na základě podkladových sloupců tabulky OrderDetails unitprice a diskont.

Dotaz na zobrazení:

SELECT * FROM dbo.OrderDetailsNetPrice;

Získáte následující výstup:

id produktu číslo objednávky množství nettunitprice sleva----------- ----------- ----------- --------- ---- --------- 1 1001 5 9,975000 0,05001 1004 2 20,000000 0,00002 1003 1 47,691000 0,10003 1001 1 9,975000 0,05003 1003 2 49,491000 0,10004 1001 1006 2 11,316000 0,0800

Zkuste vložit řádek skrz zobrazení:

INSERT INTO dbo.OrderDetailsNetPrice(id objednávky, id produktu, množství, netunitprice, sleva) VALUES(1, 1005, 1, 28,595, 0,05);

Teoreticky můžete zjistit, jaký řádek je třeba vložit do podkladové tabulky OrderDetails reverzním inženýrstvím hodnoty jednotkové ceny základní tabulky z hodnot nettunitprice a diskontu zobrazení. SQL Server se o takové zpětné inženýrství nepokouší, ale pokus o vložení odmítne s následující chybou:

Zpráva 4406, úroveň 16, stav 1, řádek 412
Aktualizace nebo vložení pohledu nebo funkce 'dbo.OrderDetailsNetPrice' se nezdařilo, protože obsahuje odvozené nebo konstantní pole.

Zkuste z vložení vynechat vypočítaný sloupec:

INSERT INTO dbo.OrderDetailsNetPrice(ID objednávky, ID produktu, množství, sleva) VALUES(1, 1005, 1, 0,05);

Nyní se vracíme k požadavku, že součástí vložení musí být všechny sloupce z podkladové tabulky, které nějakým způsobem nezískají své hodnoty automaticky, a zde nám chybí sloupec jednotkové ceny. Toto vložení se nezdaří s následující chybou:

Msg 515, Level 16, State 2, Line 421
Cannot insert the value NULL into column 'unitprice', table 'tempdb.dbo.OrderDetails'; column does not allow nulls. INSERT fails.

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.OrdersAS 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;

Shrnutí

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.


  1. Mýty o výkonu:Zkrácení nelze vrátit zpět

  2. Jak využít WebDev.WebServer.exe (VS Web Server) v x64?

  3. Jak nasadit Teamcity s PostgreSQL pro vysokou dostupnost

  4. Jak DATE_SUB() funguje v MariaDB