郊外、州、郵便番号に基づいて住所をジオコーディングするためのテーブル値関数を作成しています。さまざまな方法を使用して住所をジオコーディングしようとします精度の高い順に:
(私は、郊外-郵便番号-州の関係が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 を試してみます。
単一のクエリを使用する必要がある場合(単一のインライン関数での必要に応じて)、以下の2つのオプションのいずれかを使用できます(私の最近の回答 2つのテーブルを可能なワイルドカードに関連付けていますか? )。
複数のAPPLY
句を、チェーンの前の適用からの外部参照を使用するそれぞれの開始条件で使用します。この方法の効率は、実行プラン内の起動フィルターの存在に依存します。正しい結果は保証されますが、平面形状は保証されません。
ユニオンの各句に定数リテラルを含む追加の列を追加します。 _[Priority] = 1
_次に、TOP (1)
スコープに_ORDER BY [Priority] ASC
_を追加します。効率的な操作は、ソートを回避する計画に依存します。
リフレクションでは、これはこの場合に必要なことではありません。プランのマージ連結では各オプションから1行が必要になるためです。それにもかかわらず、これはより一般的な状況(代替入力が複数の行を生成し、最初の行を低コストで生成する)のオプションです。
加えて:
単一の行しか返さないので、代わりに複数ステートメントのテーブル値関数を使用し、明示的なロジックを使用して(個別のクエリで)各オプションを順番に試し、最初の結果が見つかるとすぐに返すことができます。これにより、正しい結果が効率的に生成されることが保証されます。
注
現在の関数は技術的に非決定的です。 SQL Serverは、選択した任意の順序でユニオンをすべて評価し、優先順位の高い結果を評価する前に、優先順位の低い結果を返す可能性があります。
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を評価することが義務付けられています。
LEFT OUTER JOINS
を使用してすべてのクエリを結合し、結果の列を逆順で [〜#〜] coalesce [〜#〜] 関数のパラメーターとして使用します。この関数は、すべてのパラメーターを左から右に評価し、最初の非ヌル値を取ります。
ポールホワイト、レナート、クナルチトカラの回答に感謝します。
また、_ORDER BY
_クエリが複数のUNION
のサブクエリで構成されている場合でも_SELECT TOP 1 *
_が必要であることを最初に指摘してくれたypercubeᵀᴹに感謝します。
4つの方法で記述されたクエリのパフォーマンスを比較しました。
次の形式のUNIONされたサブクエリからSELECT TOP 1を注文しました:
_SELECT TOP 1 *
FROM (
subquery1
UNION ALL
subquery2
UNION ALL
...
subquery7
) t
ORDER BY [Preference]
_
チェインされた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
_
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
_
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クエリオプティマイザーが並列プランを生成できるようにクエリを記述することが望まれました。私は次のことを認識する必要がありました:
WITH SCHEMABINDING
_制約が含まれている必要があります。つまり、TVFNによって参照されるすべてのオブジェクト(テーブル、ビューなど)は同じデータベースに存在する必要があります。TOP
を使用すると、ゾーン(TOP
が適用される[サブ]クエリ)が強制的に順次実行されます_-- 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 |
==========================================================================
_
注意:
OPTION (MAXDOP 1)
で実行されました結果からメソッド1が最速であることがわかりますが、常にシリアル計画が生成されます(最も外側のクエリでTOP
を使用しているため)。
方法は、シリアルプラン(_OPTION MAXDOP 1
_)で実行すると遅くなりましたが、マルチCPUマシンでパラレルプランで実行すると速くなりました。サーバーで使用可能なCPUの数によっては、この方法が最適なオプションになる場合があります。特に、クエリが並列処理の恩恵を受けるより大きなクエリの一部として実行される場合は特にそうです。
方法4も良い候補ですが、SQL Serverで並列プランを生成することができませんでした。
方法2がはるかに遅い。
テーブルにデータを入力して行を確認できます
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);