web-dev-qa-db-ja.com

テーブル値関数SQL Serverの最適化

このテーブル値関数を最適化しようとしています。できれば手順に変更しますができません。問題は、2つの更新ステートメントにあります。主なパフォーマンスの問題を引き起こしているので、これら2つだけを関数に残しました。最初の1つを外部適用から内部結合に書き直し、統計を確認したところ、それらが間違っていたため、オプション(再コンパイル)を追加しました。問題は2番目のアップデート内にあります。統計が間違っており、適切な実行計画を立ててヒントで最適化する方法がわかりません。どうすれば時間を短縮できるかわかりますか?テーブル変数にインデックスを付けようとしましたが、結果がありませんでした。

これが実行計画です https://www.brentozar.com/pastetheplan/?id=B1EdBo5e4

ありがとう。

CREATE FUNCTION [dbo].[cfn_PlanServis_Seznam](
   @IDVazRole INT,
   @IDUzivatel INT,
   @IDRole INT,
   @IDLokalita INT,
   @lCid INT
)

RETURNS @PlanServis TABLE(
   lIDAuto INT,
   szSPZ VARCHAR(100),
   lDepozit INT,
   szTypVozidla varchar(100),
   szTypServisu NVARCHAR(300),
   szServisniPlan NVARCHAR(300),
   lZbyvaDni INT,
   lZbyvaKm INT,
   lNajetoKm INT,
   dtServis DATETIME,
   dcZbyvaMotohodin DECIMAL(15,1),
   dcNajetoMotohodin DECIMAL(15,1),
   IDVazPlanServisAuto INT,
   IDPlanServisDefinice INT,
   lBarva INT

)

AS

BEGIN
    DECLARE @Auto TABLE(
       lIDAuto INT,
       szSPZ VARCHAR(100),
       szTyp VARCHAR(100),
       IDCisTypServis INT,
       szTypServisu NVARCHAR(500),
       szServisniPlan NVARCHAR(500),
       lKmStart INT,
       dtStart DATETIME,
       lKmPriZavedeni INT,
       lUjetoPredZavedenim INT,
       dcMotohodinyStart DECIMAL(15,1),
       lIntervalKm INT,
       dcIntervalMotohodiny DECIMAL(15,1),
       lUjeto INT,
       dcMotohodiny DECIMAL(15,1),
       IDServis INT,
       lKmServis INT,
       dcMotohodinyServis DECIMAL(15,1),
       dtServis DATETIME,
       lIntervalDatum INT,
       lDniUbehlo INT,
       lBarva INT,
       lZbyvaKm INT,
       dcZbyvaMotohodin DECIMAL(15,2),
       lZbyvaDni INT,
       lDepozit INT,
       IDVazPlanServisAuto INT,
       IDPlanServisDefinice INT,
       lMaxTachograf INT,
       lKmPretaceni INT,
       dtOd DATE,
       lKmPosledniServis INT
    )

    DECLARE @IDCisAutoParametrKmPriZavadeni INT = 10012
    DECLARE @lKmPred INT = 30000
    DECLARE @lKmPredMensi INT = 15000
    DECLARE @lDniPred INT = 60
    DECLARE @lDniPredMensi INT = 30
    DECLARE @lMotohodinyPred INT = 100
    DECLARE @lMotohodinyPredMensi INT = 50
    DECLARE @IDBarvaBlizi INT = 1010 --Odkaz do CisTermBarva
    DECLARE @IDBarvaBliziMensi INT = 1016 --Odkaz do CisTermBarva
    DECLARE @IDBarvaPres INT  = 1017 --Odkaz do CisTermBarva
    --============ Koenc deklarace promennych ===========

    INSERT INTO @Auto (lIDAuto, szSPZ, szTyp, IDCisTypServis, szTypServisu, lKmStart, dtStart, lKmPriZavedeni, dcMotohodinyStart,[@Auto].lIntervalKm,[@Auto].dcIntervalMotohodiny,[@Auto].lIntervalDatum,szServisniPlan,[@Auto].IDVazPlanServisAuto,[@Auto].IDPlanServisDefinice)
    SELECT Auto.lIDAuto,
           Auto.szSpz,
           CASE WHEN Auto.lTyp = 0 THEN 'Taha?'  WHEN Auto.lTyp =  1 THEN 'N?v?s' ELSE '' END,
           PlanServisDefinice.IDCisTypServis,
           dbo.GetLocalText('CisTypServis','szNazev',CisTypServis.lIDCisTypServis,@lCid,CisTypServis.szNazev,''),
           PlanServisDefinice.lStartKM,
           PlanServisDefinice.dtStartDatum,
           CONVERT(INT,VazAutoParametr.varHodnota),
           PlanServisDefinice.dcMotohodinyStart,
           PlanServisDefinice.lIntervalKM,
           PlanServisDefinice.dcIntervalMotohodin,
           PlanServisDefinice.lIntervalDatum,
           PlanServis.szNazev,
           PlanServisDefinice.IDVazPlanServisAuto,
           PlanServisDefinice.lIDPlanServisDefinice
    FROM Auto INNER JOIN VazPlanServisAuto ON Auto.lIDAuto = VazPlanServisAuto.IDAuto
             INNER JOIN PlanServisDefinice ON VazPlanServisAuto.lIDVazPlanServisAuto = PlanServisDefinice.IDVazPlanServisAuto   
             INNER JOIN CisTypServis ON PlanServisDefinice.IDCisTypServis = CisTypServis.lIDCisTypServis
             LEFT OUTER JOIN VazAutoParametr ON VazAutoParametr.IDAuto = Auto.lIDAuto AND VazAutoParametr.IDCisAutoParametr = @IDCisAutoParametrKmPriZavadeni
             INNER JOIN PlanServis ON VazPlanServisAuto.IDPlanServisu = PlanServis.lIDPlanServis


    UPDATE @Auto SET lUjeto = Km.lKm
        FROM @Auto INNER JOIN 
        (SELECT
                    SUM(JizdaTachograf.lkmDo - JizdaTachograf.lkmOd) AS lKm, JizdaTachograf.IDAuto,JizdaTachograf.IDNaves
        FROM Jizda
            INNER JOIN JizdaTachograf ON JizdaTachograf.IDJizda = Jizda.lIDJizda
        WHERE JizdaTachograf.lkmOd IS NOT NULL
            AND JizdaTachograf.lkmDo IS NOT NULL
            AND Jizda.lProvozne = 1
        GROUP BY 
            JizdaTachograf.IDAuto,JizdaTachograf.IDNaves    
        ) as Km 
            ON Km.IDAuto = [@Auto].lIDAuto OR Km.IDNaves = [@Auto].lIDAuto
            OPTION  (RECOMPILE)


    UPDATE @Auto SET lMaxTachograf = ISNULL(ISNULL(Km.lKm, [@Auto].lKmServis),[@Auto].lKmStart)
    FROM @Auto 
         OUTER APPLY 
    (SELECT TOP 1 JizdaTachograf.lkmDo lKm 
     FROM Jizda INNER JOIN 
     JizdaTachograf ON JizdaTachograf.IDJizda = Jizda.lIDJizda 
     WHERE JizdaTachograf.lkmOd IS NOT NULL AND 
           JizdaTachograf.lkmDo IS NOT NULL AND Jizda.lProvozne = 1 
           AND (JizdaTachograf.IDAuto = [@Auto].lIDAuto 
           OR JizdaTachograf.IDNaves = [@Auto].lIDAuto) 
           AND Jizda.dtZacatek > ISNULL(ISNULL([@Auto].dtServis, 
           [@Auto].dtStart),DATEADD(YEAR,-100,GETDATE())) 
     ORDER BY 
           Jizda.dtZacatek DESC, JizdaTachograf.lkmDo desc) Km



    INSERT INTO @PlanServis (lIDAuto, 
                             szSPZ, 
                             lDepozit, 
                             szTypVozidla, 
                             szTypServisu, 
                             szServisniPlan, 
                             lZbyvaDni, 
                             lZbyvaKm, 
                             lNajetoKm, 
                             dtServis, 
                             dcZbyvaMotohodin, 
                             dcNajetoMotohodin, 
                             IDVazPlanServisAuto, 
                             IDPlanServisDefinice,
                             lBarva)
    SELECT lIDAuto, 
            szSPZ, 
            lDepozit, 
            szTyp, 
            szTypServisu, 
            szServisniPlan, 
            lZbyvaDni, 
            lZbyvaKm, 
            lUjeto,--lNajetoKm, 
            dtServis, 
            dcZbyvaMotohodin, 
            dcMotohodiny,--dcNajetoMotohodin, 
            IDVazPlanServisAuto, 
            IDPlanServisDefinice,
            lBarva
      FROM @Auto 
    RETURN
END
GO
4
jindrich

このような質問では、 [〜#〜] mcve [〜#〜] を指定すると非常に役立ちます。このように、テーブルの構造とデータの分布について、多くの推測をしなければなりませんでした。クエリプランのこの部分は、さらに詳しく説明しなければ遅すぎると言います。

old query plan

その部分が遅いかもしれない3つの理由を見ることができます。最初の問題は、両方のテーブルの合計行数が176kであるのに、インデックスシークが両方のテーブルから800k行をプルすることです。 2番目の問題は、JizdaTachografでのインデックスシークには、シーク述語[Lori_MDL].[dbo].[JizdaTachograf].lkmOd IS NOT NULLしかないことです。それは選択的である可能性があると思いますが、そうでない場合は、インデックス845テーブルのほとんどを効果的にスキャンしています。 3番目の問題は、合計800k行がソートされることですが、ソートは846回の反復に分割されます。

両方のテーブルを1回だけスキャンする計画を取得する方法があるかもしれませんが、データの分布を理解していないと、それが価値があるかどうかわかりません。クエリの要件(不等式、並べ替え、ORロジック))により、マージ結合またはハッシュ結合が機能しにくくなります。

解決できる1つの問題は2番目の問題です。適切なインデックスを定義し、(JizdaTachograf.IDAuto = [@Auto].lIDAuto OR JizdaTachograf.IDNaves = [@Auto].lIDAuto)を2つのサブクエリに分割すると、関連する行を直接検索するJizdaTachografでより効果的なインデックスシークを取得できます。テーブル内のほとんどの行にlkmOdのNULL以外の値がある場合、これにより多くの時間を節約できます。機能するさまざまなインデックス定義があります。 2つは以下のとおりです。

CREATE INDEX IX2 ON JizdaTachograf (IDAuto, IDJizda, lkmDo) INCLUDE (lkmOd)
WHERE lkmOd IS NOT NULL AND lkmDo IS NOT NULL;

CREATE INDEX IX3 ON JizdaTachograf (IDNaves, IDJizda, lkmDo) INCLUDE (lkmOd)
WHERE lkmOd IS NOT NULL AND lkmDo IS NOT NULL;

次に、クエリを分割して、SQL Serverがインデックスを利用できるようにします。

UPDATE @Auto SET lMaxTachograf = ISNULL(ISNULL(Km.lKm, [@Auto].lKmServis),[@Auto].lKmStart)
    FROM @Auto 
    OUTER APPLY 
    (
    SELECT TOP (1) lKm
    FROM
    (

    SELECT TOP (1) Jizda.dtZacatek, JizdaTachograf.lkmDo lKm 
     FROM JizdaTachograf
     INNER JOIN Jizda WITH (INDEX(1)) ON JizdaTachograf.IDJizda = Jizda.lIDJizda 
     WHERE JizdaTachograf.lkmOd IS NOT NULL AND 
           JizdaTachograf.lkmDo IS NOT NULL AND Jizda.lProvozne = 1 
           AND JizdaTachograf.IDAuto = [@Auto].lIDAuto -- first half
           AND Jizda.dtZacatek > ISNULL(ISNULL([@Auto].dtServis, [@Auto].dtStart),DATEADD(YEAR,-100,GETDATE())) 
     ORDER BY  Jizda.dtZacatek DESC, JizdaTachograf.lkmDo desc

     UNION ALL

     SELECT TOP (1) Jizda.dtZacatek, JizdaTachograf.lkmDo lKm 
     FROM JizdaTachograf
     INNER JOIN Jizda WITH (INDEX(1)) ON JizdaTachograf.IDJizda = Jizda.lIDJizda 
     WHERE JizdaTachograf.lkmOd IS NOT NULL AND 
           JizdaTachograf.lkmDo IS NOT NULL AND Jizda.lProvozne = 1 
           AND JizdaTachograf.IDNaves = [@Auto].lIDAuto -- second half
           AND Jizda.dtZacatek > ISNULL(ISNULL([@Auto].dtServis, [@Auto].dtStart),DATEADD(YEAR,-100,GETDATE())) 
     ORDER BY Jizda.dtZacatek DESC, JizdaTachograf.lkmDo desc
    ) IDNaves_IDAuto
    ORDER BY dtZacatek DESC, lKm DESC   
    ) Km;

私は空のテーブルで作業していますが、目的のプラン形状を取得することが少なくとも可能であることを示すことができます。

plan 1

このプランの利点は、IO on JizdaTachograf)の実行が少なくなることと、並べ替えがさらに分割されることです。ただし、同じ数の行を取得します両方のインデックスから、同じ合計行数をソートします。

このクエリを作成して、並べ替えを行わないようにすることができます。 IOパターンは異なるため、全体的な読み取りが少なくなる可能性があります。別のインデックスが必要になります。以下は機能するものです。

CREATE INDEX IX1 ON Jizda (dtZacatek) INCLUDE (lIDJizda, lProvozne)
WHERE lProvozne = 1;

オプティマイザーは常に、並べ替えられたデータについてできるのと同じ推論を行うことができないため、並べ替えが不要であることを理解できるようにクエリを変更しました。

UPDATE @Auto SET lMaxTachograf = ISNULL(ISNULL(Km.lKm, [@Auto].lKmServis),[@Auto].lKmStart)
    FROM @Auto 
    OUTER APPLY 
    (
    SELECT TOP (1) lkmDo lKm
    FROM
    (
        SELECT TOP (1) Jizda.dtZacatek, ca.lkmDo
        FROM Jizda 
        CROSS APPLY (
             SELECT TOP (1) JizdaTachograf.lkmDo
             FROM JizdaTachograf
             WHERE JizdaTachograf.IDJizda = Jizda.lIDJizda 
             AND JizdaTachograf.lkmOd IS NOT NULL AND 
             JizdaTachograf.lkmDo IS NOT NULL
             AND JizdaTachograf.IDNaves = [@Auto].lIDAuto  -- this line is different    
             ORDER BY JizdaTachograf.lkmDo DESC          
        ) ca
        WHERE Jizda.lProvozne = 1 
        AND Jizda.dtZacatek > ISNULL(ISNULL([@Auto].dtServis,[@Auto].dtStart),DATEADD(YEAR,-100,GETDATE())) 
        ORDER BY Jizda.dtZacatek DESC

        UNION ALL

        SELECT TOP (1) Jizda.dtZacatek, ca.lkmDo
        FROM Jizda 
        CROSS APPLY (
             SELECT TOP (1) JizdaTachograf.lkmDo
             FROM JizdaTachograf
             WHERE JizdaTachograf.IDJizda = Jizda.lIDJizda 
             AND JizdaTachograf.lkmOd IS NOT NULL AND 
             JizdaTachograf.lkmDo IS NOT NULL
             AND JizdaTachograf.IDAuto = [@Auto].lIDAuto -- this line is different  
             ORDER BY JizdaTachograf.lkmDo DESC          
        ) ca
        WHERE Jizda.lProvozne = 1 
        AND Jizda.dtZacatek > ISNULL(ISNULL([@Auto].dtServis,[@Auto].dtStart),DATEADD(YEAR,-100,GETDATE())) 
        ORDER BY Jizda.dtZacatek DESC
    )  IDNaves_IDAuto

    ORDER BY dtZacatek DESC, lkmDo DESC
    ) Km

現在、並べ替えはありません。

plan 2

ただし、これはやや危険な最適化です。ここで、Jizdaはネストされたループ結合の外部テーブルです。 @AutoのNULLと[@Auto].dtServisのNULL、[@Auto].dtStartのNULL、およびJizdaTachografIDNavesIDAutoによる一致がない行を考えます。 。 SQL ServerはJizda内の180k行すべてを読み取り、JizdaTachografに対して180kのインデックスシークを実行して、最終的に行を返しません。これがどのくらい起こりそうかはわかりませんが、起こり得ることです。

質問で提供された情報に基づいて、私のアドバイスは最初のクエリを試して、それが十分に速くなるかどうかを確認することです。そうでない場合は、両方のクエリにフィルタを実装します。 845行のテーブル変数をスキャンするのに時間はまったくかからないため、テーブルの異なる部分を操作する2つの別々のUPDATE`ステートメントを使用して、両方のクエリを最大限に活用できる可能性があります。最初のクエリは、NULL以外の日付列がない場合により効率的です。

WHERE [@Auto].dtServis IS NULL AND [@Auto].dtStart IS NULL;

NULL以外の日付列がある場合、2番目のクエリの方が効率的です(列が多少選択的であると想定しています)。

WHERE [@Auto].dtServis IS NOT NULL OR [@Auto].dtStart IS NOT NULL
4
Joe Obbish

@Autoテーブルを更新しているクエリの一部は、テーブル変数にさらに挿入できると思います。

そのため、同じクエリが繰り返されないため、パフォーマンスが向上します。

アイデアを理解し、必要に応じて正しいことを行ってください。

declare @table TABLE(lkmDo int, lkmOd int,IDAuto int, IDNaves int,dtZacatek datetime )
insert into @table
SELECT JizdaTachograf.lkmDo ,JizdaTachograf.lkmOd ,IDAuto,IDNaves,dtZacatek
     FROM Jizda INNER JOIN 
     JizdaTachograf ON JizdaTachograf.IDJizda = Jizda.lIDJizda 
     WHERE JizdaTachograf.lkmOd IS NOT NULL AND 
           JizdaTachograf.lkmDo IS NOT NULL AND Jizda.lProvozne = 1 
           AND (JizdaTachograf.IDAuto = [@Auto].lIDAuto 
           OR JizdaTachograf.IDNaves = [@Auto].lIDAuto) 
-- OPTION  (RECOMPILE)

    UPDATE @Auto SET lUjeto = Km.lKm
        FROM @Auto INNER JOIN 
        (SELECT
                    SUM(lkmDo - lkmOd) AS lKm, IDAuto,IDNaves
        FROM @table
        GROUP BY 
            JizdaTachograf.IDAuto,JizdaTachograf.IDNaves    
        ) as Km 
            ON Km.IDAuto = [@Auto].lIDAuto OR Km.IDNaves = [@Auto].lIDAuto



    UPDATE @Auto SET lMaxTachograf = ISNULL(ISNULL(Km.lKm, [@Auto].lKmServis),[@Auto].lKmStart)
    FROM @Auto 
         OUTER APPLY 
    (SELECT TOP 1 JizdaTachograf.lkmDo lKm 
     FROM @table
     WHERE
           Jizda.dtZacatek > ISNULL(ISNULL([@Auto].dtServis, 
           [@Auto].dtStart),DATEADD(YEAR,-100,GETDATE())) 
     ORDER BY 
           Jizda.dtZacatek DESC, JizdaTachograf.lkmDo desc) Km
0
KumarHarsh