Bagaimana saya bisa menjalankan total baris terbaru lebih cepat?

8

Saat ini saya sedang merancang tabel transaksi. Saya menyadari bahwa menghitung total running untuk setiap baris akan diperlukan dan ini mungkin memperlambat kinerja. Jadi saya membuat tabel dengan 1 juta baris untuk tujuan pengujian.

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

Dan saya mencoba untuk mendapatkan 10 baris baru dan total berjalan, tetapi butuh sekitar 10 detik.

--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.

Rencana pelaksanaan percobaan pertama

Saya menduga TOPkarena alasan kinerja lambat dari rencana, jadi saya mengubah permintaan seperti ini, dan butuh sekitar 1 ~ 2 detik. Tapi saya pikir ini masih lambat untuk produksi dan bertanya-tanya apakah ini dapat diperbaiki lebih lanjut.

--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.

Rencana pelaksanaan upaya 2

Pertanyaan saya adalah:

  • Mengapa kueri dari upaya 1 lebih lambat dari yang ke-2?
  • Bagaimana saya dapat meningkatkan kinerja lebih lanjut? Saya juga dapat mengubah skema.

Supaya jelas, kedua kueri mengembalikan hasil yang sama seperti di bawah ini.

hasil

pengguna2652379
sumber
1
Saya biasanya tidak menggunakan fungsi jendela, tetapi saya ingat saya membaca beberapa artikel yang bermanfaat. Lihatlah satu Pengantar Fungsi Jendela T-SQL , terutama di bagian Window Aggregate Enhancements pada tahun 2012 . Mungkin itu memberi Anda beberapa jawaban. ... dan satu artikel lagi dari penulis yang sangat baik, T-SQL Window, Fungsi dan Kinerja
Denis Rubashkin
Sudahkah Anda mencoba memasang indeks value?
Jacob H

Jawaban:

5

Saya merekomendasikan pengujian dengan sedikit lebih banyak data untuk mendapatkan ide yang lebih baik tentang apa yang terjadi dan untuk melihat bagaimana kinerja berbagai pendekatan. Saya memuat 16 juta baris ke sebuah tabel dengan struktur yang sama. Anda dapat menemukan kode untuk mengisi tabel di bagian bawah jawaban ini.

Pendekatan berikut ini memakan waktu 19 detik di mesin saya:

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

Rencana sebenarnya di sini . Sebagian besar waktu dihabiskan untuk menghitung jumlah dan melakukan semacam itu. Yang mengkhawatirkan, rencana kueri mengerjakan hampir semua pekerjaan untuk seluruh rangkaian hasil dan memfilter ke 10 baris yang Anda minta di bagian paling akhir. Runtime kueri ini menskala dengan ukuran tabel alih-alih dengan ukuran set hasil.

Opsi ini membutuhkan 23 detik pada mesin saya:

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;

Rencana sebenarnya di sini . Pendekatan ini menskala dengan jumlah baris yang diminta dan ukuran tabel. Hampir 160 juta baris dibaca dari tabel:

Halo

Untuk mendapatkan hasil yang benar, Anda harus menjumlahkan baris untuk seluruh tabel. Idealnya Anda akan melakukan penjumlahan ini hanya sekali. Dimungkinkan untuk melakukan ini jika Anda mengubah cara Anda mendekati masalah. Anda bisa menghitung jumlah untuk seluruh tabel lalu mengurangi total berjalan dari baris di set hasil. Itu memungkinkan Anda menemukan jumlah untuk baris ke-N. Salah satu cara untuk melakukan ini:

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;

Rencana sebenarnya di sini . Kueri baru berjalan dalam 644 ms pada mesin saya. Tabel dipindai sekali untuk mendapatkan total lengkap kemudian baris tambahan dibaca untuk setiap baris dalam set hasil. Tidak ada penyortiran dan hampir semua waktu dihabiskan untuk menghitung jumlah pada bagian paralel dari rencana:

cukup bagus

Jika Anda ingin agar kueri ini berjalan lebih cepat, Anda hanya perlu mengoptimalkan bagian yang menghitung jumlah keseluruhan. Permintaan di atas melakukan pemindaian indeks berkerumun. Indeks berkerumun termasuk semua kolom tetapi Anda hanya perlu [value]kolom. Salah satu opsi adalah membuat indeks yang tidak tercakup pada kolom itu. Pilihan lain adalah membuat indeks toko kolom yang tidak tercakup pada kolom itu. Keduanya akan meningkatkan kinerja. Jika Anda menggunakan Enterprise, pilihan yang bagus adalah membuat tampilan yang diindeks seperti berikut:

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);

Tampilan ini mengembalikan satu baris sehingga hampir tidak membutuhkan ruang. Akan ada penalti ketika melakukan DML tetapi seharusnya tidak jauh berbeda dengan pemeliharaan indeks. Dengan tampilan yang diindeks dalam permainan kueri sekarang membutuhkan 0 ms:

masukkan deskripsi gambar di sini

Rencana sebenarnya di sini . Bagian terbaik tentang pendekatan ini adalah runtime tidak diubah oleh ukuran tabel. Satu-satunya hal yang penting adalah berapa banyak baris yang dikembalikan. Misalnya, jika Anda mendapatkan 10.000 baris pertama, permintaan sekarang membutuhkan 18 ms untuk dieksekusi.

Kode untuk mengisi tabel:

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]);
Joe Obbish
sumber
4

Perbedaan dalam dua pendekatan pertama

Paket pertama menghabiskan sekitar 7 dari 10 detik di operator Window Spool, jadi ini adalah alasan utama sangat lambat. Ini melakukan banyak I / O di tempdb untuk membuat ini. Statistik I / O dan waktu saya terlihat seperti ini:

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.

The Rencana kedua adalah mampu menghindari spool, dan dengan demikian meja kerja seluruhnya. Ini hanya mengambil 10 baris teratas dari indeks berkerumun, dan kemudian apakah loop bersarang bergabung dengan agregasi (jumlah) yang keluar dari pemindaian indeks berkerumun terpisah. Sisi dalam masih berakhir dengan membaca seluruh tabel, tetapi meja ini sangat padat, jadi ini cukup efisien dengan jutaan baris.

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

Meningkatkan kinerja

Kolom toko

Jika Anda benar-benar menginginkan pendekatan "pelaporan online", toko kolom kemungkinan adalah pilihan terbaik Anda.

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

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

Maka pertanyaan ini sangat cepat:

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

Berikut ini statistik dari mesin saya:

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.

Anda mungkin tidak akan mengalahkan itu (kecuali jika Anda benar - benar pintar - bagus, Joe). Columnstore sangat pandai memindai dan mengagregasi sejumlah besar data.

Menggunakan opsi fungsi jendela ROWdaripadaRANGE

Anda bisa mendapatkan kinerja yang sangat mirip dengan permintaan kedua Anda dengan pendekatan ini, yang disebutkan dalam jawaban lain, dan yang saya gunakan dalam contoh kolom di atas ( rencana eksekusi ):

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

Ini menghasilkan lebih sedikit bacaan daripada pendekatan kedua Anda, dan tidak ada aktivitas tempdb vs pendekatan pertama Anda karena spool jendela terjadi di memori :

... RANGE menggunakan spool di disk, sementara ROWS menggunakan spool di memori

Sayangnya, runtime hampir sama dengan pendekatan kedua Anda.

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.

Solusi berbasis skema: total async running

Karena Anda terbuka untuk ide-ide lain, Anda dapat mempertimbangkan memperbarui "running total" secara tidak sinkron. Anda bisa secara berkala mengambil hasil dari salah satu pertanyaan ini, dan memuatnya ke tabel "total". Jadi, Anda akan melakukan sesuatu seperti ini:

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

Muat setiap hari / jam / apa pun (butuh sekitar 2 detik pada mesin saya dengan baris 1mm, dan bisa dioptimalkan):

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;

Maka permintaan pelaporan Anda sangat efisien:

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;

Berikut adalah statistik baca:

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

Solusi berbasis skema: total baris dengan batasan

Solusi yang sangat menarik untuk hal ini dibahas secara terperinci dalam jawaban untuk pertanyaan ini: Menulis skema bank sederhana: Bagaimana saya harus menjaga saldo saya sinkron dengan riwayat transaksi mereka?

Pendekatan dasar adalah untuk melacak total berjalan saat ini di-baris bersama dengan jumlah total dan urutan berjalan sebelumnya. Kemudian Anda bisa menggunakan batasan untuk memvalidasi total yang berjalan selalu benar dan terbaru.

Penghargaan untuk Paul White karena memberikan contoh implementasi untuk skema dalam T&J ini:

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)
        )
);
Josh Darnell
sumber
2

Ketika berhadapan dengan sekelompok kecil baris yang dikembalikan, gabungan segitiga adalah pilihan yang baik. Namun, saat menggunakan fungsi jendela Anda memiliki lebih banyak opsi yang dapat meningkatkan kinerjanya. Opsi default untuk opsi jendela adalah RANGE, tetapi opsi optimal adalah ROWS. Ketahuilah bahwa perbedaannya bukan hanya dalam kinerja, tetapi juga dalam hasil saat ikatan terlibat.

Kode berikut sedikit lebih cepat daripada yang Anda sajikan.

SELECT TOP 10 seq
    ,value
    ,sum(value) OVER (ORDER BY seq ROWS UNBOUNDED PRECEDING) total
FROM Table_1
ORDER BY seq DESC
Luis Cazares
sumber
Terima kasih sudah memberitahu ROWS. Saya mencobanya tetapi saya tidak bisa mengatakan itu lebih cepat dari permintaan saya yang ke-2. Hasilnya adalahCPU time = 1438 ms, elapsed time = 1537 ms.
user2652379
Tapi ini hanya pada opsi ini. Kueri kedua Anda tidak memiliki skala yang baik. Coba kembalikan lebih banyak baris dan perbedaannya menjadi sangat jelas.
Luis Cazares
Mungkin di luar t-sql? Saya dapat mengubah skema.
user2652379