Meniru fungsi skalar yang ditentukan pengguna dengan cara yang tidak mencegah paralelisme

12

Saya mencoba untuk melihat apakah ada cara untuk mengelabui SQL Server untuk menggunakan rencana tertentu untuk kueri.

1. Lingkungan

Bayangkan Anda memiliki beberapa data yang dibagi di antara berbagai proses. Jadi, anggaplah kita memiliki beberapa hasil percobaan yang memakan banyak ruang. Kemudian, untuk setiap proses, kami tahu tahun / bulan hasil percobaan yang ingin kami gunakan.

if object_id('dbo.SharedData') is not null
    drop table SharedData

create table dbo.SharedData (
    experiment_year int,
    experiment_month int,
    rn int,
    calculated_number int,
    primary key (experiment_year, experiment_month, rn)
)
go

Sekarang, untuk setiap proses kami memiliki parameter yang disimpan dalam tabel

if object_id('dbo.Params') is not null
    drop table dbo.Params

create table dbo.Params (
    session_id int,
    experiment_year int,
    experiment_month int,
    primary key (session_id)
)
go

2. Uji data

Mari kita tambahkan beberapa data uji:

insert into dbo.Params (session_id, experiment_year, experiment_month)
select 1, 2014, 3 union all
select 2, 2014, 4 
go

insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
    2014, 3, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
    cross join master.dbo.spt_values as v2
go

insert into dbo.SharedData (experiment_year, experiment_month, rn, calculated_number)
select
    2014, 4, row_number() over(order by v1.name), abs(Checksum(newid())) % 10
from master.dbo.spt_values as v1
    cross join master.dbo.spt_values as v2
go

3. Mengambil hasil

Sekarang, sangat mudah untuk mendapatkan hasil eksperimen dengan @experiment_year/@experiment_month:

create or alter function dbo.f_GetSharedData(@experiment_year int, @experiment_month int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.SharedData as d
    where
        d.experiment_year = @experiment_year and
        d.experiment_month = @experiment_month
)
go

Rencananya bagus dan paralel:

select
    calculated_number,
    count(*)
from dbo.f_GetSharedData(2014, 4)
group by
    calculated_number

kueri 0 paket

masukkan deskripsi gambar di sini

4. Masalah

Tetapi, untuk membuat penggunaan data sedikit lebih umum, saya ingin memiliki fungsi lain - dbo.f_GetSharedDataBySession(@session_id int). Jadi, cara mudah adalah dengan membuat fungsi skalar, menerjemahkan @session_id-> @experiment_year/@experiment_month:

create or alter function dbo.fn_GetExperimentYear(@session_id int)
returns int
as
begin
    return (
        select
            p.experiment_year
        from dbo.Params as p
        where
            p.session_id = @session_id
    )
end
go

create or alter function dbo.fn_GetExperimentMonth(@session_id int)
returns int
as
begin
    return (
        select
            p.experiment_month
        from dbo.Params as p
        where
            p.session_id = @session_id
    )
end
go

Dan sekarang kita dapat membuat fungsi kita:

create or alter function dbo.f_GetSharedDataBySession1(@session_id int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.f_GetSharedData(
        dbo.fn_GetExperimentYear(@session_id),
        dbo.fn_GetExperimentMonth(@session_id)
    ) as d
)
go

rencana kueri 1

masukkan deskripsi gambar di sini

Paketnya sama, kecuali tentu saja, tidak paralel, karena fungsi skalar yang melakukan akses data membuat keseluruhan paket serial .

Jadi saya sudah mencoba beberapa pendekatan berbeda, seperti, menggunakan subqueries alih-alih fungsi skalar:

create or alter function dbo.f_GetSharedDataBySession2(@session_id int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.f_GetSharedData(
       (select p.experiment_year from dbo.Params as p where p.session_id = @session_id),
       (select p.experiment_month from dbo.Params as p where p.session_id = @session_id)
    ) as d
)
go

rencana kueri 2

masukkan deskripsi gambar di sini

Atau menggunakan cross apply

create or alter function dbo.f_GetSharedDataBySession3(@session_id int)
returns table
as
return (
    select
        d.rn,
        d.calculated_number
    from dbo.Params as p
        cross apply dbo.f_GetSharedData(
            p.experiment_year,
            p.experiment_month
        ) as d
    where
        p.session_id = @session_id
)
go

rencana kueri 3

masukkan deskripsi gambar di sini

Tetapi saya tidak dapat menemukan cara untuk menulis kueri ini sebaik yang menggunakan fungsi skalar.

Beberapa pemikiran:

  1. Pada dasarnya apa yang saya inginkan adalah bisa entah bagaimana memberitahu SQL Server untuk pra-menghitung nilai-nilai tertentu dan kemudian meneruskannya lebih lanjut sebagai konstanta.
  2. Apa yang bisa membantu adalah jika kita punya petunjuk materialisasi menengah . Saya telah memeriksa beberapa varian (multi-statement TVF atau cte dengan top), tetapi tidak ada rencana yang sebaik yang memiliki fungsi skalar sejauh ini.
  3. Saya tahu tentang peningkatan mendatang dari SQL Server 2017 - Froid: Optimalisasi Program Imperatif dalam Database Relasional. Namun, saya tidak yakin itu akan membantu. Akan lebih baik dibuktikan salah di sini.

Informasi tambahan

Saya menggunakan fungsi (daripada memilih data langsung dari tabel) karena jauh lebih mudah digunakan dalam berbagai pertanyaan, yang biasanya memiliki @session_id sebagai parameter.

Saya diminta untuk membandingkan waktu eksekusi aktual. Dalam kasus khusus ini

  • permintaan 0 berjalan untuk ~ 500 ms
  • query 1 berjalan untuk ~ 1500ms
  • query 2 berjalan untuk ~ 1500ms
  • query 3 berjalan untuk ~ 2000 ms.

Plan # 2 memiliki pemindaian indeks, bukan pencarian, yang kemudian disaring oleh predikat pada loop bersarang. Plan # 3 tidak seburuk itu, tetapi masih bekerja lebih banyak dan bekerja lebih lambat dari rencana # 0.

Mari kita asumsikan itu dbo.Params jarang diubah, dan biasanya memiliki sekitar 1-200 baris, tidak lebih dari, katakanlah 2000 yang pernah diharapkan. Sekarang sekitar 10 kolom dan saya tidak berharap untuk menambahkan kolom terlalu sering.

Jumlah baris di Params tidak tetap, jadi untuk setiap @session_idakan ada satu baris. Jumlah kolom di sana tidak diperbaiki, itu salah satu alasan saya tidak ingin menelepon dbo.f_GetSharedData(@experiment_year int, @experiment_month int)dari mana-mana, jadi saya bisa menambahkan kolom baru ke kueri ini secara internal. Saya akan senang mendengar pendapat / saran tentang ini, bahkan jika itu memiliki beberapa batasan.

Roman Pekar
sumber
Paket kueri dengan Froid akan mirip dengan kueri2 di atas, jadi ya, itu tidak akan membawa Anda ke solusi yang ingin Anda capai dalam kasus ini.
Karthik

Jawaban:

13

Anda tidak dapat benar-benar aman mencapai apa yang Anda inginkan dalam SQL Server hari ini, yaitu dalam satu pernyataan dan dengan eksekusi paralel, dalam batasan yang ditetapkan dalam pertanyaan (seperti yang saya pahami).

Jadi jawaban sederhana saya adalah tidak . Sisa dari jawaban ini sebagian besar merupakan pembahasan mengapa itu, jika itu menarik.

Dimungkinkan untuk mendapatkan rencana paralel, seperti disebutkan dalam pertanyaan, tetapi ada dua varietas utama, yang keduanya tidak cocok untuk kebutuhan Anda:

  1. Loop bersarang berkorelasi bergabung, dengan round-robin mendistribusikan stream di tingkat atas. Mengingat bahwa satu baris dijamin berasal dari nilai Paramstertentu session_id, sisi dalam akan berjalan pada satu utas, meskipun ditandai dengan ikon paralelisme. Inilah sebabnya mengapa rencana paralel 3 tampaknya tidak berkinerja baik; itu sebenarnya seri.

  2. Alternatif lain adalah untuk paralelisme independen di sisi dalam loop bersarang bergabung. Independen di sini berarti bahwa utas dimulai di sisi dalam, dan bukan hanya utas yang sama seperti yang mengeksekusi sisi luar dari loop bersarang bergabung. SQL Server hanya mendukung pararelisme paralel sisi-sisi independen ketika dijamin ada satu baris sisi luar dan tidak ada parameter gabungan yang berkorelasi ( paket 2 ).

Jadi, kami memiliki pilihan rencana paralel yang bersifat serial (karena satu utas) dengan nilai-nilai berkorelasi yang diinginkan; atau rencana paralel sisi dalam yang harus dipindai karena tidak memiliki parameter untuk dicari. (Selain itu: Ini benar-benar harus diizinkan untuk mendorong paralelisme sisi dalam menggunakan tepat satu set parameter berkorelasi, tetapi belum pernah diterapkan, mungkin karena alasan yang baik).

Maka pertanyaan alami adalah: mengapa kita perlu parameter berkorelasi sama sekali? Mengapa SQL Server tidak hanya mencari langsung ke nilai skalar yang disediakan oleh misalnya subquery?

Yah, SQL Server hanya bisa 'indeks mencari' menggunakan referensi skalar sederhana, misalnya referensi konstan, variabel, kolom, atau ekspresi (sehingga hasil fungsi skalar juga dapat memenuhi syarat). Subquery (atau konstruksi serupa lainnya) terlalu rumit (dan berpotensi tidak aman) untuk dimasukkan ke dalam keseluruhan mesin penyimpanan. Jadi, operator rencana kueri terpisah diperlukan. Ini pada gilirannya memerlukan korelasi, yang berarti tidak ada paralelisme seperti yang Anda inginkan.

Semua dalam semua, saat ini benar-benar tidak ada solusi yang lebih baik daripada metode seperti menetapkan nilai-nilai pencarian ke variabel dan kemudian menggunakan mereka dalam parameter fungsi dalam pernyataan terpisah.

Sekarang Anda mungkin memiliki pertimbangan lokal tertentu yang berarti menyimpan nilai saat ini dari tahun dan bulan SESSION_CONTEXTyang bermanfaat yaitu:

SELECT FGSD.calculated_number, COUNT_BIG(*)
FROM dbo.f_GetSharedData
(
    CONVERT(integer, SESSION_CONTEXT(N'experiment_year')), 
    CONVERT(integer, SESSION_CONTEXT(N'experiment_month'))
) AS FGSD
GROUP BY FGSD.calculated_number;

Tapi ini termasuk dalam kategori solusi.

Di sisi lain, jika kinerja agregasi adalah sangat penting, Anda dapat mempertimbangkan tetap dengan fungsi sebaris dan membuat indeks kolom toko (primer atau sekunder) di atas tabel. Anda mungkin menemukan manfaat dari penyimpanan kolom, pemrosesan mode batch, dan pushdown agregat memberikan manfaat yang lebih besar daripada mencari mode baris paralel.

Namun waspadalah terhadap fungsi skalar T-SQL, terutama dengan penyimpanan columnstore, karena mudah berakhir dengan fungsi yang dievaluasi per-baris dalam Filter mode baris terpisah. Secara umum cukup sulit untuk menjamin berapa kali SQL Server akan memilih untuk mengevaluasi skalar, dan lebih baik tidak mencoba.

Paul White 9
sumber
Terima kasih, Paul, jawaban yang bagus! Saya berpikir untuk menggunakan session_contexttetapi saya memutuskan bahwa itu adalah ide yang terlalu gila untuk saya dan saya tidak yakin bagaimana itu akan cocok dengan arsitektur saya saat ini. Apa yang akan berguna adalah, mungkin, beberapa petunjuk yang dapat saya gunakan untuk memberi tahu pengoptimal bahwa itu harus memperlakukan hasil dari subquery seperti referensi skalar sederhana.
Roman Pekar
8

Sejauh yang saya tahu bentuk rencana yang Anda inginkan tidak mungkin hanya dengan T-SQL. Sepertinya Anda menginginkan bentuk paket asli (kueri 0 paket) dengan subqueries dari fungsi Anda yang diterapkan sebagai filter langsung terhadap pemindaian indeks berkerumun. Anda tidak akan pernah mendapatkan rencana kueri seperti itu jika Anda tidak menggunakan variabel lokal untuk menyimpan nilai kembali fungsi skalar. Penyaringan sebagai gantinya akan diimplementasikan sebagai loop bersarang bergabung Ada tiga cara berbeda (dari sudut pandang paralelisme) agar loop join dapat diimplementasikan:

  1. Seluruh paket serial. Ini tidak dapat Anda terima. Ini adalah paket yang Anda dapatkan untuk kueri 1.
  2. Loop bergabung berjalan secara serial. Saya percaya dalam hal ini sisi dalam dapat berjalan secara paralel, tetapi tidak mungkin untuk menurunkan predikat ke sana. Jadi sebagian besar pekerjaan akan dilakukan secara paralel, tetapi Anda memindai seluruh tabel dan agregat parsial jauh lebih mahal daripada sebelumnya. Ini adalah paket yang Anda dapatkan untuk kueri 2.
  3. Loop bergabung berjalan secara paralel. Dengan loop bersarang paralel bergabung sisi dalam loop berjalan secara serial tetapi Anda dapat memiliki hingga benang DOP berjalan di sisi dalam sekaligus. Kumpulan hasil luar Anda hanya akan memiliki satu baris, sehingga paket paralel Anda akan menjadi serial secara efektif. Ini adalah paket yang Anda dapatkan untuk kueri 3.

Itulah satu-satunya bentuk rencana yang mungkin saya sadari. Anda bisa mendapatkan beberapa yang lain jika Anda menggunakan tabel temp tetapi tidak satupun dari mereka memecahkan masalah mendasar Anda jika Anda ingin kinerja permintaan sama baiknya dengan itu untuk permintaan 0.

Anda dapat mencapai kinerja permintaan yang setara dengan menggunakan skalar UDFs untuk menetapkan nilai kembali ke variabel lokal dan menggunakan variabel lokal tersebut dalam permintaan Anda. Anda dapat membungkus kode itu dalam prosedur tersimpan atau UDF multi-pernyataan untuk menghindari masalah pemeliharaan. Sebagai contoh:

DECLARE @experiment_year int = dbo.fn_GetExperimentYear(@session_id);
DECLARE @experiment_month int = dbo.fn_GetExperimentMonth(@session_id);

select
    calculated_number,
    count(*)
from dbo.f_GetSharedData(@experiment_year, @experiment_month)
group by
    calculated_number;

Skalar UDF telah dipindahkan di luar kueri yang Anda ingin memenuhi syarat untuk paralelisme. Paket kueri yang saya dapatkan tampaknya adalah yang Anda inginkan:

rencana kueri paralel

Kedua pendekatan memiliki kelemahan jika Anda perlu menggunakan hasil ini ditetapkan dalam permintaan lain. Anda tidak dapat langsung bergabung dengan prosedur tersimpan. Anda harus menyimpan hasilnya ke tabel temp yang memiliki masalah sendiri. Anda dapat bergabung dengan MS-TVF, tetapi dalam SQL Server 2016 Anda mungkin melihat masalah perkiraan kardinalitas. SQL Server 2017 menawarkan eksekusi interleaved untuk MS-TVF yang bisa menyelesaikan masalah sepenuhnya.

Hanya untuk menjernihkan beberapa hal: T-SQL Scalar UDFs selalu melarang paralelisme dan Microsoft belum mengatakan bahwa FROID akan tersedia di SQL Server 2017.

Joe Obbish
sumber
tentang Froid di SQL 2017 - tidak yakin mengapa saya pikir itu ada di sana. Ini dikonfirmasi berada di vNext - brentozar.com/archive/2018/01/...
Roman Pekar
4

Ini kemungkinan besar dapat dilakukan dengan menggunakan SQLCLR. Salah satu manfaat dari SQLCLR Scalar UDFs adalah mereka tidak mencegah paralelisme jika mereka tidak melakukan akses data apa pun (dan kadang-kadang perlu juga ditandai sebagai "deterministik"). Jadi, bagaimana Anda menggunakan sesuatu yang tidak memerlukan akses data ketika operasi itu sendiri membutuhkan akses data?

Yah, karena dbo.Paramsmeja diharapkan untuk:

  1. umumnya tidak pernah memiliki lebih dari 2000 baris di dalamnya,
  2. jarang mengubah struktur,
  3. hanya (saat ini) perlu memiliki dua INTkolom

layak untuk men-cache tiga kolom - session_id, experiment_year int, experiment_month- ke dalam koleksi statis (misalnya Kamus, mungkin) yang dihuni di luar proses dan dibaca oleh UDF Scalar yang mendapatkan experiment_year intdan experiment_monthnilai. Yang saya maksud dengan "out-of-process" adalah: Anda dapat memiliki SQLCLR Scalar UDF atau Stored Procedure yang benar-benar terpisah yang dapat melakukan akses data dan membaca dari dbo.Paramstabel untuk mengisi koleksi statis. UDF atau Prosedur Tersimpan itu akan dieksekusi sebelum menggunakan UDF yang mendapatkan nilai "tahun" dan "bulan", sehingga UDF yang mendapatkan nilai "tahun" dan "bulan" tidak melakukan akses data DB apa pun.

UDF atau Stored Procedure yang membaca data dapat memeriksa terlebih dahulu untuk melihat apakah koleksi memiliki 0 entri dan jika demikian, populasikan, atau lewati. Anda bahkan dapat melacak waktu yang dihuni dan jika sudah lebih dari X menit (atau sesuatu seperti itu), lalu hapus dan isi kembali bahkan jika ada entri dalam koleksi. Tetapi melewatkan populasi akan membantu karena akan perlu sering dieksekusi untuk memastikan bahwa itu selalu dihuni dua UDF utama untuk mendapatkan nilai dari.

Perhatian utama adalah ketika SQL Server memutuskan untuk membongkar App Domain untuk alasan apa pun (atau dipicu oleh sesuatu yang menggunakan DBCC FREESYSTEMCACHE('ALL');). Anda tidak ingin mengambil risiko bahwa koleksi akan dihapus antara pelaksanaan UDF "isi" atau Prosedur Tersimpan dan UDF untuk mendapatkan nilai "tahun" dan "bulan". Dalam hal ini Anda dapat memiliki pemeriksaan di awal kedua UDF untuk mengeluarkan pengecualian jika koleksi kosong, karena lebih baik kesalahan daripada berhasil memberikan hasil yang salah.

Tentu saja, keprihatinan yang disebutkan di atas mengasumsikan bahwa keinginan untuk memiliki Majelis ditandai SAFE. Jika Majelis dapat ditandai sebagai EXTERNAL_ACCESS, maka mungkin untuk memiliki konstruktor statis menjalankan metode yang membaca data dan mengisi koleksi, sehingga Anda hanya perlu secara manual mengeksekusi itu untuk menyegarkan baris, tetapi mereka akan selalu diisi (karena konstruktor kelas statis selalu berjalan ketika kelas dimuat, yang terjadi setiap kali metode di kelas ini dieksekusi setelah restart atau Domain App diturunkan). Ini memerlukan penggunaan koneksi reguler dan bukan Koneksi Konteks dalam proses (yang tidak tersedia untuk konstruktor statis, karenanya perlu EXTERNAL_ACCESS).

Harap dicatat: agar tidak diharuskan untuk menandai Majelis sebagai UNSAFE, Anda harus menandai variabel kelas statis apa pun sebagai readonly. Ini berarti, paling tidak, koleksi. Ini bukan masalah karena koleksi read-only dapat memiliki item ditambahkan atau dihapus dari mereka, mereka hanya tidak dapat diinisialisasi di luar konstruktor atau memuat awal. Melacak waktu pengumpulan dimuat dengan tujuan untuk kedaluwarsa setelah menit X lebih sulit karena static readonly DateTimevariabel kelas tidak dapat diubah di luar konstruktor atau beban awal. Untuk mengatasi pembatasan ini, Anda perlu menggunakan koleksi statis, hanya baca yang berisi satu item yang DateTimebernilai sehingga dapat dihapus dan ditambahkan kembali saat refresh.

Solomon Rutzky
sumber
Tidak tahu mengapa seseorang menurunkan ini. Meskipun tidak terlalu umum, saya pikir itu bisa diterapkan dalam kasus saya saat ini. Saya lebih suka memiliki solusi SQL murni, tapi saya pasti akan lebih dekat melihat ini dan mencoba untuk melihat apakah berfungsi
Roman Pekar
@RomanPekar Tidak yakin, tetapi ada banyak orang di luar sana yang anti-SQLCLR. Dan mungkin beberapa yang anti-saya ;-). Either way, saya tidak bisa memikirkan mengapa solusi ini tidak berhasil. Saya mengerti preferensi untuk T-SQL murni, tetapi saya tidak tahu bagaimana mewujudkannya, dan jika tidak ada jawaban yang bersaing, maka mungkin tidak ada orang lain yang melakukannya. Saya tidak tahu apakah tabel yang dioptimalkan memori dan UDF yang dikompilasi secara alami akan lebih baik di sini. Juga, saya baru saja menambahkan paragraf dengan beberapa catatan implementasi yang perlu diingat.
Solomon Rutzky
1
Saya tidak pernah sepenuhnya yakin bahwa menggunakan readonly staticsaman atau bijak dalam SQLCLR. Apalagi saya yakin tentang kemudian menipu sistem dengan membuat readonlyjenis referensi, yang kemudian Anda pergi dan ubah . Memberi saya tekad mutlak tbh.
Paul White 9
@PaulWhite Dipahami, dan saya ingat ini muncul dalam percakapan pribadi bertahun-tahun yang lalu. Mengingat sifat bersama dari App Domains (dan karenanya staticobjek) di SQL Server, ya, ada risiko untuk kondisi balapan. Itulah mengapa saya pertama kali memutuskan dari OP bahwa data ini minimal dan stabil, dan mengapa saya memenuhi syarat pendekatan ini karena memerlukan "jarang berubah", dan memberikan cara menyegarkan ketika dibutuhkan. Dalam hal ini kasus penggunaan saya tidak melihat banyak jika resiko apapun. Saya menemukan posting tahun yang lalu tentang kemampuan untuk memperbarui koleksi hanya baca sebagai desain (dalam C #, tidak ada diskusi kembali: SQLCLR). Akan berusaha menemukannya.
Solomon Rutzky
2
Tidak perlu, tidak ada cara Anda akan membuat saya nyaman dengan ini selain dari dokumentasi SQL Server resmi mengatakan itu baik-baik saja, yang saya cukup yakin tidak ada.
Paul White 9