web-dev-qa-db-ja.com

一時テーブルの代わりにテーブル変数を使用すると、クエリの実行が遅くなります

AutoDataの履歴データと、クラスター化キーCas(DateTime)+ GCom(Car ID)を組み合わせたテーブルがあります。 1つのレコードには、燃料レベル、車両状態などのさまざまな指標が含まれています。

AutoDataテーブルの1台の車の個々のレコード間の間隔は不規則で、120秒、数秒、数時間などの場合があります。レコードを表示用に正規化して、30ごとに1つのレコードを表示する必要があります。秒。

次のスクリプトがあります。

DECLARE @GCom int = 2563,
    @Od DateTime2(0) = '20170210', 
    @Do DateTime2(0) = '20170224'    

--Create a table with intervals by 30 seconds
declare @temp Table ([cas] datetime2(0))
INSERT @temp([cas])
SELECT d
FROM
(
  SELECT
      d = DATEADD(SECOND, (rn - 1)*30, @Od)
  FROM 
  (
      SELECT TOP (DATEDIFF(MINUTE, @Od, @Do)*2) 
          rn = ROW_NUMBER() OVER (ORDER BY s1.[object_id])
      FROM
          sys.all_objects AS s1
      CROSS JOIN
          sys.all_objects AS s2
      ORDER BY
          s1.[object_id]
  ) AS x
) AS y;

--Create temp table
CREATE TABLE #AutoData (
    [Cas] [datetime2](0) NOT NULL PRIMARY KEY,
    [IDProvozniRezim] [tinyint] NOT NULL,
    [IDRidic] [smallint] NULL,
    [Stav] [tinyint] NOT NULL,
    [Klicek] [bit] NOT NULL,
    [Alarm] [bit] NOT NULL,
    [MAlarm] [tinyint] NOT NULL,
    [DAlarm] [bit] NOT NULL,
    [Bypass] [bit] NOT NULL,
    [Lat] [real] NULL,
    [Lon] [real] NULL,
    [ObjemAktualni] [real] NOT NULL,
    [RychlostMaxV1] [real] NOT NULL,
    [RychlostV2] [real] NOT NULL,
    [Otacky] [smallint] NOT NULL,
    [Nadspotreba] [real] NOT NULL,
    [Vzdalenost] [real] NOT NULL,
    [Motor] [smallint] NOT NULL
)

--Populate the temp table selecting only relevant AutoData records
INSERT INTO #AutoData
SELECT [Cas]
      ,[IDProvozniRezim]
      ,[IDRidic]
      ,[Stav]
      ,[Klicek]
      ,[Alarm]
      ,[MAlarm]
      ,[DAlarm]
      ,[Bypass]
      ,[Lat]
      ,[Lon]
      ,[ObjemAktualni]
      ,[RychlostMaxV1]
      ,[RychlostV2]
      ,[Otacky]
      ,[Nadspotreba]
      ,[Vzdalenost]
      ,[Motor]
FROM AutoData a 
WHERE a.GCom = @GCom AND a.cas BETWEEN @Od AND @do

--Select final data
SELECT t.cas, ad.malarm, ad.IDProvoznirezim, ad.Otacky, ad.motor, ad.objemAktualni, ad.Nadspotreba 
FROM @temp t
OUTER APPLY (
SELECT TOP 1 stav, malarm, otacky,motor, objemAktualni, Nadspotreba, IDProvoznirezim  FROM #AutoData a
                     WHERE DATEDIFF(SECOND, a.cas, t.cas)<=CASE WHEN Motor>120 THEN Motor ELSE 120 END 
                     AND DATEDIFF(SECOND,  a.cas, t.cas)>-30 
                     ORDER BY CASE WHEN DATEDIFF(SECOND, a.cas, t.cas)>0 THEN DATEDIFF(SECOND, a.cas, t.cas) ELSE (DATEDIFF(SECOND, a.cas, t.cas)*-1) +120 END
) ad

DROP TABLE #AutoData

最初は、最後の選択に条件WHERE a.GCom = @GCom AND a.cas BETWEEN @Od AND @doを配置して、1つのテーブル変数@tempのみでスクリプトを記述しようとしました。スクリプトの実行には39秒かかりました。

上記のスクリプトに示すように、#AutoData一時テーブルを使用してデータサブセットを一時テーブルにプリロードすると、5秒になりました。

次に、@AutoDataの代わりにテーブル変数#AutoDataを使用しようとしましたが、再び非常に長くかかりました(22秒)。

この例では、@tempテーブルには40320レコードがあり、#AutoDataテーブルには1904レコードがあります。しかし、驚いたことに、#temp変数の代わりに@tempテーブルを使用しただけで、実行が再び遅くなりました。

一時テーブル/変数を使用した場合と使用しない場合の違いに驚きました。どうやらSQL Serverはそれ自体ではOUTER APPLY句の内部を最適化できませんでした。

しかし、なぜテーブル変数と一時テーブルを使用する場合にそのような大きな違いがあるのでしょうか?それを試すだけでなく、他に何を使用するかを知る方法はありますか?


一時テーブル#AutoDataを使用した実行プラン: TempTable

https://www.brentozar.com/pastetheplan/?id=B1y2x2Zcg

変数@AutoDataを使用した実行計画: Variable

https://www.brentozar.com/pastetheplan/?id=r1rAZnbqx

3
Vojtěch Dohnal

キーはあなたの質問のこの部分にあります:

@tempテーブルには40320レコードがあります

実行プランで、@ tempテーブルのスキャンの上にマウスを置きます。推定行数と実際の行数を比較します。 (計画を http://PasteThePlan.com に投稿する場合は、より具体的な詳細をお知らせします。免責事項:これは私の会社のサイトです。)

推定行数が本当に少ないことがわかります。

SQL Serverは、1〜3行がテーブル変数から返されると推定します(SQL Serverのバージョン、カーディナリティエスティメータ、トレースフラグなどによって異なります)。これは、SQL Serverが作業量を過小評価しているため、実際に悪い実行計画を与えます。他のテーブル、どれだけのメモリを確保する必要があるかなどが必要です。

より正確な見積もりを取得する最も一般的な2つの方法を次に示します。

  • 代わりに一時テーブルを試してください(計画の推定行と実際の行を確認してください)
  • クエリでOPTION(RECOMPILE)を使用します。これにより、より正確な見積もりが得られますが、プランキャッシュの可視性とCPU使用率に関して非常に大きな欠点があります。

私がライブで実行しているのを見るには、1時間 ブレントチューンクエリを見る を見てください(免責事項:それは私です。自分のビデオにリンクしています)。ここで、テーブル変数を使用するスタックオーバーフロークエリを実行しています。 SQL Rally Norwayの聴衆の前でライブチューニングしてください。

4
Brent Ozar

#tempを使用すると、クエリプランナーがより効率的になります。テーブル変数では、最初の数行のみが考慮されます。

テーブル変数(および使用する場合は#temp)は、主キーを宣言することでメリットを得られる可能性があります。

#AutoDataにキーを配置し、必要な行のみを入力します。

行を追加するときにキーでソート

以下はrow_number()で最適化できると思います

SELECT t.cas
     , ad.malarm, ad.IDProvoznirezim, ad.Otacky
     , ad.motor, ad.objemAktualni, ad.Nadspotreba 
FROM @temp t
OUTER APPLY ( SELECT TOP 1 malarm, IDProvoznirezim, Otacky
                         , motor, objemAktualni, Nadspotreba   
               FROM #AutoData a
              WHERE DATEDIFF(SECOND, a.cas, t.cas) <= CASE WHEN Motor > 120 THEN Motor ELSE 120 END 
                AND DATEDIFF(SECOND, a.cas, t.cas)  > -30 
              ORDER BY CASE WHEN DATEDIFF(SECOND, a.cas, t.cas) > 0 THEN DATEDIFF(SECOND, a.cas, t.cas) 
                            ELSE DATEDIFF(SECOND, t.cas, a.cas) + 120 END
            ) ad  

これはrow_number()としての試みです

select * from 
( SELECT t.cas
       , a.malarm, a.IDProvoznirezim, a.Otacky
       , a.motor, a.objemAktualni, a.Nadspotreba 
       , row_nunber() over (partition by t.cas 
                            ORDER BY CASE WHEN DATEDIFF(SECOND, a.cas, t.cas) > 0 THEN DATEDIFF(SECOND, a.cas, t.cas) 
                            ELSE DATEDIFF(SECOND, t.cas, a.cas) + 120 END) rn
    FROM @temp t  -- with primay key t.cas order by
    join AutoData a
      on a.GCom = @GCom 
     AND a.cas BETWEEN @Od AND @do
     AND DATEDIFF(SECOND, a.cas, t.cas) <= CASE WHEN Motor > 120 THEN Motor ELSE 120 END 
     AND DATEDIFF(SECOND, a.cas, t.cas)  > -30 
) ad 
where ad.rn = 1  
1
paparazzo