Một định nghĩa đơn giản về giá trị trung bình là:
Giá trị trung bình là giá trị giữa trong danh sách các số đã được sắp xếpĐể làm rõ điều đó một chút, chúng ta có thể tìm giá trị trung bình của danh sách các số bằng cách sử dụng quy trình sau:
- Sắp xếp các số (theo thứ tự tăng dần hoặc giảm dần, không quan trọng).
- Số ở giữa (theo vị trí) trong danh sách đã sắp xếp là số trung vị.
- Nếu có hai số "ở giữa bằng nhau" thì trung vị là giá trị trung bình của hai giá trị ở giữa.
Aaron Bertrand trước đây đã kiểm tra hiệu suất một số cách để tính giá trị trung bình trong SQL Server:
- Cách nhanh nhất để tính số trung vị là gì?
- Các phương pháp tốt nhất cho giá trị trung bình được nhóm theo nhóm
Rob Farley gần đây đã thêm một cách tiếp cận khác nhằm vào các bản cài đặt trước năm 2012:
- Medians trước SQL 2012
Bài viết này giới thiệu một phương pháp mới sử dụng con trỏ động.
Phương pháp OFFSET-FETCH 2012
Chúng tôi sẽ bắt đầu bằng cách xem xét triển khai hoạt động hiệu quả nhất, được tạo bởi Peter Larsson. Nó sử dụng SQL Server 2012 OFFSET phần mở rộng cho ORDER BY để định vị một cách hiệu quả một hoặc hai hàng giữa cần thiết để tính giá trị trung bình.
OFFSET Trung vị Đơn
Bài báo đầu tiên của Aaron đã thử nghiệm tính toán một giá trị trung bình duy nhất trên một bảng mười triệu hàng:
CREATE TABLE dbo.obj
(
id integer NOT NULL IDENTITY(1,1),
val integer NOT NULL
);
INSERT dbo.obj WITH (TABLOCKX)
(val)
SELECT TOP (10000000)
AO.[object_id]
FROM sys.all_columns AS AC
CROSS JOIN sys.all_objects AS AO
CROSS JOIN sys.all_objects AS AO2
WHERE AO.[object_id] > 0
ORDER BY
AC.[object_id];
CREATE UNIQUE CLUSTERED INDEX cx
ON dbo.obj(val, id);
Giải pháp của Peter Larsson sử dụng OFFSET phần mở rộng là:
DECLARE @Start datetime2 = SYSUTCDATETIME();
DECLARE @Count bigint = 10000000
--(
-- SELECT COUNT_BIG(*)
-- FROM dbo.obj AS O
--);
SELECT
Median = AVG(1.0 * SQ1.val)
FROM
(
SELECT O.val
FROM dbo.obj AS O
ORDER BY O.val
OFFSET (@Count - 1) / 2 ROWS
FETCH NEXT 1 + (1 - @Count % 2) ROWS ONLY
) AS SQ1;
SELECT Peso = DATEDIFF(MILLISECOND, @Start, SYSUTCDATETIME()); Đoạn mã trên mã hóa kết quả của việc đếm các hàng trong bảng. Tất cả các phương pháp đã thử nghiệm để tính toán giá trị trung bình đều cần số lượng này để tính số hàng trung bình, do đó, nó là một chi phí không đổi. Bỏ thao tác đếm hàng ra khỏi thời gian sẽ tránh được một nguồn biến thể có thể xảy ra.
Kế hoạch thực thi cho OFFSET giải pháp được hiển thị bên dưới:
Toán tử Hàng đầu nhanh chóng bỏ qua các hàng không cần thiết, chỉ chuyển một hoặc hai hàng cần thiết để tính giá trị trung bình vào Tổng hợp luồng. Khi chạy với bộ nhớ cache ấm và bộ sưu tập kế hoạch thực thi bị tắt, truy vấn này sẽ chạy trong 910 mili giây trung bình trên máy tính xách tay của tôi. Đây là máy có bộ xử lý Intel i7 740QM chạy ở tốc độ 1,73 GHz với Turbo bị tắt (để nhất quán).
OFFSET Trung bình được nhóm theo nhóm
Bài báo thứ hai của Aaron đã kiểm tra hiệu suất của việc tính giá trị trung bình cho mỗi nhóm, sử dụng bảng Bán hàng một triệu hàng với mười nghìn mục nhập cho mỗi một trăm người bán hàng:
CREATE TABLE dbo.Sales
(
SalesPerson integer NOT NULL,
Amount integer NOT NULL
);
WITH X AS
(
SELECT TOP (100)
V.number
FROM master.dbo.spt_values AS V
GROUP BY
V.number
)
INSERT dbo.Sales WITH (TABLOCKX)
(
SalesPerson,
Amount
)
SELECT
X.number,
ABS(CHECKSUM(NEWID())) % 99
FROM X
CROSS JOIN X AS X2
CROSS JOIN X AS X3;
CREATE CLUSTERED INDEX cx
ON dbo.Sales
(SalesPerson, Amount);
Một lần nữa, giải pháp hoạt động tốt nhất sử dụng OFFSET :
DECLARE @s datetime2 = SYSUTCDATETIME();
DECLARE @Result AS table
(
SalesPerson integer PRIMARY KEY,
Median float NOT NULL
);
INSERT @Result
SELECT d.SalesPerson, w.Median
FROM
(
SELECT SalesPerson, COUNT(*) AS y
FROM dbo.Sales
GROUP BY SalesPerson
) AS d
CROSS APPLY
(
SELECT AVG(0E + Amount)
FROM
(
SELECT z.Amount
FROM dbo.Sales AS z WITH (PAGLOCK)
WHERE z.SalesPerson = d.SalesPerson
ORDER BY z.Amount
OFFSET (d.y - 1) / 2 ROWS
FETCH NEXT 2 - d.y % 2 ROWS ONLY
) AS f
) AS w(Median);
SELECT Peso = DATEDIFF(MILLISECOND, @s, SYSUTCDATETIME()); Phần quan trọng của kế hoạch thực hiện được trình bày dưới đây:
Hàng trên cùng của kế hoạch liên quan đến việc tìm số hàng trong nhóm cho mỗi người bán hàng. Hàng dưới sử dụng các phần tử kế hoạch giống nhau được thấy cho giải pháp trung bình của một nhóm để tính giá trị trung bình cho từng người bán hàng. Khi chạy với bộ nhớ cache ấm và các kế hoạch thực thi bị tắt, truy vấn này sẽ thực thi trong 320 mili giây trung bình trên máy tính xách tay của tôi.
Sử dụng con trỏ động
Có vẻ điên rồ khi nghĩ đến việc sử dụng con trỏ để tính giá trị trung bình. Con trỏ SQL giao dịch có một danh tiếng (hầu hết là xứng đáng) là chậm và kém hiệu quả. Người ta cũng thường cho rằng con trỏ động là loại con trỏ tồi nhất. Những điểm này có giá trị trong một số trường hợp, nhưng không phải lúc nào cũng có hiệu lực.
Các con trỏ SQL giao dịch được giới hạn trong việc xử lý một hàng tại một thời điểm, vì vậy chúng thực sự có thể chậm nếu nhiều hàng cần được tìm nạp và xử lý. Tuy nhiên, đó không phải là trường hợp của phép tính trung bình:tất cả những gì chúng ta cần làm là xác định vị trí và tìm nạp một hoặc hai giá trị giữa một cách hiệu quả . Con trỏ động rất thích hợp cho tác vụ này như chúng ta sẽ thấy.
Con trỏ động trung bình đơn
Giải pháp con trỏ động cho một phép tính trung vị bao gồm các bước sau:
- Tạo một con trỏ động có thể cuộn qua danh sách các mục đã được sắp xếp.
- Tính vị trí của hàng trung vị đầu tiên.
- Đặt lại vị trí con trỏ bằng cách sử dụng
FETCH RELATIVE. - Quyết định xem có cần hàng thứ hai để tính giá trị trung bình hay không.
- Nếu không, hãy trả về giá trị trung bình duy nhất ngay lập tức.
- Nếu không, hãy tìm nạp giá trị thứ hai bằng cách sử dụng
FETCH NEXT. - Tính giá trị trung bình của hai giá trị và trả về.
Lưu ý rằng danh sách đó phản ánh đúng thủ tục đơn giản để tìm giá trị trung bình được đưa ra ở đầu bài viết này như thế nào. Việc triển khai mã Transact SQL hoàn chỉnh được hiển thị bên dưới:
-- Dynamic cursor
DECLARE @Start datetime2 = SYSUTCDATETIME();
DECLARE
@RowCount bigint, -- Total row count
@Row bigint, -- Median row number
@Amount1 integer, -- First amount
@Amount2 integer, -- Second amount
@Median float; -- Calculated median
SET @RowCount = 10000000;
--(
-- SELECT COUNT_BIG(*)
-- FROM dbo.obj AS O
--);
DECLARE Median CURSOR
LOCAL
SCROLL
DYNAMIC
READ_ONLY
FOR
SELECT
O.val
FROM dbo.obj AS O
ORDER BY
O.val;
OPEN Median;
-- Calculate the position of the first median row
SET @Row = (@RowCount + 1) / 2;
-- Move to the row
FETCH RELATIVE @Row
FROM Median
INTO @Amount1;
IF @Row = (@RowCount + 2) / 2
BEGIN
-- No second row, median is the single value we have
SET @Median = @Amount1;
END
ELSE
BEGIN
-- Get the second row
FETCH NEXT
FROM Median
INTO @Amount2;
-- Calculate the median value from the two values
SET @Median = (@Amount1 + @Amount2) / 2e0;
END;
SELECT Median = @Median;
SELECT DynamicCur = DATEDIFF(MILLISECOND, @Start, SYSUTCDATETIME());
Kế hoạch thực thi cho FETCH RELATIVE câu lệnh hiển thị con trỏ động định vị lại một cách hiệu quả hàng đầu tiên cần thiết cho phép tính trung bình:
Kế hoạch cho FETCH NEXT (chỉ bắt buộc nếu có hàng giữa thứ hai, như trong các thử nghiệm này) là một hàng đơn lẻ được tìm nạp từ vị trí đã lưu của con trỏ:
Những lợi thế của việc sử dụng con trỏ động ở đây là:
- Nó tránh đi ngang qua toàn bộ tập hợp (việc đọc dừng lại sau khi tìm thấy các hàng ở giữa); và
- Không có bản sao dữ liệu tạm thời nào được tạo trong tempdb , như đối với con trỏ tĩnh hoặc bộ bàn phím.
Lưu ý rằng chúng tôi không thể chỉ định FAST_FORWARD con trỏ ở đây (để lại sự lựa chọn của một kế hoạch giống như tĩnh hoặc giống như động cho trình tối ưu hóa) vì con trỏ cần phải cuộn để hỗ trợ FETCH RELATIVE . Động vẫn là lựa chọn tối ưu ở đây.
Khi chạy với bộ nhớ cache ấm và bộ sưu tập kế hoạch thực thi bị tắt, truy vấn này chạy trong 930 mili giây trung bình trên máy thử nghiệm của tôi. Điều này chậm hơn một chút so với 910 mili giây cho OFFSET giải pháp, nhưng con trỏ động nhanh hơn đáng kể so với các phương pháp khác mà Aaron và Rob đã thử nghiệm và nó không yêu cầu SQL Server 2012 (hoặc mới hơn).
Tôi sẽ không lặp lại thử nghiệm các phương pháp khác trước năm 2012 ở đây, nhưng để làm ví dụ về kích thước của khoảng cách hiệu suất, giải pháp đánh số hàng sau mất 1550 mili giây trung bình (chậm hơn 70%):
DECLARE @Start datetime2 = SYSUTCDATETIME();
DECLARE @Count bigint = 10000000
--(
-- SELECT COUNT_BIG(*)
-- FROM dbo.obj AS O
--);
SELECT AVG(1.0 * SQ1.val) FROM
(
SELECT
O.val,
rn = ROW_NUMBER() OVER (
ORDER BY O.val)
FROM dbo.obj AS O WITH (PAGLOCK)
) AS SQ1
WHERE
SQ1.rn BETWEEN (@Count + 1)/2 AND (@Count + 2)/2;
SELECT RowNumber = DATEDIFF(MILLISECOND, @Start, SYSUTCDATETIME());
Kiểm tra con trỏ động trung bình được nhóm
Thật đơn giản để mở rộng giải pháp con trỏ động trung bình duy nhất để tính toán các phương tiện được nhóm lại. Vì lợi ích của sự nhất quán, tôi sẽ sử dụng các con trỏ lồng nhau (vâng, thực sự):
- Mở con trỏ tĩnh trên những người bán hàng và số lượng hàng.
- Mỗi lần tính toán giá trị trung bình cho mỗi người bằng cách sử dụng một con trỏ động mới.
- Lưu từng kết quả vào một biến bảng khi chúng ta tiếp tục.
Đoạn mã được hiển thị bên dưới:
-- Timing
DECLARE @s datetime2 = SYSUTCDATETIME();
-- Holds results
DECLARE @Result AS table
(
SalesPerson integer PRIMARY KEY,
Median float NOT NULL
);
-- Variables
DECLARE
@SalesPerson integer, -- Current sales person
@RowCount bigint, -- Current row count
@Row bigint, -- Median row number
@Amount1 float, -- First amount
@Amount2 float, -- Second amount
@Median float; -- Calculated median
-- Row counts per sales person
DECLARE SalesPersonCounts
CURSOR
LOCAL
FORWARD_ONLY
STATIC
READ_ONLY
FOR
SELECT
SalesPerson,
COUNT_BIG(*)
FROM dbo.Sales
GROUP BY SalesPerson
ORDER BY SalesPerson;
OPEN SalesPersonCounts;
-- First person
FETCH NEXT
FROM SalesPersonCounts
INTO @SalesPerson, @RowCount;
WHILE @@FETCH_STATUS = 0
BEGIN
-- Records for the current person
-- Note dynamic cursor
DECLARE Person CURSOR
LOCAL
SCROLL
DYNAMIC
READ_ONLY
FOR
SELECT
S.Amount
FROM dbo.Sales AS S
WHERE
S.SalesPerson = @SalesPerson
ORDER BY
S.Amount;
OPEN Person;
-- Calculate median row 1
SET @Row = (@RowCount + 1) / 2;
-- Move to median row 1
FETCH RELATIVE @Row
FROM Person
INTO @Amount1;
IF @Row = (@RowCount + 2) / 2
BEGIN
-- No second row, median is the single value
SET @Median = @Amount1;
END
ELSE
BEGIN
-- Get the second row
FETCH NEXT
FROM Person
INTO @Amount2;
-- Calculate the median value
SET @Median = (@Amount1 + @Amount2) / 2e0;
END;
-- Add the result row
INSERT @Result (SalesPerson, Median)
VALUES (@SalesPerson, @Median);
-- Finished with the person cursor
CLOSE Person;
DEALLOCATE Person;
-- Next person
FETCH NEXT
FROM SalesPersonCounts
INTO @SalesPerson, @RowCount;
END;
---- Results
--SELECT
-- R.SalesPerson,
-- R.Median
--FROM @Result AS R;
-- Tidy up
CLOSE SalesPersonCounts;
DEALLOCATE SalesPersonCounts;
-- Show elapsed time
SELECT NestedCur = DATEDIFF(MILLISECOND, @s, SYSUTCDATETIME()); Con trỏ bên ngoài cố tình tĩnh vì tất cả các hàng trong tập hợp đó sẽ được chạm vào (ngoài ra, con trỏ động không khả dụng do hoạt động nhóm trong truy vấn bên dưới). Không có gì đặc biệt mới hoặc thú vị để xem trong các kế hoạch thực hiện vào khoảng thời gian này.
Điều thú vị là màn trình diễn. Mặc dù lặp đi lặp lại việc tạo và định vị con trỏ động bên trong, giải pháp này hoạt động thực sự tốt trên tập dữ liệu thử nghiệm. Khi tắt bộ nhớ đệm ấm và các kế hoạch thực thi, tập lệnh con trỏ sẽ hoàn thành sau 330 mili giây trung bình trên máy thử nghiệm của tôi. Điều này lại chậm hơn một chút so với 320 mili giây được ghi lại bởi OFFSET nhóm trung bình, nhưng Nó đánh bại các giải pháp tiêu chuẩn khác được liệt kê trong các bài báo của Aaron và Rob bởi một biên độ lớn.
Một lần nữa, như một ví dụ về khoảng cách hiệu suất so với các phương pháp không phải của năm 2012, giải pháp đánh số hàng sau chạy trong 485 mili giây trung bình trên giàn thử nghiệm của tôi (kém hơn 50%):
DECLARE @s datetime2 = SYSUTCDATETIME();
DECLARE @Result AS table
(
SalesPerson integer PRIMARY KEY,
Median numeric(38, 6) NOT NULL
);
INSERT @Result
SELECT
S.SalesPerson,
CA.Median
FROM
(
SELECT
SalesPerson,
NumRows = COUNT_BIG(*)
FROM dbo.Sales
GROUP BY SalesPerson
) AS S
CROSS APPLY
(
SELECT AVG(1.0 * SQ1.Amount) FROM
(
SELECT
S2.Amount,
rn = ROW_NUMBER() OVER (
ORDER BY S2.Amount)
FROM dbo.Sales AS S2 WITH (PAGLOCK)
WHERE
S2.SalesPerson = S.SalesPerson
) AS SQ1
WHERE
SQ1.rn BETWEEN (S.NumRows + 1)/2 AND (S.NumRows + 2)/2
) AS CA (Median);
SELECT RowNumber = DATEDIFF(MILLISECOND, @s, SYSUTCDATETIME());
Tóm tắt kết quả
Trong thử nghiệm trung vị duy nhất, con trỏ động đã chạy trong 930 mili giây so với 910 mili giây cho OFFSET .
Trong thử nghiệm trung vị được nhóm, con trỏ lồng nhau chạy trong 330 mili giây so với 320 mili giây cho OFFSET .
Trong cả hai trường hợp, phương thức con trỏ nhanh hơn đáng kể so với phương thức không phải OFFSET khác các phương pháp. Nếu bạn cần tính toán giá trị trung bình của một nhóm hoặc đơn lẻ trên phiên bản trước năm 2012, con trỏ động hoặc con trỏ lồng nhau thực sự có thể là lựa chọn tối ưu.
Hiệu suất cache lạnh
Một số bạn có thể thắc mắc về hiệu suất bộ đệm lạnh. Chạy phần sau trước mỗi bài kiểm tra:
CHECKPOINT; DBCC DROPCLEANBUFFERS;
Đây là các kết quả cho thử nghiệm trung vị duy nhất:
OFFSET phương pháp: 940 mili giây
Con trỏ động: 955 mili giây
Đối với trung vị được nhóm lại:
OFFSET phương pháp: 380 mili giây
Con trỏ lồng nhau: 385 mili giây
Lời kết
Các giải pháp con trỏ động thực sự nhanh hơn đáng kể so với các giải pháp không phải OFFSET phương pháp cho cả phương tiện đơn và phương tiện nhóm, ít nhất là với các tập dữ liệu mẫu này. Tôi đã cố tình chọn sử dụng lại dữ liệu thử nghiệm của Aaron để các tập dữ liệu không bị lệch về phía con trỏ động một cách cố ý. Có có thể là các bản phân phối dữ liệu khác mà con trỏ động không phải là một lựa chọn tốt. Tuy nhiên, nó cho thấy rằng vẫn có những lúc con trỏ có thể là một giải pháp nhanh chóng và hiệu quả cho đúng loại vấn đề. Ngay cả con trỏ động và con trỏ lồng nhau.
Người đọc có đôi mắt đại bàng có thể đã nhận thấy PAGLOCK gợi ý trong OFFSET kiểm tra trung vị nhóm. Điều này là cần thiết để có hiệu suất tốt nhất, vì những lý do tôi sẽ đề cập trong bài viết tiếp theo của mình. Nếu không có nó, giải pháp 2012 thực sự thua con trỏ lồng nhau một lề khá tốt ( 590ms so với 330ms ).