Phân nhóm là một tính năng quan trọng giúp tổ chức và sắp xếp dữ liệu. Có rất nhiều cách để làm điều đó và một trong những phương pháp hiệu quả nhất là mệnh đề GROUP BY trong SQL.
Bạn có thể sử dụng SQL GROUP BY để chia các hàng trong kết quả thành các nhóm bằng hàm tổng hợp . Nghe có vẻ dễ dàng để tính tổng, trung bình hoặc đếm các bản ghi với nó.
Nhưng bạn có đang làm đúng không?
“Đúng” có thể là chủ quan. Khi nó chạy mà không có lỗi nghiêm trọng với đầu ra chính xác, nó được coi là ổn. Tuy nhiên, nó cũng cần phải nhanh chóng.
Trong bài viết này, tốc độ cũng sẽ được xem xét. Bạn sẽ thấy nhiều phân tích truy vấn bằng cách sử dụng các kế hoạch đọc và thực thi hợp lý ở tất cả các điểm.
Hãy bắt đầu.
1. Lọc sớm
Nếu bạn phân vân không biết khi nào sử dụng WHERE và HAVING, thì cái này là dành cho bạn. Bởi vì tùy thuộc vào điều kiện bạn cung cấp, cả hai đều có thể cho cùng một kết quả.
Nhưng chúng khác nhau.
HAVING lọc các nhóm bằng cách sử dụng các cột trong mệnh đề GROUP BY trong SQL. WHERE lọc các hàng trước khi nhóm và tổng hợp xảy ra. Vì vậy, nếu bạn lọc bằng mệnh đề HAVING, thì nhóm sẽ xảy ra cho tất cả các hàng được trả lại.
Và điều đó thật tệ.
Tại sao? Câu trả lời ngắn gọn là:nó chậm. Hãy chứng minh điều này bằng 2 truy vấn. Kiểm tra mã dưới đây. Trước khi chạy nó trong SQL Server Management Studio, hãy nhấn Ctrl-M trước.
SET STATISTICS IO ON
GO
-- using WHERE
SELECT
MONTH(soh.OrderDate) AS OrderMonth
,YEAR(soh.OrderDate) AS OrderYear
,p.Name AS Product
,SUM(sod.LineTotal) AS ProductSales
FROM Sales.SalesOrderHeader soh
INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
INNER join Production.Product p ON sod.ProductID = p.ProductID
WHERE soh.OrderDate BETWEEN '01/01/2012' AND '12/31/2012'
GROUP BY p.Name, YEAR(soh.OrderDate), MONTH(soh.OrderDate)
ORDER BY Product, OrderYear, OrderMonth;
-- using HAVING
SELECT
MONTH(soh.OrderDate) AS OrderMonth
,YEAR(soh.OrderDate) AS OrderYear
,p.Name AS Product
,SUM(sod.LineTotal) AS ProductSales
FROM Sales.SalesOrderHeader soh
INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
INNER join Production.Product p ON sod.ProductID = p.ProductID
GROUP BY p.Name, YEAR(soh.OrderDate), MONTH(soh.OrderDate)
HAVING YEAR(soh.OrderDate) = 2012
ORDER BY Product, OrderYear, OrderMonth;
SET STATISTICS IO OFF
GO
Phân tích
2 câu lệnh SELECT ở trên sẽ trả về các hàng giống nhau. Cả hai đều đúng trong việc trả lại đơn đặt hàng sản phẩm theo tháng trong năm 2012. Nhưng lần SELECT đầu tiên mất 136 mili giây. để chạy trên máy tính xách tay của tôi, trong khi một cái khác mất 764 mili giây.!
Tại sao?
Trước tiên, chúng ta hãy kiểm tra các lần đọc logic trong Hình 1. IO THỐNG KÊ trả về các kết quả này. Sau đó, tôi dán nó vào StatisticsParser.com để có đầu ra được định dạng.
Hình 1 . Đọc logic của việc lọc sớm bằng cách sử dụng WHERE so với lọc muộn bằng cách sử dụng HAVING.
Nhìn vào tổng số lần đọc hợp lý của mỗi. Để hiểu những con số này, càng mất nhiều thời gian đọc logic thì truy vấn càng chậm. Vì vậy, điều đó chứng tỏ rằng sử dụng HAVING chậm hơn và lọc sớm bằng WHERE nhanh hơn.
Tất nhiên, điều này không có nghĩa là HẠNH PHÚC là vô ích. Một ngoại lệ là khi sử dụng HAVING với tổng thể như HAVING SUM (sod.Linetotal)> 100000 . Bạn có thể kết hợp mệnh đề WHERE và mệnh đề HAVING trong một truy vấn.
Xem kế hoạch thực hiện trong Hình 2.
Hình 2 . Kế hoạch thực thi lọc sớm so với lọc muộn.
Cả hai kế hoạch thực hiện trông giống nhau ngoại trừ những kế hoạch được đóng hộp màu đỏ. Bộ lọc ban đầu sử dụng toán tử Tìm kiếm chỉ mục trong khi một toán tử khác sử dụng Quét chỉ mục. Tìm kiếm nhanh hơn so với quét trong các bảng lớn.
Không te: Lọc sớm có ít chi phí hơn lọc muộn. Vì vậy, điểm mấu chốt là lọc các hàng sớm có thể cải thiện hiệu suất.
2. Nhóm trước, tham gia sau
Tham gia một số bảng bạn cần sau này cũng có thể cải thiện hiệu suất.
Giả sử bạn muốn có doanh số bán sản phẩm hàng tháng. Bạn cũng cần lấy tất cả tên sản phẩm, số và danh mục phụ trong cùng một truy vấn. Các cột này nằm trong một bảng khác. Và tất cả chúng cần được thêm vào mệnh đề GROUP BY để thực hiện thành công. Đây là mã.
SET STATISTICS IO ON
GO
SELECT
p.Name AS Product
,p.ProductNumber
,ps.Name AS ProductSubcategory
,SUM(sod.LineTotal) AS ProductSales
FROM Sales.SalesOrderHeader soh
INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
INNER JOIN Production.Product p ON sod.ProductID = p.ProductID
INNER JOIN Production.ProductSubcategory ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID
WHERE soh.OrderDate BETWEEN '01/01/2012' AND '12/31/2012'
GROUP BY p.name, p.ProductNumber, ps.Name
ORDER BY Product
SET STATISTICS IO OFF
GO
Điều này sẽ chạy tốt. Nhưng có một cách tốt hơn, nhanh hơn. Điều này sẽ không yêu cầu bạn thêm 3 cột cho tên sản phẩm, số và danh mục phụ trong mệnh đề GROUP BY. Tuy nhiên, điều này sẽ yêu cầu nhiều lần nhấn phím hơn một chút. Đây rồi.
SET STATISTICS IO ON
GO
;WITH Orders2012 AS
(
SELECT
sod.ProductID
,SUM(sod.LineTotal) AS ProductSales
FROM Sales.SalesOrderHeader soh
INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
WHERE soh.OrderDate BETWEEN '01/01/2012' AND '12/31/2012'
GROUP BY sod.ProductID
)
SELECT
P.Name AS Product
,P.ProductNumber
,ps.Name AS ProductSubcategory
,o.ProductSales
FROM Orders2012 o
INNER JOIN Production.Product p ON o.ProductID = p.ProductID
INNER JOIN Production.ProductSubcategory ps ON p.ProductSubcategoryID = ps.ProductSubcategoryID
ORDER BY Product;
SET STATISTICS IO OFF
GO
Phân tích
Tại sao điều này nhanh hơn? Tham gia vào Sản phẩm và ProductSubcategory được thực hiện sau. Cả hai đều không liên quan đến mệnh đề GROUP BY. Hãy chứng minh điều này bằng các con số trong IO THỐNG KÊ. Xem Hình 4.
Hình 3 . Việc tham gia sớm sau đó nhóm tiêu thụ nhiều lần đọc logic hơn so với việc kết hợp sau đó.
Xem những bài đọc hợp lý? Sự khác biệt là rất xa, và người chiến thắng là điều hiển nhiên.
Hãy so sánh kế hoạch thực thi của 2 truy vấn để xem lý do đằng sau những con số ở trên. Đầu tiên, hãy xem Hình 4 để biết kế hoạch thực thi của truy vấn với tất cả các bảng được tham gia khi được nhóm lại.
Hình 4 . Kế hoạch thực thi khi tất cả các bảng được kết hợp với nhau.
Và chúng tôi có những nhận xét sau:
- GROUP BY và SUM đã được thực hiện muộn trong quá trình sau khi tham gia tất cả các bảng.
- Rất nhiều dòng và mũi tên dày hơn - điều này giải thích cho 1.277 lần đọc logic.
- Hai truy vấn kết hợp tạo thành 100% chi phí truy vấn. Nhưng kế hoạch của truy vấn này có chi phí truy vấn cao hơn (56%).
Bây giờ, đây là kế hoạch thực hiện khi chúng tôi nhóm trước và tham gia Sản phẩm và ProductSubcategory bảng sau. Xem Hình 5.
Hình 5 . Kế hoạch thực thi khi nhóm trước, tham gia sau được hoàn thành.
Và chúng ta có những quan sát sau đây trong Hình 5.
- GROUP BY và SUM đã hoàn thành sớm.
- Ít đường kẻ và mũi tên dày hơn - điều này chỉ giải thích cho 348 lần đọc logic.
- Chi phí truy vấn thấp hơn (44%).
3. Nhóm một cột được lập chỉ mục
Bất cứ khi nào SQL GROUP BY được thực hiện trên một cột, cột đó phải có một chỉ mục. Bạn sẽ tăng tốc độ thực thi khi bạn nhóm cột với một chỉ mục. Hãy sửa đổi truy vấn trước đó và sử dụng ngày giao hàng thay vì ngày đặt hàng. Cột ngày giao hàng không có chỉ mục trong SalesOrderHeader .
SET STATISTICS IO ON
GO
SELECT
MONTH(soh.ShipDate) AS ShipMonth
,YEAR(soh.ShipDate) AS ShipYear
,p.Name AS Product
,SUM(sod.LineTotal) AS ProductSales
FROM Sales.SalesOrderHeader soh
INNER JOIN Sales.SalesOrderDetail sod ON soh.SalesOrderID = sod.SalesOrderID
INNER join Production.Product p ON sod.ProductID = p.ProductID
WHERE soh.ShipDate BETWEEN '01/01/2012' AND '12/31/2012'
GROUP BY p.Name, YEAR(soh.ShipDate), MONTH(soh.ShipDate)
ORDER BY Product, ShipYear, ShipMonth;
SET STATISTICS IO OFF
GO
Nhấn Ctrl-M, sau đó chạy truy vấn ở trên trong SSMS. Sau đó, tạo chỉ mục không phân cụm trên Ngày giao hàng cột. Lưu ý kế hoạch đọc và thực hiện hợp lý. Cuối cùng, chạy lại truy vấn ở trên trong một tab truy vấn khác. Lưu ý sự khác biệt trong kế hoạch đọc và thực thi logic.
Dưới đây là so sánh các lần đọc lôgic trong Hình 6.
Hình 6 . Các lần đọc logic về ví dụ truy vấn của chúng tôi có và không có chỉ mục trên ShipDate.
Trong Hình 6, có các lần đọc truy vấn logic cao hơn mà không có chỉ mục trên ShipDate .
Bây giờ, hãy có kế hoạch thực hiện khi không có chỉ mục nào trên ShipDate tồn tại trong Hình 7.
Hình 7 . Kế hoạch thực thi khi sử dụng GROUP BY trên ShipDate không được lập chỉ mục.
Quét chỉ mục toán tử được sử dụng trong kế hoạch trong Hình 7 giải thích các lần đọc logic cao hơn (475). Đây là kế hoạch thực thi sau khi lập chỉ mục Ngày giao hàng cột.
Hình 8 . Kế hoạch thực thi khi sử dụng GROUP BY trên ShipDate được lập chỉ mục.
Thay vì Quét lập chỉ mục, Tìm kiếm chỉ mục được sử dụng sau khi lập chỉ mục Ngày giao hàng cột. Điều này giải thích các lần đọc logic thấp hơn trong Hình 6.
Vì vậy, để cải thiện hiệu suất khi sử dụng GROUP BY, hãy xem xét lập chỉ mục các cột bạn đã sử dụng để nhóm.
Những điểm rút ra khi sử dụng SQL GROUP BY
SQL GROUP BY rất dễ sử dụng. Nhưng bạn cần thực hiện bước tiếp theo để vượt ra ngoài việc tóm tắt dữ liệu cho các báo cáo. Đây là các điểm một lần nữa:
- Lọc sớm . Loại bỏ các hàng bạn không cần tóm tắt bằng mệnh đề WHERE thay vì mệnh đề HAVING.
- Nhóm trước, tham gia sau . Đôi khi, sẽ có những cột bạn cần thêm ngoài những cột bạn đang nhóm. Thay vì đưa chúng vào mệnh đề GROUP BY, hãy chia truy vấn bằng CTE và nối các bảng khác sau đó.
- Sử dụng GROUP BY với các cột được lập chỉ mục . Điều cơ bản này có thể hữu ích khi cơ sở dữ liệu nhanh như một con ốc sên.
Hy vọng điều này sẽ giúp bạn nâng cấp trò chơi của mình trong kết quả nhóm.
Nếu bạn thích bài đăng này, hãy chia sẻ nó trên các nền tảng mạng xã hội yêu thích của bạn.