Оптимизация таблицы миллионов строк SQL Server

#sql #sql-server #ado.net #database-optimization

#sql #sql-сервер #ado.net #оптимизация базы данных

Вопрос:

У меня есть таблица с миллионами строк:

 CREATE TABLE [dbo].[RequestIdentities]
(
    [Id] [bigint] IDENTITY(1,1) NOT NULL,
    [UniqueKey] [nvarchar](256) NULL,
    [Timestamp] [datetime] NULL,

    CONSTRAINT [PK_RequestIdentities] 
        PRIMARY KEY CLUSTERED ([Id] ASC)
                    WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, 
                          IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, 
                          ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[RequestIdentities] 
    ADD CONSTRAINT [DF_RequestIdentities_Timestamp]  
        DEFAULT (GETDATE()) FOR [Timestamp]
GO 
  

Веб-API управляет базой данных с помощью ADO.NET и делает:

  1. Выполняет этот запрос:

      SELECT 1 
     FROM RequestIdentities WITH (nolock) 
     WHERE UniqueKey = @key
      
  2. Если существует:

      if(reader.HasRows)
      
  3. Возвращает HTTP-ответ.

  4. В противном случае он вставляет идентификатор в таблицу:

      INSERT INTO RequestIdentities(UniqueKey) 
     VALUES(@key)
      

Есть сотни вставок / обновлений каждую минуту, могу ли я что-то сделать для оптимизации таблицы, например, пользовательскую статистику / индексы?

Комментарии:

1. Это небольшие данные и низкий трафик. Индексы необходимы для всех таблиц. TOP 1 не имеет смысла, поскольку уникальный ключ гарантирует единый результат. В лучшем случае это не операция, в худшем случае это вызывает дополнительную операцию. WITH (NOLOCK) на самом деле это ужасная идея, приводящая к грязным или повторяющимся результатам и большему количеству блокировок. Вы не предоставили никакой информации об этой таблице, кроме того факта, что у нее есть идентификатор, поэтому невозможно сказать, что, если что-либо, может потребовать оптимизации

2. Сталкивались ли вы с какими-либо реальными проблемами? Кстати, если вы хотите получить только одно значение, используйте ExecuteScalar вместо ExecuteReader . Если вы хотите избежать проблем, избегайте использования долговременных соединений и транзакций. Соединения и транзакции накапливают блокировки, что приводит к блокировке и задержкам. Вот почему обычный способ создания и использования соединения находится внутри using блока.

3. Кстати, почему SELECT 1 ? Если вам потребуется прочитать какие-либо данные для этой строки позже, вам придется выполнить другой запрос. Это в два раза больше времени ожидания, а если вы используете транзакции, блокировки с более длительным сроком службы. Если вы хотите вставить что-то, если оно еще не существует, вы можете использовать INSERT ... WHERE NOT EXISTS() или MERGE для выполнения операции в одном запросе.

4.BTW UniqueKey нужен УНИКАЛЬНЫЙ индекс или ограничение, иначе он не может быть уникальным

5. Если у вас действительно высокий трафик и достаточно памяти, вы можете использовать таблицы в памяти для уменьшения задержек и блокировок. В этом случае думайте о таблице как о надежном кэше Redis, только лучше.

Ответ №1:

1 МЛН строк — это не так много данных для современной базы данных на компьютерах, использующих твердотельные накопители. Несколько сотен вставок тоже не так много. Хотя вы можете оптимизировать это, например, с помощью таблиц в памяти, сначала необходимо устранить существующие проблемы.

Таблицы в памяти также могут упростить обслуживание в некоторых сценариях.

Проблемы

Этот код содержит несколько проблем, которые снижают производительность.

  • WITH (NOLOCK) это очень плохая идея, которая вообще не улучшает производительность. На самом деле это требует * более обширных блокировок (на уровне схемы), считывает грязные, незафиксированные данные, может возвращать одни и те же данные дважды и даже может выдавать ошибку.
  • Для вставки одной строки код выполняет два удаленных вызова, что приводит к удвоенной задержке. Помимо задержки, это означает, что блокировки, полученные во время SELECT операции, должны удерживаться намного дольше, чем необходимо, что потенциально блокирует другие соединения, которые пытаются использовать ту же таблицу.
  • TOP 1 в лучшем случае не работает. Если UniqueKey действительно уникально, будет возвращен только один результат.

Исправить

Вы можете улучшить это, удалив подсказки и проверку существования. INSERT Запрос может включать FROM WHERE предложение and, что означает, что вы можете написать один запрос только для вставки новой строки. Вы можете использовать предложение OUTPUT для возврата идентификатора новой строки.

Во-первых, вам нужно добавить либо УНИКАЛЬНЫЙ индекс, либо ограничение на UniqueKey столбец. Без одного из них это просто не уникально. Любой может вставлять повторяющиеся значения. УНИКАЛЬНОЕ ограничение фактически создает УНИКАЛЬНЫЙ индекс:

 CREATE UNIQUE INDEX IX_ RequestIdentities_UniqueKey   
   ON RequestIdentities (UniqueKey);
  

После этого вы можете условно вставить и получить новый идентификатор с помощью :

 INSERT INTO RequestIdentities (UniqueKey)
    OUTPUT inserted.ID
SELECT @key
FROM RequestIdentities
WHERE NOT EXISTS ( select * 
                   from RequestIdentities
                   where UniqueKey = @key)
  

Оптимизатор запросов знает, что ему не нужно выдавать никаких результатов EXISTS ( SELECT * , чтобы это не влияло на производительность.

Этот запрос вставит новую строку и вернет новый идентификатор. Эта операция является атомарной (она либо завершается успешно, либо выполняется откат), поэтому нет необходимости в явной транзакции.

Вы можете выполнить этот запрос через SqlCommand с помощью ExecuteScalar() . Это вернет новый идентификатор или null , если результатов нет, потому что строка не была вставлена :

 using(var connection=new SqlConnection(connString))
using(var cmd=new SqlCommand(query,connection))
{
    cmd.Parameters.Add("@key",SqlDbType.NVarChar,256).Value=key;
    connection.Open();
    var result = cmd.ExecuteScalar();
    if (result!=null)
    {
        var newID=(long)result;
        //Use the ID
        ...
    }
}
  

Вы можете пофантазировать, используя синтаксис сопоставления с образцом в C # 8 :

 if(result is long newId)
{
   //Use the ID
}
  

Если этого кода становится слишком много, вы можете использовать микро-ORM, такой как Dapper :

 using(var connection=new SqlConnection(connString))
{
    var result=connection.ExecuteScalar(query,new {key=keyValue});
    if (result is long new ID)
    {
        ...
    }
}
  

Dapper используется StackOverflow, поэтому его производительность гарантирована.

Другие оптимизации

Если вы обнаружите, что в этой таблице слишком много блокировок, возможной оптимизацией является использование таблиц, оптимизированных для памяти. Серверы баз данных уже активно буферизуют данные.

Реальное преимущество таблицы с оптимизацией памяти — это другая модель ведения журнала, блокировки и доступа. Вместо блокировок используются облегченные объекты защелки в памяти. Поскольку данные уже находятся в памяти, сервер может использовать разные операторы и разные типы индексов для извлечения и изменения объектов.

В этом примере документации используются таблицы в памяти для двух таблиц с высоким трафиком:

  • Корзина — это надежная таблица в памяти, данные которой сохраняются на диске. Если сервер выходит из строя, корзины сохраняются.
  • Пользовательские сеансы — это ненадежная таблица в памяти. Если сервер выходит из строя, нам плевать на сеансы

В этом случае таблица может быть:

 CREATE TABLE [dbo].[RequestIdentities]
(
    [Id] [bigint] IDENTITY(1,1) NOT NULL 
        PRIMARY KEY NONCLUSTERED,
    [UniqueKey] [nvarchar](256) NULL,
    [Timestamp] [datetime] NULL
)  
WITH (  
    MEMORY_OPTIMIZED = ON,  
    //Assuming we want to retain the data
    DURABILITY = SCHEMA_AND_DATA);  
go  

ALTER TABLE RequestIdentities  
    ADD CONSTRAINT RequestIdentities_UniqueKey  
    UNIQUE NONCLUSTERED (UniqueKey);  
go  
  

Комментарии:

1. Давайте добавим к этому еще одну оптимизацию. Возможно, «исправление» — лучший термин. Фактические столбцы данных не должны быть обнуляемыми. В настоящее время все они обнуляются, что является логической бессмыслицей.

2. @SMor в вопросе отсутствует много информации.

3. @PanagiotisKanavos . , , Честно говоря, несколько миллионов строк и несколько сотен вставок в минуту не были такой уж большой проблемой двадцать лет назад до появления SSD и на более медленных машинах.

4. @GordonLinoff учитывая, сколько вопросов SO возникает по поводу больших данных и таблиц из 50 тыс. строк, это нужно повторять каждый раз.

Ответ №2:

Вы можете добавить столбец, содержащий hash значение поля, которое вы ищете.

Сначала добавьте новый столбец в таблицу:

 ALTER TABLE [...]
ADD [UniqueKeyHash] VARBINARY(64);
  

Затем добавьте к ней индекс:

 CREATE INDEX IX_..._UniqueKeyHash ON [...]
(
    [UniqueKeyHash] 
);
  

Заполните значения:

 UPDATE [...]
SET [UniqueKeyHash] =  HASHBYTES('SHA2_512', UniqueKey);
  

Измените процедуры CRUD, чтобы также вычислить HASH .

Затем в поиске:

 DECLARE @UniqueKeyHash VARBINARY(64);

SET @UniqueKeyHash = HASHBYTES('SHA2_512', 'some value');

SELECT *
FROM [...]
WHERE [UniqueKeyHas] = @UniqueKeyHash;
  

Кроме того, вы можете добавить столбец as computed and persisted , чтобы пропустить изменение CRUD процедур.

Я использую такой поиск в разных местах — один из них находится в таблице IP-адресов, из которой выполняется поиск при каждом входе пользователя в систему и содержит миллионы записей.

Если это слишком сложно для вас, вы можете начать с создания индекса на UniqueKey .

Комментарии:

1. Зачем это делать? Проблема заключается в двух удаленных запросах, отсутствии индексации и NOLOCK . Без УНИКАЛЬНОГО индекса UniqueKey он не может быть уникальным, так что индекс все равно нужен

2. Возможно, вы думали о таблицах в памяти?

3. @PanagiotisKanavos Нет, как я уже сказал, поиск строки, даже имеющей уникальный индекс, может быть медленным. В моем случае запрос занял 1-2 секунды, что слишком много, когда пользователь входит в систему. Добавление hash столбца и индекса решает проблему.

4. Индексированный хэш-столбец ничем не отличается от столбца индексированной строки, за исключением накладных расходов на ввод-вывод. Которые могут быть обработаны более эффективно с помощью сжатия. Это привело бы к замене ограниченных ресурсов ввода-вывода на более обильные ресурсы ЦП. В сценарии входа в систему время, необходимое для вычисления хэша пароля, будет больше 1 «.

5. Конечно, есть разница 🙂 и в моем случае это было 2 секунды. Кроме того, обратите внимание, что я говорю об IP-адресе, который является короткой строкой… визуализация чего-то большего. Я не вижу, как сжатие будет работать в таком случае — это более медленный вариант и излишество для такой задачи.

Ответ №3:

  1. Коэффициент ЗАПОЛНЕНИЯ = 80 для очень большой таблицы, особенно для КЛАСТЕРИЗОВАННОГО ИДЕНТИФИКАЦИОННОГО ИНДЕКСА, — это нонсенс. Вы теряете 20% свободного места!
  2. если столбец uniqueKey УНИКАЛЕН, добавьте ограничение УНИКАЛЬНОСТИ.
  3. [Временная метка] — это зарезервированное слово, не используйте его в качестве имени столбца.

После выполнения этой работы вы можете использовать следующий запрос :

 INSERT INTO dbo.RequestIdentities(UniqueKey)
OUTPUT inserted.* INTO ...
SELECT @key
EXCEPT
SELECT UniqueKey
FROM   dbo.RequestIdentities
WHERE  UniqueKey = @Key;
  

Aand возвращает предложение OUTPUT result в таблицу вашему клиентскому приложению