Database
 sql >> Cơ Sở Dữ Liệu >  >> RDS >> Database

Các nguyên tắc cơ bản về biểu thức bảng, Phần 5 - CTE, các cân nhắc logic

Bài viết này là phần thứ năm trong loạt bài về các biểu thức bảng. Trong Phần 1, tôi đã cung cấp nền tảng cho các biểu thức bảng. Trong Phần 2, Phần 3 và Phần 4, tôi đã đề cập đến cả khía cạnh logic và tối ưu hóa của các bảng dẫn xuất. Tháng này, tôi bắt đầu đề cập đến các biểu thức bảng phổ biến (CTE). Giống như với các bảng dẫn xuất, trước tiên tôi sẽ đề cập đến việc xử lý hợp lý các CTE và trong tương lai, tôi sẽ xem xét việc tối ưu hóa.

Trong các ví dụ của mình, tôi sẽ sử dụng cơ sở dữ liệu mẫu có tên là TSQLV5. Bạn có thể tìm thấy tập lệnh tạo và điền nó tại đây và sơ đồ ER của nó tại đây.

CTE

Hãy bắt đầu với thuật ngữ biểu thức bảng thông thường . Thuật ngữ này cũng như từ viết tắt CTE của nó đều không xuất hiện trong thông số kỹ thuật tiêu chuẩn SQL ISO / IEC. Vì vậy, có thể thuật ngữ này bắt nguồn từ một trong những sản phẩm cơ sở dữ liệu và sau đó được một số nhà cung cấp cơ sở dữ liệu khác chấp nhận. Bạn có thể tìm thấy nó trong tài liệu của Microsoft SQL Server và Azure SQL Database. T-SQL hỗ trợ nó bắt đầu với SQL Server 2005. Tiêu chuẩn sử dụng thuật ngữ biểu thức truy vấn để biểu diễn một biểu thức xác định một hoặc nhiều CTE, bao gồm cả truy vấn bên ngoài. Nó sử dụng thuật ngữ với phần tử danh sách để đại diện cho cái mà T-SQL gọi là CTE. Tôi sẽ sớm cung cấp cú pháp cho biểu thức truy vấn.

Nguồn của thuật ngữ sang một bên, biểu thức bảng chung hoặc CTE , là thuật ngữ thường được các học viên T-SQL sử dụng cho cấu trúc là trọng tâm của bài viết này. Vì vậy, trước tiên, hãy giải quyết xem đó có phải là một thuật ngữ thích hợp hay không. Chúng tôi đã kết luận rằng thuật ngữ biểu thức bảng thích hợp cho một biểu thức trả về một bảng về mặt khái niệm. Bảng gốc, CTE, dạng xem và các hàm có giá trị trong bảng nội tuyến là tất cả các loại biểu thức bảng được đặt tên mà T-SQL hỗ trợ. Vì vậy, biểu thức bảng một phần của biểu thức bảng chung chắc chắn có vẻ thích hợp. Về phần chung một phần của thuật ngữ, nó có thể liên quan đến một trong những ưu điểm thiết kế của CTE so với các bảng dẫn xuất. Hãy nhớ rằng bạn không thể sử dụng lại tên bảng dẫn xuất (hoặc chính xác hơn là tên biến phạm vi) nhiều lần trong truy vấn bên ngoài. Ngược lại, tên CTE có thể được sử dụng nhiều lần trong truy vấn bên ngoài. Nói cách khác, tên CTE là phổ biến cho truy vấn bên ngoài. Tất nhiên, tôi sẽ trình bày khía cạnh thiết kế này trong bài viết này.

CTE cung cấp cho bạn những lợi ích tương tự đối với các bảng dẫn xuất, bao gồm cho phép phát triển các giải pháp mô-đun, sử dụng lại bí danh cột, tương tác gián tiếp với các hàm cửa sổ trong các mệnh đề thường không cho phép chúng, hỗ trợ các sửa đổi gián tiếp dựa vào TOP hoặc OFFSET FETCH với đặc điểm thứ tự, và những người khác. Nhưng có một số ưu điểm về thiết kế so với các bảng dẫn xuất, mà tôi sẽ trình bày chi tiết sau khi tôi cung cấp cú pháp cho cấu trúc.

Cú pháp

Đây là cú pháp tiêu chuẩn cho một biểu thức truy vấn:

7.17


Chức năng
Chỉ định một bảng.


Định dạng
::=
[]
[] [] [ ]
::=WITH [RECURSIVE]
::= [{ }…]
::=
[ ]
AS [ ]
::=
::=

| UNION [ALL | DISTINCT]
[]
| NGOẠI TRỪ [TẤT CẢ | DISTINCT]
[]
::=

| INTERSECT [TẤT CẢ | DISTINCT]
[]
::=

|
[] [] []

::=
<đặc tả truy vấn> | |
::=TABLE
::=
GÓP LẠI [BẰNG ]
::=
::=ORDER BY
::=OFFSET {ROW | ROWS}
::=
FETCH {FIRST | NEXT} [] {ROW | ROWS} {CHỈ | VỚI TIES}
::=

|
::=<đặc tả giá trị đơn giản>
::=<đặc tả giá trị đơn giản>
::=<đặc tả giá trị đơn giản> PERCENT


7.18


Chức năng
Chỉ định việc tạo thông tin phát hiện thứ tự và chu trình trong kết quả của biểu thức truy vấn đệ quy.


Định dạng
::=
| |
::=
TÌM KIẾM ĐẶT
::=
CHẾT ĐẦU BẰNG | BREADTH FIRST BY
::=
::=
CYCLE SET ĐẾN
DEFAULT SỬ DỤNG
::= [{ }… ]
::=
::=
::=
::=
::=


7.3


Chức năng
Chỉ định một tập hợp các được tạo thành một bảng.


Định dạng
::=VALUES
::=
[{ }…]
::=
GIÁ TRỊ
::=

[{ }…]

Thuật ngữ chuẩn biểu thức truy vấn đại diện cho một biểu thức liên quan đến mệnh đề WITH, một với danh sách , được tạo từ một hoặc nhiều với các phần tử danh sách và một truy vấn bên ngoài. T-SQL đề cập đến tiêu chuẩn với phần tử danh sách với tư cách là một CTE.

T-SQL không hỗ trợ tất cả các phần tử cú pháp chuẩn. Ví dụ:nó không hỗ trợ một số phần tử truy vấn đệ quy nâng cao hơn cho phép bạn kiểm soát hướng tìm kiếm và xử lý các chu trình trong cấu trúc đồ thị. Truy vấn đệ quy là trọng tâm của bài viết tháng tới.

Đây là cú pháp T-SQL cho một truy vấn đơn giản đối với CTE:

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
SELECT < select list >
FROM < table name >;

Dưới đây là ví dụ cho một truy vấn đơn giản đối với một CTE đại diện cho khách hàng Hoa Kỳ:

WITH UC AS
(
  SELECT custid, companyname
  FROM Sales.Customers
  WHERE country = N'USA'
)
SELECT custid, companyname
FROM UC;

Bạn sẽ tìm thấy ba phần giống nhau trong một câu lệnh đối với CTE giống như cách bạn làm với một câu lệnh đối với bảng dẫn xuất:

  1. Biểu thức bảng (truy vấn bên trong)
  2. Tên được gán cho biểu thức bảng (tên biến phạm vi)
  3. Truy vấn bên ngoài

Điều khác biệt về thiết kế của CTE so với các bảng dẫn xuất là vị trí của ba phần tử này trong mã. Với các bảng dẫn xuất, truy vấn bên trong được lồng trong mệnh đề FROM của truy vấn bên ngoài và tên của biểu thức bảng được chỉ định sau chính biểu thức bảng. Các yếu tố được sắp xếp đan xen vào nhau. Ngược lại, với CTE, mã phân tách ba phần tử:đầu tiên bạn gán tên biểu thức bảng; thứ hai, bạn chỉ định biểu thức bảng — từ đầu đến cuối không bị gián đoạn; thứ ba, bạn chỉ định truy vấn bên ngoài — từ đầu đến cuối mà không bị gián đoạn. Sau đó, trong phần “Cân nhắc về thiết kế”, tôi sẽ giải thích ý nghĩa của những khác biệt về thiết kế này.

Một từ về CTE và việc sử dụng dấu chấm phẩy làm dấu chấm câu lệnh. Thật không may, không giống như SQL tiêu chuẩn, T-SQL không buộc bạn phải kết thúc tất cả các câu lệnh bằng dấu chấm phẩy. Tuy nhiên, có rất ít trường hợp trong T-SQL mà không có dấu chấm dứt thì mã không rõ ràng. Trong những trường hợp đó, việc chấm dứt hợp đồng là bắt buộc. Một trường hợp như vậy liên quan đến thực tế là mệnh đề WITH được sử dụng cho nhiều mục đích. Một là xác định CTE, một là xác định gợi ý bảng cho một truy vấn và có một vài trường hợp sử dụng bổ sung. Ví dụ, trong câu lệnh sau, mệnh đề WITH được sử dụng để buộc mức cách ly có thể nối tiếp hóa với gợi ý bảng:

SELECT custid, country FROM Sales.Customers WITH (SERIALIZABLE);

Khả năng mơ hồ là khi bạn có một câu lệnh chưa kết thúc trước định nghĩa CTE, trong trường hợp đó, trình phân tích cú pháp có thể không biết liệu mệnh đề WITH thuộc câu lệnh thứ nhất hay thứ hai. Dưới đây là một ví dụ chứng minh điều này:

SELECT custid, country FROM Sales.Customers
 
WITH UC AS
(
  SELECT custid, companyname
  FROM Sales.Customers
  WHERE country = N'USA'
)
SELECT custid, companyname
FROM UC

Ở đây trình phân tích cú pháp không thể cho biết mệnh đề WITH được sử dụng để xác định gợi ý bảng cho bảng Khách hàng trong câu lệnh đầu tiên hay bắt đầu định nghĩa CTE. Bạn gặp lỗi sau:

Msg 336, Level 15, State 1, Line 159
Sai cú pháp gần 'UC'. Nếu đây là một biểu thức bảng thông thường, bạn cần phải kết thúc rõ ràng câu lệnh trước đó bằng dấu chấm phẩy.

Tất nhiên, bản sửa lỗi là chấm dứt câu lệnh trước định nghĩa CTE, nhưng như một phương pháp hay nhất, bạn thực sự nên chấm dứt tất cả các câu lệnh của mình:

SELECT custid, country FROM Sales.Customers;
 
WITH UC AS
(
  SELECT custid, companyname
  FROM Sales.Customers
  WHERE country = N'USA'
)
SELECT custid, companyname
FROM UC;

Bạn có thể nhận thấy rằng một số người bắt đầu định nghĩa CTE của họ bằng dấu chấm phẩy như một cách thực hành, như sau:

;WITH UC AS
(
  SELECT custid, companyname
  FROM Sales.Customers
  WHERE country = N'USA'
)
SELECT custid, companyname
FROM UC;

Mục đích của phương pháp này là giảm thiểu khả năng xảy ra lỗi trong tương lai. Điều gì sẽ xảy ra nếu sau đó, ai đó thêm một câu lệnh chưa kết thúc ngay trước định nghĩa CTE của bạn trong tập lệnh và không bận tâm đến việc kiểm tra tập lệnh hoàn chỉnh, thay vì chỉ phát biểu của họ? Dấu chấm phẩy của bạn ngay trước mệnh đề WITH trở thành dấu chấm dứt câu lệnh của chúng. Bạn chắc chắn có thể thấy tính thực tế của cách làm này, nhưng nó hơi mất tự nhiên. Điều được khuyến nghị, mặc dù khó đạt được hơn, là áp dụng các phương pháp lập trình tốt trong tổ chức, bao gồm cả việc chấm dứt tất cả các tuyên bố.

Về mặt các quy tắc cú pháp áp dụng cho biểu thức bảng được sử dụng làm truy vấn bên trong trong định nghĩa CTE, chúng giống như các quy tắc áp dụng cho biểu thức bảng được sử dụng làm truy vấn bên trong trong định nghĩa bảng dẫn xuất. Đó là:

  • Tất cả các cột của biểu thức bảng phải có tên
  • Tất cả các tên cột của biểu thức bảng phải là duy nhất
  • Các hàng của biểu thức bảng không có thứ tự

Để biết chi tiết, hãy xem phần “Biểu thức bảng là một bảng” trong Phần 2 của loạt bài này.

Cân nhắc thiết kế

Nếu bạn khảo sát các nhà phát triển T-SQL có kinh nghiệm về việc họ thích sử dụng bảng dẫn xuất hay CTE, thì không phải ai cũng đồng ý về cách nào tốt hơn. Đương nhiên, những người khác nhau có sở thích tạo kiểu tóc khác nhau. Đôi khi tôi sử dụng các bảng dẫn xuất và đôi khi là CTE. Sẽ rất tốt nếu bạn có thể xác định một cách có ý thức sự khác biệt về thiết kế ngôn ngữ cụ thể giữa hai công cụ và lựa chọn dựa trên mức độ ưu tiên của bạn trong bất kỳ giải pháp nhất định nào. Với thời gian và kinh nghiệm, bạn đưa ra lựa chọn của mình một cách trực quan hơn.

Hơn nữa, điều quan trọng là không được nhầm lẫn giữa việc sử dụng các biểu thức bảng và bảng tạm thời, mà đó là một cuộc thảo luận liên quan đến hiệu suất mà tôi sẽ đề cập trong một bài viết trong tương lai.

CTE có khả năng truy vấn đệ quy và các bảng dẫn xuất thì không. Vì vậy, nếu bạn cần dựa vào những thứ đó, bạn sẽ sử dụng CTE một cách tự nhiên. Truy vấn đệ quy là trọng tâm của bài viết tháng tới.

Trong Phần 2, tôi đã giải thích rằng tôi thấy việc lồng các bảng dẫn xuất làm tăng thêm độ phức tạp cho mã, vì nó làm cho nó khó tuân theo logic. Tôi đã cung cấp ví dụ sau, xác định các năm đặt hàng trong đó hơn 70 khách hàng đã đặt hàng:

SELECT orderyear, numcusts
FROM ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
         FROM ( SELECT YEAR(orderdate) AS orderyear, custid
                FROM Sales.Orders ) AS D1
         GROUP BY orderyear ) AS D2
  WHERE numcusts > 70;

CTE không hỗ trợ lồng ghép. Vì vậy, khi bạn xem xét hoặc khắc phục sự cố một giải pháp dựa trên CTE, bạn sẽ không bị lạc vào logic lồng ghép. Thay vì lồng vào nhau, bạn xây dựng nhiều giải pháp mô-đun hơn bằng cách xác định nhiều CTE trong cùng một câu lệnh WITH, được phân tách bằng dấu phẩy. Mỗi CTE dựa trên một truy vấn được viết từ đầu đến cuối mà không bị gián đoạn. Tôi thấy đó là một điều tốt từ góc độ mã rõ ràng và khả năng bảo trì.

Đây là một giải pháp cho nhiệm vụ nói trên bằng cách sử dụng CTE:

WITH C1 AS
(
  SELECT YEAR(orderdate) AS orderyear, custid
  FROM Sales.Orders
),
C2 AS
(
  SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
  FROM C1
  GROUP BY orderyear
)
SELECT orderyear, numcusts
FROM C2
WHERE numcusts > 70;

Tôi thích giải pháp dựa trên CTE hơn. Nhưng một lần nữa, hãy hỏi các nhà phát triển có kinh nghiệm xem họ thích giải pháp nào trong số hai giải pháp trên và họ sẽ không đồng ý. Một số thực sự thích logic lồng nhau hơn và có thể xem mọi thứ ở một nơi.

Một lợi thế rất rõ ràng của CTE so với các bảng dẫn xuất, là khi bạn cần tương tác với nhiều trường hợp của cùng một biểu thức bảng trong giải pháp của mình. Hãy nhớ ví dụ sau dựa trên các bảng dẫn xuất từ ​​Phần 2 trong loạt bài này:

SELECT CUR.orderyear, CUR.numorders,
  CUR.numorders - PRV.numorders AS diff
FROM ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders
         FROM Sales.Orders
         GROUP BY YEAR(orderdate) ) AS CUR
    LEFT OUTER JOIN
       ( SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders
         FROM Sales.Orders
         GROUP BY YEAR(orderdate) ) AS PRV
      ON CUR.orderyear = PRV.orderyear + 1;

Giải pháp này trả về số năm đặt hàng, số lượng đơn đặt hàng mỗi năm và sự khác biệt giữa số năm hiện tại và năm trước. Có, bạn có thể làm điều đó dễ dàng hơn với hàm LAG, nhưng trọng tâm của tôi ở đây không phải là tìm ra cách tốt nhất để đạt được nhiệm vụ rất cụ thể này. Tôi sử dụng ví dụ này để minh họa các khía cạnh thiết kế ngôn ngữ nhất định của các biểu thức bảng được đặt tên.

Vấn đề với giải pháp này là bạn không thể gán tên cho biểu thức bảng và sử dụng lại nó trong cùng một bước xử lý truy vấn logic. Bạn đặt tên một bảng dẫn xuất sau chính biểu thức bảng trong mệnh đề FROM. Nếu bạn xác định và đặt tên một bảng dẫn xuất làm đầu vào đầu tiên của một phép nối, bạn cũng không thể sử dụng lại tên bảng dẫn xuất đó làm đầu vào thứ hai của cùng một phép nối. Nếu bạn cần tự nối hai phiên bản của cùng một biểu thức bảng, với các bảng dẫn xuất, bạn không có lựa chọn nào khác ngoài việc sao chép mã. Đó là những gì bạn đã làm trong ví dụ trên. Ngược lại, tên CTE được chỉ định làm phần tử đầu tiên của mã trong số ba phần tử nói trên (tên CTE, truy vấn bên trong, truy vấn bên ngoài). Theo thuật ngữ xử lý truy vấn lôgic, vào thời điểm bạn truy cập vào truy vấn bên ngoài, tên CTE đã được xác định và có sẵn. Điều này có nghĩa là bạn có thể tương tác với nhiều trường hợp của tên CTE trong truy vấn bên ngoài, như sau:

WITH OrdCount AS
(
  SELECT YEAR(orderdate) AS orderyear, COUNT(*) AS numorders
  FROM Sales.Orders
  GROUP BY YEAR(orderdate)
)
SELECT CUR.orderyear, CUR.numorders,
  CUR.numorders - PRV.numorders AS diff
FROM OrdCount AS CUR
  LEFT OUTER JOIN OrdCount AS PRV
    ON CUR.orderyear = PRV.orderyear + 1;

Giải pháp này có lợi thế về khả năng lập trình rõ ràng so với giải pháp dựa trên các bảng dẫn xuất ở chỗ bạn không cần phải duy trì hai bản sao của cùng một biểu thức bảng. Còn nhiều điều cần nói từ góc độ xử lý vật lý và so sánh với việc sử dụng các bảng tạm thời, nhưng tôi sẽ làm như vậy trong một bài viết tập trung vào hiệu suất trong tương lai.

Một lợi thế mà mã dựa trên các bảng dẫn xuất có được so với mã dựa trên CTE là có thuộc tính đóng mà một biểu thức bảng được cho là có. Hãy nhớ rằng thuộc tính đóng của một biểu thức quan hệ nói rằng cả đầu vào và đầu ra đều là quan hệ và do đó, một biểu thức quan hệ có thể được sử dụng khi một quan hệ được mong đợi, làm đầu vào cho một biểu thức quan hệ khác. Tương tự, một biểu thức bảng trả về một bảng và được cho là sẵn dùng làm bảng đầu vào cho một biểu thức bảng khác. Điều này đúng với một truy vấn dựa trên các bảng dẫn xuất — bạn có thể sử dụng nó khi một bảng được mong đợi. Ví dụ:bạn có thể sử dụng truy vấn dựa trên các bảng dẫn xuất làm truy vấn bên trong của định nghĩa CTE, như trong ví dụ sau:

WITH C AS
(
  SELECT orderyear, numcusts
  FROM ( SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
         FROM ( SELECT YEAR(orderdate) AS orderyear, custid
                FROM Sales.Orders ) AS D1
         GROUP BY orderyear ) AS D2
  WHERE numcusts > 70
)
SELECT orderyear, numcusts
FROM C;

Tuy nhiên, điều này không đúng đối với một truy vấn dựa trên CTE. Mặc dù về mặt khái niệm nó được coi là một biểu thức bảng, bạn không thể sử dụng nó làm truy vấn bên trong trong các định nghĩa bảng dẫn xuất, truy vấn con và chính CTE. Ví dụ:mã sau không hợp lệ trong T-SQL:

SELECT orderyear, custid
FROM (WITH C1 AS
      (
        SELECT YEAR(orderdate) AS orderyear, custid
        FROM Sales.Orders
      ),
      C2 AS
      (
        SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
        FROM C1
        GROUP BY orderyear
      )
      SELECT orderyear, numcusts
      FROM C2
      WHERE numcusts > 70) AS D;

Tin tốt là bạn có thể sử dụng truy vấn dựa trên CTE làm truy vấn bên trong trong các chế độ xem và các hàm có giá trị bảng nội tuyến, mà tôi sẽ đề cập trong các bài viết trong tương lai.

Ngoài ra, hãy nhớ rằng bạn luôn có thể xác định một CTE khác dựa trên truy vấn cuối cùng và sau đó yêu cầu truy vấn ngoài cùng tương tác với CTE đó:

WITH C1 AS
(
  SELECT YEAR(orderdate) AS orderyear, custid
  FROM Sales.Orders
),
C2 AS
(
  SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
  FROM C1
  GROUP BY orderyear
),
C3 AS
(
  SELECT orderyear, numcusts
  FROM C2
  WHERE numcusts &gt; 70
)
SELECT orderyear, numcusts
FROM C3;

Từ quan điểm khắc phục sự cố, như đã đề cập, tôi thường thấy việc tuân theo logic của mã dựa trên CTE dễ dàng hơn so với mã dựa trên bảng dẫn xuất. Tuy nhiên, các giải pháp dựa trên các bảng dẫn xuất có một lợi thế là bạn có thể đánh dấu bất kỳ mức lồng ghép nào và chạy nó một cách độc lập, như thể hiện trong Hình 1.

Hình 1:Có thể đánh dấu và chạy một phần mã với các bảng dẫn xuất

Với CTEs, mọi thứ phức tạp hơn. Để mã liên quan đến CTE có thể chạy được, nó phải bắt đầu bằng mệnh đề WITH, theo sau là một hoặc nhiều biểu thức bảng được đặt tên trong ngoặc đơn được phân tách bằng dấu phẩy, theo sau là truy vấn không dấu ngoặc đơn không có dấu phẩy đứng trước. Bạn có thể đánh dấu và chạy bất kỳ truy vấn bên trong nào thực sự độc lập, cũng như mã của giải pháp hoàn chỉnh; tuy nhiên, bạn không thể đánh dấu và chạy thành công bất kỳ phần trung gian nào khác của giải pháp. Ví dụ:Hình 2 cho thấy một nỗ lực không thành công để chạy mã đại diện cho C2.

Hình 2:Không thể đánh dấu và chạy một phần mã bằng CTE

Vì vậy, với CTE, bạn phải sử dụng các phương tiện hơi khó xử để có thể khắc phục sự cố ở bước trung gian của giải pháp. Ví dụ:một giải pháp phổ biến là tạm thời đưa vào một truy vấn SELECT * FROM your_cte ngay bên dưới CTE có liên quan. Sau đó, bạn đánh dấu và chạy mã bao gồm cả truy vấn được đưa vào và khi hoàn tất, bạn xóa truy vấn được chèn. Hình 3 thể hiện kỹ thuật này.

Hình 3:Chèn SELECT * bên dưới CTE có liên quan

Vấn đề là bất cứ khi nào bạn thực hiện các thay đổi đối với mã — ngay cả những thay đổi nhỏ tạm thời như ở trên — có khả năng khi bạn cố gắng hoàn nguyên về mã ban đầu, bạn sẽ gặp phải một lỗi mới.

Một tùy chọn khác là tạo kiểu mã của bạn hơi khác một chút, sao cho mỗi định nghĩa CTE không phải đầu tiên bắt đầu bằng một dòng mã riêng biệt trông giống như sau:

, cte_name AS (

Sau đó, bất cứ khi nào bạn muốn chạy một phần trung gian của mã xuống một CTE nhất định, bạn có thể làm như vậy với những thay đổi tối thiểu đối với mã của mình. Sử dụng nhận xét dòng, bạn chỉ nhận xét một dòng mã tương ứng với CTE đó. Sau đó, bạn đánh dấu và chạy mã xuống và bao gồm cả truy vấn bên trong của CTE, hiện được coi là truy vấn ngoài cùng, như được minh họa trong Hình 4.

Hình 4:Sắp xếp lại cú pháp để cho phép nhận xét một dòng mã

Nếu không hài lòng với phong cách này, bạn vẫn có một lựa chọn khác. Bạn có thể sử dụng một nhận xét khối bắt đầu ngay trước dấu phẩy đứng trước CTE quan tâm và kết thúc sau dấu ngoặc đơn mở, như được minh họa trong Hình 5.

Hình 5:Sử dụng nhận xét khối

Nó phụ thuộc vào sở thích cá nhân. Tôi thường sử dụng kỹ thuật truy vấn SELECT * được chèn tạm thời.

Hàm tạo giá trị bảng

Có một hạn chế nhất định trong hỗ trợ của T-SQL cho các hàm tạo giá trị bảng so với tiêu chuẩn. Nếu bạn không quen thuộc với cấu trúc, hãy nhớ xem Phần 2 của loạt bài trước, nơi tôi mô tả chi tiết về cấu trúc này. Trong khi T-SQL cho phép bạn xác định bảng dẫn xuất dựa trên phương thức tạo giá trị bảng, nó không cho phép bạn xác định CTE dựa trên phương thức tạo giá trị bảng.

Dưới đây là một ví dụ được hỗ trợ sử dụng bảng dẫn xuất:

SELECT custid, companyname, contractdate
FROM ( VALUES( 2, 'Cust 2', '20200212' ),
             ( 3, 'Cust 3', '20200118' ),
             ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate);

Rất tiếc, mã tương tự sử dụng CTE không được hỗ trợ:

WITH MyCusts(custid, companyname, contractdate) AS
(
  VALUES( 2, 'Cust 2', '20200212' ),
        ( 3, 'Cust 3', '20200118' ),
        ( 5, 'Cust 5', '20200401' )
)
SELECT custid, companyname, contractdate
FROM MyCusts;

Mã này tạo ra lỗi sau:

Msg 156, Level 15, State 1, Line 337
Cú pháp không chính xác gần từ khóa 'VALUES'.

Tuy nhiên, có một số cách giải quyết. Một là sử dụng truy vấn chống lại bảng dẫn xuất, đến lượt nó lại dựa trên hàm tạo giá trị bảng, như truy vấn bên trong của CTE, như sau:

WITH MyCusts AS
(
  SELECT *
  FROM ( VALUES( 2, 'Cust 2', '20200212' ),
               ( 3, 'Cust 3', '20200118' ),
               ( 5, 'Cust 5', '20200401' ) )
       AS MyCusts(custid, companyname, contractdate)
)
SELECT custid, companyname, contractdate
FROM MyCusts;

Một phương pháp khác là sử dụng kỹ thuật mà mọi người đã sử dụng trước khi các hàm tạo giá trị bảng được đưa vào T-SQL — sử dụng một loạt các truy vấn FROMless được phân tách bằng các toán tử UNION ALL, như sau:

WITH MyCusts(custid, companyname, contractdate) AS
(
            SELECT 2, 'Cust 2', '20200212'
  UNION ALL SELECT 3, 'Cust 3', '20200118'
  UNION ALL SELECT 5, 'Cust 5', '20200401'
)
SELECT custid, companyname, contractdate
FROM MyCusts;

Lưu ý rằng bí danh cột được gán ngay sau tên CTE.

Hai phương pháp được đại số hóa và tối ưu hóa giống nhau, vì vậy hãy sử dụng phương pháp nào bạn cảm thấy thoải mái hơn.

Tạo một dãy số

Một công cụ mà tôi sử dụng khá thường xuyên trong các giải pháp của mình là một bảng số bổ trợ. Một tùy chọn là tạo một bảng số thực trong cơ sở dữ liệu của bạn và điền nó bằng một chuỗi có kích thước hợp lý. Một giải pháp khác là phát triển một giải pháp tạo ra một chuỗi số đang bay. Đối với tùy chọn thứ hai, bạn muốn các đầu vào là dấu phân cách của phạm vi mong muốn (chúng tôi sẽ gọi chúng là @low@high ). Bạn muốn giải pháp của mình hỗ trợ các phạm vi tiềm năng lớn. Đây là giải pháp của tôi cho mục đích này, sử dụng CTE, với yêu cầu cho phạm vi 1001 đến 1010 trong ví dụ cụ thể sau:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
WITH
  L0 AS ( SELECT 1 AS c FROM (VALUES(1),(1)) AS D(c) ),
  L1 AS ( SELECT 1 AS c FROM L0 AS A CROSS JOIN L0 AS B ),
  L2 AS ( SELECT 1 AS c FROM L1 AS A CROSS JOIN L1 AS B ),
  L3 AS ( SELECT 1 AS c FROM L2 AS A CROSS JOIN L2 AS B ),
  L4 AS ( SELECT 1 AS c FROM L3 AS A CROSS JOIN L3 AS B ),
  L5 AS ( SELECT 1 AS c FROM L4 AS A CROSS JOIN L4 AS B ),
  Nums AS ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
            FROM L5 )
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM Nums
ORDER BY rownum;

Mã này tạo ra kết quả sau:

n
-----
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010

CTE đầu tiên được gọi là L0 dựa trên một hàm tạo giá trị bảng có hai hàng. Các giá trị thực tế ở đó là không đáng kể; điều quan trọng là nó có hai hàng. Sau đó, có một chuỗi năm CTE bổ sung được đặt tên từ L1 đến L5, mỗi CTE áp dụng phép nối chéo giữa hai phiên bản của CTE trước đó. Đoạn mã sau sẽ tính toán số hàng có thể được tạo bởi mỗi CTE, trong đó @L là số cấp CTE:

DECLARE @L AS INT = 5;
 
SELECT POWER(2., POWER(2., @L));

Dưới đây là những con số mà bạn nhận được cho mỗi CTE:

CTE Bản số
L0 2
L1 4
L2 16
L3 256
L4 65.536
L5 4.294.967.296

Lên đến cấp độ 5 cung cấp cho bạn hơn bốn tỷ hàng. Điều này là đủ cho bất kỳ trường hợp sử dụng thực tế nào mà tôi có thể nghĩ đến. Bước tiếp theo diễn ra trong CTE được gọi là Nums. Bạn sử dụng hàm ROW_NUMBER để tạo một chuỗi các số nguyên bắt đầu bằng 1 dựa trên không có thứ tự xác định (ORDER BY (SELECT NULL)) và đặt tên cho cột kết quả là rownum. Cuối cùng, truy vấn bên ngoài sử dụng bộ lọc TOP dựa trên thứ tự rownum để lọc bao nhiêu số theo thứ tự mong muốn (@high - @low + 1) và tính số kết quả n là @low + rownum - 1.

Tại đây, bạn thực sự có thể đánh giá cao vẻ đẹp trong thiết kế CTE và sự tiết kiệm mà nó mang lại khi bạn xây dựng các giải pháp theo kiểu mô-đun. Cuối cùng, quá trình unnesting giải nén 32 bảng, mỗi bảng bao gồm hai hàng dựa trên các hằng số. Có thể thấy rõ điều này trong kế hoạch thực thi cho đoạn mã này, như trong Hình 6 bằng cách sử dụng SentryOne Plan Explorer.

Hình 6:Kế hoạch tạo truy vấn chuỗi số

Mỗi toán tử quét hằng đại diện cho một bảng hằng số có hai hàng. Vấn đề là, toán tử Top là người yêu cầu các hàng đó và nó ngắn mạch sau khi nhận được số lượng mong muốn. Lưu ý 10 hàng được chỉ ra phía trên mũi tên chảy vào toán tử Trên cùng.

Tôi biết rằng trọng tâm của bài viết này là việc xử lý khái niệm CTE chứ không phải xem xét vật lý / hiệu suất, nhưng bằng cách nhìn vào kế hoạch, bạn thực sự có thể đánh giá cao tính ngắn gọn của mã so với độ dài dòng của những gì nó chuyển sang hậu trường.

Sử dụng các bảng dẫn xuất, bạn thực sự có thể viết một giải pháp thay thế mỗi tham chiếu CTE bằng truy vấn cơ bản mà nó đại diện. Những gì bạn nhận được khá đáng sợ:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
       FROM ( SELECT 1 AS C
              FROM ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
 
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D7
                CROSS JOIN
                   ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D8 ) AS D9
         CROSS JOIN
            ( SELECT 1 AS C
              FROM ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D7
                CROSS JOIN
                   ( SELECT 1 AS C
                     FROM ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D5
                       CROSS JOIN
                          ( SELECT 1 AS C
                            FROM ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c) 
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D3 
                              CROSS JOIN 
                                 ( SELECT 1 AS C
                                   FROM (VALUES(1),(1)) AS D01(c)
                                     CROSS JOIN
                                        (VALUES(1),(1)) AS D02(c) ) AS D4 ) AS D6 ) AS D8 ) AS D10 ) AS Nums
ORDER BY rownum;

Obviously, you don’t want to write a solution like this, but it’s a good way to illustrate what SQL Server does behind the scenes with your CTE code.

If you were really planning to write a solution based on derived tables, instead of using the above nested approach, you’d be better off simplifying the logic to a single query with 31 cross joins between 32 table value constructors, each based on two rows, like so:

DECLARE @low AS BIGINT = 1001, @high AS BIGINT = 1010;
 
SELECT TOP(@high - @low + 1) @low + rownum - 1 AS n
FROM ( SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS rownum
       FROM         (VALUES(1),(1)) AS D01(c)
         CROSS JOIN (VALUES(1),(1)) AS D02(c)
         CROSS JOIN (VALUES(1),(1)) AS D03(c)
         CROSS JOIN (VALUES(1),(1)) AS D04(c)
         CROSS JOIN (VALUES(1),(1)) AS D05(c)
         CROSS JOIN (VALUES(1),(1)) AS D06(c)
         CROSS JOIN (VALUES(1),(1)) AS D07(c)
         CROSS JOIN (VALUES(1),(1)) AS D08(c)
         CROSS JOIN (VALUES(1),(1)) AS D09(c)
         CROSS JOIN (VALUES(1),(1)) AS D10(c)
         CROSS JOIN (VALUES(1),(1)) AS D11(c)
         CROSS JOIN (VALUES(1),(1)) AS D12(c)
         CROSS JOIN (VALUES(1),(1)) AS D13(c)
         CROSS JOIN (VALUES(1),(1)) AS D14(c)
         CROSS JOIN (VALUES(1),(1)) AS D15(c)
         CROSS JOIN (VALUES(1),(1)) AS D16(c)
         CROSS JOIN (VALUES(1),(1)) AS D17(c)
         CROSS JOIN (VALUES(1),(1)) AS D18(c)
         CROSS JOIN (VALUES(1),(1)) AS D19(c)
         CROSS JOIN (VALUES(1),(1)) AS D20(c)
         CROSS JOIN (VALUES(1),(1)) AS D21(c)
         CROSS JOIN (VALUES(1),(1)) AS D22(c)
         CROSS JOIN (VALUES(1),(1)) AS D23(c)
         CROSS JOIN (VALUES(1),(1)) AS D24(c)
         CROSS JOIN (VALUES(1),(1)) AS D25(c)
         CROSS JOIN (VALUES(1),(1)) AS D26(c)
         CROSS JOIN (VALUES(1),(1)) AS D27(c)
         CROSS JOIN (VALUES(1),(1)) AS D28(c)
         CROSS JOIN (VALUES(1),(1)) AS D29(c)
         CROSS JOIN (VALUES(1),(1)) AS D30(c)
         CROSS JOIN (VALUES(1),(1)) AS D31(c)
         CROSS JOIN (VALUES(1),(1)) AS D32(c) ) AS Nums
ORDER BY rownum;

Still, the solution based on CTEs is obviously significantly simpler. The plans are identical.

Used in modification statements

CTEs can be used as the source and target tables in INSERT, UPDATE, DELETE and MERGE statements. They cannot be used in the TRUNCATE statement.

The syntax is pretty straightforward. You start the statement as usual with a WITH clause, followed by one or more CTEs separated by commas. Then you specify the outer modification statement, which interacts with the CTEs that were defined under the WITH clause as the source tables, target table, or both. Just like I explained in Part 2 about derived tables, also with CTEs what really gets modified is the underlying base table that the table expression uses. I’ll show a couple of examples using DELETE and UPDATE statements, but remember that you can use CTEs in MERGE and INSERT statements as well.

Here’s the general syntax of a DELETE statement against a CTE:

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
DELETE [ FROM ] <table name>
[ WHERE <filter predicate> ];

As an example (don’t actually run it), the following code deletes the 10 oldest orders:

WITH OldestOrders AS
(
  SELECT TOP (10) *
  FROM Sales.Orders
  ORDER BY orderdate, orderid
)
DELETE FROM OldestOrders;

Here’s the general syntax of an UPDATE statement against a CTE:

WITH < table name > [ (< target columns >) ] AS
(
  < table expression >
)
UPDATE <table name>
  SET <assignments>
[ WHERE <filter predicate> ];

As an example, the following code updates the 10 oldest unshipped orders that have an overdue required date, increasing the required date to 10 days from today:

BEGIN TRAN;
 
WITH OldestUnshippedOrders AS
(
  SELECT TOP (10) orderid, requireddate,
    DATEADD(day, 10, CAST(SYSDATETIME() AS DATE)) AS newrequireddate
  FROM Sales.Orders
  WHERE shippeddate IS NULL
    AND requireddate &lt; CAST(SYSDATETIME() AS DATE)
  ORDER BY orderdate, orderid
)
UPDATE OldestUnshippedOrders
  SET requireddate = newrequireddate
    OUTPUT
      inserted.orderid,
      deleted.requireddate AS oldrequireddate,
      inserted.requireddate AS newrequireddate;
 
ROLLBACK TRAN;

The code applies the update in a transaction that it then rolls back so that the change won’t stick.

This code generates the following output, showing both the old and the new required dates:

orderid     oldrequireddate newrequireddate
----------- --------------- ---------------
11008       2019-05-06      2020-07-16
11019       2019-05-11      2020-07-16
11039       2019-05-19      2020-07-16
11040       2019-05-20      2020-07-16
11045       2019-05-21      2020-07-16
11051       2019-05-25      2020-07-16
11054       2019-05-26      2020-07-16
11058       2019-05-27      2020-07-16
11059       2019-06-10      2020-07-16
11061       2019-06-11      2020-07-16

(10 rows affected)

Of course you will get a different new required date based on when you run this code.

Tóm tắt

I like CTEs. They have a few advantages compared to derived tables. Instead of nesting the code, you define multiple CTEs separated by commas, typically leading to a more modular solution that is easier to review and maintain. Also, you can have multiple references to the same CTE name in the outer statement, so you don’t need to repeat the inner table expression’s code. However, unlike derived tables, CTEs cannot be defined directly based on a table value constructor, and you cannot highlight and execute some of the intermediate parts of the code. The following table summarizes the differences between derived tables and CTEs:

Item Derived table CTE
Supports nesting Yes No
Supports multiple references No Yes
Supports table value constructor Yes No
Can highlight and run part of code Yes No
Supports recursion No Yes

As the last item says, derived tables do not support recursive capabilities, whereas CTEs do. Recursive queries are the focus of next month’s article.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Vấn đề Halloween - Phần 2

  2. T-SQL so với SQL

  3. Câu lệnh SQL SELECT INTO

  4. Xử lý cơ sở dữ liệu SQL với PyQt:Khái niệm cơ bản

  5. Sử dụng dữ liệu được bảo vệ bằng Azure Key Vault từ Linux