Я написал приложение с бэкэндом SQL Server, которое собирает и хранит огромное количество записей. Я подсчитал, что на пике среднее количество записей составляет где-то на проспекте 3-4 миллиарда в день (20 часов работы).
Мое первоначальное решение (до того, как я выполнил фактический расчет данных) состояло в том, чтобы мое приложение вставляло записи в ту же таблицу, к которой запрашивают мои клиенты. Это разбилось и сгорело довольно быстро, очевидно, потому что невозможно запросить таблицу, в которую вставлено столько записей.
Мое второе решение состояло в том, чтобы использовать 2 базы данных, одну для данных, полученных приложением, и одну для данных, готовых для клиента.
Мое приложение будет получать данные, разбивать их на пакеты по ~ 100 тыс. Записей и массово вставлять в промежуточную таблицу. После ~ 100 тыс. Записей приложение на лету создаст другую промежуточную таблицу с той же схемой, что и раньше, и начнет вставлять в эту таблицу. Это создаст запись в таблице заданий с именем таблицы, содержащей 100 тыс. Записей, а хранимая процедура на стороне SQL Server переместит данные из промежуточной таблицы в готовую для клиента рабочую таблицу, а затем отбросит таблица временная таблица, созданная моим приложением.
Обе базы данных имеют одинаковый набор из 5 таблиц с одинаковой схемой, за исключением промежуточной базы данных, в которой есть таблица заданий. Промежуточная база данных не имеет ограничений целостности, ключа, индексов и т. Д. В таблице, в которой будет находиться основная часть записей. Ниже показано имя таблицы SignalValues_staging
. Цель состояла в том, чтобы мое приложение как можно быстрее сбрасывало данные в SQL Server. Рабочий процесс создания таблиц на лету, чтобы их можно было легко перенести, работает довольно хорошо.
Ниже приведены 5 соответствующих таблиц из моей промежуточной базы данных, плюс моя таблица заданий:
Хранимая процедура, которую я написал, обрабатывает перемещение данных из всех промежуточных таблиц и вставку их в производство. Ниже приведена часть моей хранимой процедуры, которая вставляется в производство из промежуточных таблиц:
-- 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
Я использую, sp_executesql
потому что имена таблиц для промежуточных таблиц приходят в виде текста из записей в таблице заданий.
Эта хранимая процедура выполняется каждые 2 секунды, используя трюк, который я узнал из этого поста на dba.stackexchange.com .
Проблема, которую я не могу решить на всю жизнь - это скорость, с которой производятся вставки в производство. Мое приложение создает временные промежуточные таблицы и невероятно быстро заполняет их записями. Вставка в производство не может идти в ногу с количеством таблиц, и в конечном итоге излишки таблиц исчисляются тысячами. Только способ , которым я когда - либо был в состоянии идти в ногу с поступающими данными, чтобы удалить все ключи, индексы, ограничения и т.д. ... на производственном SignalValues
столе. Проблема, с которой я сталкиваюсь, состоит в том, что таблица заканчивается таким количеством записей, что запрос становится невозможным.
Я попытался разделить таблицу, используя в [Timestamp]
качестве столбца разделения, но безрезультатно. Любая форма индексации замедляет вставки настолько, что они не успевают за ней. Кроме того, мне нужно было бы создать тысячи разделов (по одному каждую минуту? Час?) За годы. Я не мог понять, как создать их на лету
Я попытался создать разбиение, добавив вычисляемый столбец в таблице под названием TimestampMinute
, значение которого было, по INSERT
, DATEPART(MINUTE, GETUTCDATE())
. Все еще слишком медленно.
Я попытался сделать это оптимизированной для памяти таблицей в соответствии с этой статьей Microsoft . Может быть, я не понимаю, как это сделать, но MOT сделал вставки как-то медленнее.
Я проверил план выполнения хранимой процедуры и обнаружил, что (я думаю?) Самая интенсивная операция
SELECT svs.* INTO #sValues
FROM '+ @qualifiedTableName +' svs
INNER JOIN SignalMetaData smd
ON smd.SignalId = svs.SignalId
Для меня это не имеет смысла: я добавил запись настенных часов в хранимую процедуру, которая доказала обратное.
С точки зрения регистрации времени, этот конкретный оператор выше выполняется за ~ 300 мс на 100 тыс. Записей.
Заявление
INSERT INTO SignalValues SELECT * FROM #sValues
выполняется за 2500-3000мс на 100к записей. Удаление из таблицы затронутых записей, за:
DELETE c FROM '+ @qualifiedTableName +' c INNER JOIN #uniqueIdentifiers u ON c.SignalId = u.SignalId
занимает еще 300 мс.
Как я могу сделать это быстрее? Может ли SQL Server обрабатывать миллиарды записей в день?
Если это уместно, это SQL Server 2014 Enterprise x64.
Конфигурация оборудования:
Я забыл включить оборудование в первый проход этого вопроса. Виноват.
Я предваряю это следующими утверждениями: я знаю, что теряю некоторую производительность из-за конфигурации моего оборудования. Я пробовал много раз, но из-за бюджета, C-Level, выравнивания планет и т. Д., К сожалению, я ничего не могу сделать, чтобы получить лучшую настройку. Сервер работает на виртуальной машине, и я даже не могу увеличить память, потому что у нас просто больше нет.
Вот моя системная информация:
Хранилище подключено к серверу VM через интерфейс iSCSI к блоку NAS (это приведет к снижению производительности). Блок NAS имеет 4 диска в конфигурации RAID 10. Это вращающиеся дисководы WD WD4000FYYZ объемом 4 ТБ с интерфейсом SATA 6 ГБ / с. На сервере настроено только одно хранилище данных, поэтому база данных tempdb и моя база данных находятся в одном хранилище данных.
Макс DOP равен нулю. Должен ли я изменить это на постоянное значение или просто позволить SQL Server обрабатывать это? Я прочитал о RCSI: правильно ли я считаю, что единственная выгода от RCSI - это обновление строк? Там никогда не будет обновлений для любой из этих конкретных записей, они будут INSERT
редактироваться и SELECT
редактироваться. RCSI все еще принесет пользу мне?
У меня tempdb 8мб. Основываясь на ответе ниже от jyao, я изменил #sValues на обычную таблицу, чтобы вообще избежать tempdb. Производительность была примерно такой же, хотя. Я постараюсь увеличить размер и рост базы данных tempdb, но, учитывая, что размер #sValues будет более или менее всегда одинаковым, я не ожидаю большого выигрыша.
Я взял план выполнения, который я приложил ниже. Этот план выполнения представляет собой одну итерацию промежуточной таблицы - 100 тыс. Записей. Выполнение запроса было довольно быстрым, около 2 секунд, но имейте в виду, что в нем нет индексов для SignalValues
таблицы, а в SignalValues
таблице, являющейся целью INSERT
, в ней нет записей.