Tại sao SQL Server sử dụng một kế hoạch thực hiện tốt hơn khi tôi nội tuyến biến?


32

Tôi có một truy vấn SQL mà tôi đang cố gắng tối ưu hóa:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'

SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

MyTable có hai chỉ số:

CREATE NONCLUSTERED INDEX IX_MyTable_SomeTimestamp_Includes
ON dbo.MyTable (SomeTimestamp ASC)
INCLUDE(Id, SomeInt)

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp)

Khi tôi thực hiện truy vấn chính xác như được viết ở trên, SQL Server sẽ quét chỉ mục đầu tiên, dẫn đến 189.703 lần đọc logic và thời lượng 2-3 giây.

Khi tôi nội tuyến @Idbiến và thực hiện lại truy vấn, SQL Server tìm kiếm chỉ mục thứ hai, dẫn đến chỉ có 104 lần đọc logic và thời lượng 0,001 giây (về cơ bản là tức thời).

Tôi cần biến, nhưng tôi muốn SQL sử dụng kế hoạch tốt. Là một giải pháp tạm thời, tôi đặt một gợi ý chỉ mục cho truy vấn và truy vấn về cơ bản là ngay lập tức. Tuy nhiên, tôi cố gắng tránh xa gợi ý về chỉ số khi có thể. Tôi thường cho rằng nếu trình tối ưu hóa truy vấn không thể thực hiện công việc của nó, thì có một số việc tôi có thể làm (hoặc ngừng thực hiện) để giúp nó mà không nói rõ ràng phải làm gì.

Vậy, tại sao SQL Server đưa ra một kế hoạch tốt hơn khi tôi nội tuyến biến?

Câu trả lời:


44

Trong SQL Server, có ba dạng vị ngữ không tham gia phổ biến:

Với một giá trị theo nghĩa đen :

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = 1;

Với một tham số :

CREATE PROCEDURE dbo.SomeProc(@Reputation INT)
AS
BEGIN
    SELECT COUNT(*) AS records
    FROM   dbo.Users AS u
    WHERE  u.Reputation = @Reputation;
END;

Với một biến cục bộ :

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

Kết quả

Khi bạn sử dụng một giá trị bằng chữ và kế hoạch của bạn không phải là một) Tầm thường và b) Thông số đơn giản hoặc c) bạn không bật Tham số cưỡng bức , trình tối ưu hóa tạo ra một kế hoạch rất đặc biệt chỉ dành cho giá trị đó.

Khi bạn sử dụng một tham số , trình tối ưu hóa sẽ tạo một kế hoạch cho tham số đó (đây được gọi là đánh hơi tham số ), và sau đó sử dụng lại kế hoạch đó, vắng mặt gợi ý biên dịch lại, lên kế hoạch xóa bộ nhớ cache, v.v.

Khi bạn sử dụng một biến cục bộ , trình tối ưu hóa sẽ lập kế hoạch cho ... Một cái gì đó .

Nếu bạn đã chạy truy vấn này:

DECLARE @Reputation INT = 1

SELECT COUNT(*) AS records
FROM   dbo.Users AS u
WHERE  u.Reputation = @Reputation;

Kế hoạch sẽ như thế này:

QUẢ HẠCH

Và số lượng hàng ước tính cho biến cục bộ đó sẽ trông như thế này:

QUẢ HẠCH

Mặc dù truy vấn trả về số lượng 4.744.427.

Các biến cục bộ, không xác định, không sử dụng phần 'tốt' của biểu đồ để ước tính cardinality. Họ sử dụng một dự đoán dựa trên vector mật độ.

QUẢ HẠCH

SELECT 5.280389E-05 * 7250739 AS [poo]

Điều đó sẽ cung cấp cho bạn 382.86722457471, đó là dự đoán mà trình tối ưu hóa đưa ra.

Những dự đoán chưa biết này thường là những dự đoán rất xấu, và thường có thể dẫn đến các kế hoạch xấu và lựa chọn chỉ số xấu.

Sửa nó?

Các lựa chọn của bạn thường là:

  • Gợi ý chỉ số giòn
  • Gợi ý biên dịch lại đắt tiền
  • SQL động được tham số hóa
  • Một thủ tục lưu trữ
  • Cải thiện chỉ số hiện tại

Các tùy chọn của bạn cụ thể là:

Cải thiện chỉ mục hiện tại có nghĩa là mở rộng nó để bao gồm tất cả các cột cần thiết cho truy vấn:

CREATE NONCLUSTERED INDEX IX_MyTable_Id_SomeBit_Includes
ON dbo.MyTable (Id, SomeBit)
INCLUDE (TotallyUnrelatedTimestamp, SomeTimestamp, SomeInt)
WITH (DROP_EXISTING = ON);

Giả sử rằng Idcác giá trị được chọn lọc hợp lý, điều này sẽ cung cấp cho bạn một kế hoạch tốt và giúp trình tối ưu hóa bằng cách cung cấp cho nó một phương thức truy cập dữ liệu 'rõ ràng'.

Đọc thêm

Bạn có thể đọc thêm về nhúng tham số ở đây:


12

Tôi sẽ giả định rằng bạn đã sai lệch dữ liệu, rằng bạn không muốn sử dụng gợi ý truy vấn để buộc trình tối ưu hóa phải làm gì và bạn cần có hiệu suất tốt cho tất cả các giá trị đầu vào có thể có @Id. Bạn có thể nhận được một gói truy vấn được đảm bảo chỉ cần một vài lần đọc logic cho bất kỳ giá trị đầu vào nào có thể nếu bạn sẵn sàng tạo cặp chỉ mục sau (hoặc tương đương với chúng):

CREATE INDEX GetMinSomeTimestamp ON dbo.MyTable (Id, SomeTimestamp) WHERE SomeBit = 1;
CREATE INDEX GetMaxSomeInt ON dbo.MyTable (Id, SomeInt) WHERE SomeBit = 1;

Dưới đây là dữ liệu thử nghiệm của tôi. Tôi đặt 13 hàng M vào bảng và làm cho một nửa trong số chúng có giá trị '3A35EA17-CE7E-4637-8319-4C517B6E48CA'cho Idcột.

DROP TABLE IF EXISTS dbo.MyTable;

CREATE TABLE dbo.MyTable (
    Id uniqueidentifier,
    SomeTimestamp DATETIME2,
    SomeInt INT,
    SomeBit BIT,
    FILLER VARCHAR(100)
);

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT NEWID(), CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

INSERT INTO dbo.MyTable WITH (TABLOCK)
SELECT '3A35EA17-CE7E-4637-8319-4C517B6E48CA', CURRENT_TIMESTAMP, 0, 1, REPLICATE('Z', 100)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;

Truy vấn này thoạt nhìn có vẻ hơi lạ:

DECLARE @Id UNIQUEIDENTIFIER = '3A35EA17-CE7E-4637-8319-4C517B6E48CA'

SELECT
  @Id,
  st.SomeTimestamp,
  si.SomeInt
FROM (
    SELECT TOP (1) SomeInt, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeInt DESC
) si
CROSS JOIN (
    SELECT TOP (1) SomeTimestamp, Id
    FROM dbo.MyTable
    WHERE Id = @Id
    AND SomeBit = 1
    ORDER BY SomeTimestamp ASC
) st;

Nó được thiết kế để tận dụng thứ tự của các chỉ mục để tìm giá trị tối thiểu hoặc tối đa với một vài lần đọc logic. Có CROSS JOINđể có kết quả chính xác khi không có bất kỳ hàng phù hợp nào cho @Idgiá trị. Ngay cả khi tôi lọc theo giá trị phổ biến nhất trong bảng (khớp với 6,5 triệu hàng) tôi chỉ nhận được 8 lần đọc logic:

Bảng 'MyTable'. Quét số 2, đọc logic 8

Đây là kế hoạch truy vấn:

nhập mô tả hình ảnh ở đây

Cả hai chỉ mục tìm kiếm 0 hoặc 1 hàng. Nó cực kỳ hiệu quả, nhưng việc tạo hai chỉ mục có thể là quá mức cần thiết cho kịch bản của bạn. Bạn có thể xem xét các chỉ số sau thay thế:

CREATE INDEX CoveringIndex ON dbo.MyTable (Id) INCLUDE (SomeTimestamp, SomeInt) WHERE SomeBit = 1;

Bây giờ kế hoạch truy vấn cho truy vấn ban đầu (với một MAXDOP 1gợi ý tùy chọn ) trông hơi khác một chút:

nhập mô tả hình ảnh ở đây

Việc tra cứu chính không còn cần thiết nữa. Với đường dẫn truy cập tốt hơn sẽ hoạt động tốt cho tất cả các đầu vào, bạn không cần phải lo lắng về trình tối ưu hóa chọn gói truy vấn sai do vectơ mật độ. Tuy nhiên, truy vấn và chỉ mục này sẽ không hiệu quả như truy vấn khác nếu bạn tìm kiếm trên một phổ biến@Id giá trị .

Bảng 'MyTable'. Quét số 1, đọc logic 33757


2

Tôi không thể trả lời tại sao ở đây, nhưng cách nhanh chóng và bẩn thỉu để đảm bảo rằng truy vấn chạy theo cách bạn muốn:

DECLARE @Id UNIQUEIDENTIFIER = 'cec094e5-b312-4b13-997a-c91a8c662962'
SELECT 
  Id,
  MIN(SomeTimestamp),
  MAX(SomeInt)
FROM dbo.MyTable WITH (INDEX(IX_MyTable_Id_SomeBit_Includes))
WHERE Id = @Id
  AND SomeBit = 1
GROUP BY Id

Điều này có nguy cơ rằng bảng hoặc chỉ số có thể thay đổi trong tương lai sao cho việc tối ưu hóa này trở nên rối loạn, nhưng nó có sẵn nếu bạn cần. Hy vọng rằng ai đó có thể cung cấp cho bạn một câu trả lời nguyên nhân gốc rễ, như bạn yêu cầu, thay vì cách giải quyết này.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.