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
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:
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