J'ai écrit une application avec un backend SQL Server qui collecte et stocke une très grande quantité d'enregistrements. J'ai calculé que, au sommet, le nombre moyen d'enregistrements se situe quelque part entre 3 et 4 milliards par jour (20 heures de fonctionnement).
Ma solution initiale (avant de faire le calcul réel des données) était que mon application insère des enregistrements dans la même table interrogée par mes clients. Cela s'est écrasé et brûlé assez rapidement, évidemment, car il est impossible d'interroger une table contenant autant d'inscriptions.
Ma deuxième solution consistait à utiliser 2 bases de données, une pour les données reçues par l'application et une pour les données prêtes pour le client.
Mon application recevait des données, les fragmentait en lots d'environ 100 000 enregistrements et les insérait en bloc dans la table de transfert. Après environ 100 000 enregistrements, l'application créerait à la volée une autre table intermédiaire avec le même schéma qu'avant et commencerait à l'insérer dans cette table. Il créerait un enregistrement dans une table de travaux avec le nom de la table contenant 100 000 enregistrements et une procédure stockée côté SQL Server déplacerait les données de la ou des tables de transfert vers la table de production prête pour le client, puis supprimerait le table table temporaire créée par mon application.
Les deux bases de données ont le même ensemble de 5 tables avec le même schéma, à l'exception de la base de données intermédiaire qui contient la table des travaux. La base de données intermédiaire n'a aucune contrainte d'intégrité, clé, index, etc. sur la table où résidera la majeure partie des enregistrements. Ci-dessous, le nom de la table est SignalValues_staging
. L'objectif était que mon application claque les données dans SQL Server le plus rapidement possible. Le flux de travail de création de tables à la volée afin qu'elles puissent facilement être migrées fonctionne plutôt bien.
Voici les 5 tables pertinentes de ma base de données intermédiaire, plus ma table des travaux:
La procédure stockée que j'ai écrite gère le déplacement des données de toutes les tables de transfert et leur insertion dans la production. Ci-dessous, la partie de ma procédure stockée qui s'insère dans la production à partir des tables de transfert:
-- Signalvalues jobs table.
SELECT *
,ROW_NUMBER() OVER (ORDER BY JobId) AS 'RowIndex'
INTO #JobsToProcess
FROM
(
SELECT JobId
,ProcessingComplete
,SignalValueStagingTableName AS 'TableName'
,(DATEDIFF(SECOND, (SELECT last_user_update
FROM sys.dm_db_index_usage_stats
WHERE database_id = DB_ID(DB_NAME())
AND OBJECT_ID = OBJECT_ID(SignalValueStagingTableName))
,GETUTCDATE())) SecondsSinceLastUpdate
FROM SignalValueJobs
) cte
WHERE cte.ProcessingComplete = 1
OR cte.SecondsSinceLastUpdate >= 120
DECLARE @i INT = (SELECT COUNT(*) FROM #JobsToProcess)
DECLARE @jobParam UNIQUEIDENTIFIER
DECLARE @currentTable NVARCHAR(128)
DECLARE @processingParam BIT
DECLARE @sqlStatement NVARCHAR(2048)
DECLARE @paramDefinitions NVARCHAR(500) = N'@currentJob UNIQUEIDENTIFIER, @processingComplete BIT'
DECLARE @qualifiedTableName NVARCHAR(128)
WHILE @i > 0
BEGIN
SELECT @jobParam = JobId, @currentTable = TableName, @processingParam = ProcessingComplete
FROM #JobsToProcess
WHERE RowIndex = @i
SET @qualifiedTableName = '[Database_Staging].[dbo].['+@currentTable+']'
SET @sqlStatement = N'
--Signal values staging table.
SELECT svs.* INTO #sValues
FROM '+ @qualifiedTableName +' svs
INNER JOIN SignalMetaData smd
ON smd.SignalId = svs.SignalId
INSERT INTO SignalValues SELECT * FROM #sValues
SELECT DISTINCT SignalId INTO #uniqueIdentifiers FROM #sValues
DELETE c FROM '+ @qualifiedTableName +' c INNER JOIN #uniqueIdentifiers u ON c.SignalId = u.SignalId
DROP TABLE #sValues
DROP TABLE #uniqueIdentifiers
IF NOT EXISTS (SELECT TOP 1 1 FROM '+ @qualifiedTableName +') --table is empty
BEGIN
-- processing is completed so drop the table and remvoe the entry
IF @processingComplete = 1
BEGIN
DELETE FROM SignalValueJobs WHERE JobId = @currentJob
IF '''+@currentTable+''' <> ''SignalValues_staging''
BEGIN
DROP TABLE '+ @qualifiedTableName +'
END
END
END
'
EXEC sp_executesql @sqlStatement, @paramDefinitions, @currentJob = @jobParam, @processingComplete = @processingParam;
SET @i = @i - 1
END
DROP TABLE #JobsToProcess
J'utilise sp_executesql
parce que les noms de table pour les tables intermédiaires proviennent sous forme de texte des enregistrements dans la table des travaux.
Cette procédure stockée s'exécute toutes les 2 secondes à l'aide de l'astuce que j'ai apprise de ce message dba.stackexchange.com .
Le problème que je ne peux pas résoudre toute ma vie est la vitesse à laquelle les insertions en production sont effectuées. Mon application crée des tables de transfert temporaires et les remplit incroyablement rapidement. L'insert dans la production ne peut pas suivre le nombre de tables et finalement il y a un surplus de tables par milliers. La seule façon dont j'ai pu suivre les données entrantes est de supprimer toutes les clés, index, contraintes etc ... sur la SignalValues
table de production . Le problème auquel je suis confronté est que la table se retrouve avec autant d'enregistrements qu'il devient impossible d'interroger.
J'ai essayé de partitionner la table en utilisant la [Timestamp]
colonne de partitionnement en vain. Toute forme d'indexation ralentit tellement les insertions qu'elles ne peuvent pas suivre. De plus, je devrais créer des milliers de partitions (une par minute? Heure?) Des années à l'avance. Je ne savais pas comment les créer à la volée
J'ai essayé de créer le partitionnement en ajoutant une colonne calculée à la table appelée TimestampMinute
dont la valeur était, sur INSERT
, DATEPART(MINUTE, GETUTCDATE())
. Encore trop lent.
J'ai essayé d'en faire une table optimisée en mémoire selon cet article Microsoft . Peut-être que je ne comprends pas comment le faire, mais le MOT a ralenti les inserts d'une manière ou d'une autre.
J'ai vérifié le plan d'exécution de la procédure stockée et constaté que (je pense?) L'opération la plus intensive est
SELECT svs.* INTO #sValues
FROM '+ @qualifiedTableName +' svs
INNER JOIN SignalMetaData smd
ON smd.SignalId = svs.SignalId
Pour moi, cela n'a pas de sens: j'ai ajouté la journalisation de l'horloge murale à la procédure stockée qui a prouvé le contraire.
En termes de journalisation du temps, cette instruction particulière ci-dessus s'exécute en ~ 300 ms sur 100 000 enregistrements.
La déclaration
INSERT INTO SignalValues SELECT * FROM #sValues
s'exécute en 2500-3000 ms sur 100 000 enregistrements. Supprimer du tableau les enregistrements concernés, par:
DELETE c FROM '+ @qualifiedTableName +' c INNER JOIN #uniqueIdentifiers u ON c.SignalId = u.SignalId
prend encore 300 ms.
Comment puis-je accélérer cela? SQL Server peut-il gérer des milliards d'enregistrements par jour?
S'il est pertinent, il s'agit de SQL Server 2014 Enterprise x64.
Configuration matérielle:
J'ai oublié d'inclure du matériel dans la première partie de cette question. Ma faute.
Je préfère ceci avec ces déclarations: je sais que je perds des performances à cause de ma configuration matérielle. J'ai essayé plusieurs fois mais à cause du budget, du niveau C, de l'alignement des planètes, etc ... je ne peux malheureusement rien faire pour obtenir une meilleure configuration. Le serveur fonctionne sur une machine virtuelle et je ne peux même pas augmenter la mémoire car nous n'en avons tout simplement plus.
Voici mes informations système:
Le stockage est attaché au serveur VM via l'interface iSCSI à un boîtier NAS (cela dégradera les performances). Le boîtier NAS dispose de 4 disques dans une configuration RAID 10. Ce sont des disques durs de 4 To WD WD4000FYYZ avec une interface SATA de 6 Go / s. Le serveur n'a qu'un seul magasin de données configuré, donc tempdb et ma base de données sont sur la même banque de données.
Le DOP max est nul. Dois-je changer cela en une valeur constante ou simplement laisser SQL Server s'en occuper? J'ai lu sur RCSI: ai-je raison de supposer que le seul avantage de RCSI vient des mises à jour des lignes? Il n'y aura jamais de mise à jour de ces enregistrements particuliers, ils seront INSERT
édités et SELECT
édités. Est-ce que RCSI me bénéficiera toujours?
Mon tempdb est de 8 Mo. Sur la base de la réponse ci-dessous de jyao, j'ai changé les #sValues en une table régulière pour éviter complètement tempdb. Cependant, les performances étaient à peu près les mêmes. J'essaierai d'augmenter la taille et la croissance de tempdb, mais étant donné que la taille de #sValues sera plus ou moins toujours la même taille, je n'anticipe pas beaucoup de gain.
J'ai pris un plan d'exécution que j'ai joint ci-dessous. Ce plan d'exécution est une itération d'une table intermédiaire - 100 000 enregistrements. L'exécution de la requête a été assez rapide, environ 2 secondes, mais gardez à l'esprit que cela est sans index sur la SignalValues
table et que la SignalValues
table, la cible de la INSERT
, ne contient aucun enregistrement.