Kullanıcı tanımlı işlev ile optimizasyon sorunu


26

SQL Server'ın neden tablodaki her değer için kullanıcı tanımlı işlevi çağırmaya karar verdiğini anlama konusunda bir sorunum var; Gerçek SQL çok daha karmaşık, ancak sorunu bu şekilde azaltabildim:

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

Bu sorgu için, SQL Server, ORDERLINE'dan döndürülen tahmini ve gerçek satır sayısı 1 olsa bile, PRODUCT Tablosunda bulunan her bir değer için GetGroupCode işlevini çağırmaya karar verir.

Sorgu Planı

Satır sayımlarını gösteren plan gezgini ile aynı plan:

Gezgini planla Tablolar:

ORDERLINE: 1.5M rows, primary key: ORDERNUMBER + ORDERLINE + RMPHASE (clustered)
ORDERHDR:  900k rows, primary key: ORDERID (clustered)
PRODUCT:   6655 rows, primary key: PRODUCT (clustered)

Tarama için kullanılan dizin:

create unique nonclustered index PRODUCT_FACTORY on PRODUCT (PRODUCT, FACTORY)

İşlev aslında biraz daha karmaşıktır, ancak aynı şey bunun gibi kukla bir çoklu ifade işlevi ile de olur:

create function GetGroupCode (@FACTORY varchar(4))
returns @t table(
    TYPE        varchar(8),
    GROUPCODE   varchar(30)
)
as begin
    insert into @t (TYPE, GROUPCODE) values ('XX', 'YY')
    return
end

SQL sunucusunu ilk 1 ürünü almaya zorlayarak performansı "düzeltebildim";

select  
    S.GROUPCODE,
    H.ORDERCAT
from    
    ORDERLINE L
    join ORDERHDR H
        on H.ORDERID = M.ORDERID
    cross apply (select top 1 P.FACTORY from PRODUCT P where P.PRODUCT = L.PRODUCT) P
    cross apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

O zaman plan şekli aynı zamanda başlangıçta olmasını beklediğim bir şey olarak değişir:

Top ile Sorgu Planı

Ayrıca PRODUCT_FACTORY indeksinin, PRODUCT_PK kümelenmiş indeksinden daha küçük olması bir etki yaratacaktır, ancak PRODUCT_PK'yi kullanmak için sorguyu zorlamakla birlikte, plan hala 6655 işlev çağrılarıyla aynıdır.

ORDERHDR'yi tamamen terk edersem, plan ilk önce ORDERLINE ve PRODUCT arasında iç içe döngüle başlar ve işlev yalnızca bir kez çağrılır.

Tüm işlemler birincil anahtarlar kullanılarak yapıldığından ve bunun kolayca çözülemeyen daha karmaşık bir sorguda gerçekleşirse nasıl düzeltileceği için bunun nedeninin ne olabileceğini anlamak isterim.

Düzenleme: Tablo ifadeleri oluşturun:

CREATE TABLE dbo.ORDERHDR(
    ORDERID varchar(8) NOT NULL,
    ORDERCATEGORY varchar(2) NULL,
    CONSTRAINT ORDERHDR_PK PRIMARY KEY CLUSTERED (ORDERID)
)

CREATE TABLE dbo.ORDERLINE(
    ORDERNUMBER varchar(16) NOT NULL,
    RMPHASE char(1) NOT NULL,
    ORDERLINE char(2) NOT NULL,
    ORDERID varchar(8) NOT NULL,
    PRODUCT varchar(8) NOT NULL,
    CONSTRAINT ORDERLINE_PK PRIMARY KEY CLUSTERED (ORDERNUMBER,ORDERLINE,RMPHASE)
)

CREATE TABLE dbo.PRODUCT(
    PRODUCT varchar(8) NOT NULL,
    FACTORY varchar(4) NULL,
    CONSTRAINT PRODUCT_PK PRIMARY KEY CLUSTERED (PRODUCT)
)

Yanıtlar:


30

Yaptığın planı almanın üç ana teknik nedeni var:

  1. Optimize Edici'nin maliyet çerçevesi, satır içi olmayan işlevler için gerçek bir desteğe sahip değildir . Ne kadar pahalı olabileceğini görmek için işlev tanımının içine bakmak için herhangi bir girişimde bulunmaz, sadece çok küçük bir sabit maliyet tayin eder ve işlevin her çağrıldığında 1 satırlık çıktı üreteceğini tahmin eder. Her iki modelleme varsayımı da çoğu zaman tamamen güvensizdir. Bu durum, 2014'te, 1 sıralı sabit tahminin 100 satırlık sabit bir tahminle değiştirildiğinden beri yeni kardinalite tahmincisi etkinken, çok az iyileşmiştir. Bununla birlikte, satır içi olmayan işlevlerin içeriğini maliyetlendirmek için hala bir destek yoktur.
  2. SQL Server başlangıçta bağlantıları birleştirir ve tek bir içsel mantıksal birleşim içine uygular. Bu, daha sonra katılım siparişleriyle ilgili optimizer nedenini açıklar. Tek kişilik arsa katılımının aday birleşme siparişlerine genişletilmesi daha sonra gelir ve büyük ölçüde sezgisel tahakkuk üzerine kuruludur. Örneğin, iç birleştirmeler dış birleştirmelerden, küçük masalardan ve seçici birleştirmelerden önce büyük masalardan ve daha az seçici birleşmelerden önce gelir.
  3. SQL Server maliyet tabanlı optimizasyon gerçekleştirdiğinde, düşük maliyetli sorguları çok uzun süre optimize etme şansını en aza indirgemek için çabayı isteğe bağlı aşamalara ayırır. Üç ana aşama vardır, arama 0, arama 1 ve arama 2. Her fazın giriş koşulları vardır ve daha sonraki aşamalar öncekilerden daha fazla optimize edici keşifler sağlar. Sorgunuz, en az yetenekli arama aşaması, 0 aşaması için uygun olur. Daha sonraki aşamaların girilmemesi için yeterince düşük bir maliyet planı bulunur.

UDF'ye tahsis edilen küçük kardinalite tahmini geçerli olduğu göz önüne alındığında, n-ary birleşme genişleme sezgiselini maalesef ağaçta istediğinizden daha önce yeniden konumlandırıyor.

Sorgu ayrıca, en az üç birleştirme (geçerli olanlar dahil) olması nedeniyle arama 0 optimizasyonuna da uygun olur. Tuhaf görünümlü tarama ile elde ettiğiniz son fiziksel plan, buluşsal olarak çıkarılmış olan birleştirme sırasına dayanır. Optimize edicinin planı "yeterince iyi" olarak görmesi yeterince düşük maliyetlidir. UDF için düşük maliyet tahmini ve kardinalite bu erken sona ermeye katkıda bulunuyor.

Arama 0 (İşlem İşleme aşaması olarak da bilinir) düşük kardinaliteli OLTP tipi sorguları hedefler, genellikle iç içe döngüler içeren son planlar birleştirilir. Daha da önemlisi, 0 araması, optimize edicinin keşif kabiliyetlerinin nispeten küçük bir alt kümesini çalıştırır. Bu alt küme, bir birleştirme (kural PullApplyOverJoin) üzerine sorgu ağacına bir başvuru getirmeyi içermez . Bu, UDF'nin başvuruların üzerinde son sıralarda görünmesi için (olduğu gibi) son başvuruda yer almak üzere UDF'nin başvuruların üstündeki başvurusunu yeniden konumlandırması için gereken tam olarak budur.

İyileştiricinin, saf iç içe geçmiş halkalar birleştirme (birleşme kendiliğinden birleştirme öngörüsü) ile birleştirilmiş bir dizinlenmiş birleştirme (uygulama) arasında, birleştirilmiş bir tahminin bir birleştirme araması kullanılarak birleştirmenin iç tarafına uygulandığı bir karar da vardır. İkincisi, genellikle istenen plan şeklidir, ancak optimize edici her ikisini de keşfedebilir. Yanlış maliyetlendirme ve kardinalite tahminleri ile, sunulan planlarda olduğu gibi uygulanmayan NL birleştirmeyi seçebilir (taramayı açıklar).

Bu nedenle, normalde aşırı kaynaklar kullanmadan kısa sürede iyi planlar bulmak için iyi çalışan birkaç genel optimize edici özellik içeren çok sayıda etkileşimli neden vardır. Sebeplerden herhangi birinin kaçınılması, boş sorgularda bile, örnek sorgu için 'beklenen' plan şeklini üretmek için yeterlidir:

Arama 0 devre dışıyken boş tabloları planlayın

Arama 0 planı seçimi, erken optimizer sonlandırması veya UDF'lerin maliyetini arttırmanın (bunun için SQL Server 2014 CE modelindeki sınırlı geliştirmelerin yanı sıra) iyileştirilmesinin desteklenen bir yolu yoktur. Bu, plan kılavuzları, el ile yapılan sorgu yeniden yazma ( TOP (1)fikir dahil veya ara geçici tablolar kullanma) ve satır içi olmayan işlevler gibi düşük maliyetli 'kara kutular' (QO bakış açısından) kaçınılması gibi şeyler bırakır .

Halihazırda bir araya getirme çalışmalarının bir kısmını engellediğinden, çalışabileceği CROSS APPLYgibi yeniden yazma OUTER APPLYda, ancak orijinal sorgu anlambilimini korumak için dikkatli olmanız gerekir (örneğin NULL, eniyileştirici geri çekilmeden yeniden başlatılabilen tüm uzamış satırları reddetme) çapraz uygulayın). Bu davranışın sabit kalmasının garanti edilmemesine rağmen farkında olmanız gerekir; bu nedenle, SQL Server'ı her yamaladığınızda veya yükseltirken bu tür gözlemlenen davranışları yeniden test etmeyi hatırlamanız gerekir.

Genel olarak, sizin için doğru çözüm sizin için yargılayamayacağımız çeşitli faktörlere bağlıdır. Bununla birlikte, gelecekte her zaman çalışabileceği garanti edilen ve mümkün olan her yerde optimizer ile çalışan (bununla karşı değil) çalışan çözümleri göz önünde bulundurmanızı tavsiye ederim.


24

Bu, optimizer tarafından maliyete dayalı bir karar ama oldukça kötü bir karar gibi görünüyor.

PRODUCT ürününe 50000 satır eklerseniz, optimizer taramanın çok çalıştığını düşünüyor ve size üç arama ve bir UDF çağrısı ile bir plan veriyor.

PRODUCT ürününde 6655 satır için aldığım plan

görüntü tanımını buraya girin

PRODUCT ürünündeki 50000 satırla bunun yerine bu planı alıyorum.

görüntü tanımını buraya girin

Sanırım UDF'yi aramanın maliyeti fena halde hafife alınmış.

Bu durumda iyi çalışan bir geçici çözüm, UDF'ye karşı dış uygulama kullanmak için sorguyu değiştirmektir. PRODUCT tablosunda kaç satır olduğuna bakılmaksızın iyi bir plan yapıyorum.

select  
    S.GROUPCODE,
    H.ORDERCATEGORY
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT    
    outer apply dbo.GetGroupCode (P.FACTORY) S
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01' and
    S.GROUPCODE is not null

görüntü tanımını buraya girin

Durumunuzdaki en iyi geçici çözüm muhtemelen ihtiyacınız olan değerleri temp tablosuna almak ve temp tablosunu UDF için geçerli bir çarpı işareti ile sorgulamaktır. Bu şekilde UDF'nin gereğinden fazla çalıştırılmayacağından emin olabilirsiniz.

select  
    P.FACTORY,
    H.ORDERCATEGORY
into #T
from    
    ORDERLINE L
    join ORDERHDR H on H.ORDERID = L.ORDERID
    join PRODUCT P  on P.PRODUCT = L.PRODUCT
where   
    L.ORDERNUMBER = 'XXX/YYY-123456' and
    L.RMPHASE = '0' and
    L.ORDERLINE = '01'

select  
    S.GROUPCODE,
    T.ORDERCATEGORY
from #T as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

drop table #T

Geçici tabloya devam etmek yerine, top()türetilmiş bir tabloda, SQL Server'ı UDF çağrılmadan önceki birleşimlerin sonucunu değerlendirmeye zorlamak için kullanabilirsiniz . Sadece SQL Server'ın sorgunun o kısmı için satırlarınızı sayması ve UDF kullanmadan önce sayması gerekir.

select S.GROUPCODE,
       T.ORDERCATEGORY
from (
     select top(2147483647)
         P.FACTORY,
         H.ORDERCATEGORY
     from    
         ORDERLINE L
         join ORDERHDR H on H.ORDERID = L.ORDERID
         join PRODUCT P  on P.PRODUCT = L.PRODUCT    
     where   
         L.ORDERNUMBER = 'XXX/YYY-123456' and
         L.RMPHASE = '0' and
         L.ORDERLINE = '01'
     ) as T
  cross apply dbo.GetGroupCode (T.FACTORY) S

görüntü tanımını buraya girin

Tüm işlemler birincil anahtarlar kullanılarak yapıldığından ve bunun kolayca çözülemeyen daha karmaşık bir sorguda gerçekleşirse nasıl düzeltileceği için bunun nedeninin ne olabileceğini anlamak isterim.

Buna gerçekten cevap veremiyorum ama yine de bildiklerimi paylaşmam gerektiğini düşündüm. Neden bir PRODUCT tablosunun taranmasının hiç göz önüne alındığını bilmiyorum. Bunun yapılması gereken en iyi şey olduğu durumlar olabilir ve optimize edicilerin bilmediğim UDF'lere nasıl davrandığıyla ilgili şeyler olabilir.

Ek gözlemlerden biri, sorgunuzun yeni kardinalite tahmincisi ile SQL Server 2014'te iyi bir plana sahip olduğuydu. Bunun nedeni, UDF'ye yapılan her çağrı için tahmini satır sayısının, SQL Server 2012 ve önceki sürümlerde olduğu gibi 1 yerine 100 olmasıdır. Ancak, tarama sürümü ile planın arama sürümü arasında hala aynı maliyete dayalı kararı verecek. PRODUCT ürününde 500'den az (benim durumumda 497) satır bulunan SQL Server 2014'te bile planın tarama sürümünü elde edersiniz.


2
Her nasılsa, Adam Machanic'in
James Z
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.