web-dev-qa-db-ja.com

大きなテーブルで毎回ほとんど変更されないデータに対してクエリを実行する代わりの方法

私は内部Webアプリケーションを実行していて、ユーザーが「検索」ビューにアクセスするたびに、データベース内の3つの異なるテーブルにクエリを実行して、ビューの3つのドロップダウンの値を生成します。

基本的には

SELECT DISTINCT (PortName)
FROM Ports
ORDER BY PortName ASC

しかし、テーブルには〜10'000'000行が含まれており、負荷が非常に高いため、ページの読み込み時間(ドロップダウンにデータが読み込まれるため)が10-15秒以上になる場合があります。

したがって、これを行うにはより良い方法があります。たとえば、一定の間隔でスクリプトを実行し、別の場所にテーブル/ビュー/その他を作成して、大きなテーブルへのクエリをオフロードして、10000から80行が返されるようにします。メインテーブルの '000?

6
JaggenSWE

どういうわけか、誰も indexed view について言及していません。インデックス付きビューの簡単な紹介は、 インデックス付きビューでできること(およびできないこと) にあります。

本質的にはキャッシュであり、エンジンによってバックグラウンドで自動的に維持されます。インデックス付きビューはディスクに保存され、基になるテーブルが変更されると自動的に更新されます。

したがって、メインテーブルへの更新、削除、および挿入は多少遅くなりますが、インデックス付きビューのクエリはメインテーブルの10M行をスキャンしないため、瞬時に行われます。いずれの場合でも、エンジンは、インデックス付きビューに格納されている値を調整するために更新されたときに10M行テーブル全体をスキャンしないように十分にスマートです。

さらに、質問のタイトルは「ほとんど変更されないデータに対してクエリを実行する代替手段」と言っているので、この大きなテーブルはそれほど頻繁に変更されないと思います。ここでは、インデックス付きビューが最適だと思います。

インデックス付きビューにDISTINCTを含めることはできませんが、クエリは次のようにせずに書き換えることができます。

_SELECT PortName, COUNT_BIG(*) AS cc 
FROM Ports 
GROUP BY PortName
_

インデックス付きビューに_GROUP BY_が含まれている場合、COUNT_BIG(*)が必要なので、追加しました。

9

DISTINCTから、PortNamesがテーブルで重複しており、1,000万の異なるポート名が返されていないと想定しています。

最小限の労力の解決策は、その列にインデックスを配置することです。

CREATE INDEX IX_Ports_PortName ON Ports(PortName);

もちろん、これとストレージのオーバーヘッドによるDBの負荷はまだあります。そのため、Aaron Bertrandが彼の回答で十分にカバーしている、Cachingなどのより高度なソリューションが必要になる場合があります。

さらに正規化を使用することもできます。ポート名が重複していて、それらを明確に知ることが重要である場合は、[PortNames]テーブルを作成し、[Ports]テーブルでPortNameIDを使用できます。そうすれば、[PortNames]テーブルをスキャンするだけで、おそらくはるかに小さくて高速になります。もちろん、それには追加のコストと独自の考慮事項があります。

11
RBarryYoung

頻繁に変更されないデータの場合は、これらのクエリが進むキャッシュレイヤーを使用できます。 [memcached]のような多くの代替手段があり、多くの議論がすでに存在しています:

また、自分でこれを非常に簡単に行うことができ、データのスコープとサイズによっては、安価でそれを行うことができます。私は前の人生でこの種のことをしました。そこでは、SQL Server Expressインスタンスを各アプリ/ Webサーバーに配置し、それらのインスタンスのデータを最小限の中断で定期的にスワップアウトする独自のスクリプトを書きました。これにより、すべての負荷の高い読み取りアクティビティがプライマリインスタンスから切り離され、キャッシュされたデータのコピーが(更新ジョブの頻度を変更するだけで)古くなる可能性のある柔軟性も提供されました。私はこのプロセスについてここに書きました:

もう1つの方法は、ログ配布を使用して貧しい人の可用性グループを実装することです。基本的に、一連のログ配布ターゲットがあり、それらを循環して最新のログをスケジュールどおりに復元します。動的アプリは、取得する次の読み取りリクエストに使用するターゲットを認識しています。私はそのプロセスについてここに書きました:

データが10GBより大きい場合、または将来的にそれを超える場合、Expressは機能せず、少なくともStandard Editionを使用する必要があります。しかし、このタイプの操作では、スケール[〜#〜] out [〜#〜]を一般的なハードウェアに読み取りますが、コアを増やすよりもはるかに安価です。スケーリングするプライマリサーバーの/ memory/disk[〜#〜] up [〜#〜]

読み取りと書き込みを分離することが主な目的ではない場合、この非常に特殊なケースでは、インデックス付きビューなどの他のローカルソリューションを使用できます。オーバーヘッドが発生することを覚えておいてください。データが複製される頻度を調整する(したがって、読み取りコピーの古さを調整する)など、柔軟に対応することはできません。他のクエリシナリオは、インデックス付きビューには適していません。

10
Aaron Bertrand

これは、各エントリにポートがあるテーブルがあるようですが、ポートの小さなプールからのみです。この場合、通常、すべてのポートを1つ含む2つ目のテーブルを作成し、外部キーを介してリンクすることをお勧めします。次に、このはるかに小さいテーブルをクエリできます。

また、2番目のテーブルの既存の行にリンクする必要があるため、ポートのスペルが間違っている行を挿入することもできません。

ただし、データベースアーキテクチャを変更したくない場合は、ポートの名前だけで新しいテーブルを作成できます(例:CREATE TABLE portnames (name varchar(50));)。次に、最初のテーブル(INSERT INTO portnames (SELECT DISTINCT PortName FROM Ports);)のコンテンツを入力します。代わりに、このテーブルをクエリできます!更新したままにする場合は、最初のテーブルにエントリを追加するたびに再作成(または切り捨て/挿入)する必要があります。

3
Flourid

80個の異なる値のみを返すクエリは、適切なインデックスと適切な物理実装戦略を使用して、ほぼ瞬時に終了できます。クエリの実行時間は、テーブルのサイズではなく、個別の値の数によって決まります。

まず、テーブルに1000万行をスローします。

DROP TABLE IF EXISTS dbo.[Ports];

CREATE TABLE dbo.[Ports] (
    PortName VARCHAR(100),
    Filler VARCHAR(200)
);

INSERT INTO dbo.[Ports] WITH (TABLOCK)
SELECT TOP (10000000)
'Port ' + CAST(ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 80 AS VARCHAR(2))
, REPLICATE('Z', 200)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3
OPTION (MAXDOP 1);

CREATE INDEX IX_PortName ON [Ports] (PortName);

私がこのようなクエリを書いた場合:

SELECT DISTINCT (PortName)
FROM [Ports]
ORDER BY PortName ASC;

次の計画で私のマシンで完了するには、1329 msのCPUが必要です。

bad plan

そのクエリプランは必ずしも悪いわけではありませんが、SQL Serverは80の異なる行を返すためだけに1000万行すべてをスキャンしています。利用可能なより効率的なアルゴリズムがありますが、それを得るには多少の努力が必要です。インデックスから最小値を取得するのは非常に高速です。インデックスから次の値を取得するのも非常に高速です。どちらも、ほんの一握りの論理読み取りです。インデックス全体を読み取る代わりに、個別の値だけを効果的に読み取って次の値にスキップできるとしたらどうでしょうか。

これは、再帰的SQLで実行できます。ポールホワイトはアプローチ here について説明しています。以下は、テーブルに対して作成されたT-SQLです。

WITH RecursiveCTE
AS
(
    -- Anchor
    SELECT TOP (1)
        PortName
    FROM dbo.[Ports] AS T
    ORDER BY
        T.PortName

    UNION ALL

    -- Recursive
    SELECT R.PortName
    FROM
    (
        -- Number the rows
        SELECT 
            T.PortName,
            rn = ROW_NUMBER() OVER (
                ORDER BY T.PortName)
        FROM dbo.[Ports] AS T
        JOIN RecursiveCTE AS R
            ON R.PortName < T.PortName
    ) AS R
    WHERE
        -- Only the row that sorts lowest
        R.rn = 1
)
SELECT *
FROM RecursiveCTE
OPTION (MAXRECURSION 0);

私のマシンでは、クエリのCPU時間は0 msです。 IOも非常に低いです:

テーブル「ポート」。スキャンカウント81、論理読み取り246、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。

テーブルの行数を1億に増やすと、元のクエリのCPU時間は17203 msになります。再帰クエリには、CPU時間の0ミリ秒がかかります。インデックスを作成し、いくつかの凝ったコードを書くだけで、クエリのパフォーマンスの問題を解決できます。この場合、他のレイヤーにキャッシュを実装する必要はありません。

1
Joe Obbish

Flourid、RBarryYoung、およびDForck42による他のコメントと回答は、これが正規化の問題のように見えることをすでに強調しています。数百万の行に対してクエリを実行して、80個の一意のレコードを抽出することはできません。最初にデータを正規化して、80行からドロップダウンにデータを入力できるようにします。次に、必要に応じて、インデックスをパフォーマンス強化として適用します。次に、キャッシュを適用して、必要に応じてドロップダウンをキャッシュから入力できるようにします。

とはいえ、これらの変更を実装するためのスキーマ(正規化)またはインフラストラクチャ(キャッシュ)を制御できない場合は(そして、これがコメントではなく回答である理由)、sys.dm_db_index_usage_stats.last_user_updateを確認してください。これは、テーブルの最後の挿入/更新/削除を確認するために使用できる比較的安価なクエリである必要があります。高価なクエリを安価なクエリでラップして、データが変更された場合にのみテーブルから更新するようにします。それは実質的に貧乏人のキャッシュです。

注:サーバーの再起動時に、最初の挿入/更新/削除が発生するまでlast_user_updateを保持しないというEdgeケースがあります。

1
Doug

クエリの実行には10〜15秒かかる可能性があると述べましたが、「特定の間隔でスクリプトを実行して...クエリをオフロードする」ことをお勧めします。これは、2つの非常に異なる要件を満たすことで際立っています。

  • 現在のソリューションは、UIでリアルタイムデータを提供します
  • 提案により、UIでデータを遅延させることができます

要件、具体的には新しい個別のPortNameがUIドロップダウンに表示されるためのターゲットレイテンシを決定する必要があります。

  • 「リアルタイム」の場合は、同期クエリのバリエーションが必要です。他の回答はクエリのパフォーマンスの最適化をカバーしていますが、UIは常に最良のクエリの最適化と同じくらい遅くなります。

  • 「X秒」の場合は、内部Webサイトをバッキングするコードを変更して、非同期クエリを使用する必要があります。データベースからではなく、アプリケーション変数からUIを設定します。バックグラウンドタスクを使用して、目的のレイテンシに基づいて変数を更新します。タスクトリガーには次のものがあります。

    • データが古くなった場合にのみデータを更新する単純なチェック(10〜15秒の遅延が発生しますが、古くなるまでX秒ごとに1回のみ)
    • x秒ごとのタイマー(継続的なリフレッシュでデータベースを圧迫してはならないため、Xが大きいと想定)
    • 上記の両方に加えて、誰かがすぐにクエリページに移動する可能性がある他のアプリケーションのヒント(ログイン、ページの読み込み、朝のレポートのラッシュアワーなど)に基づいたより洗練されたもの

    提供される他の回答を使用してクエリを最適化し、おそらくwhere句を含めて、最後の更新以降に追加された新しいレコードのみを検査して、結果をアプリケーションコードにマージする必要があります。

    冒とくは、データベースのフォーラムでアプリケーションの変更を提案します...しかし、最終的なデータベースの最適化は、データベースをまったく使用しないことです。

0
Doug

正規化は答えのように見えますが、コメントがあります。「正規化に関する主な問題は、残念ながら、このプロジェクトでデータベース構造自体を変更する権限がないことです。これは、基本的に、別のシステムからの複製されたインデックス付きバージョンです。」– "

したがって、誰かがデータをコピーしてテーブルにダンプしました。しかし、なぜそれを維持する必要があるのでしょうか。これはきちんとした小さなプロジェクトです。チームとチームに腰を下ろし、どのテーブルとリレーションシップがユーザーに最適かを話し合ってください。次に、インポートしたテーブルのコピーをクリーンアップしてください。

クリーンなコピーを作成したら、決定したテーブルにデータをインポートします。テスト環境をセットアップし、新しいテーブルを徹底的にテストし、インデックスなどを使用して微調整します。テストが成功したら、新しいセットアップをテストするか、古いセットアップを使い続ける機会をユーザーに提供します。ユーザーの大多数が新しいデータを好んだら、古いテーブルの使用を中止しますが、そのままにしておきます。

他のシステムが更新を行ったことを通知されたら、古いテーブルを使用して変更を特定し、新しいデータ構造に追加します。

0
Elise van Looij