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 7 - CTE, cân nhắc tối ưu hóa

Bài viết này là phần thứ 7 của loạt bài về các biểu thức bảng được đặt tên. Trong Phần 5 và Phần 6, tôi đã trình bày các khía cạnh khái niệm của các biểu thức bảng thông dụng (CTE). Tháng này và tháng tới, trọng tâm của tôi chuyển sang các cân nhắc tối ưu hóa của CTE.

Tôi sẽ bắt đầu bằng cách nhanh chóng xem lại khái niệm hấp dẫn về các biểu thức bảng đã đặt tên và chứng minh khả năng áp dụng của nó cho các CTE. Sau đó, tôi sẽ chuyển trọng tâm của mình sang các cân nhắc về tính bền bỉ. Tôi sẽ nói về các khía cạnh liên tục của CTE đệ quy và không đệ quy. Tôi sẽ giải thích thời điểm hợp lý khi gắn bó với CTE so với thời điểm thực sự có ý nghĩa hơn khi làm việc với các bảng tạm thời.

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

Thay thế / hủy ghi chú

Trong Phần 4 của loạt bài này, tập trung vào việc tối ưu hóa các bảng dẫn xuất, tôi đã mô tả một quá trình hủy ghi / thay thế các biểu thức bảng. Tôi đã giải thích rằng khi SQL Server tối ưu hóa một truy vấn liên quan đến các bảng dẫn xuất, nó áp dụng các quy tắc chuyển đổi cho cây ban đầu của các toán tử logic do trình phân tích cú pháp tạo ra, có thể thay đổi mọi thứ xung quanh những gì ban đầu là ranh giới biểu thức bảng. Điều này xảy ra ở một mức độ mà khi bạn so sánh một kế hoạch cho một truy vấn sử dụng các bảng dẫn xuất với một kế hoạch cho một truy vấn đối chiếu trực tiếp với các bảng cơ sở bên dưới mà bạn đã tự áp dụng logic unnesting, chúng trông giống nhau. Tôi cũng đã mô tả một kỹ thuật để ngăn chặn việc bỏ ghi chú bằng cách sử dụng bộ lọc TOP với một số lượng rất lớn các hàng làm đầu vào. Tôi đã chứng minh một số trường hợp mà kỹ thuật này khá tiện dụng — một trường hợp mục đích là để tránh lỗi và một trường hợp khác vì lý do tối ưu hóa.

Phiên bản TL; DR của việc thay thế / hủy lưu trữ CTE là quy trình giống như quy trình với các bảng dẫn xuất. Nếu bạn hài lòng với tuyên bố này, bạn có thể bỏ qua phần này và chuyển thẳng đến phần tiếp theo về Tính kiên trì. Bạn sẽ không bỏ lỡ bất kỳ điều gì quan trọng mà bạn chưa đọc trước đây. Tuy nhiên, nếu bạn giống tôi, bạn có thể muốn bằng chứng rằng thực sự là như vậy. Sau đó, bạn có thể sẽ muốn tiếp tục đọc phần này và kiểm tra mã mà tôi sử dụng khi truy cập lại các ví dụ về khóa không lưu ý mà tôi đã trình bày trước đó với các bảng dẫn xuất và chuyển đổi chúng để sử dụng CTE.

Trong Phần 4, tôi đã trình bày truy vấn sau (chúng tôi sẽ gọi nó là Truy vấn 1):

USE TSQLV5;
 
  SELECT orderid, orderdate
  FROM ( SELECT *
     FROM ( SELECT *
            FROM ( SELECT *
                   FROM Sales.Orders
                   WHERE orderdate >= '20180101' ) AS D1
            WHERE orderdate >= '20180201' ) AS D2
     WHERE orderdate >= '20180301' ) AS D3
  WHERE orderdate >= '20180401';

Truy vấn liên quan đến ba cấp độ lồng nhau của các bảng dẫn xuất, cộng với một truy vấn bên ngoài. Mỗi cấp độ lọc một phạm vi ngày đặt hàng khác nhau. Kế hoạch cho Truy vấn 1 được thể hiện trong Hình 1.

Hình 1:Kế hoạch thực thi cho Truy vấn 1

Kế hoạch trong Hình 1 cho thấy rõ ràng rằng việc bỏ ghi chú các bảng dẫn xuất đã diễn ra vì tất cả các vị từ bộ lọc được hợp nhất thành một vị từ bộ lọc bao trùm duy nhất.

Tôi đã giải thích rằng bạn có thể ngăn quá trình bỏ ghi chú bằng cách sử dụng bộ lọc TOP có ý nghĩa (trái ngược với TOP 100 PERCENT) với số lượng hàng rất lớn làm đầu vào, như truy vấn sau đây hiển thị (chúng tôi sẽ gọi nó là Truy vấn 2):

  SELECT orderid, orderdate
  FROM ( SELECT TOP (9223372036854775807) *
     FROM ( SELECT TOP (9223372036854775807) *
            FROM ( SELECT TOP (9223372036854775807) *
                   FROM Sales.Orders
                   WHERE orderdate >= '20180101' ) AS D1
            WHERE orderdate >= '20180201' ) AS D2
     WHERE orderdate >= '20180301' ) AS D3
  WHERE orderdate >= '20180401';

Kế hoạch cho Truy vấn 2 được thể hiện trong Hình 2.

Hình 2:Kế hoạch thực thi cho Truy vấn 2

Kế hoạch cho thấy rõ ràng rằng việc hủy ghi chú đã không diễn ra vì bạn có thể nhìn thấy ranh giới bảng dẫn xuất một cách hiệu quả.

Hãy thử các ví dụ tương tự bằng cách sử dụng CTE. Đây là Truy vấn 1 được chuyển đổi để sử dụng CTE:

  WITH C1 AS
  (
    SELECT *
    FROM Sales.Orders
    WHERE orderdate >= '20180101'
  ),
  C2 AS
  (
    SELECT *
    FROM C1
    WHERE orderdate >= '20180201'
  ),
  C3 AS
  (
    SELECT *
    FROM C2
    WHERE orderdate >= '20180301'
  )
  SELECT orderid, orderdate
  FROM C3
  WHERE orderdate >= '20180401';

Bạn nhận được cùng một kế hoạch được hiển thị trước đó trong Hình 1, nơi bạn có thể thấy rằng quá trình hủy lưu trữ đã diễn ra.

Đây là Truy vấn 2 được chuyển đổi để sử dụng CTE:

  WITH C1 AS
  (
    SELECT TOP (9223372036854775807) *
    FROM Sales.Orders
    WHERE orderdate >= '20180101'
  ),
  C2 AS
  (
    SELECT TOP (9223372036854775807) *
    FROM C1
    WHERE orderdate >= '20180201'
  ),
  C3 AS
  (
    SELECT TOP (9223372036854775807) *
    FROM C2
    WHERE orderdate >= '20180301'
  )
  SELECT orderid, orderdate
  FROM C3
  WHERE orderdate >= '20180401';

Bạn nhận được cùng một kế hoạch như được hiển thị trước đó trong Hình 2, nơi bạn có thể thấy rằng việc hủy đăng ký đã không diễn ra.

Tiếp theo, hãy xem lại hai ví dụ mà tôi đã sử dụng để chứng minh tính thực tế của kỹ thuật này để ngăn chặn việc bỏ qua — chỉ lần này bằng cách sử dụng CTE.

Hãy bắt đầu với truy vấn sai. Truy vấn sau cố gắng trả về các dòng đặt hàng có chiết khấu lớn hơn chiết khấu tối thiểu và trong đó đối ứng của chiết khấu lớn hơn 10:

  SELECT orderid, productid, discount
  FROM Sales.OrderDetails
  WHERE discount > (SELECT MIN(discount) FROM Sales.OrderDetails)
  AND 1.0 / discount > 10.0;

Mức chiết khấu tối thiểu không được là số âm, đúng hơn là bằng 0 hoặc cao hơn. Vì vậy, có thể bạn đang nghĩ rằng nếu một hàng có chiết khấu bằng không, thì vị từ đầu tiên sẽ đánh giá thành false và rằng một đoạn ngắn mạch sẽ ngăn nỗ lực đánh giá vị từ thứ hai, do đó tránh được lỗi. Tuy nhiên, khi bạn chạy mã này, bạn nhận được lỗi chia cho 0:

  Msg 8134, Level 16, State 1, Line 99
  Divide by zero error encountered.

Vấn đề là mặc dù SQL Server hỗ trợ khái niệm ngắn mạch ở cấp độ xử lý vật lý, không có gì đảm bảo rằng nó sẽ đánh giá các vị từ bộ lọc theo thứ tự viết từ trái sang phải. Một nỗ lực phổ biến để tránh những lỗi như vậy là sử dụng biểu thức bảng được đặt tên để xử lý phần logic lọc mà bạn muốn được đánh giá trước tiên và yêu cầu truy vấn bên ngoài xử lý logic lọc mà bạn muốn được đánh giá thứ hai. Đây là giải pháp đã thử sử dụng CTE:

  WITH C AS
  (
    SELECT *
    FROM Sales.OrderDetails
    WHERE discount > (SELECT MIN(discount) FROM Sales.OrderDetails)
  )
  SELECT orderid, productid, discount
  FROM C
  WHERE 1.0 / discount > 10.0;

Tuy nhiên, thật không may, việc bỏ ghi chú của biểu thức bảng dẫn đến một logic tương đương với truy vấn giải pháp ban đầu và khi bạn cố gắng chạy mã này, bạn lại gặp lỗi chia cho 0:

  Msg 8134, Level 16, State 1, Line 108
  Divide by zero error encountered.

Sử dụng thủ thuật của chúng tôi với bộ lọc TOP trong truy vấn bên trong, bạn ngăn chặn việc bỏ ghi chú của biểu thức bảng, như vậy:

  WITH C AS
  (
    SELECT TOP (9223372036854775807) *
    FROM Sales.OrderDetails
    WHERE discount > (SELECT MIN(discount) FROM Sales.OrderDetails)
  )
  SELECT orderid, productid, discount
  FROM C
  WHERE 1.0 / discount > 10.0;

Lần này mã chạy thành công mà không có bất kỳ lỗi nào.

Hãy tiếp tục với ví dụ mà bạn sử dụng kỹ thuật này để ngăn chặn việc bỏ qua vì lý do tối ưu hóa. Mã sau chỉ trả lại cho những người gửi hàng có ngày đặt hàng tối đa là vào hoặc sau ngày 1 tháng 1 năm 2018:

  USE PerformanceV5; 
 
  WITH C AS
  (
    SELECT S.shipperid,
      (SELECT MAX(O.orderdate)
       FROM dbo.Orders AS O
       WHERE O.shipperid = S.shipperid) AS maxod
    FROM dbo.Shippers AS S
  )
  SELECT shipperid, maxod
  FROM C
  WHERE maxod >= '20180101';

Nếu bạn đang thắc mắc tại sao không sử dụng một giải pháp đơn giản hơn nhiều với truy vấn được nhóm và bộ lọc HAVING, thì nó liên quan đến mật độ của cột shipperid. Bảng Đơn đặt hàng có 1.000.000 đơn đặt hàng và các lô hàng của những đơn đặt hàng đó được xử lý bởi năm chủ hàng, có nghĩa là trung bình, mỗi chủ hàng xử lý 20% đơn đặt hàng. Kế hoạch cho một truy vấn được nhóm tính toán ngày đặt hàng tối đa cho mỗi người gửi hàng sẽ quét tất cả 1.000.000 hàng, dẫn đến hàng nghìn lần đọc trang. Thật vậy, nếu bạn chỉ đánh dấu truy vấn bên trong của CTE (chúng tôi sẽ gọi nó là Truy vấn 3) tính toán ngày đặt hàng tối đa cho mỗi người gửi hàng và kiểm tra kế hoạch thực hiện của nó, bạn sẽ nhận được kế hoạch được hiển thị trong Hình 3.

Hình 3:Kế hoạch thực thi cho Truy vấn 3

Kế hoạch quét năm hàng trong chỉ mục nhóm trên Người gửi hàng. Đối với mỗi người gửi hàng, kế hoạch áp dụng tìm kiếm chống lại chỉ mục bao gồm trên Đơn đặt hàng, trong đó (shipperid, ngày đặt hàng) là các khóa dẫn đầu chỉ mục, đi thẳng đến hàng cuối cùng trong mỗi phần của người gửi hàng ở cấp độ lá để kéo ngày đặt hàng tối đa cho hiện tại người giao hàng. Vì chúng tôi chỉ có năm người gửi hàng, nên chỉ có năm hoạt động tìm kiếm chỉ mục, dẫn đến một kế hoạch rất hiệu quả. Dưới đây là các thước đo hiệu suất mà tôi nhận được khi thực hiện truy vấn bên trong của CTE:

  duration: 0 ms, CPU: 0 ms, reads: 15

Tuy nhiên, khi bạn chạy giải pháp hoàn chỉnh (chúng tôi sẽ gọi nó là Truy vấn 4), bạn sẽ nhận được một kế hoạch hoàn toàn khác, như thể hiện trong Hình 4.

Hình 4:Kế hoạch thực thi cho Truy vấn 4

Điều đã xảy ra là SQL Server đã hủy thiết lập biểu thức bảng, chuyển đổi giải pháp thành tương đương logic của một truy vấn được nhóm lại, dẫn đến việc quét toàn bộ chỉ mục trên Đơn hàng. Dưới đây là các con số hiệu suất mà tôi nhận được cho giải pháp này:

  duration: 316 ms, CPU: 281 ms, reads: 3854

Những gì chúng ta cần ở đây là ngăn việc bỏ ghi chú biểu thức bảng diễn ra, để truy vấn bên trong sẽ được tối ưu hóa với các tìm kiếm chống lại chỉ mục trên Đơn hàng và để truy vấn bên ngoài chỉ dẫn đến việc bổ sung toán tử Bộ lọc trong kế hoạch. Bạn đạt được điều này bằng cách sử dụng thủ thuật của chúng tôi bằng cách thêm bộ lọc TOP vào truy vấn bên trong, giống như vậy (chúng tôi sẽ gọi giải pháp này là Truy vấn 5):

  WITH C AS
  (
    SELECT TOP (9223372036854775807) S.shipperid,
      (SELECT MAX(O.orderdate)
       FROM dbo.Orders AS O
       WHERE O.shipperid = S.shipperid) AS maxod
    FROM dbo.Shippers AS S
  )
  SELECT shipperid, maxod
  FROM C
  WHERE maxod >= '20180101';

Kế hoạch cho giải pháp này được thể hiện trong Hình 5.

Hình 5:Kế hoạch thực thi cho Truy vấn 5

Kế hoạch cho thấy rằng đã đạt được hiệu quả mong muốn và theo đó, các con số hiệu suất xác nhận điều này:

  duration: 0 ms, CPU: 0 ms, reads: 15

Vì vậy, thử nghiệm của chúng tôi xác nhận rằng SQL Server xử lý việc thay thế / hủy ghi chú CTE giống như đối với các bảng dẫn xuất. Điều này có nghĩa là bạn không nên thích cái này hơn cái kia vì lý do tối ưu hóa, thay vào đó là do sự khác biệt về khái niệm quan trọng đối với bạn, như đã thảo luận trong Phần 5.

Tính bền bỉ

Một quan niệm sai lầm phổ biến liên quan đến CTE và các biểu thức bảng được đặt tên nói chung là chúng đóng vai trò như một phương tiện bền bỉ nào đó. Một số người nghĩ rằng SQL Server duy trì tập kết quả của truy vấn bên trong với một bảng làm việc và truy vấn bên ngoài thực sự tương tác với bảng công việc đó. Trong thực tế, các CTE không đệ quy thông thường và các bảng dẫn xuất không tồn tại lâu dài. Tôi đã mô tả logic không lưu ý mà SQL Server áp dụng khi tối ưu hóa một truy vấn liên quan đến các biểu thức bảng, dẫn đến một kế hoạch tương tác thẳng với các bảng cơ sở bên dưới. Lưu ý rằng trình tối ưu hóa có thể chọn sử dụng các bảng làm việc để duy trì các tập kết quả trung gian nếu việc này hợp lý vì lý do hiệu suất hoặc các lý do khác, chẳng hạn như bảo vệ Halloween. Khi nó làm như vậy, bạn sẽ thấy các toán tử Spool hoặc Index Spool trong kế hoạch. Tuy nhiên, những lựa chọn như vậy không liên quan đến việc sử dụng các biểu thức bảng trong truy vấn.

CTE đệ quy

Có một số trường hợp ngoại lệ trong đó SQL Server vẫn giữ nguyên dữ liệu của biểu thức bảng. Một là việc sử dụng các khung nhìn được lập chỉ mục. Nếu bạn tạo chỉ mục nhóm trên một chế độ xem, SQL Server sẽ duy trì tập hợp kết quả của truy vấn bên trong trong chỉ mục nhóm của chế độ xem và giữ nó đồng bộ với bất kỳ thay đổi nào trong các bảng cơ sở bên dưới. Ngoại lệ khác là khi bạn sử dụng truy vấn đệ quy. SQL Server cần duy trì các tập kết quả trung gian của các truy vấn neo và đệ quy trong một ống để nó có thể truy cập tập kết quả của vòng cuối cùng được biểu thị bằng tham chiếu đệ quy đến tên CTE mỗi khi thành viên đệ quy được thực thi.

Để chứng minh điều này, tôi sẽ sử dụng một trong các truy vấn đệ quy từ Phần 6 trong loạt bài này.

Sử dụng mã sau để tạo bảng Nhân viên trong cơ sở dữ liệu tempdb, điền nó với dữ liệu mẫu và tạo chỉ mục hỗ trợ:

  SET NOCOUNT ON;
 
  USE tempdb;
 
  DROP TABLE IF EXISTS dbo.Employees;
  GO
 
  CREATE TABLE dbo.Employees
  (
    empid   INT         NOT NULL
    CONSTRAINT PK_Employees PRIMARY KEY,
    mgrid   INT         NULL     
    CONSTRAINT FK_Employees_Employees REFERENCES dbo.Employees,
    empname VARCHAR(25) NOT NULL,
    salary  MONEY       NOT NULL,
    CHECK (empid <> mgrid)
  );
 
  INSERT INTO dbo.Employees(empid, mgrid, empname, salary)
  VALUES(1,  NULL, 'David'  , $10000.00),
        (2,     1, 'Eitan'  ,  $7000.00),
        (3,     1, 'Ina'    ,  $7500.00),
        (4,     2, 'Seraph' ,  $5000.00),
        (5,     2, 'Jiru'   ,  $5500.00),
        (6,     2, 'Steve'  ,  $4500.00),
        (7,     3, 'Aaron'  ,  $5000.00),
        (8,     5, 'Lilach' ,  $3500.00),
        (9,     7, 'Rita'   ,  $3000.00),
        (10,    5, 'Sean'   ,  $3000.00),
        (11,    7, 'Gabriel',  $3000.00),
        (12,    9, 'Emilia' ,  $2000.00),
        (13,    9, 'Michael',  $2000.00),
        (14,    9, 'Didi'   ,  $1500.00);
 
  CREATE UNIQUE INDEX idx_unc_mgrid_empid
  ON dbo.Employees(mgrid, empid)
  INCLUDE(empname, salary);
  GO

Tôi đã sử dụng CTE đệ quy sau để trả về tất cả cấp dưới của người quản lý gốc cây con đầu vào, sử dụng nhân viên 3 làm người quản lý đầu vào trong ví dụ này:

DECLARE @root AS INT = 3;
 
  WITH C AS
  (
    SELECT empid, mgrid, empname
    FROM dbo.Employees
    WHERE empid = @root
    UNION ALL
    SELECT S.empid, S.mgrid, S.empname
    FROM C AS M
      INNER JOIN dbo.Employees AS S
      ON S.mgrid = M.empid
  )
  SELECT empid, mgrid, empname
  FROM C;

Kế hoạch cho truy vấn này (chúng tôi sẽ gọi nó là Truy vấn 6) được thể hiện trong Hình 6.

Hình 6:Kế hoạch thực thi cho Truy vấn 6

Hãy quan sát rằng điều đầu tiên xảy ra trong kế hoạch, ở bên phải của nút CHỌN gốc, là việc tạo một bảng làm việc dựa trên cây B được đại diện bởi toán tử Bộ đệm chỉ mục. Phần trên cùng của kế hoạch xử lý logic của thành viên neo. Nó kéo hàng của nhân viên đầu vào từ chỉ mục được nhóm trên Nhân viên và ghi nó vào ống chỉ. Phần dưới cùng của kế hoạch thể hiện logic của thành viên đệ quy. Nó được thực thi lặp đi lặp lại cho đến khi trả về một tập kết quả trống. Đầu vào bên ngoài cho toán tử Vòng lặp lồng nhau nhận được các trình quản lý từ vòng trước từ bộ đệm (toán tử Bộ đệm bảng). Đầu vào bên trong sử dụng toán tử Tìm kiếm chỉ mục dựa trên chỉ mục không phân biệt được tạo trên Nhân viên (mgrid, empid) để có được cấp dưới trực tiếp của người quản lý từ vòng trước. Tập hợp kết quả của mỗi lần thực hiện phần dưới cùng của kế hoạch cũng được ghi vào ống chỉ mục. Lưu ý rằng tất cả có 7 hàng được ghi vào ống chỉ. Một được trả về bởi thành viên neo và 6 khác được trả về bởi tất cả các lần thực thi của thành viên đệ quy.

Ngoài ra, thật thú vị khi để ý cách kế hoạch xử lý giới hạn đệ quy tối đa mặc định, là 100. Hãy quan sát rằng toán tử Vô hướng tính toán dưới cùng tiếp tục tăng bộ đếm bên trong có tên là Expr1011 lên 1 với mỗi lần thực thi thành viên đệ quy. Sau đó, toán tử Assert đặt cờ thành 0 nếu bộ đếm này vượt quá 100. Nếu điều này xảy ra, SQL Server sẽ dừng việc thực thi truy vấn và tạo ra lỗi.

Khi nào thì không nên tiếp tục

Quay lại CTE không đệ quy, thường không tồn tại lâu dài, thay vào đó bạn phải tìm ra từ góc độ tối ưu hóa khi sử dụng chúng so với các công cụ duy trì thực tế như bảng tạm thời và biến bảng. Tôi sẽ xem qua một số ví dụ để chứng minh khi nào mỗi cách tiếp cận là tối ưu hơn.

Hãy bắt đầu với một ví dụ trong đó CTE hoạt động tốt hơn các bảng tạm thời. Điều đó thường xảy ra khi bạn không có nhiều đánh giá cho cùng một CTE, thay vào đó, có lẽ chỉ là một giải pháp mô-đun trong đó mỗi CTE chỉ được đánh giá một lần. Đoạn mã sau (chúng tôi sẽ gọi là Truy vấn 7) truy vấn bảng Đơn hàng trong cơ sở dữ liệu Hiệu suất, có 1.000.000 hàng, để trả về số năm đặt hàng trong đó hơn 70 khách hàng khác nhau đã đặt hàng:

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

Truy vấn này tạo ra kết quả sau:

  orderyear   numcusts
  ----------- -----------
  2015        992
  2017        20000
  2018        20000
  2019        20000
  2016        20000

Tôi đã chạy mã này bằng SQL Server 2019 Developer Edition và nhận được kế hoạch được hiển thị trong Hình 7.

Hình 7:Kế hoạch thực thi cho Truy vấn 7

Lưu ý rằng việc hủy ghi chú CTE dẫn đến một kế hoạch lấy dữ liệu từ một chỉ mục trên bảng Đơn hàng và không liên quan đến bất kỳ tập hợp kết quả truy vấn bên trong của CTE. Tôi nhận được các số hiệu suất sau khi thực hiện truy vấn này trên máy của mình:

  duration: 265 ms, CPU: 828 ms, reads: 3970, writes: 0

Bây giờ chúng ta hãy thử một giải pháp sử dụng các bảng tạm thời thay vì CTE (chúng tôi sẽ gọi nó là Giải pháp 8), như sau:

  SELECT YEAR(orderdate) AS orderyear, custid
  INTO #T1
  FROM dbo.Orders;
 
  SELECT orderyear, COUNT(DISTINCT custid) AS numcusts
  INTO #T2
  FROM #T1
  GROUP BY orderyear;
 
  SELECT orderyear, numcusts
  FROM #T2
  WHERE numcusts > 70;
 
  DROP TABLE #T1, #T2;

Các kế hoạch cho giải pháp này được thể hiện trong Hình 8.

Hình 8:Các kế hoạch cho Giải pháp 8

Chú ý các toán tử Chèn bảng ghi các tập hợp kết quả vào các bảng tạm thời # T1 và # T2. Cái đầu tiên đặc biệt đắt vì nó ghi 1.000.000 hàng vào # T1. Đây là số hiệu suất mà tôi nhận được cho lần thực thi này:

  duration: 454 ms, CPU: 1517 ms, reads: 14359, writes: 359

Như bạn có thể thấy, giải pháp với CTE tối ưu hơn nhiều.

Khi nào thì tiếp tục

Vì vậy, có phải trường hợp giải pháp mô-đun chỉ liên quan đến một đánh giá duy nhất của mỗi CTE luôn được ưu tiên sử dụng các bảng tạm thời không? Không cần thiết. Trong các giải pháp dựa trên CTE bao gồm nhiều bước và dẫn đến các kế hoạch phức tạp trong đó trình tối ưu hóa cần áp dụng nhiều ước tính cơ bản tại nhiều điểm khác nhau trong kế hoạch, bạn có thể nhận được sự thiếu chính xác tích lũy dẫn đến các lựa chọn không tối ưu. Một trong những kỹ thuật để cố gắng giải quyết các trường hợp như vậy là duy trì một số kết quả trung gian tự đặt bạn thành các bảng tạm thời và thậm chí tạo các chỉ mục trên chúng nếu cần, giúp trình tối ưu hóa khởi đầu mới với số liệu thống kê mới, tăng khả năng cho các ước tính số lượng chất lượng tốt hơn hy vọng sẽ dẫn đến nhiều sự lựa chọn tối ưu hơn. Liệu điều này có tốt hơn giải pháp không sử dụng bảng tạm thời hay không là điều bạn cần phải kiểm tra. Đôi khi, việc đánh đổi thêm chi phí cho các tập hợp kết quả trung gian liên tục vì lợi ích của việc ước tính số lượng bản chất chất lượng tốt hơn sẽ rất đáng giá.

Một trường hợp điển hình khác trong đó sử dụng bảng tạm thời là cách tiếp cận được ưa thích là khi giải pháp dựa trên CTE có nhiều đánh giá của cùng một CTE và truy vấn bên trong của CTE khá tốn kém. Hãy xem xét giải pháp dựa trên CTE sau (chúng tôi sẽ gọi nó là Truy vấn 9), giải pháp này phù hợp với mỗi năm và tháng đơn đặt hàng, một năm và tháng đặt hàng khác có số lượng đơn hàng gần nhất:

  WITH OrdCount AS
  (
    SELECT YEAR(orderdate) AS orderyear, MONTH(orderdate) AS ordermonth,
      COUNT(*) AS numorders
    FROM dbo.Orders
    GROUP BY YEAR(orderdate), MONTH(orderdate)
  )
  SELECT O1.orderyear, O1.ordermonth, O1.numorders,
    O2.orderyear AS orderyear2, O2.ordermonth AS ordermonth2,
    O2.numorders AS numorders2
  FROM OrdCount AS O1 
  CROSS APPLY ( SELECT TOP (1) O2.orderyear, O2.ordermonth, O2.numorders
                FROM OrdCount AS O2 
                WHERE O2.orderyear <> O1.orderyear
                  OR O2.ordermonth <> O1.ordermonth
                ORDER BY ABS(O1.numorders - O2.numorders),
                  O2.orderyear, O2.ordermonth ) AS O2;

Truy vấn này tạo ra kết quả sau:

  orderyear   ordermonth  numorders   orderyear2  ordermonth2 numorders2
  ----------- ----------- ----------- ----------- ----------- -----------
  2016        1           21262       2017        3           21267
  2019        1           21227       2016        5           21229
  2019        2           19145       2018        2           19125
  2018        4           20561       2016        9           20554
  2018        5           21209       2019        5           21210
  2018        6           20515       2016        11          20513
  2018        7           21194       2018        10          21197
  2017        9           20542       2017        11          20539
  2017        10          21234       2019        3           21235
  2017        11          20539       2019        4           20537
  2017        12          21183       2016        8           21185
  2018        1           21241       2019        7           21238
  2016        2           19844       2019        12          20184
  2018        3           21222       2016        10          21222
  2016        4           20526       2019        9           20527
  2019        4           20537       2017        11          20539
  2017        5           21203       2017        8           21199
  2019        6           20531       2019        9           20527
  2017        7           21217       2016        7           21218
  2018        8           21283       2017        3           21267
  2018        10          21197       2017        8           21199
  2016        11          20513       2018        6           20515
  2019        11          20494       2017        4           20498
  2018        2           19125       2019        2           19145
  2016        3           21211       2016        12          21212
  2019        3           21235       2017        10          21234
  2016        5           21229       2019        1           21227
  2019        5           21210       2016        3           21211
  2017        6           20551       2016        9           20554
  2017        8           21199       2018        10          21197
  2018        9           20487       2019        11          20494
  2016        10          21222       2018        3           21222
  2018        11          20575       2016        6           20571
  2016        12          21212       2016        3           21211
  2019        12          20184       2018        9           20487
  2017        1           21223       2016        10          21222
  2017        2           19174       2019        2           19145
  2017        3           21267       2016        1           21262
  2017        4           20498       2019        11          20494
  2016        6           20571       2018        11          20575
  2016        7           21218       2017        7           21217
  2019        7           21238       2018        1           21241
  2016        8           21185       2017        12          21183
  2019        8           21189       2016        8           21185
  2016        9           20554       2017        6           20551
  2019        9           20527       2016        4           20526
  2019        10          21254       2016        1           21262
  2015        12          1018        2018        2           19125
  2018        12          21225       2017        1           21223

  (49 rows affected)

Kế hoạch cho Truy vấn 9 được thể hiện trong Hình 9.

Hình 9:Kế hoạch thực thi cho Truy vấn 9

Phần trên cùng của kế hoạch tương ứng với phiên bản của OrdCount CTE có bí danh là O1. Tài liệu tham khảo này dẫn đến một đánh giá về Số thứ tự CTE. Phần này của kế hoạch kéo các hàng từ một chỉ mục trên bảng Đơn hàng, nhóm chúng theo năm và tháng và tổng hợp số lượng đơn đặt hàng cho mỗi nhóm, dẫn đến 49 hàng. Phần dưới cùng của kế hoạch tương ứng với bảng dẫn xuất tương quan O2, được áp dụng cho mỗi hàng từ O1, do đó được thực hiện 49 lần. Mỗi lần thực thi sẽ truy vấn CTE của OrdCount và do đó dẫn đến đánh giá riêng về truy vấn bên trong của CTE. Bạn có thể thấy rằng phần dưới cùng của kế hoạch quét tất cả các hàng từ chỉ mục trên Đơn hàng, nhóm và tổng hợp chúng. Về cơ bản, bạn nhận được tổng cộng 50 đánh giá về CTE, dẫn đến 50 lần quét 1.000.000 hàng từ Đơn hàng, nhóm và tổng hợp chúng. Nó có vẻ không phải là một giải pháp rất hiệu quả. Dưới đây là các thước đo hiệu suất mà tôi nhận được khi thực hiện giải pháp này trên máy của mình:

  duration: 16 seconds, CPU: 56 seconds, reads: 130404, writes: 0

Cho rằng chỉ có vài chục tháng liên quan, điều sẽ hiệu quả hơn nhiều là sử dụng bảng tạm thời để lưu trữ kết quả của một hoạt động duy nhất nhóm và tổng hợp các hàng từ Đơn đặt hàng, sau đó có cả đầu vào bên ngoài và bên trong của toán tử ÁP DỤNG tương tác với bảng tạm thời. Đây là giải pháp (chúng tôi sẽ gọi là Giải pháp 10) bằng cách sử dụng bảng tạm thời thay vì CTE:

  SELECT YEAR(orderdate) AS orderyear, MONTH(orderdate) AS ordermonth,
    COUNT(*) AS numorders
  INTO #OrdCount
  FROM dbo.Orders
  GROUP BY YEAR(orderdate), MONTH(orderdate);
 
  SELECT O1.orderyear, O1.ordermonth, O1.numorders,
    O2.orderyear AS orderyear2, O2.ordermonth AS ordermonth2,
    O2.numorders AS numorders2
  FROM #OrdCount AS O1
  CROSS APPLY ( SELECT TOP (1) O2.orderyear, O2.ordermonth, O2.numorders
                FROM #OrdCount AS O2
                WHERE O2.orderyear <> O1.orderyear 
                  OR O2.ordermonth <> O1.ordermonth
                ORDER BY ABS(O1.numorders - O2.numorders),
                  O2.orderyear, O2.ordermonth ) AS O2;
 
  DROP TABLE #OrdCount;

Ở đây, không có nhiều điểm trong việc lập chỉ mục bảng tạm thời, vì bộ lọc TOP dựa trên tính toán trong đặc tả thứ tự của nó và do đó, việc sắp xếp là không thể tránh khỏi. Tuy nhiên, rất có thể trong các trường hợp khác, với các giải pháp khác, bạn cũng nên xem xét lập chỉ mục các bảng tạm thời của mình. Ở bất kỳ mức độ nào, kế hoạch cho giải pháp này được thể hiện trong Hình 10.

Hình 10:Các kế hoạch thực thi cho Giải pháp 10

Hãy quan sát trong sơ đồ hàng đầu về cách thức hoạt động nặng nhọc liên quan đến việc quét 1.000.000 hàng, nhóm và tổng hợp chúng chỉ diễn ra một lần. 49 hàng được ghi vào bảng tạm thời #OrdCount, sau đó sơ đồ dưới cùng tương tác với bảng tạm thời cho cả đầu vào bên ngoài và bên trong của toán tử Vòng lặp lồng nhau, xử lý logic của toán tử APPLICY.

Dưới đây là số hiệu suất mà tôi nhận được để thực hiện giải pháp này:

  duration: 0.392 seconds, CPU: 0.5 seconds, reads: 3636, writes: 3

Nó nhanh hơn theo thứ tự về mức độ so với giải pháp dựa trên CTE.

Tiếp theo là gì?

Trong bài viết này, tôi bắt đầu đề cập đến các cân nhắc tối ưu hóa liên quan đến CTE. Tôi đã chỉ ra rằng quá trình hủy đăng ký / thay thế diễn ra với các bảng dẫn xuất hoạt động theo cùng một cách với CTE. Tôi cũng đã thảo luận về thực tế là các CTE không đệ quy không được duy trì và giải thích rằng khi tính bền bỉ là yếu tố quan trọng đối với hiệu suất của giải pháp của bạn, bạn phải tự xử lý nó bằng cách sử dụng các công cụ như bảng tạm thời và biến bảng. Tháng tới, tôi sẽ tiếp tục thảo luận bằng cách đề cập đến các khía cạnh bổ sung của tối ưu hóa CTE.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. ) Toán tử cho người mới bắt đầu

  2. Cách sắp xếp hàng theo tổng nhóm trong SQL

  3. Lựa chọn kiểu dữ liệu có thể có tác động như thế nào?

  4. SQL SELECT DISTINCT:Các phương pháp hay nhất về hiệu suất

  5. Cách sao chép dữ liệu từ bảng này sang bảng khác trong SQL