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
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
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
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
Tetapi saya tidak dapat menemukan cara untuk menulis kueri ini sebaik yang menggunakan fungsi skalar.
Beberapa pemikiran:
- 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.
- 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.
- 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_id
akan 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.
sumber
Jawaban:
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:
Loop bersarang berkorelasi bergabung, dengan round-robin mendistribusikan stream di tingkat atas. Mengingat bahwa satu baris dijamin berasal dari nilai
Params
tertentusession_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.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_CONTEXT
yang bermanfaat yaitu: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.
sumber
session_context
tetapi 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.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:
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:
Skalar UDF telah dipindahkan di luar kueri yang Anda ingin memenuhi syarat untuk paralelisme. Paket kueri yang saya dapatkan tampaknya adalah yang Anda inginkan:
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.
sumber
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.Params
meja diharapkan untuk:INT
kolomlayak 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 mendapatkanexperiment_year int
danexperiment_month
nilai. 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 daridbo.Params
tabel 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 sebagaiEXTERNAL_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 perluEXTERNAL_ACCESS
).Harap dicatat: agar tidak diharuskan untuk menandai Majelis sebagai
UNSAFE
, Anda harus menandai variabel kelas statis apa pun sebagaireadonly
. 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 karenastatic readonly DateTime
variabel 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 yangDateTime
bernilai sehingga dapat dihapus dan ditambahkan kembali saat refresh.sumber
readonly statics
aman atau bijak dalam SQLCLR. Apalagi saya yakin tentang kemudian menipu sistem dengan membuatreadonly
jenis referensi, yang kemudian Anda pergi dan ubah . Memberi saya tekad mutlak tbh.static
objek) 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.