web-dev-qa-db-ja.com

SQL Serverにデータベース全体をメモリに事前キャッシュさせる

100 Gb以上のRAMを搭載したサーバー上に50 Gb SQL 2012データベースを持つクライアントサイトがあります。

アプリケーションを使用すると、SQLサーバーはdbをメモリにキャッシュするという素晴らしい仕事をしますが、キャッシュによるパフォーマンスの向上は、最初ではなく、2番目のクエリの実行時に発生します。

クエリの初回実行時にキャッシュヒットを最大化するために、DB全体のすべてのテーブルのすべてのインデックスを反復処理するプロシージャを作成し、これを実行します。

SELECT * INTO #Cache 
FROM ' + @tablename + ' WITH (INDEX (' + @indexname + '))'

可能な限り多くのデータに対して、大きくてugい、不自然な読み取りを強制する試み。 15分ごとに実行するようにスケジュールされており、一般的に素晴らしい仕事をしています。

他のボトルネック、ハードウェアの仕様、クエリプラン、クエリの最適化について議論することなく、誰もこの同じタスクを達成する方法についてより良いアイデアを持っていますか?

更新
提案をありがとう。 「INTO #Cache」を削除しました。テスト済みであり、バッファの充填に関して違いはありませんでした。
追加:Select *の代わりに、Indexからキーのみを選択しています。これは(明らかに)より正確であり、はるかに高速です。
追加:制約インデックスの読み取りとキャッシュも行います。

現在のコードは次のとおりです:(他の人に役立つことを願っています)

CREATE VIEW _IndexView
as
-- Easy way to access sysobject and sysindex data
SELECT 
so.name as tablename,
si.name as indexname,
CASE si.indid WHEN 1 THEN 1 ELSE 0 END as isClustered,
CASE WHEN (si.status & 2)<>0 then 1 else 0 end as isUnique,
dbo._GetIndexKeys(so.name, si.indid) as Keys,
    CONVERT(bit,CASE WHEN EXISTS (SELECT * FROM sysconstraints sc WHERE object_name(sc.constid) = si.name) THEN 1 ELSE 0 END) as IsConstraintIndex
FROM    sysobjects so
INNER JOIN sysindexes si ON so.id = si.id
WHERE   (so.xtype = 'U')--User Table
AND     ((si.status & 64) = 0) --Not statistics index
AND (   (si.indid = 0) AND (so.name <> si.name) --not a default clustered index
        OR
        (si.indid > 0)
    )
AND si.indid <> 255 --is not a system index placeholder

UNION
SELECT 
so.name as tablename,
si.name as indexname,
CASE si.indid WHEN 1 THEN 1 ELSE 0 END as isClustered,
CASE WHEN (si.status & 2)<>0 then 1 else 0 end as isUnique,
dbo._GetIndexKeys(so.name, si.indid) as Keys,
CONVERT(bit,0) as IsConstraintIndex
FROM    sysobjects so
INNER JOIN sysindexes si ON so.id = si.id
WHERE   (so.xtype = 'V')--View
AND     ((si.status & 64) = 0) --Not statistics index
GO


CREATE PROCEDURE _CacheTableToSQLMemory
@tablename varchar(100)
AS
BEGIN
DECLARE @indexname varchar(100)
DECLARE @xtype varchar(10)
DECLARE @SQL varchar(MAX)
DECLARE @keys varchar(1000)

DECLARE @cur CURSOR
SET @cur = CURSOR FOR
SELECT  v.IndexName, so.xtype, v.keys
FROM    _IndexView v
INNER JOIN sysobjects so ON so.name = v.tablename
WHERE   tablename = @tablename

PRINT 'Caching Table ' + @Tablename
OPEN @cur
FETCH NEXT FROM @cur INTO @indexname, @xtype, @keys
WHILE (@@FETCH_STATUS = 0)
BEGIN
        PRINT '    Index ' + @indexname
        --BEGIN TRAN
            IF @xtype = 'V'
                SET @SQL = 'SELECT ' + @keys + ' FROM ' + @tablename + ' WITH (noexpand, INDEX (' + @indexname + '))' --
            ELSE
                SET @SQL = 'SELECT ' + @keys + ' FROM ' + @tablename + ' WITH (INDEX (' + @indexname + '))' --

            EXEC(@SQL)
        --ROLLBACK TRAN
        FETCH NEXT FROM @cur INTO @indexname, @xtype, @keys
END
CLOSE @cur
DEALLOCATE @cur

END
GO
26
El Mark

まず第一に、魅力的なように見える「最小サーバーメモリ」と呼ばれる設定があります。それを無視します。 MSDNから

データベースエンジンによって取得されるメモリの量は、インスタンスにかかるワークロードに完全に依存します。多くの要求を処理していないSQL Serverインスタンスが、最小サーバーメモリに到達することはありません。

これにより、最小メモリを大きく設定しても、事前キャッシュが強制されたり、推奨されたりすることはありません。 これを設定する他の理由 がありますが、バッファプールを事前に埋めることはそれらの1つではありません。

それでは、データをプリロードするために何ができますか?それは簡単です。エージェントジョブをセットアップしてselect *すべてのテーブルから。 「SQLエージェントの起動時に自動的に起動する」ようにスケジュールできます。言い換えれば、あなたがすでにやっていることは、これを処理する標準的な方法にかなり近いということです。

ただし、3つの変更を提案する必要があります。

  1. 一時テーブルを使用しようとしないでください。テーブルから選択するだけです。 Sql Serverにバッファープールを読み込ませるために、結果を何もする必要はありません。選択するだけです。一時テーブルは、ロード後にsqlサーバーにバッファプールからデータをコピーさせることができます...(簡単に)ものを保存することになりますtwice
  2. これを15分ごとに実行しないでください。起動時に一度実行してから、そのままにしておきます。割り当てられると、SQL Serverがメモリを解放するのに多くの時間がかかります。これを何度も再実行する必要はありません。
  3. インデックスをヒントしようとしないでください。ヒントはそれだけです:ヒント。 SQL Serverは、これらのヒントを自由に無視できます。また、インデックスを明確に使用していないクエリの場合は無視します。インデックスがプリロードされていることを確認する最善の方法は、明らかにそのインデックスを使用するクエリを作成することです。ここでの具体的な提案の1つは、インデックスと同じ順序で結果を並べることです。これは、SQL Serverがそのインデックスを使用するのに役立ちます。これは、「インデックスをたどって」結果を生成できるためです。
19
Joel Coehoorn

これは答えではありませんが、Joel Coehoornの答えを補足するために、このステートメントを使用してキャッシュ内のテーブルデータを調べることができます。これを使用して、すべてのページが期待どおりにキャッシュに残っているかどうかを判断します。

USE DBMaint
GO
SELECT COUNT(1) AS cached_pages_count, SUM(s.used_page_count)/COUNT(1) AS total_page_count,
name AS BaseTableName, IndexName,
IndexTypeDesc
FROM sys.dm_os_buffer_descriptors AS bd
INNER JOIN
(
SELECT s_obj.name, s_obj.index_id,
s_obj.allocation_unit_id, s_obj.OBJECT_ID,
i.name IndexName, i.type_desc IndexTypeDesc
FROM
(
SELECT OBJECT_NAME(OBJECT_ID) AS name,
index_id ,allocation_unit_id, OBJECT_ID
FROM sys.allocation_units AS au
INNER JOIN sys.partitions AS p
ON au.container_id = p.hobt_id
AND (au.type = 1 OR au.type = 3)
UNION ALL
SELECT OBJECT_NAME(OBJECT_ID) AS name,
index_id, allocation_unit_id, OBJECT_ID
FROM sys.allocation_units AS au
INNER JOIN sys.partitions AS p
ON au.container_id = p.partition_id
AND au.type = 2
) AS s_obj
LEFT JOIN sys.indexes i ON i.index_id = s_obj.index_id
AND i.OBJECT_ID = s_obj.OBJECT_ID ) AS obj
ON bd.allocation_unit_id = obj.allocation_unit_id
INNER JOIN sys.dm_db_partition_stats s ON s.index_id = obj.index_id AND s.object_id = obj.object_ID
WHERE database_id = DB_ID()
GROUP BY name, obj.index_id, IndexName, IndexTypeDesc
ORDER BY obj.name;
GO
1
Dave.Gugg