Apa yang menyebabkan penggunaan CPU tinggi dari kueri / rencana eksekusi ini?

9

Saya memiliki Database Azure SQL yang mendukung aplikasi .NET Core API. Menjelajahi laporan ikhtisar kinerja di Portal Azure menunjukkan bahwa sebagian besar beban (penggunaan DTU) pada server database saya berasal dari CPU, dan satu permintaan khusus:

masukkan deskripsi gambar di sini

Seperti yang dapat kita lihat, kueri 3780 bertanggung jawab untuk hampir semua penggunaan CPU di server.

Ini agak masuk akal, karena permintaan 3780 (lihat di bawah) pada dasarnya adalah inti dari aplikasi dan sering dipanggil oleh pengguna. Ini juga permintaan yang agak rumit dengan banyak gabungan yang diperlukan untuk mendapatkan set data yang diperlukan. Kueri berasal dari sproc yang akhirnya tampak seperti ini:

-- @UserId UNIQUEIDENTIFIER

SELECT
    C.[Id],
    C.[UserId],
    C.[OrganizationId],
    C.[Type],
    C.[Data],
    C.[Attachments],
    C.[CreationDate],
    C.[RevisionDate],
    CASE
        WHEN
            @UserId IS NULL
            OR C.[Favorites] IS NULL
            OR JSON_VALUE(C.[Favorites], CONCAT('$."', @UserId, '"')) IS NULL
        THEN 0
        ELSE 1
    END [Favorite],
    CASE
        WHEN
            @UserId IS NULL
            OR C.[Folders] IS NULL
        THEN NULL
        ELSE TRY_CONVERT(UNIQUEIDENTIFIER, JSON_VALUE(C.[Folders], CONCAT('$."', @UserId, '"')))
    END [FolderId],
    CASE 
        WHEN C.[UserId] IS NOT NULL OR OU.[AccessAll] = 1 OR CU.[ReadOnly] = 0 OR G.[AccessAll] = 1 OR CG.[ReadOnly] = 0 THEN 1
        ELSE 0
    END [Edit],
    CASE 
        WHEN C.[UserId] IS NULL AND O.[UseTotp] = 1 THEN 1
        ELSE 0
    END [OrganizationUseTotp]
FROM
    [dbo].[Cipher] C
LEFT JOIN
    [dbo].[Organization] O ON C.[UserId] IS NULL AND O.[Id] = C.[OrganizationId]
LEFT JOIN
    [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = O.[Id] AND OU.[UserId] = @UserId
LEFT JOIN
    [dbo].[CollectionCipher] CC ON C.[UserId] IS NULL AND OU.[AccessAll] = 0 AND CC.[CipherId] = C.[Id]
LEFT JOIN
    [dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id]
LEFT JOIN
    [dbo].[GroupUser] GU ON C.[UserId] IS NULL AND CU.[CollectionId] IS NULL AND OU.[AccessAll] = 0 AND GU.[OrganizationUserId] = OU.[Id]
LEFT JOIN
    [dbo].[Group] G ON G.[Id] = GU.[GroupId]
LEFT JOIN
    [dbo].[CollectionGroup] CG ON G.[AccessAll] = 0 AND CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
WHERE
    C.[UserId] = @UserId
    OR (
        C.[UserId] IS NULL
        AND OU.[Status] = 2
        AND O.[Enabled] = 1
        AND (
            OU.[AccessAll] = 1
            OR CU.[CollectionId] IS NOT NULL
            OR G.[AccessAll] = 1
            OR CG.[CollectionId] IS NOT NULL
        )
)

Jika Anda peduli, sumber lengkap untuk database ini dapat ditemukan di GitHub di sini . Sumber dari kueri di atas:

Saya telah menghabiskan beberapa waktu pada permintaan ini selama berbulan-bulan menyesuaikan rencana eksekusi sebaik yang saya tahu, berakhir dengan keadaan saat ini. Pertanyaan dengan rencana eksekusi ini sangat cepat di jutaan baris (<1 detik), tetapi seperti disebutkan di atas, semakin memakan CPU server semakin banyak seiring dengan bertambahnya ukuran aplikasi.

Saya telah melampirkan rencana permintaan aktual di bawah ini (tidak yakin ada cara lain untuk membagikannya di sini di pertukaran tumpukan), yang menunjukkan eksekusi sproc dalam produksi terhadap dataset yang dikembalikan sebesar ~ 400 hasil.

Beberapa poin yang saya cari klarifikasi tentang:

  • Indeks Cari [IX_Cipher_UserId_Type_IncludeAll]membutuhkan 57% dari total biaya rencana. Pemahaman saya tentang rencana ini adalah bahwa biaya ini terkait dengan IO, yang membuat sejak tabel Cipher berisi jutaan catatan. Namun, laporan kinerja Azure SQL menunjukkan kepada saya bahwa masalah saya berasal dari CPU pada permintaan ini, bukan IO, jadi saya tidak yakin apakah ini benar-benar masalah atau tidak. Plus itu sudah melakukan pencarian indeks di sini, jadi saya tidak begitu yakin ada ruang untuk perbaikan.

  • Operasi Pencocokan Hash dari semua gabungan tampaknya menjadi yang menunjukkan penggunaan CPU yang signifikan dalam paket (saya pikir?), Tapi saya tidak begitu yakin bagaimana ini bisa dibuat lebih baik. Sifat kompleks dari bagaimana saya perlu mendapatkan data mengharuskan banyak bergabung di beberapa tabel. Saya sudah melakukan hubungan singkat dengan banyak dari gabungan ini jika memungkinkan (berdasarkan hasil dari gabungan sebelumnya) di ONklausa mereka .

Unduh paket eksekusi lengkap di sini: https://www.dropbox.com/s/lua1awsc0uz1lo9/CipherDetails_ReadByUserId.sqlplan?dl=0

Saya merasa seperti saya bisa mendapatkan kinerja CPU yang lebih baik dari permintaan ini, tetapi saya berada pada tahap di mana saya tidak yakin bagaimana untuk melanjutkan penyempurnaan rencana eksekusi lebih jauh. Apa optimasi lain yang bisa dilakukan untuk mengurangi beban CPU? Operasi apa dalam rencana eksekusi yang merupakan pelanggar terburuk penggunaan CPU?

kspearrin
sumber

Jawaban:

4

Anda dapat melihat CPU tingkat operator dan metrik waktu yang berlalu di SQL Server Management Studio, meskipun saya tidak bisa mengatakan seberapa andal mereka untuk kueri yang selesai secepat Anda. Paket Anda hanya memiliki operator mode baris sehingga metrik waktu berlaku untuk operator itu serta operator di subtree di bawahnya. Menggunakan gabungan nested loop sebagai contoh, SQL Server memberi tahu Anda bahwa seluruh subtree membutuhkan waktu 60 ms dari waktu CPU dan 80 ms dari waktu yang telah berlalu:

biaya subtree

Sebagian besar waktu subtree dihabiskan untuk pencarian indeks. Indeks berusaha mengambil CPU juga. Sepertinya indeks Anda memiliki kolom yang tepat sehingga tidak jelas bagaimana Anda dapat mengurangi biaya CPU dari operator itu. Selain mencari sebagian besar waktu CPU dalam paket tersebut dihabiskan untuk pertandingan hash yang mengimplementasikan gabungan Anda.

Ini adalah penyederhanaan yang sangat besar, tetapi CPU yang diambil oleh gabungan hash akan tergantung pada ukuran input untuk tabel hash dan jumlah baris yang diproses pada sisi probe. Mengamati beberapa hal tentang rencana kueri ini:

  • Paling banyak 461 baris yang dikembalikan C.[UserId] = @UserId. Baris-baris ini sama sekali tidak peduli dengan sambungan.
  • Untuk baris yang membutuhkan gabungan, SQL Server tidak dapat menerapkan pemfilteran mana pun lebih awal (kecuali untuk OU.[UserId] = @UserId).
  • Hampir semua baris yang diproses dihilangkan di dekat akhir rencana kueri (dibaca dari kanan ke kiri) oleh filter: [vault].[dbo].[Cipher].[UserId] as [C].[UserId]=[@UserId] OR ([vault].[dbo].[OrganizationUser].[AccessAll] as [OU].[AccessAll]=(1) OR [vault].[dbo].[CollectionUser].[CollectionId] as [CU].[CollectionId] IS NOT NULL OR [vault].[dbo].[Group].[AccessAll] as [G].[AccessAll]=(1) OR [vault].[dbo].[CollectionGroup].[CollectionId] as [CG].[CollectionId] IS NOT NULL) AND [vault].[dbo].[Cipher].[UserId] as [C].[UserId] IS NULL AND [vault].[dbo].[OrganizationUser].[Status] as [OU].[Status]=(2) AND [vault].[dbo].[Organization].[Enabled] as [O].[Enabled]=(1)

Akan lebih alami untuk menulis kueri Anda sebagai UNION ALL. Bagian pertama dari UNION ALLdapat menyertakan baris di mana C.[UserId] = @UserIddan bagian kedua dapat menyertakan baris di mana C.[UserId] IS NULL. Anda sudah melakukan dua pencarian indeks [dbo].[Cipher](satu untuk @UserIddan satu untuk NULL) sehingga tampaknya tidak mungkin bahwa UNION ALLversi akan lebih lambat. Menuliskan pertanyaan secara terpisah akan memungkinkan Anda melakukan beberapa penyaringan lebih awal, baik pada sisi pembuatan maupun sisi penyelidikan. Kueri bisa lebih cepat jika mereka perlu memproses data yang kurang menengah.

Saya tidak tahu apakah versi SQL Server Anda mendukung ini, tetapi jika itu tidak membantu Anda bisa mencoba menambahkan indeks kolom toko ke permintaan Anda untuk membuat hash Anda memenuhi syarat untuk mode batch . Cara pilihan saya adalah membuat tabel kosong dengan CCI di atasnya dan bergabung dengan tabel itu. Hash joins bisa jauh lebih efisien ketika dijalankan dalam mode batch dibandingkan dengan mode baris.

Joe Obbish
sumber
Seperti yang disarankan, saya dapat menulis ulang sproc dengan 2 pertanyaan itu UNION ALL(satu untuk C.[UserId] = @UserIddan satu untuk C.[UserId] IS NULL AND ...). Ini mengurangi set gabungan hasil dan menghilangkan kebutuhan untuk hash cocok sama sekali (sekarang melakukan loop bersarang pada set gabungan kecil). Permintaan sekarang jauh lebih baik pada CPU. Terima kasih!
kspearrin
0

Jawaban wiki komunitas :

Anda dapat mencoba membaginya menjadi dua pertanyaan dan UNION ALLmengembalikannya menjadi satu.

WHEREKlausa Anda terjadi pada akhirnya, tetapi jika Anda membaginya menjadi:

  • Satu permintaan di mana C.[UserId] = @UserId
  • Di tempat lain C.[UserId] IS NULL AND OU.[Status] = 2 AND O.[Enabled] = 1

... masing-masing mungkin mendapatkan rencana yang cukup baik untuk membuatnya bernilai sementara.

Jika setiap kueri menerapkan predikat awal dalam paket, Anda tidak perlu bergabung dengan begitu banyak baris yang pada akhirnya disaring.

user126897
sumber