Việc bao gồm ORDER BY vào truy vấn không trả về hàng nào ảnh hưởng lớn đến hiệu suất


15

Đưa ra một phép nối ba bảng đơn giản, hiệu năng truy vấn thay đổi mạnh mẽ khi ORDER BY được bao gồm ngay cả khi không có hàng nào được trả về. Kịch bản vấn đề thực tế mất 30 giây để trả về các hàng 0 nhưng ngay lập tức khi không bao gồm ORDER BY. Tại sao?

SELECT * 
FROM tinytable t                          /* one narrow row */
JOIN smalltable s on t.id=s.tinyId        /* one narrow row */
JOIN bigtable b on b.smallGuidId=s.GuidId /* a million narrow rows */
WHERE t.foreignId=3                       /* doesn't match */
ORDER BY b.CreatedUtc          /* try with and without this ORDER BY */

Tôi hiểu rằng tôi có thể có một chỉ mục trên bigtable.smallGuidId, nhưng, tôi tin rằng điều đó thực sự sẽ làm cho nó tồi tệ hơn trong trường hợp này.

Đây là kịch bản để tạo / điền vào các bảng để kiểm tra. Thật kỳ lạ, có vẻ như vấn đề là smalltable có trường nvarchar (max). Có vẻ như vấn đề là tôi tham gia vào bigtable với một hướng dẫn (mà tôi đoán làm cho nó muốn sử dụng khớp băm).

CREATE TABLE tinytable
  (
     id        INT PRIMARY KEY IDENTITY(1, 1),
     foreignId INT NOT NULL
  )

CREATE TABLE smalltable
  (
     id     INT PRIMARY KEY IDENTITY(1, 1),
     GuidId UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
     tinyId INT NOT NULL,
     Magic  NVARCHAR(max) NOT NULL DEFAULT ''
  )

CREATE TABLE bigtable
  (
     id          INT PRIMARY KEY IDENTITY(1, 1),
     CreatedUtc  DATETIME NOT NULL DEFAULT GETUTCDATE(),
     smallGuidId UNIQUEIDENTIFIER NOT NULL
  )

INSERT tinytable
       (foreignId)
VALUES(7)

INSERT smalltable
       (tinyId)
VALUES(1)

-- make a million rows 
DECLARE @i INT;

SET @i=20;

INSERT bigtable
       (smallGuidId)
SELECT GuidId
FROM   smalltable;

WHILE @i > 0
  BEGIN
      INSERT bigtable
             (smallGuidId)
      SELECT smallGuidId
      FROM   bigtable;

      SET @i=@i - 1;
  END 

Tôi đã thử nghiệm trên SQL 2005, 2008 và 2008R2 với cùng kết quả.

Câu trả lời:


32

Tôi đồng ý với câu trả lời của Martin Smith, nhưng vấn đề không chỉ đơn giản là một trong những số liệu thống kê. Số liệu thống kê cho cột ForeignId (giả sử đã bật thống kê tự động) cho thấy chính xác rằng không có hàng nào tồn tại cho giá trị 3 (chỉ có một, với giá trị là 7):

DBCC SHOW_STATISTICS (tinytable, foreignId) WITH HISTOGRAM

thống kê đầu ra

SQL Server biết rằng mọi thứ có thể đã thay đổi kể từ khi thống kê được ghi lại, do đó, có thể có một hàng cho giá trị 3 khi kế hoạch được thực hiện . Ngoài ra, bất kỳ khoảng thời gian nào cũng có thể trôi qua giữa quá trình biên dịch và thực hiện kế hoạch (rốt cuộc, các kế hoạch được lưu trữ để sử dụng lại). Như Martin nói, SQL Server chứa logic để phát hiện khi các sửa đổi đủ đã được thực hiện để biện minh cho việc biên dịch lại bất kỳ gói được lưu trữ nào vì lý do tối ưu.

Không ai trong số này cuối cùng có vấn đề, tuy nhiên. Với ngoại lệ một trường hợp cạnh, trình tối ưu hóa sẽ không bao giờ ước tính số lượng hàng được tạo bởi thao tác bảng là 0. Nếu nó có thể xác định tĩnh rằng đầu ra phải luôn là hàng 0, thì thao tác là dự phòng và sẽ bị xóa hoàn toàn.

Thay vào đó, mô hình của trình tối ưu hóa ước tính tối thiểu một hàng. Sử dụng heuristic này có xu hướng tạo ra các kế hoạch trung bình tốt hơn so với trường hợp ước tính thấp hơn là có thể. Một kế hoạch tạo ra ước tính hàng không ở một giai đoạn nào đó sẽ vô dụng kể từ thời điểm đó trong luồng xử lý, vì sẽ không có cơ sở để đưa ra quyết định dựa trên chi phí (hàng 0 là hàng 0 cho dù thế nào đi chăng nữa). Nếu ước tính hóa ra là sai, hình dạng kế hoạch trên ước tính hàng không gần như không có cơ hội hợp lý.

Yếu tố thứ hai là một giả định mô hình hóa khác gọi là Giả định ngăn chặn. Điều này về cơ bản nói rằng nếu một truy vấn tham gia một phạm vi các giá trị với một phạm vi giá trị khác, thì đó là do các phạm vi chồng lấp. Một cách khác để nói điều này là nói rằng phép nối đang được chỉ định vì các hàng dự kiến ​​sẽ được trả về. Nếu không có lý do này, chi phí thường sẽ bị đánh giá thấp, dẫn đến các kế hoạch kém cho một loạt các truy vấn phổ biến.

Về cơ bản, những gì bạn có ở đây là một truy vấn không phù hợp với mô hình của trình tối ưu hóa. Chúng tôi không thể làm gì để 'cải thiện' các ước tính với các chỉ mục nhiều cột hoặc được lọc; không có cách nào để có được ước tính thấp hơn 1 hàng ở đây. Một cơ sở dữ liệu thực sự có thể có khóa ngoại để đảm bảo rằng tình huống này không thể phát sinh, nhưng giả sử rằng không áp dụng được ở đây, chúng tôi chỉ còn lại việc sử dụng các gợi ý để sửa điều kiện ngoài mô hình. Bất kỳ số lượng các phương pháp gợi ý khác nhau sẽ làm việc với truy vấn này. OPTION (FORCE ORDER)là một trong đó xảy ra để làm việc tốt với các truy vấn như được viết.


21

Vấn đề cơ bản ở đây là một trong những số liệu thống kê.

Đối với cả hai truy vấn, số lượng hàng ước tính cho thấy rằng nó tin rằng trận chung kết SELECTsẽ trả về 1.048.580 hàng (cùng số lượng hàng được ước tính tồn tại bigtable) thay vì 0 thực sự xảy ra.

Cả hai JOINđiều kiện của bạn đều khớp và sẽ bảo toàn tất cả các hàng. Cuối cùng, họ bị loại vì hàng đơn tinytablekhông khớp với t.foreignId=3vị ngữ.

Nếu bạn chạy

SELECT * 
FROM tinytable t  
WHERE t.foreignId=3  AND id=1 

và nhìn vào số lượng hàng ước tính 1chứ không phải là 0lỗi này lan truyền trong suốt kế hoạch. tinytablehiện có 1 hàng. Các số liệu thống kê sẽ không được biên dịch lại cho bảng này cho đến khi sửa đổi 500 hàng đã xảy ra để có thể thêm một hàng phù hợp và nó sẽ không kích hoạt biên dịch lại.

Lý do tại sao Thứ tự tham gia thay đổi khi bạn thêm ORDER BYmệnh đề và có một varchar(max)cột smalltablelà vì nó ước tính rằng varchar(max)các cột sẽ tăng kích thước hàng trung bình lên tới 4.000 byte. Nhân số đó với 1048580 hàng và điều đó có nghĩa là thao tác sắp xếp sẽ cần khoảng 4GB ước tính để nó quyết định thực hiện SORTthao tác trước JOIN.

Bạn có thể buộc ORDER BYtruy vấn áp dụng ORDER BYchiến lược không tham gia với việc sử dụng các gợi ý như dưới đây.

SELECT *
FROM   tinytable t /* one narrow row */
       INNER MERGE JOIN smalltable s /* one narrow row */
                        INNER LOOP JOIN bigtable b
                          ON b.smallGuidId = s.GuidId /* a million narrow rows */
         ON t.id = s.tinyId
WHERE  t.foreignId = 3 /* doesn't match */
ORDER  BY b.CreatedUtc
OPTION (MAXDOP 1) 

Kế hoạch cho thấy một toán tử sắp xếp với chi phí cây phụ 12,000ước tính với số lượng hàng ước tính gần và sai và kích thước dữ liệu ước tính.

Kế hoạch

BTW Tôi không tìm thấy việc thay thế các UNIQUEIDENTIFIERcột bằng các số nguyên đã thay đổi những thứ trong thử nghiệm của mình.


2

Bật nút Hiển thị kế hoạch thực hiện của bạn và bạn có thể thấy những gì đang xảy ra. Đây là kế hoạch cho truy vấn "chậm": nhập mô tả hình ảnh ở đây

Và đây là truy vấn "nhanh": nhập mô tả hình ảnh ở đây

Nhìn vào đó - chạy cùng nhau, truy vấn đầu tiên là "đắt hơn" 33 lần (tỷ lệ 97: 3). SQL đang tối ưu hóa truy vấn đầu tiên để đặt hàng BigTable theo datetime, sau đó chạy một vòng lặp "tìm kiếm" nhỏ trên SmallTable & TinyTable, thực hiện chúng 1 triệu lần mỗi lần (bạn có thể di chuột qua biểu tượng "Tìm kiếm chỉ mục cụm" để có thêm số liệu thống kê). Vì vậy, loại (27%) và 2 x 1 triệu "tìm kiếm" trên các bảng nhỏ (23% và 46%) là phần lớn của truy vấn đắt tiền. So sánh, ORDER BYtruy vấn không thực hiện tổng cộng 3 lần quét.

Về cơ bản, bạn đã tìm thấy một lỗ hổng trong logic tối ưu hóa SQL cho kịch bản cụ thể của bạn. Nhưng như TysHTTP đã nêu, nếu bạn thêm một chỉ mục (làm chậm phần chèn / cập nhật của bạn một số), quá trình quét của bạn sẽ trở nên điên rồ nhanh chóng.


2

Điều gì đang xảy ra là SQL đang quyết định chạy thứ tự trước khi hạn chế.

Thử đi:

SELECT *
(
SELECT * 
FROM tinytable t
    INNER JOIN smalltable s on t.id=s.tinyId
    INNER JOIN bigtable b on b.smallGuidId=s.GuidId
WHERE t.foreignId=3
) X
ORDER BY b.CreatedUtc

Điều này mang lại cho bạn hiệu suất được cải thiện (trong trường hợp này khi số lượng kết quả được trả về là rất nhỏ), mà không thực sự có hiệu suất đạt được khi thêm chỉ số khác. Mặc dù thật kỳ quặc khi trình tối ưu hóa SQL quyết định thực hiện thứ tự trước khi tham gia, nhưng có khả năng là vì nếu bạn thực sự có dữ liệu trả về thì việc sắp xếp nó sau khi các phép nối sẽ mất nhiều thời gian hơn so với việc sắp xếp mà không có.

Cuối cùng, hãy thử chạy tập lệnh sau và xem liệu số liệu thống kê và chỉ mục được cập nhật có khắc phục được sự cố bạn đang gặp phải không:

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

EXEC [sp_MSforeachtable] @command1="RAISERROR('DBCC DBREINDEX(''?'') ...',10,1) WITH NOWAIT DBCC DBREINDEX('?')"

EXEC [sp_MSforeachtable] @command1="RAISERROR('UPDATE STATISTICS(''?'') ...',10,1) WITH NOWAIT UPDATE STATISTICS ? "

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.