比較運算子 (續)
Demonstration:如何在多重值子查詢使用比較運算子
子查詢 select * from sales.orders where orderid in ( select orderid from sales.orderdetails where productid in ( select productid from Production.Products where categoryid = 1 ) );
聯結 select o.* from sales.orders o join sales.orderdetails od on od.orderid = o.orderid join Production.Products p on p.productid = od.productid where p.categoryid = 1;
子查詢 1 select CustID, CompanyName from sales.Customers where custid not in ( select distinct custid from sales.orders );
聯結 1 select c.CustID, c.CompanyName from sales.customers c left join sales.orders o on o.custid = c.custid where o.custid is null; go
子查詢 2 (改傳回客戶的所有欄位資料) select * from sales.Customers where custid not in ( select distinct custid from sales.orders );
聯結 2 (改傳回客戶的所有欄位資料) select * from sales.customers c left join sales.orders o on o.custid = c.custid where o.custid is null; go
子查詢 3 (改用存在測試運算子) select CustID, CompanyName from Sales.Customers c where not exists ( select * from Sales.Orders o where c.custid=o.custid );
子查詢 4 (改用 COUNT(*) = 0) select CustID, CompanyName from Sales.Customers c where ( select count(*) from Sales.Orders o where c.custid=o.custid ) = 0;
注意事項: 子查詢 3 和 子查詢 4 必須使用相互關聯子查詢的寫法才更有意義,後續課程會說明 若只在乎子查詢有或沒有資料,使用 EXISTS 存在測試運算子是不錯的選擇 使用 COUNT(*) = 0 也能達到類似的效果
子查詢 select * from hr.Employees where salary > any ( select salary from hr.Employees where empid in (5, 6, 7) );
子查詢 1 select * from sales.orders where orderid < all ( select orderid from sales.OrderDetails where productid in ( select productid from Production.Products where supplierid = ( select supplierid from Production.Suppliers where companyname = 'Supplier STUAZ' ) ) );
子查詢 2 select * from sales.orders where orderid < all ( select orderid from sales.OrderDetails where productid in ( select productid from Production.Products where supplierid = ( select supplierid from Production.Suppliers where companyname = 'Supplier STUAZ' ) ) order by orderid offset 0 rows fetch next 1 rows only );
子查詢 3 + 聯結 select * from sales.orders where orderid < all ( select od.orderid from sales.OrderDetails od join Production.Products p on p.productid = od.productid join Production.Suppliers s on s.supplierid = p.supplierid where s.companyname = 'Supplier STUAZ' order by od.orderid offset 0 rows fetch next 1 rows only );
INSERT INTO Production.Products(productname, supplierid, categoryid, listprice, discontinued, InStock) VALUES(N'Product LUNZZ', 15, 2, 13.30, 0, 10);
子查詢 select * from Production.Products where listprice < any ( select listprice from Production.Products where productname = N'Product LUNZZ' ) and productname = N'Product LUNZZ';
聯結 1 (自我聯結 Self-Join) select p2.* from Production.Products p1 join Production.Products p2 on p2.productname = p1.productname where p2.listprice < p1.listprice and p1.productname = N'Product LUNZZ';
聯結 2 (自我聯結 Self-Join) select p2.* from Production.Products p1 join Production.Products p2 on p2.productname = p1.productname where p2.listprice < p1.listprice and p1.productname = N'Product LUNZZ' and p2.productname = N'Product LUNZZ';
聯結 3 (自我聯結 Self-Join) select p2.* from Production.Products p1 join Production.Products p2 on p2.productname = p1.productname where p2.listprice < p1.listprice;
ANSWER: select top 3 orderid from sales.OrderDetails where orderid IN ( select orderid from sales.orders where empid = ( select empid from hr.Employees where lastname = N'Funk' ) and year(orderdate) = 2007 ) group by orderid order by SUM(qty * unitprice) desc
傳回訂單所有欄位資料
曾經有問題的產品之訂單要持續追踪 (有瑕疵產品之後的訂單)
必須是已訂講該供應商產品的訂單
找出這些爆發瑕疵產品之後的近 4 個月內 (6月份到9月份) 的高風險訂單
NSWER: select * from sales.Orders where orderid IN ( select distinct orderid from sales.OrderDetails where productid IN ( select productid from Production.Products where supplierid = ( select supplierid from [Production].[Suppliers] where [companyname] = N'Supplier SVIYA' ) ) ) and orderdate >= '20070601' and orderdate < '20071001'
依照設計邏輯分類,可分成以下兩種類型:
自包含子查詢 Self-Contained Subqueries (又稱為獨立子查詢) :
相互關聯子查詢 Correlated Subqueries:
注意事項: 巢狀層級不得超過 32 層,此限制仍將取決於可用的記憶體,以及查詢中其他運算式的複雜性
自包含子查詢 Self-Contained Subqueries
相互關聯子查詢 Correlated Subqueries
注意事項:(相互關聯子查詢 和 聯結 的比較) 從設計邏輯的可讀性來說,在多重資料表 (至少 4 個以上的資料表) 的關聯條件和篩選條件的部份: 開發人員在解讀相互關聯子查詢的設計邏輯時,資料的流向會較為清楚,因為有模組化的感覺,有利於未來的修改作業 開發人員在解讀聯結的設計邏輯時,因為都寫在一起(擠成一團),這種比較不利於未來的修改作業,且要花比較長的時間去釐清 從執行效能來說,兩者是不分軒輊的,但相互關聯子查詢還是有機會可以再超越聯結
基本語法: SELECT <select-list> FROM 表1 WHERE 表1.欄名 = ( SELECT <select-list> <-- 傳回單一值 Scalar Value FROM 表2 WHERE 表2.欄名x = 表1.欄名x <-- 可看成是聯結 JOIN 的關聯條件 (欄名對欄名) );
Demonstration:如何使用 相互關聯子查詢 Correlated Subqueries
子查詢 1 select empid, orderid, orderdate from Sales.Orders O1 where orderdate = ( select max(orderdate) from Sales.Orders O2 where O2.empid = O1.empid ) order by empid, orderdate;
子查詢 2 select empid, orderid, orderdate from Sales.Orders O1 where orderdate = ( select orderdate from Sales.Orders O2 where O2.empid = O1.empid order by orderdate desc offset 0 rows fetch next 1 rows only ) order by empid, orderdate;
注意事項 來自外部查詢的 O1.empid,出現在內部查詢中 留意資料表別名並未全程標示 共 10 位員工,只有 9 位員工曾接訂單
範例 B:查詢 2007 年 5 月份的訂單明細筆數大於 4 的訂單,並傳回該客戶的 OrderID, CompanyName
子查詢 + 聯結 select o.orderid, c.companyname from sales.orders o join sales.Customers c on c.custid = o.custid where o.orderdate between '20070501' and '20070531' and o.orderid in ( select od.orderid from sales.OrderDetails od where od.orderid = o.orderid group by od.orderid having count(*) > 4 );
聯結 select o.orderid, c.companyname from sales.orders o join sales.OrderDetails od on od.orderid = o.orderid join sales.Customers c on c.custid = o.custid where o.orderdate between '20070501' and '20070531' group by o.orderid, c.companyname having count(*) > 4;
Exercises:使用和實作 相互關聯子查詢 Correlated Subqueries
ANSWER: select * from sales.Orders where orderid IN ( select orderid from [Sales].[OrderDetails] od where productid IN ( select productid from [Production].[Products] p where p.listprice = od.unitprice --< 非典型 ) ) select * from sales.Orders where orderid IN ( select orderid from [Sales].[OrderDetails] od where unitprice = ( select listprice from [Production].[Products] p where p.productid = od.productid --< 典型 ) )
ANSWER: select * from Production.Suppliers where supplierid IN ( select supplierid from Production.Products p where productid IN ( select productid from sales.OrderDetails od where od.unitprice = p.listprice ) )
典型子查詢是在外部查詢的 WHERE 子句,事實上,子查詢可用在 SELECT、INSERT、UPDATE 或 DELETE 陳述式中,甚至任何可執行子查詢有效的陳述式中
從外部查詢 WHERE 以外的子句來看,子查詢還能應用的類型有:
注意事項: 子查詢傳回「單一值」適合用於「取代運算式」。 子查詢傳回「多重值」適合用於「衍生資料表」。
取代運算式
傳回「單一值」的子查詢,幾乎可以用在 T-SQL 任何允許運算式的子句中
位於 SELECT 的子查詢,可解決典型子查詢 (位於外部查詢 WHERE 子句) 的資料行無法顯示在外部查詢結果集的問題
若取代運算式傳回空值 NULL,除非來源資料本來就是空值 NULL,否則一定是相互關聯子查詢的篩選條件(關聯條件)不成立所導致
基本語法: SELECT (子查詢) ...省略... ORDER BY (子查詢);
注意事項 SELECT 子句有子查詢時,要注意 GROUP BY 的限制 ORDER BY 子句使用子查詢,可做出部份排序的效果,而不是整個資料表全部排序 子查詢若傳回空的結果集,會轉換成空值 NULL ORDER BY 會對空值 NULL 排序在一起並置頂或置末端 搭配「相互關聯子查詢 (Correlated Subquery)」才會更有意義
Demonstration:如何應用子查詢在取代運算式中
子查詢 (位於 SELECT 的子查詢) select O.OrderID, O.OrderDate, ( select max(unitprice) from sales.OrderDetails OD where OD.orderid = O.orderid ) 產品最高售價 from sales.orders O;
注意事項: 位於 SELECT 的子查詢可解決位於 WHERE 子查詢的結果無法顯示在外部查詢的問題
聯結 select O.OrderID, O.OrderDate, max(OD.unitprice) 產品最高售價 from sales.orders O join sales.OrderDetails OD on OD.orderid = O.orderid group by O.OrderID, O.OrderDate;
子查詢 (位於 select 的子查詢,完全以子查詢設計,重要) select o.orderid, ( select companyname from sales.customers c where c.custid = o.custid ) CompanyName from sales.orders o where o.orderdate between '20070501' and '20070531' and o.orderid in ( select od.orderid from sales.OrderDetails od where od.orderid = o.orderid group by od.orderid having count(*) > 4 );
子查詢 (位於 order by 的子查詢) select e1.Lastname, e1.EmpID from hr.Employees e1 order by ( select e2.empid from hr.Employees e2 where e2.empid = e1.empid and e2.empid between 4 and 7 );
注意事項: 那些子查詢範圍以外的記錄,因傳回 NULL 而排序在前面,這是 SQL Server 的特性 有條件的排序,可以再透過 COALESCE() 或 CASE…WHEN… 或 IIF() 解決
使用 IIF() 升序排序特定員工編號,其它的將其置頂 select Lastname, EmpID from HR.Employees order by iif(empid between 4 and 7, empid, 0);
注意事項: 若要將符合條件的置頂,只要將 0 改為大於現有員工編號的數值即可
升序排序主管編號並將 NULL 置末 select lastname, mgrid from HR.Employees order by iif(mgrid is null, 999, mgrid);
注意事項: 使用 999 是因為沒有一位主管編號如此大,所以只要大於現有主管編號的數值即可
Exercises:使用和實作 取代運算式的子查詢
1.查詢供應商國籍來自 Japan 所有產品的訂單,以及訂單明細資料表中各單價(UnitPrice)小於產品資料表的各單價(ListPrice)的資料,並傳回訂單的 (非明細):
ANSWER: select OrderID, sum(SubTotal) SubTotal, (select orderdate from sales.orders o where o.orderid = od.orderid) OrderDate from sales.OrderDetails od where productid IN ( select productid from Production.Products where supplierid IN ( select supplierid from Production.Suppliers where country = N'Japan' ) ) and unitprice < ( select listprice from Production.Products p where p.productid = od.productid ) group by orderid
2.根據檢視表 Sales.EmpOrders 傳回的結果集,請統計出 2007 年各員工的最高和最低的銷售業績 (統計 val 欄位),並傳回下列欄位:
ANSWER: declare @y smallint = 2007 select *, ( select max(val) from Sales.EmpOrders e2 where e2.empid = e1.empid and year([ordermonth]) = @y ) 最高業績, ( select min(val) from Sales.EmpOrders e2 where e2.empid = e1.empid and year([ordermonth]) = @y ) 最低業績 from Sales.EmpOrders e1 where year([ordermonth]) = @y order by empid, ordermonth
傳回「多重值」的子查詢,幾乎可以用在 T-SQL 任何允許 <Table_Source> 的子句中。
從外部查詢 WHERE 以外的子句來看,子查詢還能用在 SELECT 的 FROM 子句的位置,所以,「衍生資料表」只是從 SELECT 導出來的表格。
基本語法: SELECT 選取清單 FROM (子查詢) as 資料表別名 WHERE ...省略...;
注意事項 只是將子查詢傳回的結果集加上短名,就變成虛擬資料表。 衍生資料表只存在於指令執行期間,不佔用資料庫空間,所以不能重複使用。
Demonstration:如何應用子查詢在衍生資料表中
聯結一個衍生資料表 select vt.orderid, c.companyname from sales.Customers c join ( select o.orderid, o.custid from sales.orders o join sales.OrderDetails od on od.orderid = o.orderid where o.orderdate between '20070501' and '20070531' group by o.orderid, o.custid having count(*) > 4 ) vt on vt.custid = c.custid;
不使用聯結 1 select CustID, MAX(SubQTY) QTY from ( select o.CustID, ( select sum( od.qty ) from sales.OrderDetails od where od.orderid = o.orderid ) SubQTY from sales.orders o ) vt group by custid order by custid;
聯結一個衍生資料表 2 select CustID, MAX(qty) QTY from ( SELECT O.custid, SUM(OD.qty) AS qty FROM Sales.Orders AS O INNER JOIN Sales.OrderDetails AS OD ON OD.orderid = O.orderid GROUP BY O.custid ) vt group by custid order by custid;
完全使用聯結 (不使用衍生資料表) 3 SELECT o.CustID, MAX(od.qty) QTY FROM Sales.Orders o join Sales.OrderDetails od on od.orderid = o.orderid GROUP BY custid order by custid;
Exercises:使用和實作 衍生資料表的子查詢
1.根據前面「典型子查詢 Self-Contained Subquery」的範例,查詢高於平均薪資的所有員工資料,請改以「衍生資料表」結合「聯結 JOIN」方式來設計 (將子查詢從 WHERE 移到 FROM)
ANSWER: select * from hr.Employees e join ( select avg(salary) avgSalary from hr.Employees ) dt on e.salary > dt.avgSalary
2.根據前面「典型子查詢 Self-Contained Subquery」的範例,查詢哪些訂單,是在客戶名稱 Customer UMTLM 的最後一天訂單之後才產生的,傳回那些訂單的所有欄位資料,請改以「衍生資料表」結合「聯結 JOIN」方式來設計 (將子查詢從 WHERE 移到 FROM)
ANSWER: select * from sales.orders o join ( select max(orderdate) maxOrderdate from sales.orders where custid = ( select custid from sales.Customers where companyname = 'Customer UMTLM' ) ) dt on o.orderdate > dt.maxOrderdate
何謂「資料表運算式」 (Table Expression):
它不是標準的「使用者自訂資料表」,本身也不儲存用戶資料,各位將會發現這些物件都是僅由 SELECT 定義出來的,所以傳回的結果集也是由 SELECT 所導出來的表格。
可內含運算式,它是計算公式的一部分,例如計算年薪基本公式 SALARY * 12,當更新基礎 (基底) 資料表時,運算式中的值都會進行評估,所以可視為「內含運算式的資料表」。
有些還支援輸入參數到資料表運算式中,以滿足不同運算需求。
資料表運算式皆為具名的資料表運算式 Named Table Expression,也就是說必須指定名稱給它。
資料表運算式分為:
衍生資料表 Derived Table
檢視表 View
資料表值函式(數) Table-Valued Function (TVF)
通用資料表運算式 Common Table Expression (CTE)
資料表運算式比較表:
資料表運算式 | 傳入值 | 儲存於資料庫 | 重複使用 | SELECT | INSERT | UPDATE | DELETE |
---|---|---|---|---|---|---|---|
衍生資料表 | 變數,由外部 DECLARE | N | Y | N | N | N | N |
檢視表 | None | Y | Y | Y | Y | Y | Y |
資料表值函式(數) | 變數,本體結構的一部份 | Y | Y | Y | Y | Y | Y |
通用資料表運算式 | 變數,由外部 DECLARE | N | Y | Y | Y | Y | Y |
注意事項: 僅允許來自一個基底資料表 不允許來自函數產生的資料行,例如:針對 SUM() 傳回的值之資料行執行 UPDATE 指令
何謂「衍生資料表」(Derived Table):
不同於典型子查詢的是:
注意事項: 支援輸入參數值到資料表運算式,做法是利用外部已宣告的變數,並用於衍生資料表的運算式中 衍生資料表前面課程已討論過,就不再贅述
資料表是根據資料模型設計流程所設計出來的物件,而檢視表是從使用者觀點來設計,例如一般員工看的資料和主管看的資料一定不相同,甚至同一個部門不同職位的一般員工之間,他們所看的資料也會不一樣,也因為檢視表不支援「輸入參數值」,某些情形下,必須指定不同常數值在各個檢視表中,所以檢視表的數量會比資料表數量多
因為檢視表不支援「輸入參數值」,所以 SELECT 查詢的篩選條件幾乎都是指定「常數」,例如:WHERE empid = 3
檢視表和資料表的相似之處:
檢視表和資料表的不同之處:
檢視表中的 SELECT 查詢語法所參考的資料表支援:
檢視表的好處:
注意事項: 若從網管角度來看,檢視表可視為「防火牆」一樣地只允許特定網路流量通過 ,這相當於檢視表隱藏敏感性欄位只顯示特定欄位,因此可以保護基底資料表
注意事項 (檢視表和衍生資料表比較) 衍生資料表只是非具名的資料表運算式,相關限制和檢視表相同 就使用變數來說: 檢視表:不允許包含變數在查詢定義中 衍生資料表:允許包含變數在查詢定義中 就物件的生命週期來說: 檢視表:因為是具體的物件,也儲存在資料庫中,所以可重複使用 衍生資料表:因為是非具體的物件,也沒有儲存在資料庫中,所以不可重複使用
基本語法: CREATE VIEW [結構描述名稱.] 檢視表名稱 [WITH 屬性] AS SELECT ...省略... [WITH CHECK OPTION];
WITH 屬性支援: [ ENCRYPTION ]:加密 select 定義 [ SCHEMABINDING ]:綁定「基底資料表」,以避免它被刪除,導致檢視表無法運作
WITH CHECK OPTION 強制規定對檢視表執行的所有資料修改陳述式,必須遵循 select_statement 所設定的準則 若沒有指定這個屬性,UPDATE、DELETE 仍會遵循 select_statement 所設定的篩選條件,但 INSERT 不受限制 若有指定這個屬性,INSERT 會遵循 select_statement 所設定的篩選條件 可確保已修改的資料在認可之後,仍可以透過檢視表見到資料
Demonstration:檢視表的建立和執行
-- Step 1: 建立來自單一資料表的檢視表 -- Select and execute the following to create a simple view CREATE VIEW HR.EmpPhoneList AS SELECT empid, lastname, firstname, phone FROM HR.Employees; GO -- Select from the new view SELECT empid, lastname, firstname, phone FROM HR.EmpPhoneList; GO -- Step 2: 建立來自多重資料表的檢視表 -- Create a view using a multi-table join CREATE VIEW Sales.OrdersByEmployeeYear AS SELECT emp.empid AS employee , YEAR(ord.orderdate) AS orderyear , SUM(od.qty * od.unitprice) AS totalsales FROM HR.Employees AS emp JOIN Sales.Orders AS ord ON emp.empid = ord.empid JOIN Sales.OrderDetails AS od ON ord.orderid = od.orderid GROUP BY emp.empid , YEAR(ord.orderdate) GO -- Select from the view SELECT employee, orderyear, totalsales FROM Sales.OrdersByEmployeeYear ORDER BY employee, orderyear; -- Step 3: Clean up DROP VIEW Sales.OrdersByEmployeeYear; DROP VIEW HR.EmpPhoneList;
-- START:建立測試環境 use TSQL2; -- 建立「基底資料表」 HR.Copy_Emp drop table if exists HR.Copy_Emp; SELECT empid, lastname, firstname, phone INTO HR.Copy_Emp FROM HR.Employees; GO -- 查詢「基底資料表」 HR.Copy_Emp SELECT * FROM HR.Copy_Emp; GO -- END:建立測試環境 -- START:建立加密的檢視表 -- 使用 ENCRYPTION 屬性建立加密的檢視表 HR.EmpPhoneList CREATE OR ALTER VIEW HR.EmpPhoneList WITH ENCRYPTION AS SELECT empid, lastname, firstname, phone FROM HR.Copy_Emp; GO -- 操作:從「物件總管」觀察檢視表 HR.EmpPhoneList 加密後的狀態 -- END:建立加密的檢視表 -- START:綁定「基底資料表」 -- 使用 SCHEMABINDING 綁定「基底資料表」HR.Copy_Emp CREATE OR ALTER VIEW HR.EmpPhoneList WITH SCHEMABINDING AS SELECT empid, lastname, firstname, phone FROM HR.Copy_Emp; GO -- 嘗試刪除「基底資料表」HR.Copy_Emp DROP TABLE HR.Copy_Emp; GO -- 訊息:無法 DROP TABLE HR.Copy_Emp,因為物件 'EmpPhoneList' 正在參考它。 -- 解除綁定「基底資料表」 CREATE OR ALTER VIEW HR.EmpPhoneList AS SELECT empid, lastname, firstname, phone FROM HR.Copy_Emp; GO -- 嘗試刪除「基底資料表」HR.Copy_Emp DROP TABLE HR.Copy_Emp; GO -- 訊息:命令已成功完成 -- END:綁定「基底資料表」 -- START:強制遵循 select_statement 內所設定的準則 WITH CHECK OPTION -- 再次建立「基底資料表」 HR.Copy_Emp drop table if exists HR.Copy_Emp; SELECT empid, lastname, firstname, phone INTO HR.Copy_Emp FROM HR.Employees; GO -- 建立 select_statement 含準則的檢視表 HR.EmpPhoneList CREATE OR ALTER VIEW HR.EmpPhoneList AS SELECT empid, lastname, firstname, phone FROM HR.Copy_Emp WHERE empid between 3 and 5 WITH CHECK OPTION; GO -- 查詢檢視表 HR.EmpPhoneList SELECT * FROM HR.EmpPhoneList; -- 對檢視表 HR.EmpPhoneList 嘗試 INSERT INSERT INTO HR.EmpPhoneList (lastname, firstname, phone) VALUES (N'大明', N'王', N'123-4567') --訊息:嘗試插入或更新已經失敗,因為目標檢視指定了 WITH CHECK OPTION 或跨越指定了 WITH CHECK OPTION 的檢視,而該作業產生的一個或多個資料列在 CHECK OPTION 條件約束下並不合格。 -- 對檢視表 HR.EmpPhoneList 嘗試 UPDATE UPDATE HR.EmpPhoneList SET phone = N'333-5555' WHERE empid = 8; --訊息:(0 個資料列受到影響) -- 對檢視表 HR.EmpPhoneList 嘗試 DELETE DELETE HR.EmpPhoneList WHERE empid = 8; --訊息:(0 個資料列受到影響) -- 操作:試著移除 WITH CHECK OPTION 再重試對檢視表 HR.EmpPhoneList 嘗試 INSERT、UPDATE、DELETE -- END:強制遵循 select_statement 內所設定的準則 -- 復原變更,與本主題無關 DROP VIEW HR.EmpPhoneList; DROP TABLE HR.Copy_Emp;