1. 程式人生 > >《MSSQL2008技術內幕:T-SQL語言基礎》讀書筆記(下)

《MSSQL2008技術內幕:T-SQL語言基礎》讀書筆記(下)

索引:

二、查詢

三、表表達式

四、集合運算

六、資料修改

七、事務和併發

八、可程式設計物件

五、透視、逆透視及分組

5.1 透視

  所謂透視(Pivoting)就是把資料從行的狀態旋轉為列的狀態的處理。其處理步驟為:

  相信很多人在筆試或面試的時候被問到如何通過SQL實現行轉列或列轉行的問題,可能很多人當時懵逼了,沒關係,下面我們通過例子來理解。

  (1)準備資料

--1.0準備資料
USE tempdb;

IF OBJECT_ID('dbo.Orders', 'U') IS NOT NULL DROP TABLE dbo.Orders;
GO CREATE TABLE dbo.Orders ( orderid INT NOT NULL, orderdate DATE NOT NULL, -- prior to SQL Server 2008 use DATETIME empid INT NOT NULL, custid VARCHAR(5) NOT NULL, qty INT NOT NULL, CONSTRAINT PK_Orders PRIMARY KEY(orderid) ); INSERT
INTO dbo.Orders(orderid, orderdate, empid, custid, qty) VALUES (30001, '20070802', 3, 'A', 10), (10001, '20071224', 2, 'A', 12), (10005, '20071224', 1, 'B', 20), (40001, '20080109', 2, 'A', 40), (10006, '20080118', 1, 'C', 14), (20001, '20080212', 2, 'B', 12), (40005, '20090212', 3, 'A', 10), (20002, '20090216', 1, 'C', 20), (
30003, '20090418', 2, 'B', 15), (30004, '20070418', 3, 'C', 22), (30007, '20090907', 3, 'D', 30); SELECT * FROM dbo.Orders;
View Code

  這裡使用了MS SQL2008的VALUES子句格式語法,這時2008版本的新特性。如果你使用的是2005及以下版本,你需要多個INSERT語句。最後的執行結果如下圖所示:

    

  (2)需求說明

  假設我們要生成一個報表,包含每個員工和客戶組合之間的總訂貨量。用以下簡單的分組查詢可以解決這個問題:

select empid,custid,SUM(qty) as sumqty 
from dbo.Orders
group by empid,custid;

  該查詢的執行結果如下:

  

  不過,假設現在要求要按下表所示的的格式來生成輸出結果:

  

  這時,我們就需要進行透視轉換了!

  (3)使用標準SQL進行透視轉換

  Step1.分組:GROUP BY empid;

  Step2.擴充套件:CASE WHEN custid='A' THEN qty END;

  Step3.聚合:SUM(CASE WHEN custid='A' THEN qty END);

--1.1標準SQL透視轉換
select empid,
    SUM(case when custid='A' then qty end) as A,
    SUM(case when custid='B' then qty end) as B,
    SUM(case when custid='C' then qty end) as C,
    SUM(case when custid='D' then qty end) as D
from dbo.Orders
group by empid;

  執行結果如下圖所示:

  

  (4)使用T-SQL PIVOT運算子進行透視轉換

  自SQL Server 2005開始引入了一個T-SQL獨有的表運算子-PIVOT,它可以對某個源表或表表達式進行操作、透視資料,再返回一個結果表。

  PIVOT運算子同樣涉及前面介紹的三個邏輯處理階段(分組、擴充套件和聚合)以及同樣的透視轉換元素,但使用的是不同的、SQL Server原生的語法

  下面是使用PIVOT運算子實現上面一樣的效果:

select empid,A,B,C,D
from (select empid,custid,qty
      from dbo.Orders) as D
  pivot (sum(qty) for custid in (A,B,C,D)) as P;

  其中,PIVOT運算子的圓括號內要指定聚合函式(本例中SUM)、聚合元素(本例中的qty)、擴充套件元素(custid)以及目標列名稱的列表(本例中的A、B、C、D)。在PIVOT運算子的圓括號後面,可以為結果表制定一個別名。

Tip:使用PIVOT運算子一般不直接把它應用到源表(本例中的Orders表),而是將其應用到一個表表達式(該表表達式只包含透視轉換需要的3種元素,不包含其他屬性。)此外,不需要為它顯式地指定分組元素,也就不需要再查詢中使用GROUP BY子句。

5.2 逆透視

  所謂逆透視(Unpivoting)轉換是一種把資料從列的狀態旋轉為行的狀態的技術,它將來自單個記錄中多個列的值擴充套件為單個列中具有相同值得多個記錄。換句話說,將透視表中的每個源行潛在地轉換成多個行,每行代表源透視表的一個指定的列值。

  還是通過一個栗子來理解:

  (1)首先還是準備一下資料:

USE tempdb;

IF OBJECT_ID('dbo.EmpCustOrders', 'U') IS NOT NULL DROP TABLE dbo.EmpCustOrders;

SELECT empid, A, B, C, D
INTO dbo.EmpCustOrders
FROM (SELECT empid, custid, qty
      FROM dbo.Orders) AS D
  PIVOT(SUM(qty) FOR custid IN(A, B, C, D)) AS P;

SELECT * FROM dbo.EmpCustOrders;
View Code

  下面是對這個表EmpCustOrders的查詢結果:

  

  (2)需求說明

  要求執行你透視轉換,為每個員工和客戶組合返回一行記錄,其中包含這一組合的訂貨量。期望的輸出結果如下圖所示:

  

  (3)標準SQL進行逆透視轉換

  Step1.生成副本:CROSS JOIN 交叉聯接生成多個副本

  Step2.提取元素:通過CASE語句生成qty資料列

  Step3.刪除不相關的交叉:過濾掉NULL值

select *
from (select empid, custid,
        case custid
            when 'A' then A
            when 'B' then B
            when 'C' then C
            when 'D' then D
        end as qty
      from dbo.EmpCustOrders
        cross join (VALUES('A'),('B'),('C'),('D')) as Custs(custid)) as D
where qty is not null;

  執行結果如下圖所示:

   

  (4)T-SQL UNPIVOT運算子進行逆透視轉換

  和PIVOT類似,在SQL Server 2005引入了一個UNPIVOT運算子,它的作用剛好和PIVOT運算子相反,即我們可以拿來做逆透視轉換工作。UNPIVOT同樣會經歷我們上面提到的三個階段。繼續上面的栗子,我們使用UNPIVOT來進行逆透視轉換:

select empid, custid, qty
from dbo.EmpCustOrders
  unpivot (qty for custid in (A,B,C,D)) as U;

  其中,UNPIVOT運算子後邊的括號內包括:用於儲存源表列值的目標列明(這裡是qty),用於儲存源表列名的目標列名(這裡是custid),以及源表列名列表(A、B、C、D)。同樣,在UNPIVOT括號後面也可以跟一個別名。

Tip:對經過透視轉換所得的表再進行逆透視轉換,並不能得到原來的表。因為你透視轉換隻是把經過透視轉換的值再旋轉島另一種新的格式。

5.3 分組

  首先了解一下分組集:分組集就是分組(GROUP BY子句)使用的一組屬性(或列名)。在傳統SQL中,一個聚合查詢只能定義一個分組集。為了靈活而有效地處理分組集,SQL Server 2008引入了幾個重要的新功能(他們都是GROUP BY的從屬子句,需要依賴於GROUP BY子句):

  (1)GROUPING SETS從屬子句

  使用該子句,可以方便地在同一個查詢中定義多個分組集。例如下面,我們定義了4個分組集:(empid,custid),(empid),(custid)和():

--3.1GROUPING SETS從屬子句
select empid,custid,SUM(qty) as sumqty
from dbo.Orders
group by 
  GROUPING SETS
  (
    (empid,custid),
    (empid),
    (custid),
    ()
   );

  這個查詢相當於執行了四個group by查詢的並集。

  (2)CUBE從屬子句

  CUBE子句為定義多個分組集提供了一種更簡略的方法,可以把CUBE子句看作是用於生成分組的冪集。例如:CUBE(a,b,c)等價於GROUPING SETS[(a,b,c),(a,b),(a,c),(b,c),(a),(b),(c),()]。下面我們用CUBE來實現上面的例子:

--3.2CUEE從屬子句
select empid,custid,SUM(qty) as sumqty
from dbo.Orders
group by cube(empid,custid);

  (3)ROLLUP從屬子句

  ROLLUP子句也是一種簡略的方法,只不過它與CUBE不同,它強調輸入成員之間存在一定的層次關係,從而生成讓這種層次關係有意義的所有分組集。例如:CUBE(a,b,c)會生成8個可能的分組集,而ROLLUP則認為3個輸入成員存在a>b>c的層次關係,所以只會生成4個分組集:(a,b,c),(a,b),(a),()。

  下面我們假設想要按時間層次關係:訂單年份>訂單月份>訂單日,以這樣的關係來定義所有分組集,並未每個分組集返回其總訂貨量。可能我們用GROUPING SETS需要4行,然後使用ROLLUP卻只需要一行:group by rollup(YEAR(orderdate),MONTH(orderdate),DAY(orderdate));

  完整SQL查詢如下:

--3.3ROLLUP從屬子句
select
  YEAR(orderdate) as orderyear,
  MONTH(orderdate) as ordermonth,
  DAY(orderdate) as orderday,
  SUM(qty) as sumqty
from dbo.Orders
group by rollup(YEAR(orderdate),MONTH(orderdate),DAY(orderdate));

  執行結果如下圖所示:

   

  (4)GROUPING_ID函式

  如果一個查詢定義了多個分組集,還想把結果行和分組集關聯起來,也就是說,為每個結果行標註它是和哪個分組集關聯的。SQL Server 2008中引入了一個GROUPING_ID函式,簡化了關聯結果行和分組集的處理,可以容易地計算出每一行和哪個分組集相關聯。

  例如,繼續上面的例子,我們想要將empid,custid作為輸入:

select 
  grouping_id(empid,custid) as groupingset,
  empid, custid, SUM(qty) as sumqty
from dbo.Orders
group by cube(empid,custid);
View Code

  執行結果中會出現groupingset為0,1,2,3,分別代表了empid,custid的4個可能的分組集((empid,custid),(empid),(custid),())。

六、資料修改

6.1 插入與刪除資料

6.1.1 看我花式插入資料

  ① INSERT VALUES語句 :這個語句恐怕我們再熟悉不過了把,在任何一本資料庫的書上面都可以看到這個語句的身影。

INSERT INTO dbo.Orders(orderid, orderdate, empid, custid)
  VALUES(10001, '20090212', 3, 'A');

  需要了解的是,前面也提到過,SQL Server 2008增強了VALUES語句的功能,允許在一條語句中指定由逗號分隔開的多行記錄。例如下面的語句向Orders中插入了4行資料:

INSERT INTO dbo.Orders
  (orderid, orderdate, empid, custid)
VALUES
  (10003, '20090213', 4, 'B'),
  (10004, '20090214', 1, 'A'),
  (10005, '20090213', 1, 'C'),
  (10006, '20090215', 3, 'C');
View Code

  ② INSERT SELECT語句 :將一組由SELECT查詢返回的結果行插入到目標表中。

INSERT INTO dbo.Orders(orderid, orderdate, empid, custid)
  SELECT orderid, orderdate, empid, custid
  FROM TSQLFundamentals2008.Sales.Orders
  WHERE shipcountry = 'UK';

  ③ INSERT EXEC語句:將儲存過過程或動態SQL批處理返回的結果集插入目標表。

  下面的示例演示瞭如何執行儲存過程usp_getorders並將結果插入到Orders表中:

INSERT INTO dbo.Orders(orderid, orderdate, empid, custid)
  EXEC TSQLFundamentals2008.Sales.usp_getorders @country = 'France';

  ④ SELECT INTO語句:它會建立一個目標表,並用查詢返回的結果來填充它。需要注意的是:它不是一個標準的SQL語句(即不是ANSI SQL標準的一部分),不能用這個語句向已經存在的表中插入資料

--保證目標表不存在
IF OBJECT_ID('dbo.Orders', 'U') IS NOT NULL DROP TABLE dbo.Orders;

SELECT orderid, orderdate, empid, custid
INTO dbo.Orders
FROM TSQLFundamentals2008.Sales.Orders;

  ⑤ BULK INSERT語句:用於將檔案中的資料匯入一個已經存在的表,需要制定目標表、原始檔以及一些其他的選項。

  下面的栗子演示瞭如何將檔案"C:\testdata\orders.txt"中的資料容量插入(bulk insert)到Orders表,同時還指定了檔案型別為字元格式,欄位終止符為逗號,行終止符為換行符(\t):

BULK INSERT dbo.Orders FROM 'C:\testdata\orders.txt'
  WITH 
    (
       DATAFILETYPE    = 'char',
       FIELDTERMINATOR = ',',
       ROWTERMINATOR   = '\n'
    );

6.1.2 看我花式刪除資料

  ① DELETE語句:標準SQL語句,大家最常見的用法。 

DELETE FROM dbo.Orders
WHERE orderdate < '20070101';

  ② TRUNCATE語句:不是標準的SQL語句,永於刪除表中的所有行,不需要過濾條件。

Tip:TRUNCATE與DELETE在效能上差異巨大,對一個百萬行級記錄的表,TRUNCATE幾秒內就可以解決,而DELETE可能需要幾分鐘。因為TRUNCATE會以最小模式記錄日誌,而DELETE則以完整模式記錄日誌。所以,各位,謹慎使用TRUNCATE。因此,我們可以建立一個虛擬表(Dummy Table),讓虛擬表包含一個指向產品表的外來鍵,這樣就可以保護產品表了。  

  ③ 基於聯接的DELETE:也不是標準SQL語句,可以根據另一個表中相關行的屬性定義的過濾器來刪除表中的資料行。

  例如,下面語句用以刪除美國客戶下的訂單:

DELETE FROM O
FROM dbo.Orders AS O
  JOIN dbo.Customers AS C
    ON O.custid = C.custid
WHERE C.country = N'USA';

  當然,如果要使用標準SQL語句,也可以採用下面的方式:

DELETE FROM dbo.Orders
WHERE EXISTS
  (SELECT *
   FROM dbo.Customers AS C
   WHERE Orders.custid = C.custid
     AND C.country = N'USA');

6.2 更新與合併資料

6.2.1 花式更新資料

  ① UPDATE語句:不解釋了,大家都在用

  下面來看兩個不一樣的栗子,第一個是關於同時操作的性質。看看下面的UPDATE語句:

UPDATE dbo.T1
  SET col1 = col1 + 10, col2 = col1 + 10;

  假設T1表中的col1列為100,col2列為200。在計算後是多少呢?

  答案揭曉:col=110,col=110。

  再來看一個栗子,假設我們要實現兩個數的交換該怎麼做?我們可能迫不及待的說出臨時變數。然而,在SQL中所有賦值表示式好像都是同時計算的,解決這個問題就不需要臨時變量了。

UPDATE dbo.T1
  SET col1 = col2, col2 = col1;

  ② 基於聯接的UPDATE語句:同樣不是SQL標準語法,聯接在此與基於聯接的DELETE一樣是起到過濾作用。

UPDATE OD
  SET discount = discount + 0.05
FROM dbo.OrderDetails AS OD
  JOIN dbo.Orders AS O
    ON OD.orderid = O.orderid
WHERE custid = 1;

  同樣,要使用標準SQL語法的話,可以用子查詢替代聯接:

UPDATE dbo.OrderDetails
  SET discount = discount + 0.05
WHERE EXISTS
  (SELECT * FROM dbo.Orders AS O
   WHERE O.orderid = OrderDetails.orderid
     AND custid = 1);

  ③ 賦值UPDATE:這是T-SQL特有的語法,可以對錶中的資料進行更新的同時為變數賦值。你不需要使用單獨的UPDATE和SELECT語句,就能完成同樣的任務。

  假設我們有一個表Sequence,它只有一列val,全是序號數字。我們可以通過賦值UPDATE得到一個新的序列值:

DECLARE @nextval AS INT;
UPDATE Sequence SET @nextval = val = val + 1;
SELECT @nextval;

6.2.2 新玩法:合併資料

  SQL Server 2008引入了一個叫做MERGE的語句,它能在一條語句中根據邏輯條件對資料進行不同的修改操作(INSERT/UPDATE/DELETE)。MERGE語句是SQL標準的一部分,而T-SQL版本的MERGE語句也增加了一些非標準的擴充套件。

  下面我們看看如何合併,首先我們準備兩張表Customers和CustomersStage:

--merge data
USE tempdb;

IF OBJECT_ID('dbo.Customers', 'U') IS NOT NULL DROP TABLE dbo.Customers;
GO

CREATE TABLE dbo.Customers
(
  custid      INT         NOT NULL,
  companyname VARCHAR(25) NOT NULL,
  phone       VARCHAR(20) NOT NULL,
  address     VARCHAR(50) NOT NULL,
  CONSTRAINT PK_Customers PRIMARY KEY(custid)
);

INSERT INTO dbo.Customers(custid, companyname, phone, address)
VALUES
  (1, 'cust 1', '(111) 111-1111', 'address 1'),
  (2, 'cust 2', '(222) 222-2222', 'address 2'),
  (3, 'cust 3', '(333) 333-3333', 'address 3'),
  (4, 'cust 4', '(444) 444-4444', 'address 4'),
  (5, 'cust 5', '(555) 555-5555', 'address 5');

IF OBJECT_ID('dbo.CustomersStage', 'U') IS NOT NULL DROP TABLE dbo.CustomersStage;
GO

CREATE TABLE dbo.CustomersStage
(
  custid      INT         NOT NULL,
  companyname VARCHAR(25) NOT NULL,
  phone       VARCHAR(20) NOT NULL,
  address     VARCHAR(50) NOT NULL,
  CONSTRAINT PK_CustomersStage PRIMARY KEY(custid)
);

INSERT INTO dbo.CustomersStage(custid, companyname, phone, address)
VALUES
  (2, 'AAAAA', '(222) 222-2222', 'address 2'),
  (3, 'cust 3', '(333) 333-3333', 'address 3'),
  (5, 'BBBBB', 'CCCCC', 'DDDDD'),
  (6, 'cust 6 (new)', '(666) 666-6666', 'address 6'),
  (7, 'cust 7 (new)', '(777) 777-7777', 'address 7');

-- Query tables
SELECT * FROM dbo.Customers;

SELECT * FROM dbo.CustomersStage;
View Code

  執行結果如下圖所示:

  

  現在我們想要增加還不存在的客戶,並更新已經存在的客戶。源表:CustomersStage,目標表:Customers。

MERGE INTO dbo.Customers AS TGT
USING dbo.CustomersStage AS SRC
  ON TGT.custid = SRC.custid
WHEN MATCHED THEN
  UPDATE SET
    TGT.companyname = SRC.companyname,
    TGT.phone = SRC.phone,
    TGT.address = SRC.address
WHEN NOT MATCHED THEN 
  INSERT (custid, companyname, phone, address)
  VALUES (SRC.custid, SRC.companyname, SRC.phone, SRC.address);

  謂詞條件:TGT.custid=SRC.custid用於定義什麼樣的資料是匹配的,什麼樣的資料是不匹配的。

TipsMERGE語句必須以分號結束,而對於T-SQL中的大多數其他語句來說是可選的。但是,推薦遵循最佳實踐,以分號結束。

6.3 高階資料更新方法

  ① 通過表表達式修改資料

-- 基於聯接的UPDATE
UPDATE OD
  SET discount = discount + 0.05
FROM dbo.OrderDetails AS OD
  JOIN dbo.Orders AS O
    ON OD.orderid = O.orderid
WHERE custid = 1;
-- 基於表表達式(這裡是CTE)的UPDATE
WITH C AS
(
  SELECT custid, OD.orderid,
    productid, discount, discount + 0.05 AS newdiscount
  FROM dbo.OrderDetails AS OD
    JOIN dbo.Orders AS O
      ON OD.orderid = O.orderid
  WHERE custid = 1
)
UPDATE C
  SET discount = newdiscount;

  ② 帶有TOP選項的資料更新

-- 刪除前50行
DELETE TOP(50) FROM dbo.Orders;
-- 更新前50行
UPDATE TOP(50) dbo.Orders
  SET freight = freight + 10.00;
-- 基於CTE刪除前50行
WITH C AS
(
  SELECT TOP(50) *
  FROM dbo.Orders
  ORDER BY orderid
)
DELETE FROM C;
-- 基於CTE更新前50行
WITH C AS
(
  SELECT TOP(50) *
  FROM dbo.Orders
  ORDER BY orderid DESC
)
UPDATE C
  SET freight = freight + 10.00;

6.4 OUTPUT子句

  在某些場景中,我們希望能夠從修改過的行中返回資料,這時就可以使用OUTPUT子句。SQL Server 2005引入了OUTPUT子句,通過在修改語句中新增OUTPUT子句,就可以實現從修改語句中返回資料的功能。

  ① 帶有OUTPUT的INSERT語句

INSERT INTO dbo.T1(datacol)
  OUTPUT inserted.keycol, inserted.datacol
    SELECT lastname
    FROM TSQLFundamentals2008.HR.Employees
    WHERE country = N'USA';

  ② 帶有OUTPUT的DELETE語句

DELETE FROM dbo.Orders
  OUTPUT
    deleted.orderid,
    deleted.orderdate,
    deleted.empid,