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

Viết lại các truy vấn để cải thiện hiệu suất

Trong một thế giới hoàn hảo, không quan trọng cú pháp T-SQL cụ thể nào mà chúng tôi chọn để diễn đạt một truy vấn. Bất kỳ cấu trúc nào giống hệt nhau về ngữ nghĩa sẽ dẫn đến cùng một kế hoạch thực thi vật lý, với các đặc tính hiệu suất giống hệt nhau.

Để đạt được điều đó, trình tối ưu hóa truy vấn SQL Server sẽ cần biết mọi điểm tương đương logic có thể có (giả sử chúng ta có thể biết tất cả chúng) và được cung cấp thời gian và tài nguyên để khám phá tất cả các tùy chọn. Với số lượng khổng lồ các cách khả thi mà chúng ta có thể thể hiện cùng một yêu cầu trong T-SQL và số lượng lớn các phép biến đổi có thể xảy ra, các kết hợp nhanh chóng trở nên không thể quản lý được đối với tất cả trừ những trường hợp đơn giản nhất.

Một "thế giới hoàn hảo" với sự độc lập hoàn toàn về cú pháp có thể không quá hoàn hảo đối với những người dùng phải đợi hàng ngày, hàng tuần hoặc thậm chí hàng năm để biên dịch một truy vấn phức tạp vừa phải. Vì vậy, trình tối ưu hóa truy vấn thỏa hiệp:nó khám phá một số điểm tương đương phổ biến và cố gắng tránh dành nhiều thời gian hơn cho việc biên dịch và tối ưu hóa hơn là tiết kiệm thời gian thực thi. Mục tiêu của nó có thể được tóm tắt là cố gắng tìm ra một kế hoạch thực hiện hợp lý trong một thời gian hợp lý, đồng thời tiêu tốn các nguồn lực hợp lý.

Một kết quả của tất cả những điều này là các kế hoạch thực thi thường nhạy cảm với dạng viết của truy vấn. Trình tối ưu hóa có một số logic để nhanh chóng chuyển đổi một số cấu trúc tương đương được sử dụng rộng rãi thành một dạng chung, nhưng những khả năng này không được ghi chép đầy đủ hoặc (ở bất kỳ đâu gần) toàn diện.

Chúng tôi chắc chắn có thể tối đa hóa cơ hội có được một kế hoạch thực thi tốt bằng cách viết các truy vấn đơn giản hơn, cung cấp các chỉ mục hữu ích, duy trì số liệu thống kê tốt và giới hạn bản thân với các khái niệm quan hệ hơn (ví dụ:bằng cách tránh con trỏ, vòng lặp rõ ràng và các hàm không nội tuyến) nhưng điều này là không phải là một giải pháp hoàn chỉnh. Không thể nói rằng một cấu trúc T-SQL sẽ luôn luôn tạo ra một kế hoạch thực thi tốt hơn một phương án thay thế giống hệt nhau về ngữ nghĩa.

Lời khuyên thông thường của tôi là bắt đầu với biểu mẫu truy vấn quan hệ đơn giản nhất đáp ứng nhu cầu của bạn, sử dụng bất kỳ cú pháp T-SQL nào bạn thấy thích hợp. Nếu truy vấn không thực hiện với các yêu cầu sau khi tối ưu hóa vật lý (ví dụ:lập chỉ mục), bạn nên thử diễn đạt truy vấn theo một cách hơi khác, trong khi vẫn giữ nguyên ngữ nghĩa ban đầu. Đây là phần khó khăn. Bạn nên thử viết lại phần nào của truy vấn? Bạn nên thử cách viết lại nào? Không có câu trả lời đơn giản cho tất cả các câu hỏi này. Một trong số đó là kinh nghiệm, mặc dù biết một chút về tối ưu hóa truy vấn và nội bộ công cụ thực thi cũng có thể là một hướng dẫn hữu ích.

Ví dụ

Ví dụ này sử dụng bảng AdventureWorks TransactionHistory. Tập lệnh dưới đây tạo một bản sao của bảng và tạo một chỉ mục được phân cụm và không được phân cụm. Chúng tôi sẽ không sửa đổi dữ liệu; bước này chỉ để làm cho việc lập chỉ mục rõ ràng (và đặt tên ngắn hơn cho bảng):

SELECT *
INTO dbo.TH
FROM Production.TransactionHistory;
 
CREATE UNIQUE CLUSTERED INDEX CUQ_TransactionID
ON dbo.TH (TransactionID);
 
CREATE NONCLUSTERED INDEX IX_ProductID
ON dbo.TH (ProductID);

Nhiệm vụ là tạo danh sách ID sản phẩm và lịch sử cho sáu sản phẩm cụ thể. Một cách để diễn đạt truy vấn là:

SELECT ProductID, TransactionID
FROM dbo.TH
WHERE ProductID IN (520, 723, 457, 800, 943, 360);

Truy vấn này trả về 764 hàng bằng cách sử dụng kế hoạch thực thi sau (được hiển thị trong SentryOne Plan Explorer):

Truy vấn đơn giản này đủ điều kiện để biên dịch gói TRIVIAL. Kế hoạch thực thi có sáu hoạt động tìm kiếm chỉ mục riêng biệt trong một:

Độc giả có đôi mắt đại bàng sẽ nhận thấy rằng sáu tìm kiếm được liệt kê trong tăng dần thứ tự ID sản phẩm, không theo thứ tự (tùy ý) được chỉ định trong danh sách IN của truy vấn ban đầu. Thật vậy, nếu bạn tự chạy truy vấn, bạn có nhiều khả năng thấy kết quả được trả về theo thứ tự ID sản phẩm tăng dần. Truy vấn không được đảm bảo để trả về kết quả theo thứ tự đó, vì chúng tôi không chỉ định mệnh đề ORDER BY cấp cao nhất. Tuy nhiên, chúng tôi có thể thêm điều khoản ORDER BY như vậy, mà không thay đổi kế hoạch thực hiện được tạo trong trường hợp này:

SELECT ProductID, TransactionID
FROM dbo.TH
WHERE ProductID IN (520, 723, 457, 800, 943, 360)
ORDER BY ProductID;

Tôi sẽ không lặp lại đồ họa kế hoạch thực thi, vì nó hoàn toàn giống nhau:truy vấn vẫn đủ điều kiện cho một kế hoạch tầm thường, các hoạt động tìm kiếm hoàn toàn giống nhau và hai kế hoạch có cùng chi phí ước tính. Thêm điều khoản ORDER BY chúng tôi hoàn toàn không mất chi phí, nhưng chúng tôi có được sự đảm bảo về thứ tự tập hợp kết quả.

Giờ đây, chúng tôi có đảm bảo rằng kết quả sẽ được trả lại theo thứ tự ID sản phẩm, nhưng truy vấn của chúng tôi hiện không chỉ định cách các hàng có cùng cùng ID sản phẩm sẽ được đặt hàng. Nhìn vào kết quả, bạn có thể thấy rằng các hàng cho cùng một ID sản phẩm dường như được sắp xếp theo ID giao dịch, tăng dần.

Không có ORDER BY rõ ràng, đây chỉ là một quan sát khác (tức là chúng tôi không thể dựa vào thứ tự này), nhưng chúng tôi có thể sửa đổi truy vấn để đảm bảo các hàng được sắp xếp theo ID giao dịch trong mỗi ID sản phẩm:

SELECT ProductID, TransactionID
FROM dbo.TH
WHERE ProductID IN (520, 723, 457, 800, 943, 360)
ORDER BY ProductID, TransactionID;

Một lần nữa, kế hoạch thực thi cho truy vấn này giống hệt như trước đây; cùng một kế hoạch tầm thường với cùng một chi phí ước tính được sản xuất. Sự khác biệt là kết quả hiện được đảm bảo để được đặt hàng trước theo ID sản phẩm và sau đó là ID giao dịch.

Một số người có thể bị cám dỗ để kết luận rằng hai truy vấn trước đó cũng sẽ luôn trả về các hàng theo thứ tự này, bởi vì các kế hoạch thực thi đều giống nhau. Đây không phải là một hàm ý an toàn, bởi vì không phải tất cả các chi tiết của công cụ thực thi đều được hiển thị trong các kế hoạch thực thi (ngay cả ở dạng XML). Không có mệnh đề thứ tự rõ ràng, SQL Server có thể tự do trả về các hàng theo bất kỳ thứ tự nào, ngay cả khi kế hoạch trông giống nhau đối với chúng tôi (ví dụ:nó có thể thực hiện các tìm kiếm theo thứ tự được chỉ định trong văn bản truy vấn). Vấn đề là trình tối ưu hóa truy vấn biết về và có thể thực thi một số hành vi nhất định trong công cụ mà người dùng không hiển thị.

Trong trường hợp bạn đang thắc mắc làm cách nào để chỉ mục không hợp nhất không phải duy nhất của chúng tôi trên ID sản phẩm có thể trả về các hàng trong Sản phẩm Thứ tự ID giao dịch, câu trả lời là khóa chỉ mục không phân biệt kết hợp ID giao dịch (khóa chỉ mục được phân cụm duy nhất). Trên thực tế, vật lý cấu trúc của chỉ mục không phân bổ của chúng tôi là chính xác giống nhau, ở tất cả các cấp, như thể chúng ta đã tạo chỉ mục với định nghĩa sau:

CREATE UNIQUE NONCLUSTERED INDEX IX_ProductID
ON dbo.TH (ProductID, TransactionID);

Chúng tôi thậm chí có thể viết truy vấn với DISTINCT hoặc GROUP BY rõ ràng mà vẫn nhận được chính xác cùng một kế hoạch thực thi:

SELECT DISTINCT ProductID, TransactionID
FROM dbo.TH
WHERE ProductID IN (520, 723, 457, 800, 943, 360)
ORDER BY ProductID, TransactionID;

Để rõ ràng, điều này không yêu cầu thay đổi chỉ mục không phân tán ban đầu theo bất kỳ cách nào. Ví dụ cuối cùng, lưu ý rằng chúng tôi cũng có thể yêu cầu kết quả theo thứ tự giảm dần:

SELECT DISTINCT ProductID, TransactionID
FROM dbo.TH
WHERE ProductID IN (520, 723, 457, 800, 943, 360)
ORDER BY ProductID DESC, TransactionID DESC;

Các thuộc tính kế hoạch thực thi hiện cho thấy rằng chỉ mục được quét ngược:

Bên cạnh đó, kế hoạch cũng giống nhau - nó được tạo ra ở giai đoạn tối ưu hóa kế hoạch nhỏ và vẫn có cùng chi phí ước tính.

Viết lại truy vấn

Không có gì sai với truy vấn hoặc kế hoạch thực thi trước đó, nhưng chúng tôi có thể đã chọn cách diễn đạt truy vấn theo cách khác:

SELECT ProductID, TransactionID
FROM dbo.TH
WHERE ProductID = 520
OR ProductID = 723
OR ProductID = 457
OR ProductID = 800
OR ProductID = 943
OR ProductID = 360;

Rõ ràng biểu mẫu này chỉ định chính xác các kết quả giống như kết quả ban đầu và thực sự là truy vấn mới tạo ra cùng một kế hoạch thực thi (kế hoạch nhỏ, nhiều lần tìm kiếm trong một, cùng một chi phí ước tính). Biểu mẫu OR có lẽ làm cho nó rõ ràng hơn một chút rằng kết quả là sự kết hợp của các kết quả cho sáu ID sản phẩm riêng lẻ, điều này có thể khiến chúng tôi thử một biến thể khác làm cho ý tưởng này trở nên rõ ràng hơn:

SELECT ProductID, TransactionID FROM dbo.TH WHERE ProductID = 520 
UNION ALL
SELECT ProductID, TransactionID FROM dbo.TH WHERE ProductID = 723 
UNION ALL
SELECT ProductID, TransactionID FROM dbo.TH WHERE ProductID = 457 
UNION ALL
SELECT ProductID, TransactionID FROM dbo.TH WHERE ProductID = 800 
UNION ALL
SELECT ProductID, TransactionID FROM dbo.TH WHERE ProductID = 943 
UNION ALL
SELECT ProductID, TransactionID FROM dbo.TH WHERE ProductID = 360;

Kế hoạch thực thi cho truy vấn UNION ALL là khá khác nhau:

Bên cạnh sự khác biệt rõ ràng về hình ảnh, kế hoạch này yêu cầu tối ưu hóa dựa trên chi phí (ĐẦY ĐỦ) (nó không đủ điều kiện cho một kế hoạch tầm thường) và chi phí ước tính (nói một cách tương đối) cao hơn một chút, khoảng 0,02 > đơn vị so với khoảng 0,005 đơn vị trước đây.

Điều này quay trở lại nhận xét mở đầu của tôi:trình tối ưu hóa truy vấn không biết về mọi sự tương đương logic và không phải lúc nào cũng có thể nhận ra các truy vấn thay thế khi chỉ định cùng một kết quả. Điểm tôi đang thực hiện ở giai đoạn này là việc thể hiện truy vấn cụ thể này bằng UNION ALL thay vì IN dẫn đến một kế hoạch thực thi kém tối ưu hơn.

Ví dụ thứ hai

Ví dụ này chọn một bộ sáu ID sản phẩm khác nhau và yêu cầu dẫn đến thứ tự ID giao dịch:

SELECT ProductID, TransactionID
FROM dbo.TH
WHERE ProductID IN (870, 873, 921, 712, 707, 711)
ORDER BY TransactionID;

Chỉ mục không phân nhóm của chúng tôi không thể cung cấp các hàng theo thứ tự được yêu cầu, do đó, trình tối ưu hóa truy vấn có lựa chọn giữa việc tìm kiếm trên chỉ mục không phân nhóm và sắp xếp, hoặc quét chỉ mục được nhóm (chỉ được khóa trên ID giao dịch) và áp dụng các vị từ ID sản phẩm như một phần dư. Các ID sản phẩm được liệt kê có khả năng chọn lọc thấp hơn so với nhóm trước đó, vì vậy trình tối ưu hóa chọn quét chỉ mục theo nhóm trong trường hợp này:

Bởi vì có một lựa chọn dựa trên chi phí để thực hiện, kế hoạch thực hiện này không đủ điều kiện cho một kế hoạch tầm thường. Chi phí ước tính của kế hoạch cuối cùng là khoảng 0,714 các đơn vị. Quét chỉ mục được nhóm yêu cầu 797 các lần đọc logic tại thời điểm thực thi.

Có lẽ ngạc nhiên rằng truy vấn không sử dụng chỉ mục sản phẩm, chúng tôi có thể thử buộc tìm kiếm chỉ mục không phân biệt bằng cách sử dụng gợi ý chỉ mục hoặc bằng cách chỉ định FORCESEEK:

SELECT ProductID, TransactionID
FROM dbo.TH WITH (FORCESEEK)
WHERE ProductID IN (870, 873, 921, 712, 707, 711)
ORDER BY TransactionID;

Điều này dẫn đến một sắp xếp rõ ràng theo ID giao dịch. Loại mới được ước tính chiếm 96% trong tổng số 1,15 của gói mới đơn giá. Chi phí ước tính cao hơn này giải thích lý do tại sao trình tối ưu hóa chọn quét chỉ mục theo cụm có vẻ rẻ hơn khi để cho các thiết bị của chính nó. Tuy nhiên, chi phí I / O của truy vấn mới thấp hơn:khi được thực thi, tìm kiếm chỉ mục chỉ tiêu thụ 49 số lần đọc logic (giảm từ 797).

Chúng tôi cũng có thể đã chọn để thể hiện truy vấn này bằng cách sử dụng ý tưởng UNION ALL (trước đó không thành công):

SELECT ProductID, TransactionID FROM dbo.TH WHERE ProductID = 870 
UNION ALL
SELECT ProductID, TransactionID FROM dbo.TH WHERE ProductID = 873 
UNION ALL
SELECT ProductID, TransactionID FROM dbo.TH WHERE ProductID = 921 
UNION ALL
SELECT ProductID, TransactionID FROM dbo.TH WHERE ProductID = 712 
UNION ALL
SELECT ProductID, TransactionID FROM dbo.TH WHERE ProductID = 707 
UNION ALL
SELECT ProductID, TransactionID FROM dbo.TH WHERE ProductID = 711
ORDER BY TransactionID;

Kế hoạch thực hiện sau (nhấp vào hình ảnh để phóng to trong cửa sổ mới):

Kế hoạch này có vẻ phức tạp hơn nhưng chỉ có chi phí ước tính là 0,099 đơn vị, thấp hơn nhiều so với quét chỉ mục theo nhóm ( 0,714 đơn vị) hoặc tìm kiếm cộng với sắp xếp ( 1,15 các đơn vị). Ngoài ra, gói mới chỉ tiêu thụ 49 số đọc logic tại thời điểm thực thi - giống như kế hoạch sắp xếp + tìm kiếm và thấp hơn nhiều so với 797 cần thiết cho quá trình quét chỉ mục theo cụm.

Lần này, việc thể hiện truy vấn bằng UNION ALL đã tạo ra một kế hoạch tốt hơn nhiều, cả về chi phí ước tính và số lần đọc logic. Tập dữ liệu nguồn hơi quá nhỏ để so sánh thực sự có ý nghĩa giữa thời lượng truy vấn hoặc mức sử dụng CPU, nhưng quá trình quét chỉ mục theo nhóm mất gấp đôi (26ms) so với hai lần khác trên hệ thống của tôi.

Loại bổ sung trong kế hoạch gợi ý có thể vô hại trong ví dụ đơn giản này vì nó không có khả năng tràn ra đĩa, nhưng dù sao thì nhiều người sẽ thích gói UNION ALL vì nó không bị chặn, tránh cấp bộ nhớ và không yêu cầu gợi ý truy vấn.

Kết luận

Chúng tôi đã thấy rằng cú pháp truy vấn có thể ảnh hưởng đến kế hoạch thực thi được chọn bởi trình tối ưu hóa, mặc dù các truy vấn chỉ định một cách hợp lý chính xác cùng một tập kết quả. Việc viết lại giống nhau (ví dụ:UNION ALL) đôi khi sẽ dẫn đến một sự cải tiến và đôi khi khiến một phương án kém hơn được chọn.

Viết lại các truy vấn và thử cú pháp thay thế là một kỹ thuật điều chỉnh hợp lệ, nhưng cần phải cẩn thận. Một rủi ro là những thay đổi trong tương lai đối với sản phẩm có thể khiến biểu mẫu truy vấn khác đột ngột ngừng tạo ra kế hoạch tốt hơn, nhưng người ta có thể lập luận rằng đó luôn là rủi ro và được giảm thiểu bằng cách thử nghiệm trước khi nâng cấp hoặc sử dụng hướng dẫn kế hoạch.

Kỹ thuật này cũng có nguy cơ mắc phải:sử dụng cấu trúc truy vấn 'kỳ lạ' hoặc 'bất thường' để có được một kế hoạch hoạt động tốt hơn thường là một dấu hiệu cho thấy một đường thẳng đã bị cắt ngang. Sự phân biệt chính xác nằm ở đâu giữa cú pháp thay thế hợp lệ và 'bất thường / kỳ lạ' có lẽ khá chủ quan; hướng dẫn cá nhân của riêng tôi là làm việc với các biểu mẫu truy vấn quan hệ tương đương và giữ mọi thứ đơn giản nhất có thể.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Khớp Cung với Cầu - Giải pháp, Phần 1

  2. Cách thêm vị trí xếp hạng của hàng trong SQL với RANK ()

  3. Trường hợp con cá trích đỏ ước tính hồng y

  4. Di chuyển Cụm Cassandra của bạn

  5. Zombie PerfMon đếm không bao giờ chết!