Kullanıcı tanımlı skaler işlevi paralelliği engellemeyecek şekilde taklit


12

Sorgu için belirli bir planı kullanmak için SQL Server kandırmak için bir yol olup olmadığını görmeye çalışıyorum.

1. Çevre

Farklı süreçler arasında paylaşılan bazı verileriniz olduğunu düşünün. Diyelim ki çok yer kaplayan bazı deney sonuçlarımız var. Ardından, her işlem için hangi yıl / ay deneme sonucunu kullanmak istediğimizi biliyoruz.

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

Şimdi, her işlem için tabloya kaydedilmiş parametrelerimiz var

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. Test verileri

Biraz test verisi ekleyelim:

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. Sonuçlar getiriliyor

Şimdi, aşağıdakilerle deneme sonuçları almak çok kolay @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

Plan güzel ve paralel:

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

sorgu 0 planı

resim açıklamasını buraya girin

4. Sorun

Ancak, verilerin kullanımını biraz daha genel hale getirmek için başka bir işleve sahip olmak istiyorum dbo.f_GetSharedDataBySession(@session_id int). Yani, çevirmenin basit bir yolu, skaler fonksiyonlar oluşturmaktır @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

Ve şimdi fonksiyonumuzu yaratabiliriz:

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

sorgu 1 planı

resim açıklamasını buraya girin

Veri erişimi gerçekleştiren skaler fonksiyonlar tüm planı seri hale getirdiğinden, plan elbette paralel olmadığı dışında aynıdır .

Bu nedenle, skaler fonksiyonlar yerine alt sorguları kullanmak gibi birkaç farklı yaklaşım denedim:

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

sorgu 2 planı

resim açıklamasını buraya girin

Veya kullanarak 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

sorgu 3 planı

resim açıklamasını buraya girin

Ama bu sorguyu skaler fonksiyonları kullanan kadar iyi olmak için bir yol bulamıyorum.

Birkaç düşünce:

  1. Temelde ne istiyorum bir şekilde SQL Server belirli değerleri önceden hesaplamak ve sonra sabit olarak daha fazla iletmek için söylemek mümkün olmaktır.
  2. Yararlı olabilecek şey, bazı ara materyalizasyon ipucumuz olması. Birkaç varyantı kontrol ettim (çoklu ifade TVF veya üstte cte), ancak şimdiye kadar skaler fonksiyonları olan plan kadar iyi değil
  3. SQL Server 2017 - Froid: İlişkisel Veritabanında Zorunlu Programların Optimizasyonu'nun geleceğini biliyorum . Yine de yardımcı olacağından emin değilim. Yine de burada yanlış ispatlanmış olmak güzel olurdu.

Ek bilgi

Genellikle @session_idparametre olarak sahip birçok farklı sorgularda kullanmak çok daha kolay olduğu için (doğrudan tablolardan veri seçmek yerine) bir işlev kullanıyorum .

Gerçek yürütme sürelerini karşılaştırmam istendi. Bu özel durumda

  • 0 sorgusu ~ 500 ms boyunca çalışır
  • sorgu 1 ~ 1500ms için çalışır
  • sorgu 2 ~ 1500ms çalışır
  • sorgu 3 ~ 2000ms için çalışır.

Plan # 2, arama yerine bir indeks taramasına sahiptir ve daha sonra iç içe döngülerdeki tahminlerle filtrelenir. Plan # 3 o kadar da kötü değil, ama yine de daha fazla iş yapıyor ve # 0 planını daha yavaş çalışıyor.

Diyelim ki dbo.Paramsnadiren değişti ve genellikle yaklaşık 1-200 sıra var, daha fazla değil, diyelim ki 2000 bekleniyor. Şimdi 10 sütun civarında ve çok sık sütun eklemeyi beklemiyorum.

Params'daki satır sayısı sabit değildir, bu nedenle her biri @session_idiçin bir satır olacaktır. Orada sütun sayısı sabit değil dbo.f_GetSharedData(@experiment_year int, @experiment_month int), her yerden aramak istemem nedenlerinden biri, bu nedenle bu sorguya dahili olarak yeni sütun ekleyebilirim. Bazı kısıtlamaları olsa bile, bu konuda herhangi bir görüş / öneri duymaktan memnuniyet duyarım.


Froid ile sorgu planı yukarıdaki sorgu2 ile benzer olacaktır, bu yüzden evet, sizi bu durumda elde etmek istediğiniz çözüme götürmez.
Karthik

Yanıtlar:


13

Gerçekten SQL Server'da tam olarak ne istediğinizi, yani tek bir ifadede ve paralel yürütmeyle, soruda belirtilen kısıtlamalar içinde (onları algıladığım gibi) gerçekten güvenli bir şekilde elde edemezsiniz.

Yani basit cevabım hayır . Bu cevabın geri kalanı çoğunlukla bunun neden ilgilendiğini tartışıyor.

Soruda belirtildiği gibi paralel bir plan elde etmek mümkündür, ancak ikisi de ihtiyaçlarınız için uygun olmayan iki ana çeşit vardır:

  1. İlişkili bir iç içe ilmekler, akışları en üst düzeyde bir yuvarlak-robin dağıtımı ile birleştirir. ParamsBelirli bir session_iddeğer için tek bir satırın geleceği garanti edilirse, iç taraf paralellik simgesiyle işaretlenmiş olsa bile tek bir iş parçacığında çalışacaktır. Bu yüzden görünüşte paralel plan 3 de iyi performans göstermemektedir; aslında seri.

  2. Diğer alternatif, iç içe ilmeklerin birleştiği iç taraftaki bağımsız paralellik içindir. Burada bağımsız , dişlerin iç tarafta başlatıldığı anlamına gelir ve iç içe ilmeklerin birleştiği dış kenarı yürütmekle aynı iplik (ler) değil. SQL Server, yalnızca bir dış taraf sırası olması garanti edildiğinde ve ilişkili birleştirme parametreleri olmadığında bağımsız iç taraf iç içe döngüler paralelliğini destekler ( plan 2 ).

Yani, istenen korelasyon değerleriyle seri (bir iş parçacığı nedeniyle) olan paralel bir plan seçeneğimiz var; veya taranması gereken bir iç taraf paralel planı, çünkü aranacak hiçbir parametresi yoktur. (Kenara: Gerçekten gerektiğini kullanılarak iç tarafı paralellik götürmek için izin verilmesi için tam bir korelasyon parametreleri kümesi, ama muhtemelen iyi bir neden için, hiçbir zaman uygulanmamıştır).

Öyleyse doğal bir soru şudur: neden ilişkili parametrelere ihtiyacımız var? SQL Server neden doğrudan bir alt sorgu tarafından sağlanan skaler değerlerini doğrudan arayamıyor?

SQL Server yalnızca sabit, değişken, sütun veya ifade referansı gibi basit skaler referanslar kullanarak 'indeks arama' yapabilir (böylece skaler fonksiyon sonucu da yeterlidir). Bir alt sorgu (veya başka bir benzer yapı), depolama motoru bütününe itmek için çok karmaşıktır (ve potansiyel olarak güvenli değildir). Bu nedenle, ayrı sorgu planı operatörleri gereklidir. Bu sırayla korelasyon gerektirir, yani istediğiniz türde paralellik yoktur.

Sonuç olarak, şu anda değişkenlere arama değerlerini atama ve daha sonra ayrı bir ifadede işlev parametrelerindeki değerleri kullanma gibi yöntemlerden daha iyi bir çözüm yoktur.

Şimdi, yılın ve ayın mevcut değerlerinin önbelleğe SESSION_CONTEXTalınmasının değerli olduğu anlamına gelen belirli yerel düşünceleriniz olabilir :

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;

Ancak bu geçici çözüm kategorisine girer.

Öte yandan, toplama performansı birincil öneme sahipse, satır içi işlevlere bağlı kalmayı ve tabloda sütun sütunu dizini (birincil veya ikincil) oluşturmayı düşünebilirsiniz. Sütun deposu depolama, toplu iş işleme ve toplam aşağı itme avantajlarının, satır modu paralel aramasından yine de daha fazla fayda sağladığını görebilirsiniz.

Ancak, ayrı bir satır modu filtresinde satır başına değerlendirilen işlevin sonlandırılması kolay olduğundan, özellikle sütun deposu depolamasında skaler T-SQL işlevlerine dikkat edin. SQL Server'ın skaleri değerlendirmeyi kaç kez seçeceğini ve denememek daha iyi olur.


Teşekkürler Paul, harika cevap! Kullanmayı düşündüm session_contextama benim için biraz çılgınca bir fikir olduğuna karar verdim ve mevcut mimarime nasıl uyacağından emin değilim. Ancak, yararlı olacak şey, optimize edicinin, alt sorgunun sonucunu basit bir skaler referans gibi ele alması gerektiğini bildirmek için kullanabileceğim bazı ipuçları olabilir.
Roman Pekar

8

Bildiğim kadarıyla istediğiniz plan şekli sadece T-SQL ile mümkün değil. İşlevlerinizden alınan alt sorguların doğrudan kümelenmiş dizin taramasına karşı filtreler olarak uygulandığı orijinal plan şeklini (sorgu 0 planı) istediğiniz gibi görünüyor. Skaler fonksiyonların dönüş değerlerini tutmak için yerel değişkenleri kullanmazsanız asla böyle bir sorgu planı elde edemezsiniz. Filtreleme bunun yerine iç içe bir döngü birleştirme olarak uygulanacaktır. Döngü birleştirmenin uygulanabilmesinin üç farklı yolu (paralellik açısından) vardır:

  1. Planın tamamı seri. Bu sizin için kabul edilemez. Bu sorgu 1 için aldığınız plan.
  2. Döngü birleşimi seri olarak çalışır. Bu durumda iç tarafın paralel olarak çalışabileceğine inanıyorum, ancak herhangi bir tahminde bulunmak mümkün değil. Bu yüzden işin çoğu paralel olarak yapılacak, ancak tüm tabloyu tarıyorsunuz ve kısmi agrega öncekinden çok daha pahalı. Sorgu 2 için alacağınız plan budur.
  3. Döngü birleşimi paralel olarak çalışır. Paralel iç içe ilmek birleştirildiğinde, ilmek iç tarafı seri olarak çalışır ancak iç tarafta aynı anda çalışan en fazla DOP dişine sahip olabilirsiniz. Dış sonuç kümenizde yalnızca tek bir satır olacaktır, bu nedenle paralel planınız etkili bir şekilde seri olacaktır. Sorgu 3 için aldığınız plan budur.

Bunlar farkında olduğum tek plan şekilleri. Bir geçici tablo kullanırsanız bazılarını alabilirsiniz, ancak sorgu performansının sorgu 0 için olduğu kadar iyi olmasını istiyorsanız hiçbiri temel sorununuzu çözmez.

Yerel değişkenlere dönüş değerleri atamak için skaler UDF'leri kullanarak ve sorgunuzdaki bu yerel değişkenleri kullanarak eşdeğer sorgu performansı elde edebilirsiniz. Sürdürülebilirlik sorunlarından kaçınmak için bu kodu saklı bir yordama veya çok ifadeli bir UDF'ye sarabilirsiniz. Örneğin:

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;

Skaler UDF'ler, paralellik için uygun olmasını istediğiniz sorgunun dışına taşınmıştır. Aldığım sorgu planı istediğiniz gibi görünüyor:

paralel sorgu planı

Bu sonuç kümesini başka sorgularda kullanmanız gerekiyorsa her iki yaklaşımın dezavantajları vardır. Kayıtlı bir prosedüre doğrudan katılamazsınız. Sonuçları kendi sorunları olan geçici bir tabloya kaydetmeniz gerekir. Bir MS-TVF'ye katılabilirsiniz, ancak SQL Server 2016'da kardinalite tahmini sorunları görebilirsiniz. SQL Server 2017 , sorunu tamamen çözebilecek MS-TVF için serpiştirilmiş yürütme sunar .

Sadece birkaç şeyi temizlemek için: T-SQL Skaler UDF'ler her zaman paralelliği yasaklar ve Microsoft FROID'in SQL Server 2017'de mevcut olacağını söylemedi.


2017'de Froid ile ilgili - neden orada olduğunu düşündüğümden emin değilim. VNext'te
Roman Pekar

4

Bu büyük olasılıkla SQLCLR kullanılarak yapılabilir. SQLCLR Skaler UDF biri yararı da paralellik engel kalmamasıdır eğer onlar do not herhangi bir veri erişimi yapmak (ve bazen de "deterministik" olarak işaretlenmesi gerekir). İşlemin kendisi veri erişimi gerektirdiğinde veri erişimi gerektirmeyen bir şeyden nasıl faydalanırsınız?

Eh, çünkü dbo.Paramstablo bekleniyor:

  1. genellikle hiçbir zaman 2000'den fazla satır içermez,
  2. nadiren yapıyı değiştirir,
  3. yalnızca (şu anda) iki INTsütun olması gerekir

üç sütunu önbelleğe almak mümkündür - session_id, experiment_year int, experiment_month- işlem dışında doldurulmuş ve experiment_year intve experiment_monthdeğerlerini alan Skaler UDF'ler tarafından okunan statik bir koleksiyona (örneğin bir Sözlük) . "İşlem dışı" ile kastediyorum: tamamen ayrı bir SQLCLR Skaler UDF veya dbo.Paramsstatik toplama doldurmak için tablodan veri erişimi ve okuma yapabilirsiniz Saklı Yordam olabilir . Bu UDF veya Saklı Yordam, "yıl" ve "ay" değerlerini alan UDF'leri kullanmadan önce yürütülür, böylece "yıl" ve "ay" değerlerini alan UDF'ler herhangi bir DB veri erişimi yapmaz.

Verileri okuyan UDF veya Saklı Yordam önce koleksiyonda 0 giriş olup olmadığını kontrol edebilir ve varsa doldurun, sonra atlayın. Hatta, doldurulduğu zamanı ve X dakikadan uzun süre (veya bunun gibi bir şey) geçtiyse, koleksiyonda girişler olsa bile temizleyip yeniden doldurun. Ancak, nüfusu atlamak yardımcı olacaktır çünkü iki ana UDF'nin değerleri alması için her zaman doldurulmasını sağlamak için sık sık yürütülmesi gerekecektir.

Ana endişe, SQL Server'ın herhangi bir nedenle App Domain'i kaldırmaya karar vermesidir (veya kullanan bir şey tarafından tetiklenir DBCC FREESYSTEMCACHE('ALL');). "Nüfus" UDF veya Saklı Yordamın yürütülmesi ile UDF'ler arasında "yıl" ve "ay" değerlerini elde etmek için koleksiyonun temizlenmesini istemezsiniz. Bu durumda, koleksiyon boşsa bir istisna atmak için bu iki UDF'nin en başında bir kontrol yapabilirsiniz, çünkü hata yapmak başarılı bir şekilde yanlış sonuçlar vermekten daha iyidir.

Tabii ki, yukarıda belirtilen endişe, Meclisin işaretlenmesini arzu ettiğini varsayar SAFE. Derleme olarak işaretlenebilirse EXTERNAL_ACCESS, verileri okuyan ve koleksiyonu dolduran yöntemi statik bir kurucu çalıştırmak mümkündür, böylece satırları yenilemek için yalnızca el ile yürütmeniz gerekir, ancak her zaman doldurulur (statik sınıf yapıcısı sınıf yüklendiğinde her zaman çalıştığından, bu sınıftaki bir yöntem her yeniden başlatma veya Uygulama Etki Alanı kaldırıldıktan sonra yürütüldüğünde gerçekleşir). Bunun için işlem sırasında Bağlam Bağlantısı (statik kurucular tarafından kullanılamadığı için ihtiyaç duyulduğu için EXTERNAL_ACCESS) normal bir bağlantı kullanılması gerekir .

Lütfen dikkat: Montajı işaretlemek zorunda kalmamak için UNSAFEstatik sınıf değişkenlerini olarak işaretlemeniz gerekir readonly. Bu, en azından koleksiyon anlamına gelir. Salt okunur koleksiyonlara öğeler eklenmiş veya onlardan kaldırılmış öğeler olabileceğinden, bu sorun oluşturmaz, yalnızca kurucu veya ilk yük dışında başlatılamaz. Bir static readonly DateTimesınıf değişkeni yapıcı veya ilk yükün dışında değiştirilemediğinden , koleksiyonun X dakikadan sonra sona ermek amacıyla yüklendiği zamanı izlemek daha zordur . Bu kısıtlamayı aşmak için, DateTimebir yenileme sonrasında kaldırılabilmesi ve yeniden eklenebilmesi için tek bir öğe içeren statik, salt okunur bir koleksiyon kullanmanız gerekir .


Birinin bunu neden küçümsediğini bilmiyorum. Çok genel olmasa da, mevcut durumumda geçerli olabileceğini düşünüyorum. Saf SQL çözümüne sahip olmayı tercih ederim, ama kesinlikle buna daha yakından bakacağım ve işe
yarayıp

@RomanPekar Emin değilim, ancak SQLCLR karşıtı olan çok sayıda insan var. Ve belki de bana karşı olan birkaç kişi ;-). Her iki durumda da, bu çözümün neden işe yaramayacağını düşünemiyorum. Saf T-SQL tercihini anlıyorum, ama bunu nasıl yapacağımı bilmiyorum ve eğer rakip bir cevap yoksa, belki de başka hiç kimse yapmaz. Bellek için optimize edilmiş tabloların ve yerel olarak derlenmiş UDF'lerin burada daha iyi olup olmayacağını bilmiyorum. Ayrıca, akılda tutmak için bazı uygulama notları içeren bir paragraf ekledim.
Solomon Rutzky

1
readonly staticsSQLCLR'de kullanımın güvenli veya akıllıca olduğuna asla tam olarak ikna olmadım . Çok daha azı readonly, daha sonra gittiğiniz ve değiştirdiğiniz bir referans türü yaparak sistemi kandırmaya ikna oldum . Bana mutlak willies tbh verir.
Paul White 9

@PaulWhite Anladım ve bunun yıllar önce özel görüşmede ortaya çıktığını hatırlıyorum. staticSQL Server'daki Uygulama Etki Alanları'nın (ve dolayısıyla nesnelerin) paylaşılan yapısı göz önüne alındığında , evet, yarış koşulları için risk vardır. Bu yüzden OP'den bu verilerin minimum ve kararlı olduğunu ilk kez belirledim ve bu yaklaşımı neden "nadiren değişme" gerektirdiğini nitelendirdim ve gerektiğinde yenilenme aracı sağladım. Gelen bu kullanım durumunda herhangi riske eğer çok görmüyorum. Yıllar önce, salt okunur koleksiyonları tasarım gereği güncelleme yeteneği hakkında bir yazı buldum (C # 'da, tartışma yok: SQLCLR). Onu bulmaya çalışacağım.
Solomon Rutzky

2
Gerek yok, beni resmi SQL Server belgelerinin yanı sıra, bunun rahat olduğundan emin olmanın hiçbir yolu yok, ki eminim ki yok.
Paul White 9
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.