Làm cách nào tôi có thể chạy tổng số hàng gần đây nhanh hơn?


8

Tôi hiện đang thiết kế một bảng giao dịch. Tôi nhận ra rằng việc tính toán tổng số chạy cho mỗi hàng sẽ là cần thiết và điều này có thể chậm trong hiệu suất. Vì vậy, tôi đã tạo một bảng với 1 triệu hàng cho mục đích thử nghiệm.

CREATE TABLE [dbo].[Table_1](
    [seq] [int] IDENTITY(1,1) NOT NULL,
    [value] [bigint] NOT NULL,
 CONSTRAINT [PK_Table_1] PRIMARY KEY CLUSTERED 
(
    [seq] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

Và tôi đã cố gắng để có được 10 hàng gần đây và tổng số chạy của nó, nhưng mất khoảng 10 giây.

--1st attempt
SELECT TOP 10 seq
    ,value
    ,sum(value) OVER (ORDER BY seq) total
FROM Table_1
ORDER BY seq DESC

--(10 rows affected)
--Table 'Worktable'. Scan count 1000001, logical reads 8461526, physical reads 2, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--Table 'Table_1'. Scan count 1, logical reads 2608, physical reads 516, read-ahead reads 2617, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--
--(1 row affected)
--
-- SQL Server Execution Times:
--   CPU time = 8483 ms,  elapsed time = 9786 ms.

Kế hoạch thực hiện lần 1

Tôi nghi ngờ TOPvì lý do hiệu suất chậm từ kế hoạch, vì vậy tôi đã thay đổi truy vấn như thế này và mất khoảng 1 ~ 2 giây. Nhưng tôi nghĩ rằng điều này vẫn còn chậm cho sản xuất và tự hỏi nếu điều này có thể được cải thiện hơn nữa.

--2nd attempt
SELECT *
    ,(
        SELECT SUM(value)
        FROM Table_1
        WHERE seq <= t.seq
        ) total
FROM (
    SELECT TOP 10 seq
        ,value
    FROM Table_1
    ORDER BY seq DESC
    ) t
ORDER BY seq DESC

--(10 rows affected)
--Table 'Table_1'. Scan count 11, logical reads 26083, physical reads 1, read-ahead reads 443, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
--
--(1 row affected)
--
-- SQL Server Execution Times:
--   CPU time = 1422 ms,  elapsed time = 1621 ms.

Kế hoạch thực hiện lần 2

Câu hỏi của tôi là:

  • Tại sao truy vấn từ lần thử thứ 1 chậm hơn lần thứ 2?
  • Làm thế nào tôi có thể cải thiện hiệu suất hơn nữa? Tôi cũng có thể thay đổi lược đồ.

Để rõ ràng, cả hai truy vấn đều trả về kết quả như dưới đây.

các kết quả


1
Tôi thường không sử dụng các chức năng của cửa sổ, nhưng tôi nhớ tôi đã đọc một số bài viết hữu ích về chúng. Hãy xem một Giới thiệu về các chức năng cửa sổ T-SQL , đặc biệt là ở phần Cải tiến tổng hợp cửa sổ năm 2012 . Có lẽ nó cung cấp cho bạn một số câu trả lời. ... và một bài viết nữa của cùng tác giả xuất sắc Chức năng và hiệu suất cửa sổ T-SQL
Denis Rubashkin

Bạn đã thử đặt một chỉ số trên value?
Jacob H

Câu trả lời:


5

Tôi khuyên bạn nên thử nghiệm với một chút dữ liệu để có ý tưởng tốt hơn về những gì đang diễn ra và để xem cách tiếp cận khác nhau thực hiện. Tôi đã tải 16 triệu hàng vào một bảng có cùng cấu trúc. Bạn có thể tìm thấy mã để điền vào bảng ở cuối câu trả lời này.

Cách tiếp cận sau đây mất 19 giây trên máy của tôi:

SELECT TOP (10) seq
    ,value
    ,sum(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) total
FROM dbo.[Table_1_BIG]
ORDER BY seq DESC;

Kế hoạch thực tế ở đây . Hầu hết thời gian được dành để tính tổng và thực hiện sắp xếp. Đáng lo ngại, kế hoạch truy vấn thực hiện gần như tất cả công việc cho toàn bộ tập kết quả và lọc thành 10 hàng mà bạn yêu cầu ở cuối. Thời gian chạy của truy vấn này chia tỷ lệ với kích thước của bảng thay vì với kích thước của tập kết quả.

Tùy chọn này mất 23 giây trên máy của tôi:

SELECT *
    ,(
        SELECT SUM(value)
        FROM dbo.[Table_1_BIG]
        WHERE seq <= t.seq
        ) total
FROM (
    SELECT TOP (10) seq
        ,value
    FROM dbo.[Table_1_BIG]
    ORDER BY seq DESC
    ) t
ORDER BY seq DESC;

Kế hoạch thực tế ở đây . Cách tiếp cận này chia tỷ lệ với cả số lượng hàng được yêu cầu và kích thước của bảng. Gần 160 triệu hàng được đọc từ bảng:

xin chào

Để có kết quả chính xác, bạn phải tính tổng các hàng cho toàn bộ bảng. Lý tưởng nhất là bạn sẽ thực hiện tổng kết này chỉ một lần. Có thể làm điều này nếu bạn thay đổi cách bạn tiếp cận vấn đề. Bạn có thể tính tổng cho toàn bộ bảng sau đó trừ tổng chạy từ các hàng trong tập kết quả. Điều đó cho phép bạn tìm tổng cho hàng thứ N. Một cách để làm điều này:

SELECT TOP (10) seq
,value
, [value]
    - SUM([value]) OVER (ORDER BY seq DESC ROWS UNBOUNDED PRECEDING)
    + (SELECT SUM([value]) FROM dbo.[Table_1_BIG]) AS total
FROM dbo.[Table_1_BIG]
ORDER BY seq DESC;

Kế hoạch thực tế ở đây . Truy vấn mới chạy trong 644 ms trên máy của tôi. Bảng được quét một lần để có được tổng số hoàn chỉnh sau đó một hàng bổ sung được đọc cho mỗi hàng trong tập kết quả. Không có sự sắp xếp và gần như toàn bộ thời gian được dành để tính tổng trong phần song song của kế hoạch:

khá tốt

Nếu bạn muốn truy vấn này nhanh hơn nữa, bạn chỉ cần tối ưu hóa phần tính tổng. Các truy vấn trên thực hiện quét chỉ mục cụm. Chỉ mục cụm bao gồm tất cả các cột nhưng bạn chỉ cần [value]cột. Một tùy chọn là tạo một chỉ mục không bao gồm trên cột đó. Một tùy chọn khác là tạo một chỉ mục cột không độc quyền trên cột đó. Cả hai sẽ cải thiện hiệu suất. Nếu bạn đang ở Doanh nghiệp, một tùy chọn tuyệt vời là tạo chế độ xem được lập chỉ mục như sau:

CREATE OR ALTER VIEW dbo.Table_1_BIG__SUM
WITH SCHEMABINDING
AS
SELECT SUM([value]) SUM_VALUE
, COUNT_BIG(*) FOR_U
FROM dbo.[Table_1_BIG];

GO

CREATE UNIQUE CLUSTERED INDEX CI ON dbo.Table_1_BIG__SUM (SUM_VALUE);

Khung nhìn này trả về một hàng duy nhất nên nó chiếm gần như không có không gian. Sẽ có một hình phạt khi thực hiện DML nhưng nó không nên khác nhiều so với bảo trì chỉ mục. Với chế độ xem được lập chỉ mục khi chơi, truy vấn hiện mất 0 ms:

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

Kế hoạch thực tế ở đây . Phần tốt nhất về phương pháp này là thời gian chạy không bị thay đổi bởi kích thước của bảng. Điều duy nhất quan trọng là có bao nhiêu hàng được trả về. Ví dụ: nếu bạn nhận được 10000 hàng đầu tiên, truy vấn hiện mất 18 ms để thực thi.

Mã để điền vào bảng:

DROP TABLE IF EXISTS dbo.[Table_1_BIG];

CREATE TABLE dbo.[Table_1_BIG] (
    [seq] [int] NOT NULL,
    [value] [bigint] NOT NULL
);

DROP TABLE IF EXISTS #t;
CREATE TABLE #t (ID BIGINT);

INSERT INTO #t WITH (TABLOCK)
SELECT TOP (4000) -1 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

INSERT INTO dbo.[Table_1_BIG] WITH (TABLOCK)
SELECT t1.ID * 4000 + t2.ID, 8 * t2.ID + t1.ID
FROM (SELECT TOP (4000) ID FROM #t) t1
CROSS JOIN #t t2;

ALTER TABLE dbo.[Table_1_BIG]
ADD CONSTRAINT [PK_Table_1] PRIMARY KEY ([seq]);

4

Sự khác biệt trong hai cách tiếp cận đầu tiên

Gói đầu tiên dành khoảng 7 trong 10 giây cho toán tử Window Spool, vì vậy đây là lý do chính khiến nó quá chậm. Nó thực hiện rất nhiều I / O trong tempdb để tạo ra cái này. Số liệu thống kê I / O và thời gian của tôi trông như thế này:

Table 'Worktable'. Scan count 1000001, logical reads 8461526
Table 'Table_1'. Scan count 1, logical reads 2609
Table 'Worktable'. Scan count 0, logical reads 0

 SQL Server Execution Times:
   CPU time = 8641 ms,  elapsed time = 8537 ms.

Các kế hoạch thứ hai là có thể tránh được những ống chỉ, và do đó bàn làm việc hoàn toàn. Nó chỉ đơn giản là lấy 10 hàng trên cùng từ chỉ mục được nhóm, và sau đó một vòng lặp lồng nhau tham gia vào tổng hợp (tổng hợp) từ một lần quét chỉ mục được phân cụm. Phía bên trong vẫn kết thúc việc đọc toàn bộ bảng, nhưng bảng rất dày đặc, vì vậy điều này là hiệu quả hợp lý với một triệu hàng.

Table 'Table_1'. Scan count 11, logical reads 26093
 SQL Server Execution Times:
   CPU time = 1563 ms,  elapsed time = 1671 ms.

Cải thiện hiệu suất

Nhà kho

Nếu bạn thực sự muốn phương pháp "báo cáo trực tuyến", cột cửa hàng có thể là lựa chọn tốt nhất của bạn.

ALTER TABLE [dbo].[Table_1] DROP CONSTRAINT [PK_Table_1];

CREATE CLUSTERED COLUMNSTORE INDEX [PK_Table_1] ON dbo.Table_1;

Sau đó, truy vấn này nhanh đến mức nực cười:

SELECT TOP 10
    seq, 
    value, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING)
FROM dbo.Table_1
ORDER BY seq DESC;

Dưới đây là số liệu thống kê từ máy của tôi:

Table 'Table_1'. Scan count 4, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 3319
Table 'Table_1'. Segment reads 1, segment skipped 0.
Table 'Worktable'. Scan count 0, logical reads 0

 SQL Server Execution Times:
   CPU time = 375 ms,  elapsed time = 205 ms.

Bạn có thể sẽ không đánh bại điều đó (trừ khi bạn thực sự thông minh - người tốt bụng, Joe). Cột lưu trữ rất tốt trong việc quét và tổng hợp số lượng lớn dữ liệu.

Sử dụng ROWthay vì RANGEtùy chọn chức năng cửa sổ

Bạn có thể nhận được hiệu suất rất giống với truy vấn thứ hai của mình với cách tiếp cận này, được đề cập trong một câu trả lời khác và tôi đã sử dụng trong ví dụ về cột lưu trữ ở trên ( kế hoạch thực hiện ):

SELECT TOP 10
    seq, 
    value, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING)
FROM dbo.Table_1
ORDER BY seq DESC;

Nó dẫn đến số lần đọc ít hơn so với cách tiếp cận thứ hai của bạn và không có hoạt động tempdb so với cách tiếp cận đầu tiên của bạn vì bộ đệm cửa sổ xảy ra trong bộ nhớ :

... RANGE sử dụng bộ đệm trên đĩa, trong khi ROWS sử dụng bộ đệm trong bộ nhớ

Thật không may, thời gian chạy gần giống như cách tiếp cận thứ hai của bạn.

Table 'Worktable'. Scan count 0, logical reads 0
Table 'Table_1'. Scan count 1, logical reads 2609
Table 'Worktable'. Scan count 0, logical reads 0

 SQL Server Execution Times:
   CPU time = 1984 ms,  elapsed time = 1474 ms.

Giải pháp dựa trên lược đồ: async chạy tổng số

Vì bạn cởi mở với các ý tưởng khác, bạn có thể xem xét cập nhật "tổng số chạy" không đồng bộ. Bạn có thể định kỳ lấy kết quả của một trong những truy vấn này và tải nó vào bảng "tổng". Vì vậy, bạn sẽ làm một cái gì đó như thế này:

CREATE TABLE [dbo].[Table_1_Totals]
(
    [seq] [int] NOT NULL,
    [running_total] [bigint] NOT NULL,
    CONSTRAINT [PK_Table_1_Totals] PRIMARY KEY CLUSTERED ([seq])
);

Tải nó mỗi ngày / giờ / bất cứ điều gì (điều này mất khoảng 2 giây trên máy của tôi với các hàng 1mm và có thể được tối ưu hóa):

INSERT INTO dbo.Table_1_Totals
SELECT
    seq, 
    SUM(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) as total
FROM dbo.Table_1 t
WHERE NOT EXISTS (
            SELECT NULL 
            FROM dbo.Table_1_Totals t2
            WHERE t.seq = t2.seq)
ORDER BY seq DESC;

Sau đó, truy vấn báo cáo của bạn rất hiệu quả:

SELECT TOP 10
    t.seq, 
    t.value, 
    t2.running_total
FROM dbo.Table_1 t
    INNER JOIN dbo.Table_1_Totals t2
        ON t.seq = t2.seq
ORDER BY seq DESC;

Dưới đây là số liệu thống kê đọc:

Table 'Table_1'. Scan count 0, logical reads 35
Table 'Table_1_Totals'. Scan count 1, logical reads 3

Giải pháp dựa trên lược đồ: tổng số liên tiếp với các ràng buộc

Một giải pháp thực sự thú vị cho vấn đề này được đề cập chi tiết trong câu trả lời này cho câu hỏi: Viết một lược đồ ngân hàng đơn giản: Làm thế nào tôi nên giữ số dư của mình đồng bộ với lịch sử giao dịch của họ?

Cách tiếp cận cơ bản sẽ là theo dõi tổng số hàng đang chạy hiện tại cùng với tổng số và số thứ tự chạy trước đó. Sau đó, bạn có thể sử dụng các ràng buộc để xác thực các tổng số đang chạy luôn chính xác và cập nhật.

Tín dụng cho Paul White để cung cấp một triển khai mẫu cho lược đồ trong Hỏi & Đáp này:

CREATE TABLE dbo.Table_1
(
    seq integer IDENTITY(1,1) NOT NULL,
    val bigint NOT NULL,
    total bigint NOT NULL,

    prev_seq integer NULL,
    prev_total bigint NULL,

    CONSTRAINT [PK_Table_1] 
        PRIMARY KEY CLUSTERED (seq ASC),

    CONSTRAINT [UQ dbo.Table_1 seq, total]
        UNIQUE (seq, total),

    CONSTRAINT [UQ dbo.Table_1 prev_seq]
        UNIQUE (prev_seq),

    CONSTRAINT [FK dbo.Table_1 previous seq and total]
        FOREIGN KEY (prev_seq, prev_total) 
        REFERENCES dbo.Table_1 (seq, total),

    CONSTRAINT [CK dbo.Table_1 total = prev_total + val]
        CHECK (total = ISNULL(prev_total, 0) + val),

    CONSTRAINT [CK dbo.Table_1 denormalized columns all null or all not null]
        CHECK 
        (
            (prev_seq IS NOT NULL AND prev_total IS NOT NULL)
            OR
            (prev_seq IS NULL AND prev_total IS NULL)
        )
);

2

Khi xử lý một tập hợp nhỏ các hàng như vậy được trả về, phép nối tam giác là một lựa chọn tốt. Tuy nhiên, khi sử dụng các chức năng của cửa sổ, bạn có nhiều tùy chọn hơn có thể tăng hiệu suất của chúng. Tùy chọn mặc định cho tùy chọn cửa sổ là RANGE, nhưng tùy chọn tối ưu là ROWS. Hãy lưu ý rằng sự khác biệt không chỉ ở hiệu suất, mà còn ở kết quả cũng như khi có mối quan hệ.

Đoạn mã sau nhanh hơn một chút so với mã bạn đã trình bày.

SELECT TOP 10 seq
    ,value
    ,sum(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) total
FROM Table_1
ORDER BY seq DESC

Cảm ơn bạn đã nói ROWS. Tôi đã thử nó nhưng tôi không thể nói nó nhanh hơn truy vấn thứ 2 của tôi. Kết quả làCPU time = 1438 ms, elapsed time = 1537 ms.
user2652379

Nhưng điều này chỉ có trên tùy chọn này. Truy vấn thứ hai của bạn không có quy mô tốt. Hãy thử trả lại nhiều hàng hơn và sự khác biệt trở nên rõ ràng.
Luis Cazares

Có lẽ bên ngoài của t-sql? Tôi có thể thay đổi lược đồ.
dùng2652379
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.