web-dev-qa-db-ja.com

複数の結合とフィルターを使用してレコードをランダムに取得するクエリの最適化

次のスキーマがあります:

DB Schema

この質問はStackOverflowにも投稿されましたが、プロジェクトの性質上、DB管理に重点を置いた専門家にも相談したいと思います。これが間違いならすみません

現在、テーブルPropertyには70Kを超えるレコードが保持されています。 500を超える同時セッションをサポートするアップデートを開発しています。アプリケーションはマップaをサポートして検索を行います。そのため、GeoLocationCoordinategeographyデータ型として宣言します。一部のクエリ(最も重要なクエリ)の応答時間が非常に遅いため、ここで大きな問題が発生しました。つまり、指定されたパラメーターに対してその数の結果がある場合、アプリケーションは一度に約1000レコードを返す必要があります。

パラメータはスキーマのすべてのテーブルに配布されます(実際には、スキーマの一部です)。 Featuresであることは、プロパティのすべての主要な「特性」(寝室の数、ガレージの数など)を保持するテーブルです。

このことを念頭に置いて、現在非常に時間がかかっているクエリは次のとおりです。

DECLARE @cols NVARCHAR(MAX), @query NVARCHAR(MAX);

DECLARE @properties TABLE(
    [ID] INT
)

INSERT INTO @properties
    SELECT p.[Id]
    FROM[Property] p
    INNER JOIN[GeoLocation] AS[g] 
        ON[p].[Id] = [g].[PropertyId]
    INNER JOIN[PropertyFeature] AS[pf] 
        ON[pf].[PropertyId] = [p].[Id]
    INNER JOIN[Feature] AS[f] 
        ON[pf].[FeatureId] = [f].[Id]
    WHERE[g].[Address] IS NOT NULL AND(([g].[Address] <> N'') OR[g].[Address] IS NULL)
        AND[pf].[FeatureId] IN(
            Select ID from feature where featuretype = 1)
    GROUP BY p.Id, p.ModificationDate
    ORDER BY [p].ModificationDate DESC, newid()
    OFFSET 0 ROWS
    FETCH NEXT 1000 ROWS ONLY

DECLARE @features TABLE(
    [Name] NVARCHAR(80)
)

INSERT INTO @features
    select Name from feature where FeatureType = 1

CREATE TABLE #temptable
(
    Id INT,
    Url NVARCHAR(200),
    Title NVARCHAR(300),
    Address NVARCHAR(200),
    Domain Tinyint,
    Price Real,
    Image NVARCHAR(150), 
    Name NVARCHAR(80),
    Value NVARCHAR(150)
)

INSERT INTO #temptable
SELECT
    [t].[Id], 
    [t].[Url], 
    [t].[GeneratedTitle] AS[Title], 
    [t].[Address], 
    [t].[Domain], 
    [t].[Price],
    (SELECT TOP(1) ISNULL([m].[Resize1200x1200], [m].Resize730x532)
     FROM [Multimedia] AS[m]
     WHERE [t].[Id] = [m].[PropertyId]
        and m.MultimediaType = 1
     ORDER BY [m].[Order]) AS[Image], 
    [t].[Name], 
    [t].[Value]
FROM
    (SELECT
        [p].[Id],
        [p].[Url],
        [p].[GeneratedTitle],
        [g].[Address],
        [p].[Domain],
        [pr].[Amount] AS Price,
        [p].[ModificationDate],
        [f].[Name],
        [pf].[Value]
    FROM [Property] AS [p]
    INNER JOIN [GeoLocation] AS[g] 
        ON [p].[Id] = [g].[PropertyId]
    INNER JOIN [PropertyFeature] AS[pf] 
        ON [pf].[PropertyId] = [p].[Id]
    INNER JOIN [Feature] AS[f] 
        ON [pf].[FeatureId] = [f].[Id]
    INNER JOIN [Operation] AS [o] 
        ON [p].[Id] = [o].[PropertyId] 
    INNER JOIN [OperationType] AS [o0] 
        ON [o].[OperationTypeId] = [o0].[Id] 
    INNER JOIN [Price] AS [pr] 
        ON [pr].[OperationId] = [o].[Id] 
    WHERE p.Id in 
        (Select Id from @properties)
    GROUP BY [p].[Id], 
             [p].[Url],
             [p].[GeneratedTitle], 
             [g].[Address],
             [p].[Domain], 
             [pr].[Amount],
             [p].[ModificationDate],
             [f].[Name],
             [pf].[Value]) AS[t]
    ORDER BY[t].[ModificationDate] DESC

SET @cols = STUFF(
                (
                    SELECT DISTINCT
                            ','+QUOTENAME(c.[Name])
                    FROM @features c FOR XML PATH(''), TYPE
                 ).value('.', 'nvarchar(max)'), 1, 1, '');
SET @query = 'SELECT [Id], 
                     [Url], 
                     [Title], 
                     [Address], 
                     [Domain], 
                     [Price], 
                     [Image], 
                     ' + @cols + '
               FROM (SELECT [Id], 
                            [Url], 
                            [Title], 
                            [Address], 
                            [Domain], 
                            [Price], 
                            [Image], 
                            [Value] AS [value], 
                            [Name] AS[name] 
                     FROM #temptable)x 
                     PIVOT(max(value) for name in ('+@cols+')) p';
EXECUTE(@query);

DROP TABLE #temptable

実行プランとライブクエリの統計から、次のことがわかります。

クエリ実行プラン

前のクエリは、X個のレコードIDをランダムに取得しようとし、すべてのフィルター基準を保持して、その基準を満たすレコードのIDのみを取得します。現在の時間は最大15秒です。アプリケーションを同時に使用している400人を超えるユーザーについて話すと、多くのことになります。

これで私を助けてください。私はこの問題を解決するために3週間成功していませんが、多くの進歩がありました(平均で2分を消費する前)。

それが役立つ場合は、同じ量のレコードを使用してDBの「ダミー」デプロイバージョンにアクセスし、問題を直接テストして確認できます。

前もって感謝します...

================================================== ================================================== =INDEXES:

現在テーブルにあるインデックスは次のとおりです。

GO
CREATE UNIQUE NONCLUSTERED INDEX IX_Property_ModificationDate 
ON [dbo].[Property] (ModificationDate DESC) 
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON)

GO
CREATE NONCLUSTERED INDEX [IX_Property_ParentId_StatusCode] 
ON [dbo].[Property] ([ParentId] ASC, [StatusCode] ASC)
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON);

GO
CREATE NONCLUSTERED INDEX [IX_Property_ParentId_StatusCode_Id_ModificationDate] 
ON [dbo].[Property] ([ParentId] ASC, [StatusCode] ASC, [Id] ASC, [ModificationDate] ASC)
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON);

GO
CREATE NONCLUSTERED INDEX [IX_Property_ParentId]
    ON [dbo].[Property]([ParentId] ASC)
    WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON);

    GO
CREATE NONCLUSTERED INDEX [IX_Property_Identity_Domain_StatusCode]
    ON [dbo].[Property]([Identity] ASC, [Domain] ASC, [StatusCode] ASC)
    WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON);

GO
CREATE NONCLUSTERED INDEX [IX_Property_Id_ModificationDate] 
ON [dbo].[Property] (Id ASC, ModificationDate ASC)
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON);

GO
CREATE NONCLUSTERED INDEX [IX_Property_PublisherId]
    ON [dbo].[Property]([PublisherId] ASC)
    WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON);


GO
CREATE NONCLUSTERED INDEX [IX_Property_RealEstateTypeId]
    ON [dbo].[Property]([RealEstateTypeId] ASC)
    WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON)


GO

CREATE INDEX FIX_Property_StatusCode_Online ON [dbo].[Property](StatusCode) WHERE StatusCode = 1
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON)
GO

CREATE INDEX FIX_Property_StatusCode_Offline ON [dbo].[Property](StatusCode) WHERE StatusCode = 0
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON)
GO

CREATE INDEX FIX_Property_Domain_Urbania ON [dbo].[Property](Domain) WHERE Domain = 1
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON)
GO

CREATE INDEX FIX_Property_Domain_Adondevivir ON [dbo].[Property](Domain) WHERE Domain = 2
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON)
GO

GO
CREATE NONCLUSTERED INDEX [IX_GeoLocation_PropertyId_ModificationDate] 
ON [dbo].[GeoLocation] (PropertyId ASC, [ModificationDate] DESC)
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON);

GO
CREATE NONCLUSTERED INDEX [IX_GeoLocation_PropertyId_Address] 
ON [dbo].[GeoLocation] (PropertyId ASC, [Address] ASC)
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON);

GO
CREATE UNIQUE NONCLUSTERED INDEX IX_GeoLocation_ModificationDate 
ON [dbo].[GeoLocation] (ModificationDate DESC) 
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON)
GO

CREATE NONCLUSTERED INDEX [IX_GeoLocation_Ubigeo]
ON [dbo].[GeoLocation]([Ubigeo] ASC)
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON)

GO
CREATE UNIQUE NONCLUSTERED INDEX [IX_GeoLocation_PropertyId]
    ON [dbo].[GeoLocation]([PropertyId] ASC)
    WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON)
GO

CREATE SPATIAL INDEX SIX_GeoLocation_Coordinate ON [dbo].[GeoLocation](Coordinate)
GO

CREATE INDEX FIX_GeoLocation_Domain_Urbania ON [dbo].[GeoLocation](Domain) WHERE Domain = 1
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON)
GO

CREATE INDEX FIX_GeoLocation_Domain_Adondevivir ON [dbo].[GeoLocation](Domain) WHERE Domain = 2
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON)
GO

GO
CREATE NONCLUSTERED INDEX [IX_Multimedia_PropertyId_Order] 
ON [dbo].[Multimedia] (PropertyId ASC, [Order] ASC)
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON);

GO
CREATE NONCLUSTERED INDEX [IX_Multimedia_PropertyId]
    ON [dbo].[Multimedia]([PropertyId] ASC)
    WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON);

GO
CREATE NONCLUSTERED INDEX [IX_Multimedia_Order]
    ON [dbo].[Multimedia]([Order] ASC)
    WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON);
GO

CREATE NONCLUSTERED INDEX [PK_Multimedia_Property]
    ON [dbo].[Multimedia]([Id] ASC, [PropertyId] ASC);
GO

CREATE INDEX FIX_Multimedia_MultimediaType_Image ON [dbo].[Multimedia](MultimediaType) WHERE MultimediaType = 1
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON)
GO

GO
CREATE NONCLUSTERED INDEX [IX_PropertyFeature_PropertyId_FeatureId] 
ON [dbo].[PropertyFeature] (PropertyId ASC, [FeatureId] ASC)
WITH( SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, FILLFACTOR = 90, ONLINE = ON);

GO
CREATE NONCLUSTERED INDEX [IX_PropertyFeature_FeatureId]
    ON [dbo].[PropertyFeature]([FeatureId] ASC);


GO
CREATE NONCLUSTERED INDEX [IX_PropertyFeature_PropertyId]
    ON [dbo].[PropertyFeature]([PropertyId] ASC);


GO
CREATE NONCLUSTERED INDEX [IX_PropertyFeature-FeatureId]
    ON [dbo].[PropertyFeature]([Id] ASC, [FeatureId] ASC);


GO
CREATE NONCLUSTERED INDEX [IX_PropertyFeature_Property]
    ON [dbo].[PropertyFeature]([Id] ASC, [PropertyId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_Operation_PropertyId]
    ON [dbo].[Operation]([PropertyId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_Operation_OperationTypeId]
    ON [dbo].[Operation]([OperationTypeId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_Price_OperationId]
    ON [dbo].[Price]([OperationId] ASC);

GO
CREATE NONCLUSTERED INDEX [IX_Price_Operation]
    ON [dbo].[Price]([Id] ASC, [OperationId] ASC);

UPDATE:使用しているAzure SQL Serverサービス層は標準S0(10 DTU)です

スタンダード層(S0)にいると、クエリが大幅に抑制され、全体のランタイムに影響します。これは、Sentry One Plan Explorerで表示される、バッチ内の5つのステートメントすべての時間です。

Screenshot of compile time, CPU time, and duration for all 5 statements

ご覧のとおり、ほとんどすべてのクエリの継続時間はCPU時間よりもはるかに長くなっています。これは、クエリがリソースを待機していることを意味します。中央のクエリを見てみると、XMLでこれらの待機統計を確認できます。

<WaitStats>
  <Wait WaitType="SOS_SCHEDULER_YIELD" WaitTimeMs="939" WaitCount="20" />
  <Wait WaitType="RESOURCE_GOVERNOR_IDLE" WaitTimeMs="858" WaitCount="61" />
</WaitStats>

クエリは基本的に、CPUでスケジュールされるのを待機していた期間全体を費やしました。これらの待機の詳細は SQLスキル待機タイプライブラリ で確認できます。

"QueryPlan"要素の統計から明らかなように、クエリイベントはコンパイルするのに長時間かかります。

<QueryPlan ... CompileTime="711" CompileCPU="67" ... >

5つのステートメントすべてに同様の特性(高いリソース待機、低いCPU)があります。


クエリに加えることができるいくつかの改善があります。たとえば、@propertiesのテーブル変数を使用しているため、真ん中のクエリには不適切な見積もりがあり、最初から理想的でないプランが選択されます(#tempテーブルは、この状況)。

ただし、すべての待機がなければ、このクエリのバッチ全体は1秒未満で実行されます。したがって、より多くのハードウェアを使用できるようになるまで、クエリとインデックスのチューニングは役に立ちません。

2
Josh Darnell