Kueri dijeda setelah mengembalikan sejumlah baris tetap

8

Saya memiliki tampilan yang berjalan cepat (beberapa detik) hingga 41 catatan (misalnya, TOP 41) tetapi membutuhkan beberapa menit untuk 44 atau lebih catatan, dengan hasil antara jika dijalankan dengan TOP 42atau TOP 43. Secara khusus, ini akan mengembalikan 39 catatan pertama dalam beberapa detik, kemudian berhenti selama hampir tiga menit sebelum mengembalikan catatan yang tersisa. Pola ini sama ketika meminta TOP 44atau TOP 100.

Tampilan ini awalnya berasal dari tampilan dasar, menambahkan hanya satu filter ke dasar, yang terakhir dalam kode di bawah ini. Tampaknya tidak ada perbedaan jika saya rantai tampilan anak dari pangkalan atau jika saya menulis tampilan anak dengan kode dari pangkalan di-lined. Tampilan dasar mengembalikan 100 catatan hanya dalam beberapa detik. Saya ingin berpikir bahwa saya bisa membuat pandangan anak berlari secepat basis, tidak 50 kali lebih lambat. Adakah yang melihat perilaku seperti ini? Adakah tebakan penyebab atau resolusi?

Perilaku ini telah konsisten selama beberapa jam terakhir karena saya telah menguji pertanyaan yang terlibat, meskipun jumlah baris yang dikembalikan sebelum hal-hal mulai melambat telah sedikit naik turun. Ini bukan hal baru; Saya melihatnya sekarang karena total waktu menjalankan sudah dapat diterima (<2 menit), tetapi saya telah melihat jeda ini dalam file log terkait selama berbulan-bulan, setidaknya.

Pemblokiran

Saya belum pernah melihat kueri diblokir, dan masalahnya ada bahkan ketika tidak ada aktivitas lain di database (seperti divalidasi oleh sp_WhoIsActive). Tampilan dasar mencakup NOLOCKseluruh, untuk apa itu layak.

Pertanyaan

Berikut ini adalah versi cut-down dari tampilan anak, dengan tampilan dasar disatukan untuk kesederhanaan. Itu masih menunjukkan lompatan waktu berjalan di sekitar 40 catatan.

SELECT TOP 100 PERCENT
    Map.SalesforceAccountID AS Id,
    CAST(C.CustomerID AS NVARCHAR(255)) AS Name,
    CASE WHEN C.StreetAddress = 'Unknown' THEN '' ELSE C.StreetAddress                 END AS BillingStreet,
    CASE WHEN C.City          = 'Unknown' THEN '' ELSE SUBSTRING(C.City,        1, 40) END AS BillingCity,
                                                       SUBSTRING(C.Region,      1, 20)     AS BillingState,
    CASE WHEN C.PostalCode    = 'Unknown' THEN '' ELSE SUBSTRING(C.PostalCode,  1, 20) END AS BillingPostalCode,
    CASE WHEN C.Country       = 'Unknown' THEN '' ELSE SUBSTRING(C.Country,     1, 40) END AS BillingCountry,
    CASE WHEN C.PhoneNumber   = 'Unknown' THEN '' ELSE C.PhoneNumber                   END AS Phone,
    CASE WHEN C.FaxNumber     = 'Unknown' THEN '' ELSE C.FaxNumber                     END AS Fax,
    TransC.WebsiteAddress AS Website,
    C.AccessKey AS AccessKey__c,
    CASE WHEN dbo.ValidateEMail(C.EMailAddress) = 1 THEN C.EMailAddress END,  -- Removing this UDF does not speed things
    TransC.EmailSubscriber
    -- A couple dozen additional TransC fields
FROM
    WarehouseCustomers AS C WITH (NOLOCK)
    INNER JOIN TransactionalCustomers AS TransC WITH (NOLOCK) ON C.CustomerID = TransC.CustomerID
    LEFT JOIN  Salesforce.AccountsMap AS Map WITH (NOLOCK) ON C.CustomerID = Map.CustomerID
WHERE
        C.DateMadeObsolete IS NULL
    AND C.EmailAddress NOT LIKE '%@volusion.%'
    AND C.AccessKey IN ('C', 'R')
    AND C.CustomerID NOT IN (243566)  -- Exclude specific test records
    AND EXISTS (SELECT * FROM Orders AS O WHERE C.CustomerID = O.CustomerID AND O.OrderDate >= '2010-06-28')  -- Only count customers who've placed a recent order
    AND Map.SalesforceAccountID IS NULL  -- Only count customers not already uploaded to Salesforce
-- Removing the ORDER BY clause does not speed things up
ORDER BY
    C.CustomerID DESC

Yang Id IS NULLmembuang saringan sebagian catatan dikembalikan oleh BaseView; tanpa TOPklausa, mereka masing-masing mengembalikan 1.100 rekaman dan 267 ribu.

Statistik

Saat berlari TOP 40:

SQL Server parse and compile time:    CPU time = 234 ms, elapsed time = 247 ms.
SQL Server Execution Times:   CPU time = 0 ms,  elapsed time = 0 ms.
SQL Server Execution Times:   CPU time = 0 ms,  elapsed time = 0 ms.

(40 row(s) affected)
Table 'CustomersHistory'. Scan count 2, logical reads 39112, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Orders'. Scan count 1, logical reads 752, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'AccountsMap'. Scan count 1, logical reads 458, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times:   CPU time = 2199 ms,  elapsed time = 7644 ms.

Saat berlari TOP 45:

(45 row(s) affected)
Table 'CustomersHistory'. Scan count 2, logical reads 98268, physical reads 1, read-ahead reads 3, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Orders'. Scan count 1, logical reads 1788, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'AccountsMap'. Scan count 1, logical reads 2152, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

SQL Server Execution Times: CPU time = 41980 ms,  elapsed time = 177231 ms.

Saya terkejut melihat jumlah lompatan membaca ~ 3x untuk perbedaan sederhana dalam output aktual.

Membandingkan rencana eksekusi, mereka sama dengan jumlah baris yang dikembalikan. Seperti halnya statistik di atas, jumlah baris aktual untuk langkah-langkah awal secara dramatis lebih tinggi dalam TOP 45kueri, bukan hanya 12,5% lebih tinggi.

Secara garis besar, ini memindai indeks yang meliputi dari Pesanan, mencari catatan yang sesuai dari WarehouseCustomers; loop-menggabungkan ini ke TransactionalCustomers (permintaan jarak jauh, rencana pasti tidak diketahui); dan menggabungkan ini dengan pemindaian tabel AccountsMap. Kueri jarak jauh adalah 94% dari perkiraan biaya.

Catatan Lain-Lain

Sebelumnya, ketika saya mengeksekusi konten yang diperluas dari tampilan sebagai permintaan yang berdiri sendiri, itu berjalan cukup cepat: 13 detik untuk 100 catatan. Saya sekarang sedang menguji versi cut-down dari query, tanpa subqueries, dan query yang lebih sederhana ini membutuhkan waktu tiga menit untuk diminta mengembalikan lebih dari 40 baris, bahkan ketika dijalankan sebagai query yang berdiri sendiri.

Pandangan anak mencakup sejumlah besar pembacaan (~ 1M per sp_WhoIsActive), tetapi pada mesin ini (delapan core, 32 GB RAM, 95% kotak SQL khusus) yang biasanya tidak menjadi masalah.

Saya telah menjatuhkan dan menciptakan kembali kedua tampilan beberapa kali, tanpa perubahan.

Data tidak termasuk bidang TEKS atau BLOB. Satu bidang melibatkan UDF; menghapusnya tidak mencegah jeda.

Waktu serupa apakah menanyakan di server itu sendiri, atau di workstation saya 1.400 mil jauhnya, sehingga penundaan tampaknya melekat dalam permintaan itu sendiri daripada mengirim hasilnya ke klien.

Catatan Re: Solusinya

Perbaikannya ternyata sederhana: mengganti LEFT JOINto Map dengan NOT EXISTSklausa. Ini hanya menyebabkan satu perbedaan kecil dalam rencana kueri, bergabung ke tabel TransactionCustomers (kueri jarak jauh) setelah bergabung ke tabel Peta daripada sebelumnya. Ini mungkin berarti bahwa itu hanya meminta catatan yang diperlukan dari server jarak jauh, yang akan mengurangi volume yang dikirimkan ~ 100 kali lipat.

Biasanya aku yang pertama bersorak untuk NOT EXISTS; seringkali lebih cepat daripada LEFT JOIN...WHERE ID IS NULLkonstruksi, dan sedikit lebih kompak. Dalam kasus ini, ini canggung karena kueri masalah dibuat berdasarkan tampilan yang ada, dan sementara bidang yang diperlukan untuk anti-gabung diekspos oleh tampilan dasar, ia terlebih dahulu dilemparkan dari integer ke teks. Jadi untuk kinerja yang layak, saya harus menghentikan pola dua lapis dan alih-alih memiliki dua pandangan yang hampir identik, dengan yang kedua termasuk NOT EXISTSklausa.

Terima kasih atas bantuan Anda dalam memecahkan masalah ini! Mungkin terlalu spesifik dengan keadaan saya untuk bisa membantu orang lain, tetapi mudah-mudahan tidak. Jika tidak ada yang lain, ini adalah contoh NOT EXISTSmenjadi lebih cepat daripada marjinal LEFT JOIN...WHERE ID IS NULL. Tetapi pelajaran sebenarnya mungkin untuk memastikan bahwa pertanyaan jarak jauh digabungkan seefisien mungkin; rencana permintaan mengklaim bahwa itu mewakili 2% dari biaya, tetapi tidak selalu memperkirakan secara akurat.

Jon dari Semua Perdagangan
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Paul White 9

Jawaban:

4

Beberapa hal untuk dicoba:

  1. Periksa indeks Anda

    • Apakah semua JOINbidang kunci diindeks? Jika Anda sering menggunakan tampilan ini, saya akan menambahkan indeks yang difilter untuk kriteria dalam tampilan. Contohnya...

    • CREATE INDEX ix_CustomerId ON WarehouseCustomers(CustomerId, EmailAddress) WHERE DateMadeObsolete IS NULL AND AccessKey IN ('C', 'R') AND CustomerID NOT IN (243566)

  2. Perbarui statistik

    • Mungkin ada masalah dengan statistik yang kedaluwarsa. Jika Anda dapat mengayunkannya, saya akan melakukan FULLSCAN. Jika ada banyak baris, ada kemungkinan data telah berubah secara signifikan tanpa memicu perhitungan ulang otomatis.
  3. Bersihkan kueri

    • Buat Map JOINa NOT EXISTS- Anda tidak memerlukan data dari tabel itu, karena Anda hanya ingin catatan yang tidak cocok

    • Hapus ORDER BY. Saya tahu komentar mengatakan bahwa itu tidak masalah tetapi saya merasa sangat sulit untuk percaya. Ini mungkin tidak penting untuk set hasil Anda yang lebih kecil karena halaman data sudah di-cache.

JNK
sumber
Poin menarik kembali: indeks yang difilter. Kueri tidak menggunakannya secara otomatis, tetapi saya akan menguji memaksa dengan petunjuk. Saya telah memperbarui statistik, dan dapat menguji ini dan rekomendasi Anda yang lain hari ini; Saya harus membiarkan tumpukan menumpuk setelah EOWD sehingga saya dapat menguji terhadap set data yang layak.
Jon of All Trades
Saya sudah mencoba berbagai kombinasi tweak ini, dan kuncinya sepertinya anti-gabung dengan Map. Seperti LEFT JOIN...WHERE Id IS NULL, saya mendapatkan jeda ini; sebagai NOT EXISTSklausa, run time adalah detik. Saya terkejut, tapi saya tidak bisa berdebat dengan hasilnya!
Jon of All Trades
2

Perbaikan 1 Hapus SubQuery untuk Pesanan dan ubah menjadi gabungan

FROM
WarehouseCustomers AS C WITH (NOLOCK)
INNER JOIN TransactionalCustomers AS TransC WITH (NOLOCK) 
                                                        ON C.CustomerID = TransC.CustomerID
LEFT JOIN  Salesforce.AccountsMap AS Map WITH (NOLOCK) 
                                                        ON C.CustomerID = Map.CustomerID
INNER Join Orders AS O 
                                                        ON C.CustomerID = O.CustomerID

 WHERE
    C.DateMadeObsolete IS NULL
    AND C.EmailAddress NOT LIKE '%@volusion.%'
    AND C.AccessKey IN ('C', 'R')
    AND C.CustomerID NOT IN (243566)
    AND O.OrderDate >= '2010-06-28'
    AND Map.SalesforceAccountID IS NULL

Perbaikan 2 - Menyimpan catatan TransactionalCustomers dalam tabel Sementara Lokal

Select 
    CAST(C.CustomerID AS NVARCHAR(255)) AS Name,
    CASE WHEN C.StreetAddress = 'Unknown' THEN '' ELSE C.StreetAddress                 END AS BillingStreet,
    CASE WHEN C.City          = 'Unknown' THEN '' ELSE SUBSTRING(C.City,        1, 40) END AS BillingCity,
                                                       SUBSTRING(C.Region,      1, 20)     AS BillingState,
    CASE WHEN C.PostalCode    = 'Unknown' THEN '' ELSE SUBSTRING(C.PostalCode,  1, 20) END AS BillingPostalCode,
    CASE WHEN C.Country       = 'Unknown' THEN '' ELSE SUBSTRING(C.Country,     1, 40) END AS BillingCountry,
    CASE WHEN C.PhoneNumber   = 'Unknown' THEN '' ELSE C.PhoneNumber                   END AS Phone,
    CASE WHEN C.FaxNumber     = 'Unknown' THEN '' ELSE C.FaxNumber                     END AS Fax,
    C.AccessKey AS AccessKey__c
Into #Temp
From  WarehouseCustomers C
Where C.DateMadeObsolete IS NULL
        AND C.EmailAddress NOT LIKE '%@volusion.%'
        AND C.AccessKey IN ('C', 'R')
        AND C.CustomerID NOT IN (243566)

Pertanyaan Terakhir

FROM
#Temp AS C WITH (NOLOCK)
INNER JOIN TransactionalCustomers AS TransC WITH (NOLOCK) 
                                                            ON C.CustomerID = TransC.CustomerID
LEFT JOIN Salesforce.AccountsMap AS Map WITH (NOLOCK) 
                                                            ON C.CustomerID = Map.CustomerID
INNER Join Orders AS O 
                                                            ON C.CustomerID = O.CustomerID

WHERE
C.DateMadeObsolete IS NULL
AND C.EmailAddress NOT LIKE '%@volusion.%'
AND C.AccessKey IN ('C', 'R')
AND C.CustomerID NOT IN (243566)
AND O.OrderDate >= '2010-06-28'
AND Map.SalesforceAccountID IS NULL

Poin 3 - Saya berasumsi bahwa Anda memiliki indeks pada CustomerID, EmailAddress, OrderDate

Pankaj Garg
sumber
1
Re: "Peningkatan" 1 - EXISTSbiasanya lebih cepat daripada JOINdalam keadaan ini, dan menghilangkan potensi dupes. Saya tidak berpikir itu akan menjadi perbaikan sama sekali.
JNK
1
masalah ini ada dua, meskipun - Ini berpotensi akan MENGUBAH HASIL, dan kecuali kedua tabel memiliki indeks berkerumun unik pada bidang yang digunakan dalam bergabung, itu akan kurang efisien daripada yang ada. Subclauses tidak selalu buruk.
JNK
@PankajGarg: Terima kasih atas sarannya, sayangnya ada beberapa pesanan per pelanggan, jadi EXISTSwajib. Juga, dalam pandangan saya tidak bisa men-cache data pelanggan yang digunakan kembali, meskipun saya telah bermain-main dengan gagasan dummy TVF tanpa parameter.
Jon of All Trades