Mengapa tabel turunan ini meningkatkan kinerja?

18

Saya punya permintaan yang menggunakan string json sebagai parameter. Json adalah array pasangan garis lintang dan bujur. Contoh input mungkin sebagai berikut.

declare @json nvarchar(max)= N'[[40.7592024,-73.9771259],[40.7126492,-74.0120867]
,[41.8662374,-87.6908788],[37.784873,-122.4056546]]';

Ini memanggil TVF yang menghitung jumlah POI di sekitar titik geografis, pada jarak 1,3,5,10 mil.

create or alter function [dbo].[fn_poi_in_dist](@geo geography)
returns table
with schemabinding as
return 
select count_1  = sum(iif(LatLong.STDistance(@geo) <= 1609.344e * 1,1,0e))
      ,count_3  = sum(iif(LatLong.STDistance(@geo) <= 1609.344e * 3,1,0e))
      ,count_5  = sum(iif(LatLong.STDistance(@geo) <= 1609.344e * 5,1,0e))
      ,count_10 = count(*)
from dbo.point_of_interest
where LatLong.STDistance(@geo) <= 1609.344e * 10

Maksud dari permintaan json adalah untuk memanggil fungsi ini secara massal. Jika saya menyebutnya seperti ini kinerjanya sangat buruk, membutuhkan waktu hampir 10 detik hanya untuk 4 poin:

select row=[key]
      ,count_1
      ,count_3
      ,count_5
      ,count_10
from openjson(@json)
cross apply dbo.fn_poi_in_dist(
            geography::Point(
                convert(float,json_value(value,'$[0]'))
               ,convert(float,json_value(value,'$[1]'))
               ,4326))

plan = https://www.brentozar.com/pastetheplan/?id=HJDCYd_o4

Namun, memindahkan konstruksi geografi di dalam tabel turunan menyebabkan kinerja meningkat secara dramatis, menyelesaikan kueri dalam waktu sekitar 1 detik.

select row=[key]
      ,count_1
      ,count_3
      ,count_5
      ,count_10
from (
select [key]
      ,geo = geography::Point(
                convert(float,json_value(value,'$[0]'))
               ,convert(float,json_value(value,'$[1]'))
               ,4326)
from openjson(@json)
) a
cross apply dbo.fn_poi_in_dist(geo)

plan = https://www.brentozar.com/pastetheplan/?id=HkSS5_OoE

Rencana terlihat hampir identik. Tidak menggunakan paralelisme dan keduanya menggunakan indeks spasial. Ada gulungan malas tambahan pada rencana lambat yang bisa saya hilangkan dengan petunjuk option(no_performance_spool). Namun kinerja kueri tidak berubah. Masih jauh lebih lambat.

Menjalankan keduanya dengan petunjuk tambahan dalam satu batch akan menimbang kedua kueri secara merata.

Sql server versi = Microsoft SQL Server 2016 (SP1-CU7-GDR) (KB4057119) - 13.0.4466.4 (X64)

Jadi pertanyaan saya adalah mengapa ini penting? Bagaimana saya bisa tahu kapan saya harus menghitung nilai di dalam tabel turunan atau tidak?

Michael B
sumber
1
Maksud "menimbang", maksud Anda estimasi biaya%? Angka itu sebenarnya tidak ada artinya, terutama ketika Anda membawa UDF, JSON, CLR melalui geografi, dll.
Aaron Bertrand
Saya sadar, tetapi melihat statistik IO mereka juga identik. Keduanya melakukan 358306 bacaan logis di atas point_of_interestmeja, keduanya memindai indeks 4602 kali, dan keduanya menghasilkan tabel kerja dan workfile. Estimator percaya bahwa rencana ini identik namun kinerja mengatakan sebaliknya.
Michael B
Sepertinya CPU yang sebenarnya adalah masalah di sini, kemungkinan karena apa yang Martin tunjukkan, bukan I / O. Sayangnya perkiraan biaya didasarkan pada CPU dan I / O digabungkan dan tidak selalu mencerminkan apa yang terjadi dalam kenyataan. Jika Anda membuat rencana aktual menggunakan SentryOne Plan Explorer ( saya bekerja di sana, tetapi alat ini gratis tanpa ikatan ), maka ubah biaya aktual menjadi CPU saja, Anda mungkin mendapatkan indikator yang lebih baik tentang di mana semua waktu CPU dihabiskan.
Aaron Bertrand
1
@MartinSmith Belum per operator, no. Kami memunculkan mereka yang ada di level pernyataan. Saat ini kami masih mengandalkan implementasi awal dari DMV sebelum metrik tambahan ditambahkan di tingkat bawah. Dan kami agak sibuk mengerjakan hal lain yang akan segera Anda lihat. :-)
Aaron Bertrand
1
NB. Anda mungkin mendapatkan lebih banyak peningkatan kinerja dengan melakukan kotak aritmatika sederhana sebelum melakukan perhitungan jarak garis lurus. Artinya, filter dulu untuk yang nilainya |LatLong.Lat - @geo.Lat| + |LatLong.Long - @geo.Long| < nsebelum Anda lakukan semakin rumit sqrt((LatLong.Lat - @geo.Lat)^2 + (LatLong.Long - @geo.Long)^2). Dan yang lebih baik, hitung batas atas dan bawah terlebih dahulu LatLong.Lat > @geoLatLowerBound && LatLong.Lat < @geoLatUpperBound && LatLong.Long > @geoLongLowerBound && LatLong.Long < @geoLongUpperBound. (Ini adalah kodesemu, adaptasi dengan tepat.)
ErikE

Jawaban:

15

Saya dapat memberi Anda jawaban parsial yang menjelaskan mengapa Anda melihat perbedaan kinerja - meskipun itu masih menyisakan beberapa pertanyaan terbuka (seperti bisakah SQL Server menghasilkan rencana yang lebih optimal tanpa memperkenalkan ekspresi tabel perantara yang memproyeksikan ekspresi sebagai kolom?)


Perbedaannya adalah bahwa dalam rencana cepat, pekerjaan yang diperlukan untuk mem-parsing elemen array JSON dan membuat Geografi dilakukan 4 kali (satu kali untuk setiap baris yang dipancarkan dari openjsonfungsi) - sedangkan itu dilakukan lebih dari 100.000 kali dalam rencana lambat.

Dalam rencana cepat ...

geography::Point(
                convert(float,json_value(value,'$[0]'))
               ,convert(float,json_value(value,'$[1]'))
               ,4326)

Ditugaskan ke Expr1000dalam scalar komputasi di sebelah kiri openjsonfungsi. Ini sesuai dengan geodefinisi tabel turunan Anda.

masukkan deskripsi gambar di sini

Dalam rencana cepat, filter dan streaming referensi agregat Expr1000. Dalam rencana yang lambat, mereka merujuk ekspresi penuh yang mendasarinya.

Streaming properti agregat

masukkan deskripsi gambar di sini

Filter dijalankan 116.995 kali dengan setiap eksekusi membutuhkan evaluasi ekspresi. Agregat aliran memiliki 110.520 baris yang mengalir ke dalamnya untuk agregasi dan membuat tiga agregat terpisah menggunakan ungkapan ini. 110,520 * 3 + 116,995 = 448,555. Bahkan jika setiap evaluasi individu membutuhkan 18 mikrodetik, ini menambahkan hingga 8 detik waktu tambahan untuk kueri secara keseluruhan.

Anda dapat melihat efeknya dalam statistik waktu aktual dalam paket XML (dijelaskan dalam warna merah di bawah dari paket lambat dan biru untuk paket cepat - waktu ada dalam ms)

masukkan deskripsi gambar di sini

Agregat aliran memiliki waktu yang berlalu 6,209 detik lebih besar dari anak langsungnya. Dan sebagian besar waktu anak diambil oleh filter. Ini sesuai dengan evaluasi ekspresi ekstra.


Ngomong-ngomong .... Secara umum itu bukan hal yang pasti bahwa ekspresi yang mendasari dengan label seperti Expr1000hanya dihitung sekali dan tidak dievaluasi kembali tetapi jelas dalam hal ini dari perbedaan waktu eksekusi ini terjadi di sini.

Martin Smith
sumber
Sebagai tambahan, jika saya mengganti kueri untuk menggunakan tanda silang berlaku untuk menghasilkan geografi, saya juga mendapatkan rencana cepat. cross apply(select geo=geography::Point( convert(float,json_value(value,'$[0]')) ,convert(float,json_value(value,'$[1]')) ,4326))f
Michael B
Sayangnya, tetapi saya bertanya-tanya apakah ada cara yang lebih mudah untuk mendapatkannya menghasilkan rencana cepat.
Michael B
Maaf untuk pertanyaan amatir, tetapi alat apa yang ditunjukkan pada gambar Anda?
BlueRaja - Danny Pflughoeft
1
@ BlueRaja-DannyPflughoeft ini adalah rencana pelaksanaan yang ditampilkan di studio manajemen (ikon yang digunakan dalam SSMS telah diperbarui dalam versi terbaru jika itu adalah alasan untuk pertanyaan)
Martin Smith