少量のデータでパーティション化するときに現実的なクエリプランを取得する


9

パーティション分割を使用して、ロックのためにOLTPシステムエクスペリエンスがブロックされる量を減らし、パーティションIDに基づいて作業テーブルを100個のパーティションに分割します。ただし、テスト中に、実行プランが予想したとおりに選択されていないことがわかりました。

テストシナリオは、300,000件の連絡先レコード(各連絡先のデータは2つのテーブルに分割されています)を持つ単一の顧客であり、すべて単一のパーティションに存在し、顧客のパーティションで500の特定の行を検索するクエリがあります。ハッシュ一致のようなものが計画のかなり早い段階で不要な299,500を排除することを期待しますが、SQL Serverはテーブル全体のレコード数を取得し、すべてのパーティションで平均化することを選択しているようです。処理する多くのレコード。これにより、ネストされたループが選択され、プロセスのかなり後の方で不要なレコードが削除されます。通常、これには、パーティション分割されていないテーブルに対する同じクエリの9倍の時間がかかります。

奇妙なことに、selectにオプション(再コンパイル)を追加すると賢明な計画が得られますが、なぜこれが違いを生むのか途方に暮れています。これはストアドプロシージャではありません。テスト中に、各テストを実行する前にプロシージャキャッシュをクリアします。

この動作は、関係するテーブルが分割されていない場合には見られません。つまり、推定される行数が実際の数と一致するため、毎回適切なプランが選択されます。

この動作についての洞察はいただければ幸いです。

スキーマのセットアップ:

USE [Scratch]
GO
CREATE SCHEMA part
GO
CREATE PARTITION FUNCTION [ContactPartition](smallint) AS RANGE LEFT FOR VALUES (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98)
GO
CREATE PARTITION SCHEME [ContactPartitionScheme] AS PARTITION [ContactPartition] ALL TO ([PRIMARY])
GO
CREATE TABLE [part].[Contact](
    [ContactId] [int] IDENTITY(1,1) NOT NULL,
    [CustomerPartitionKey] [smallint] NOT NULL,
    [CustomerId] [int] NOT NULL,
    [OptOut] [bit] NOT NULL,
 CONSTRAINT [cn_pk_cluContact_CustomerPartitionKey_ContactId] PRIMARY KEY CLUSTERED 
(
    [CustomerPartitionKey] ASC,
    [ContactId] ASC
) ON [ContactPartitionScheme]([CustomerPartitionKey])
) ON [ContactPartitionScheme]([CustomerPartitionKey])
GO
CREATE TABLE [part].[ContactIdentifier](
    [ContactIdentifierId] [int] IDENTITY(1,1) NOT NULL,
    [CustomerPartitionKey] [smallint] NOT NULL,
    [ContactId] [int] NOT NULL,
    [Identifier] [nvarchar](256) NOT NULL,
    [CustomerId] [int] NOT NULL,
 CONSTRAINT [cn_pk_cluContactIdentifier_CustomerPartitionKey_ContactId] PRIMARY KEY CLUSTERED 
(
    [CustomerPartitionKey] ASC,
    [ContactId] ASC
) ON [ContactPartitionScheme]([CustomerPartitionKey])
) ON [ContactPartitionScheme]([CustomerPartitionKey])
GO
CREATE NONCLUSTERED INDEX [idx_ncContactIdentifier_CustomerPartitionKey_ContactId] ON [part].[ContactIdentifier]
(
    [CustomerPartitionKey] ASC,
    [ContactId] ASC
)
 ON [ContactPartitionScheme]([CustomerPartitionKey])
GO
CREATE NONCLUSTERED INDEX [idx_ncContactIdentifier_CustomerPartitionKey_IdentifierType_Identifier] ON [part].[ContactIdentifier]
(
    [CustomerPartitionKey] ASC,
    [Identifier] ASC
) ON [ContactPartitionScheme]([CustomerPartitionKey])
GO
CREATE UNIQUE NONCLUSTERED INDEX [idx_ncuContactIdentifier_CustomerId_CustomerPartitionKey_Identifier] ON [part].[ContactIdentifier]
(
    [CustomerId] ASC,
    [CustomerPartitionKey] ASC,
    [Identifier] ASC
) ON [ContactPartitionScheme]([CustomerPartitionKey])
GO
ALTER TABLE [part].[ContactIdentifier]  WITH NOCHECK ADD  CONSTRAINT [cn_ContactIdentifier_CustomerPartitionKey_ContactId_fk_Contact_CustomerPartitionKey_ContactId] FOREIGN KEY([CustomerPartitionKey], [ContactId])
REFERENCES [part].[Contact] ([CustomerPartitionKey], [ContactId])
GO
ALTER TABLE [part].[ContactIdentifier] NOCHECK CONSTRAINT [cn_ContactIdentifier_CustomerPartitionKey_ContactId_fk_Contact_CustomerPartitionKey_ContactId]
GO
WITH TestData AS
(
    SELECT 1 As Ordinal
    UNION ALL
    SELECT td.Ordinal + 1
    FROM TestData td
    WHERE td.Ordinal < 30000
)
INSERT INTO part.Contact (CustomerPartitionKey, CustomerId, OptOut)
SELECT 3, 3, 0
FROM TestData OPTION (MAXRECURSION 30000);
GO 10
WITH TestData AS
(
    SELECT 1 AS Ordinal, ISNULL(MAX(ContactId) + 1, 1) AS ContactId FROM part.ContactIdentifier
    UNION ALL
    SELECT td.Ordinal + 1, td.ContactId + 1 AS ContactId
    FROM TestData td
    WHERE td.Ordinal < 30000
)
INSERT INTO part.ContactIdentifier (CustomerPartitionKey, CustomerId, ContactId, Identifier)
SELECT 3, 3, ContactId, CONCAT('User ', ContactId)
FROM TestData OPTION (MAXRECURSION 30000);
GO 10

クエリ

USE Scratch
GO
DBCC FREEPROCCACHE
SET STATISTICS IO ON
SET STATISTICS TIME ON
GO
DECLARE @CustomerId int = 3, @CustomerPartitionKey smallint = 3

SET NOCOUNT ON

CREATE TABLE #identifiers (Ordinal int NOT NULL, Identifier nvarchar(256) NOT NULL)
INSERT INTO #identifiers
VALUES(0,N'User 0')
,(1,N'User 1')
,(2,N'User 2')
,(3,N'User 3')
,(4,N'User 4')
,(5,N'User 5')
,(6,N'User 6')
,(7,N'User 7')
,(8,N'User 8')
,(9,N'User 9')
,(10,N'User 10')
,(11,N'User 11')
,(12,N'User 12')
,(13,N'User 13')
,(14,N'User 14')
,(15,N'User 15')
,(16,N'User 16')
,(17,N'User 17')
,(18,N'User 18')
,(19,N'User 19')
,(20,N'User 20')
,(21,N'User 21')
,(22,N'User 22')
,(23,N'User 23')
,(24,N'User 24')
,(25,N'User 25')
,(26,N'User 26')
,(27,N'User 27')
,(28,N'User 28')
,(29,N'User 29')
,(30,N'User 30')
,(31,N'User 31')
,(32,N'User 32')
,(33,N'User 33')
,(34,N'User 34')
,(35,N'User 35')
,(36,N'User 36')
,(37,N'User 37')
,(38,N'User 38')
,(39,N'User 39')
,(40,N'User 40')
,(41,N'User 41')
,(42,N'User 42')
,(43,N'User 43')
,(44,N'User 44')
,(45,N'User 45')
,(46,N'User 46')
,(47,N'User 47')
,(48,N'User 48')
,(49,N'User 49')
,(50,N'User 50')
,(51,N'User 51')
,(52,N'User 52')
,(53,N'User 53')
,(54,N'User 54')
,(55,N'User 55')
,(56,N'User 56')
,(57,N'User 57')
,(58,N'User 58')
,(59,N'User 59')
,(60,N'User 60')
,(61,N'User 61')
,(62,N'User 62')
,(63,N'User 63')
,(64,N'User 64')
,(65,N'User 65')
,(66,N'User 66')
,(67,N'User 67')
,(68,N'User 68')
,(69,N'User 69')
,(70,N'User 70')
,(71,N'User 71')
,(72,N'User 72')
,(73,N'User 73')
,(74,N'User 74')
,(75,N'User 75')
,(76,N'User 76')
,(77,N'User 77')
,(78,N'User 78')
,(79,N'User 79')
,(80,N'User 80')
,(81,N'User 81')
,(82,N'User 82')
,(83,N'User 83')
,(84,N'User 84')
,(85,N'User 85')
,(86,N'User 86')
,(87,N'User 87')
,(88,N'User 88')
,(89,N'User 89')
,(90,N'User 90')
,(91,N'User 91')
,(92,N'User 92')
,(93,N'User 93')
,(94,N'User 94')
,(95,N'User 95')
,(96,N'User 96')
,(97,N'User 97')
,(98,N'User 98')
,(99,N'User 99')
,(100,N'User 100')
,(101,N'User 101')
,(102,N'User 102')
,(103,N'User 103')
,(104,N'User 104')
,(105,N'User 105')
,(106,N'User 106')
,(107,N'User 107')
,(108,N'User 108')
,(109,N'User 109')
,(110,N'User 110')
,(111,N'User 111')
,(112,N'User 112')
,(113,N'User 113')
,(114,N'User 114')
,(115,N'User 115')
,(116,N'User 116')
,(117,N'User 117')
,(118,N'User 118')
,(119,N'User 119')
,(120,N'User 120')
,(121,N'User 121')
,(122,N'User 122')
,(123,N'User 123')
,(124,N'User 124')
,(125,N'User 125')
,(126,N'User 126')
,(127,N'User 127')
,(128,N'User 128')
,(129,N'User 129')
,(130,N'User 130')
,(131,N'User 131')
,(132,N'User 132')
,(133,N'User 133')
,(134,N'User 134')
,(135,N'User 135')
,(136,N'User 136')
,(137,N'User 137')
,(138,N'User 138')
,(139,N'User 139')
,(140,N'User 140')
,(141,N'User 141')
,(142,N'User 142')
,(143,N'User 143')
,(144,N'User 144')
,(145,N'User 145')
,(146,N'User 146')
,(147,N'User 147')
,(148,N'User 148')
,(149,N'User 149')
,(150,N'User 150')
,(151,N'User 151')
,(152,N'User 152')
,(153,N'User 153')
,(154,N'User 154')
,(155,N'User 155')
,(156,N'User 156')
,(157,N'User 157')
,(158,N'User 158')
,(159,N'User 159')
,(160,N'User 160')
,(161,N'User 161')
,(162,N'User 162')
,(163,N'User 163')
,(164,N'User 164')
,(165,N'User 165')
,(166,N'User 166')
,(167,N'User 167')
,(168,N'User 168')
,(169,N'User 169')
,(170,N'User 170')
,(171,N'User 171')
,(172,N'User 172')
,(173,N'User 173')
,(174,N'User 174')
,(175,N'User 175')
,(176,N'User 176')
,(177,N'User 177')
,(178,N'User 178')
,(179,N'User 179')
,(180,N'User 180')
,(181,N'User 181')
,(182,N'User 182')
,(183,N'User 183')
,(184,N'User 184')
,(185,N'User 185')
,(186,N'User 186')
,(187,N'User 187')
,(188,N'User 188')
,(189,N'User 189')
,(190,N'User 190')
,(191,N'User 191')
,(192,N'User 192')
,(193,N'User 193')
,(194,N'User 194')
,(195,N'User 195')
,(196,N'User 196')
,(197,N'User 197')
,(198,N'User 198')
,(199,N'User 199')
,(200,N'User 200')
,(201,N'User 201')
,(202,N'User 202')
,(203,N'User 203')
,(204,N'User 204')
,(205,N'User 205')
,(206,N'User 206')
,(207,N'User 207')
,(208,N'User 208')
,(209,N'User 209')
,(210,N'User 210')
,(211,N'User 211')
,(212,N'User 212')
,(213,N'User 213')
,(214,N'User 214')
,(215,N'User 215')
,(216,N'User 216')
,(217,N'User 217')
,(218,N'User 218')
,(219,N'User 219')
,(220,N'User 220')
,(221,N'User 221')
,(222,N'User 222')
,(223,N'User 223')
,(224,N'User 224')
,(225,N'User 225')
,(226,N'User 226')
,(227,N'User 227')
,(228,N'User 228')
,(229,N'User 229')
,(230,N'User 230')
,(231,N'User 231')
,(232,N'User 232')
,(233,N'User 233')
,(234,N'User 234')
,(235,N'User 235')
,(236,N'User 236')
,(237,N'User 237')
,(238,N'User 238')
,(239,N'User 239')
,(240,N'User 240')
,(241,N'User 241')
,(242,N'User 242')
,(243,N'User 243')
,(244,N'User 244')
,(245,N'User 245')
,(246,N'User 246')
,(247,N'User 247')
,(248,N'User 248')
,(249,N'User 249')
,(250,N'User 250')
,(251,N'User 251')
,(252,N'User 252')
,(253,N'User 253')
,(254,N'User 254')
,(255,N'User 255')
,(256,N'User 256')
,(257,N'User 257')
,(258,N'User 258')
,(259,N'User 259')
,(260,N'User 260')
,(261,N'User 261')
,(262,N'User 262')
,(263,N'User 263')
,(264,N'User 264')
,(265,N'User 265')
,(266,N'User 266')
,(267,N'User 267')
,(268,N'User 268')
,(269,N'User 269')
,(270,N'User 270')
,(271,N'User 271')
,(272,N'User 272')
,(273,N'User 273')
,(274,N'User 274')
,(275,N'User 275')
,(276,N'User 276')
,(277,N'User 277')
,(278,N'User 278')
,(279,N'User 279')
,(280,N'User 280')
,(281,N'User 281')
,(282,N'User 282')
,(283,N'User 283')
,(284,N'User 284')
,(285,N'User 285')
,(286,N'User 286')
,(287,N'User 287')
,(288,N'User 288')
,(289,N'User 289')
,(290,N'User 290')
,(291,N'User 291')
,(292,N'User 292')
,(293,N'User 293')
,(294,N'User 294')
,(295,N'User 295')
,(296,N'User 296')
,(297,N'User 297')
,(298,N'User 298')
,(299,N'User 299')

SELECT 
    CI.ContactId,
    I.Ordinal,
    I.Identifier
FROM    #identifiers I
JOIN    part.ContactIdentifier AS CI ON CI.CustomerId = @CustomerId AND CI.CustomerPartitionKey = @CustomerPartitionKey AND 
                                        CI.Identifier = I.Identifier
JOIN    part.Contact AS C ON C.CustomerPartitionKey = @CustomerPartitionKey AND C.ContactId = CI.ContactId
WHERE   C.OptOut = 0

DROP TABLE #identifiers

不適切な実行計画:http : //pastebin.com/Us7HY4KF


私はこれを実行しました。問題はあまりわかりません。クエリされた300人の顧客ごとに1つのポイントルックアップがあります。これは非常に効率的です。代わりに正確に何が欲しいですか?パーティションを削除しただけで、同じプランが得られました(予想どおり)。これはSQL 2014 EEです。望ましくない計画を投稿します。
usr

はい、問題はSQL 2014の新しいCardinality Estimatorによって修正されたようです。ただし、OPTION(QUERYTRACEON 9481)を使用して古いCardinality Estimatorを使用すると、SQL 2014 EEで問題を再現できるはずです。
Geoff Patterson

テストで許容可能なクエリパフォーマンスを示すのに十分なプランを生成できる場合は、別の可能なソリューションとしてプランガイドを使用できます。
キンシャー

回答:


4

SQL Serverは、@ CustomerPartitionKeyの任意の値で機能するパラメーター化されたクエリプランを生成しているようです。そのためには、@ CustomerPartitionKeyをパーティションと、求めている列の両方として扱うようです。見積もりが不適切なクエリプラン(3000行の見積もり、300000実際の見積もり)を見ると、にpart.Contact関連する2つの個別のシーク述語が実際にあることがわかります@CustomerPartitionKey

Seek Keys[1]: Prefix: PtnId1004, [Test].[part].[Contact].CustomerPartitionKey = Scalar Operator([Expr1008]), Scalar Operator([@CustomerPartitionKey])

後者は([Test].[part].[Contact].CustomerPartitionKey = Scalar Operator([@CustomerPartitionKey])の値のパラメータースニッフィングに基づいて適切な推定値を取得できると思います@CustomerPartitionKey。ただし、前者の(Prefix: PtnId1004 = Scalar Operator([Expr1008]))は、おそらくExpr1008パーティションエリミネーションを処理する複雑な式であるため、そうすることができません[Expr1008]=RangePartitionNew([@CustomerPartitionKey],(0),(0),(1),(2),...,(97),(98))

この場合、100のパーティションがあり、SQL Serverは列の実際のシークを処理し、その推定値を使用するのと同じスマートな方法でパーティションの削除を処理できないため、行の推定値は100倍になります。ランタイムパラメータ値は3です。この理論は、パーティションを削除した場合に推定される行が変化する方法によってサポートされます。代わりに90パーティションを使用する場合、見積もりは3333.33(300000/90)になります。

独自のクエリでは、通常、リテラル(3この例では)を使用するか、パーティションの削除を利用するクエリを作成するときにOPTION RECOMPILEを使用します。システム上のクエリの数が少なく、大きなパーティションテーブルに対するクエリのクエリコンパイルのオーバーヘッドが問題にならないことを考えると、この方法はかなりうまくいきました。必ずしも満足のいく答えではありませんが、それはあなたのために働くかもしれません。


はい、リテラルパーティションIDを使用すると、適切な計画が強制されることに注目してください。私たちの場合、システムの性質上、実際には実行できませんが、オプションの再コンパイルが実行プランを正しい形式に強制するための信頼できる方法であることを知ることは非常に便利です。この段階での再コンパイルのオーバーヘッドについてはあまり気にしていません。応答速度が最大の関心事です。
Simon Capewell

3

悪い計画を再現できます。3つの回避策が見つかりました。

  1. OPTION (RECOMPILE)
  2. INNER LOOP JOIN ヒント
  3. 厄介でクレイジーな書き直し:

SELECT y.*
FROM (VALUES (@CustomerPartitionKey)) x(CustomerPartitionKey)
CROSS APPLY (
    SELECT --TOP 300
        CI.ContactId,
        I.Ordinal,
        I.Identifier
    FROM    #identifiers I
    INNER  JOIN    part.ContactIdentifier AS CI ON CI.CustomerId = @CustomerId AND CI.CustomerPartitionKey = x.CustomerPartitionKey AND 
                                            CI.Identifier = I.Identifier
    JOIN    part.Contact AS C ON C.CustomerPartitionKey = @CustomerPartitionKey AND C.ContactId = CI.ContactId
    WHERE   C.OptOut = 0
) y
WHERE x.CustomerPartitionKey <> 0
OPTION (QUERYTRACEON 9481 /*2012 estimator*/)

書き直しは、Adam Machanicの講演「Manhandling parallelism」に触発されました。クエリ全体が「ドライバーループ」(CROSS APPLY)でラップされます。意味的に無意味なWHERE句が必要です。私はそれがSQL Serverがそうしないだろういくつかの単純化を妨害するのではないかと思います。

悲しいかな、真の洞察はありません。ランダムなことを試してみました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.