web-dev-qa-db-fra.com

Forcer SQL Server à pré-mettre en cache toute la base de données en mémoire

Nous avons un site client avec une base de données SQL 2012 de 50 Go sur un serveur avec plus de 100 Go de RAM.

Lorsque l'application est utilisée, SQL Server fait un excellent travail de mise en cache de la base de données dans la mémoire, mais l'augmentation des performances de la mise en cache se produit la deuxième fois qu'une requête est exécutée, pas la première.

Pour essayer de maximiser les accès au cache lors de la première exécution des requêtes, nous avons écrit un processus qui itère à travers chaque index de chaque table de la base de données entière, en exécutant ceci:

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

Dans une tentative de forcer une lecture grosse, moche et artificielle pour autant de données que possible. Nous avons prévu de l'exécuter toutes les 15 minutes, et il fait un excellent travail en général.

Sans débattre d'autres goulots d'étranglement, de spécifications matérielles, de plans de requête ou d'optimisation des requêtes, quelqu'un a-t-il de meilleures idées sur la façon d'accomplir cette même tâche?

MISE À JOUR
Merci pour les suggestions. Suppression du "INTO #Cache". Testé et cela n'a pas fait de différence sur le remplissage du tampon.
Ajouté: Au lieu de Select *, je sélectionne UNIQUEMENT les clés de l'index. C'est (évidemment) plus pertinent et beaucoup plus rapide.
Ajouté: Lecture et mise en cache des index de contraintes également.

Voici le code actuel: (espérons qu'il est utile pour quelqu'un d'autre)

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

Tout d'abord, il existe un paramètre appelé "Minumum Server Memory" qui semble tentant. Ignorez-le. De MSDN:

La quantité de mémoire acquise par le moteur de base de données dépend entièrement de la charge de travail placée sur l'instance. Une instance de SQL Server qui ne traite pas de nombreuses demandes peut ne jamais atteindre la mémoire minimale du serveur.

Cela nous indique que la définition d'une mémoire minimale plus grande ne forcera ni n'encouragera aucune mise en cache préalable. Vous pouvez avoir autres raisons de définir cela , mais le pré-remplissage du pool de tampons n'en fait pas partie.

Alors, que pouvez-vous faire pour précharger les données? C'est facile. Configurez simplement un travail d'agent pour effectuer une select * de chaque table. Vous pouvez le programmer sur "Démarrer automatiquement au démarrage de l'agent SQL". En d'autres termes, ce que vous faites déjà est assez proche de la manière standard de gérer cela.

Cependant, je dois suggérer trois changements:

  1. N'essayez pas d'utiliser une table temporaire. Sélectionnez simplement dans le tableau. Vous n'avez rien à faire avec les résultats pour que Sql Server charge votre pool de tampons: il vous suffit de sélectionner. Une table temporaire pourrait forcer SQL Server à copier les données du pool de tampons après le chargement ... vous finiriez par (brièvement) stocker des choses deux fois .
  2. Ne l'exécutez pas toutes les 15 minutes. Exécutez-le une seule fois au démarrage, puis laissez-le tranquille. Une fois alloué, il faut beaucoup pour que Sql Server libère de la mémoire. Il n'est tout simplement pas nécessaire de recommencer cela encore et encore.
  3. N'essayez pas d'indiquer un index. Les indices ne sont que cela: des indices. Sql Server est libre d'ignorer ces conseils, et il le fera pour les requêtes qui n'ont aucune utilisation claire de l'index. La meilleure façon de vous assurer que l'index est préchargé est de construire une requête qui utilise évidemment cet index. Une suggestion spécifique ici est de classer les résultats dans le même ordre que l'index. Cela aidera souvent Sql Server à utiliser cet index, car il peut alors "parcourir l'index" pour produire les résultats.
19
Joel Coehoorn

Ce n'est pas une réponse, mais pour compléter la réponse de Joel Coehoorn, vous pouvez consulter les données de la table dans le cache à l'aide de cette instruction. Utilisez-le pour déterminer si toutes les pages restent dans le cache comme prévu:

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