web-dev-qa-db-ja.com

UNION ALLまたは結果セットの最初の行を返す他の方法

郊外、州、郵便番号に基づいて住所をジオコーディングするためのテーブル値関数を作成しています。さまざまな方法を使用して住所をジオコーディングしようとします精度の高い順に

  1. ユニークな郊外の郵便番号と州の組み合わせの完全一致
  2. ユニークな郊外の郵便番号の組み合わせの完全一致
  3. ユニークな郊外と州の組み合わせの完全一致
  4. 一意の郵便番号の完全一致
  5. 一意でない郵便番号による近似一致。この郵便番号を持つすべての郊外は、互いに5 km以内にあります。

(私は、郊外-郵便番号-州の関係がall多対多である地理的地域で作業しています。つまり、1つの郊外に複数の郵便番号。1つの郵便番号に複数の郊外を含めることができ、異なる状態で存在する場合があります。)

以下は、テーブル値関数からの抜粋です。

_ALTER FUNCTION [geocode].[tvfn_Customer_Suburb_From_Address]
(   
    @Suburb NVARCHAR(100),
    @State NVARCHAR(100),
    @Postcode NVARCHAR(100),
    @Country NVARCHAR(100)
)
RETURNS TABLE 
AS
RETURN 
(

    SELECT TOP 1 *
    FROM (

            -- Unique suburb-postcode-state combinations
            SELECT   s.Suburb_DID
                    ,s.Suburb
                    ,s.State
                    ,s.Postcode
                    ,Geocode_DID = 4 -- Exact match by unique Postcode, Suburb and State
                    ,s.Geocode_Latitude
                    ,s.Geocode_Longitude
            FROM geocode.tSuburbs_XX s
            INNER JOIN [geocode].[tGeocode_Methods] gm
                ON s.Geocode_DID = gm.Geocode_DID

            WHERE s.[Is_Active] = 1
            AND s.[Suburb] = @Suburb
            AND s.[State] = @State
            AND s.[Postcode] = @Postcode
            -- Only suburbs that are geocoded with methods that can be used for geocoding customers
            AND gm.[Can_Use_For_VIP] = 1


            UNION ALL


            -- -- Unique suburb-postcode combinations
            SELECT   s.Suburb_DID
                    ,s.Suburb
                    ,s.State
                    ,s.Postcode
                    ,Geocode_DID = 3 -- Exact match by unique Postcode & Suburb
                    ,s.Geocode_Latitude
                    ,s.Geocode_Longitude
            FROM geocode.tSuburbs_XX s
                INNER JOIN [geocode].[tGeocode_Methods] gm
                    ON s.Geocode_DID = gm.Geocode_DID
            WHERE EXISTS (  SELECT *
                            FROM geocode.tSuburbs_XX
                            WHERE Is_Active = 1
                            AND Suburb = s.Suburb AND Postcode = s.Postcode
                            GROUP BY Postcode, Suburb
                            HAVING COUNT(*) = 1
                            )
            AND s.Is_Active = 1
            AND s.[Suburb] = @Suburb
            AND s.[Postcode] = @Postcode
            -- Only suburbs that are geocoded with methods that can be used for geocoding customers
            AND gm.[Can_Use_For_VIP] = 1


            UNION ALL


            -- Exact match by unique Suburb and State
            SELECT   s.Suburb_DID
                    ,s.Suburb
                    ,s.State
                    ,s.Postcode
                    ,Geocode_DID = 6 -- Exact match by unique Suburb and State
                    ,s.Geocode_Latitude
                    ,s.Geocode_Longitude
            FROM geocode.tSuburbs_XX s
                INNER JOIN [geocode].[tGeocode_Methods] gm
                    ON s.Geocode_DID = gm.Geocode_DID
            WHERE EXISTS (  SELECT *
                            FROM geocode.tSuburbs_XX
                            WHERE Is_Active = 1 AND Is_PO_Box = 0 -- Exclude PO Boxes
                            AND Suburb = s.Suburb AND Postcode = s.Postcode
                            GROUP BY Suburb, Postcode
                            HAVING COUNT(*) = 1
                            )
            AND s.Is_Active = 1
            AND s.[Suburb] = @Suburb
            AND s.[Postcode] = @Postcode
            -- Only suburbs that are geocoded with methods that can be used for geocoding customers
            AND gm.[Can_Use_For_VIP] = 1


            UNION ALL


            -- Exact match by unique Postcode
            SELECT   s.Suburb_DID
                    ,s.Suburb
                    ,s.State
                    ,s.Postcode
                    ,Geocode_DID = 2 -- Exact match by unique Postcode
                    ,s.Geocode_Latitude
                    ,s.Geocode_Longitude
            FROM geocode.tSuburbs_XX s
                INNER JOIN [geocode].[tGeocode_Methods] gm
                    ON s.Geocode_DID = gm.Geocode_DID
            WHERE EXISTS (  SELECT *
                            FROM geocode.tSuburbs_XX
                            WHERE Is_Active = 1
                            AND Postcode = s.Postcode
                            GROUP BY Postcode
                            HAVING COUNT(*) = 1
                            )
            AND s.Is_Active = 1
            AND s.[Postcode] = @Postcode
            -- Only suburbs that are geocoded with methods that can be used for geocoding customers
            AND gm.[Can_Use_For_VIP] = 1
            -- Perform this extra check to make sure we don't match a postcode in a wrong country
            AND (       @Country IN ('AAA', 'BBB', 'CCC')
                    OR  @State IN ('MMM', 'NNN', 'OOO', 'PPP')
                )


            UNION ALL


            -- Approximate match by non-unique Postcode, where all Suburbs with this Postcode are within 5 km of one another.
            SELECT   s.Suburb_DID
                    ,s.Suburb
                    ,s.State
                    ,s.Postcode
                    ,Geocode_DID = 5
                    ,s.Geocode_Latitude
                    ,s.Geocode_Longitude
            FROM [geocode].[tPostcode_Distances] pd
                INNER JOIN  geocode.tSuburbs_XX s
                    ON pd.Approx_Suburb_DID = s.Suburb_DID
                INNER JOIN [geocode].[tGeocode_Methods] gm
                    ON s.Geocode_DID = gm.Geocode_DID
            WHERE  s.Is_Active = 1
            AND pd.[Postcode] = @Postcode
            -- Only suburbs that are geocoded with methods that can be used for geocoding customers
            AND gm.[Can_Use_For_VIP] = 1
            -- Perform this extra check to make sure we don't match a postcode in a wrong country
            AND (       @Country IN ('AAA', 'BBB', 'CCC')
                    OR  @State IN ('MMM', 'NNN', 'OOO', 'PPP')
                )
            AND pd.Max_Distance <= 5000 -- within 5 km
    ) t
)
_

上記の機能は動作しますが、改善できるかどうか知りたいです。特に、結果セットを返す最初のSELECTの後でSQL ServerにSELECTステートメントの処理を強制的に停止させることは可能ですか(最初の一致結果のみに関心があるため、_TOP 1_)?

更新

これまでにご提案いただきありがとうございます。いくつかの回答とコメントで提案されているように_[Priority]_列と_ORDER BY_句を追加して、最高の結果が得られるようにします。

SQL Serverが計画を並列化できるように、TVFNに_WITH SCHEMABINDING_も追加します。この件については、マルチステートメントテーブル値関数を使用することをお勧めします( Paul White に感謝します)。マルチステートメントTVFNは常にシリアルプランを強制します。

ここで、レナートのCTEの使用を提案した answer を試してみます。

6
Serge

単一のクエリを使用する必要がある場合(単一のインライン関数での必要に応じて)、以下の2つのオプションのいずれかを使用できます(私の最近の回答 2つのテーブルを可能なワイルドカードに関連付けていますか? )。

オプション1

複数のAPPLY句を、チェーンの前の適用からの外部参照を使用するそれぞれの開始条件で使用します。この方法の効率は、実行プラン内の起動フィルターの存在に依存します。正しい結果は保証されますが、平面形状は保証されません。

オプション2

ユニオンの各句に定数リテラルを含む追加の列を追加します。 _[Priority] = 1_次に、TOP (1)スコープに_ORDER BY [Priority] ASC_を追加します。効率的な操作は、ソートを回避する計画に依存します。

リフレクションでは、これはこの場合に必要なことではありません。プランのマージ連結では各オプションから1行が必要になるためです。それにもかかわらず、これはより一般的な状況(代替入力が複数の行を生成し、最初の行を低コストで生成する)のオプションです。


加えて:

オプション3

単一の行しか返さないので、代わりに複数ステートメントのテーブル値関数を使用し、明示的なロジックを使用して(個別のクエリで)各オプションを順番に試し、最初の結果が見つかるとすぐに返すことができます。これにより、正しい結果が効率的に生成されることが保証されます。

現在の関数は技術的に非決定的です。 SQL Serverは、選択した任意の順序でユニオンをすべて評価し、優先順位の高い結果を評価する前に、優先順位の低い結果を返す可能性があります。

9
Paul White 9

Sql-serverがこのように機能するかどうかを十分に説明しているわけではありませんが、理論的には、ユニオンの一部が別の部分より先に評価されるとは言えません。つまり完全に一致している場合でも、おおよその一致になる可能性があります。

ただし、ユニオンの各部分に優先順位を追加して、この順序でこの動作を強制することができます。何かのようなもの:

SELECT TOP 1 *
FROM (
        -- Unique suburb-postcode-state combinations
        SELECT  1 as prio
                ,s.Suburb_DID
                ,s.Suburb
                ,s.State
                ,s.Postcode
                ,Geocode_DID = 4 -- Exact match by unique Postcode, Suburb and State
                ,s.Geocode_Latitude
                ,s.Geocode_Longitude
        FROM geocode.tSuburbs_XX s
        INNER JOIN [geocode].[tGeocode_Methods] gm
            ON s.Geocode_DID = gm.Geocode_DID

        WHERE s.[Is_Active] = 1
        AND s.[Suburb] = @Suburb
        AND s.[State] = @State
        AND s.[Postcode] = @Postcode
        -- Only suburbs that are geocoded with methods that can be used for geocoding customers
        AND gm.[Can_Use_For_VIP] = 1


        UNION ALL


        -- -- Unique suburb-postcode combinations
        SELECT  2 as prio
                ,s.Suburb_DID
                ,s.Suburb
                ,s.State
        [...]
) t
order by prio

これで、プリオが最小の行の1つが返されます。 DBMSは引き続き他のオプションを評価する可能性があるため、パフォーマンスが向上する保証はありません。

別のアイデアは、CTEを介して優先順にパーツをパイプライン処理することです。

 with t1 as (
        -- Unique suburb-postcode-state combinations
        SELECT  1 as prio
                ,s.Suburb_DID
                ,s.Suburb
                ,s.State
                ,s.Postcode
                ,Geocode_DID = 4 -- Exact match by unique Postcode, Suburb and State
                ,s.Geocode_Latitude
                ,s.Geocode_Longitude
        FROM geocode.tSuburbs_XX s
        INNER JOIN [geocode].[tGeocode_Methods] gm
            ON s.Geocode_DID = gm.Geocode_DID

        WHERE s.[Is_Active] = 1
        AND s.[Suburb] = @Suburb
        AND s.[State] = @State
        AND s.[Postcode] = @Postcode
        -- Only suburbs that are geocoded with methods that can be used for geocoding customers
        AND gm.[Can_Use_For_VIP] = 1
), t2 as (
        - -- Unique suburb-postcode combinations
        SELECT  2 as prio
                ,s.Suburb_DID
                ,s.Suburb
                ,s.State
        [...]
        WHERE NOT EXISTS ( SELECT 1 FROM T1 )
), t3 as (
        [...]
        WHERE NOT EXISTS ( SELECT 1 FROM T2 )
)
select * from t1
union all
select * from t2
union all
[...]

オプティマイザは、どこで停止するかを理解するのに十分なほど賢いかもしれません。少なくともt2の前にt1を評価することが義務付けられています。

4
Lennart

LEFT OUTER JOINSを使用してすべてのクエリを結合し、結果の列を逆順で [〜#〜] coalesce [〜#〜] 関数のパラメーターとして使用します。この関数は、すべてのパラメーターを左から右に評価し、最初の非ヌル値を取ります。

3
Chaos Legion

ポールホワイト、レナート、クナルチトカラの回答に感謝します。

また、_ORDER BY_クエリが複数のUNIONのサブクエリで構成されている場合でも_SELECT TOP 1 *_が必要であることを最初に指摘してくれたypercubeᵀᴹに感謝します。

4アプローチ

4つの方法で記述されたクエリのパフォーマンスを比較しました。

  1. 方法1

次の形式のUNIONされたサブクエリからSELECT TOP 1を注文しました:

_SELECT TOP 1 *
FROM (
    subquery1
    UNION ALL
    subquery2
    UNION ALL
    ...
    subquery7
) t
ORDER BY [Preference]
_
  1. 方法2

チェインされたCTEの後に、CTEのSELECT FROM UNIONのサブクエリが続きます。この形式は次のとおりです。

_WITH s AS (sharedQuery),
cte1 AS (subquery1),
cte1 AS (subquery2
    WHERE NOT EXISTS (SELECT 1 FROM q1)
), ...
cte7 AS (subquery7
    WHERE NOT EXISTS (SELECT 1 FROM cte1 UNION ALL ... SELECT 1 FROM cte6)
)
SELECT *
FROM (
    cte1
    UNION ALL
    cte2
    UNION ALL
    ...
    cte7
) t
_
  1. 方法

LEFT JOINされたサブクエリとCTEに基づくSELECT FROM COALESCEされたフィールドは、次の形式になります。

_WITH s AS (sharedQuery),
SELECT COALESCE(q1.A, q2.A, ... q7.A)
FROM (
    subquery1
) q1 LEFT JOIN (
    subquery2
) q2 ON 1=1
...
LEFT JOIN (
   subquery7
) q7 ON 1=1
_
  1. 方法4

OUTER APPLYされたサブクエリとCTEに基づくSELECT FROM COALESCEされたフィールドは、次の形式になります。

_WITH s AS (sharedQuery),
SELECT COALESCE(q1.A, q2.A, ... q7.A)
FROM (
    subquery1
) q1 OUTER APPLY (
    subquery2
) q2
...
OUTER APPLY (
   subquery7
) q7
_

この演習の主な理由はパフォーマンスだったので、SQL Serverクエリオプティマイザーが並列プランを生成できるようにクエリを記述することが望まれました。私は次のことを認識する必要がありました:

  • tVFNはmulti-statementではなく、inlineの1つです。
  • tVFNには_WITH SCHEMABINDING_制約が含まれている必要があります。つまり、TVFNによって参照されるすべてのオブジェクト(テーブル、ビューなど)は同じデータベースに存在する必要があります。
  • TOPを使用すると、ゾーン(TOPが適用される[サブ]クエリ)が強制的に順次実行されます
  • スカラーUDFに基づく計算フィールドは、計画全体を逐次実行することを強制します

試験方法

_-- Generate test data by loading some random addresses into a temporary table
SELECT TOP 1000 *
INTO #Addresses
FROM geocode.tDelivery_Address_Geocodes
ORDER BY NEWID()


-- Switch on statistics before running queries
SET STATISTICS TIME ON
SET STATISTICS IO ON

-- Clear cache before running each test
DBCC FREEPROCCACHE

-- Use a SELECT and OUTER APPLY to run the TVFN under test:

SELECT *
FROM #Addresses a
OUTER APPLY [geocode].[tvfn_Customer_Geocode_From_Address2](a.Suburb, a.TownCityState, a.PostCode, a.Country) t
_

試験結果

_==========================================================================
|          | CPU time| Elapsed |    | Est Subtree| Cached   | CPU Parse &|
| Method   | (s)     | time (s)| DOP| Cost       | plan size| Compile (s)|
|------------------------------------------------------------------------|
| Method 1 | 16.427  | 16.508  | 1  | 50.4757    |  256 KB  | 0.186      |
| Method 2 | 86.908  | 86.996  | 1  | 1540.59    |  4048 KB | 5.241      |
| Method 3*| 28.641  | 28.802  | 0  | 254.32     |  352 KB  | 0.422      |
| Method 3 | 32.088  | 15.675  | 4  | 254.506    |  360 KB  | 0.422      |
| Method 4 | 24.710  | 24.878  | 1  | 205.314    |  304 KB  | 0.344      |
==========================================================================
_

注意:

  • DOP =並列度
  • スター*でマークされたメソッドはOPTION (MAXDOP 1)で実行されました

結果からメソッド1が最速であることがわかりますが、常にシリアル計画が生成されます(最も外側のクエリでTOPを使用しているため)。

方法は、シリアルプラン(_OPTION MAXDOP 1_)で実行すると遅くなりましたが、マルチCPUマシンでパラレルプランで実行すると速くなりました。サーバーで使用可能なCPUの数によっては、この方法が最適なオプションになる場合があります。特に、クエリが並列処理の恩恵を受けるより大きなクエリの一部として実行される場合は特にそうです。

方法4も良い候補ですが、SQL Serverで並列プランを生成することができませんでした。

方法2がはるかに遅い。

1
Serge

テーブルにデータを入力して行を確認できます

CREATE FUNCTION dbo.F1 (@int int)
RETURNS @table table (col1 int, col2 varchar(10))
WITH EXECUTE AS CALLER
AS
BEGIN
     --insert into @table values (@int, 'one');
     if((select count(*) from @table) > 0)
         return;
     insert into @table values (@int, 'two');
     return;
END;
GO
SELECT *
FROM dbo.F1(1); 
1
paparazzo