Почему поиск по индексированному столбцу не выполняется быстрее, чем сравнение строк?

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

База данных состоит из одной таблицы (Products) со столбцами productName и Brand и нескольких других столбцов просто для увеличения объема данных с помощью индекса Brand:

CREATE TABLE [dbo].[Products]
(
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [ProductName] [nvarchar](500) NOT NULL,
    [Brand] [nvarchar](100) NOT NULL,
    [Field1] [nvarchar](50) NULL,
    [Field2] [nvarchar](50) NULL,
    [Field3] [nvarchar](50) NULL,
    [Field4] [nvarchar](50) NULL,
    [Field5] [nvarchar](50) NULL,
    Ix_Brands index(Brand),

    CONSTRAINT [PK_Products] 
        PRIMARY KEY CLUSTERED ([ID] ASC)
)

Затем я получаю продукты по брендам, используя 4 разных метода и определяю время, которое занимает каждый из них.

using System;
using System.Data;
using System.Data.SqlClient;
using System.Linq;

namespace SpeedTest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var connectionString = "data source=.<Redacted>";
            string[] Brands = new string[] { "Tesco", "Asda", "Boots", "Morrisons", "Amazon", "Ebay" };

            var rnd = new Random();
            DateTime startTime;

            using (var con = new SqlConnection(connectionString))
            {
                var cmd = new SqlCommand("delete from products", con);
                con.Open();
                cmd.ExecuteNonQuery();

                Console.WriteLine("Creating 100,000 products");

                for (int i = 0; i < 100000; i++)
                {
                    var brand = Brands[rnd.Next(Brands.Length)];
                    cmd.CommandText = $"insert into products(productName, brand, field1, field2, field3, field4, field5) values ('{brand}_{Guid.NewGuid()}', '{brand}', '{Guid.NewGuid()}', '{Guid.NewGuid()}', '{Guid.NewGuid()}', '{Guid.NewGuid()}', '{Guid.NewGuid()}')";
                    cmd.ExecuteNonQuery();
                }

                Console.WriteLine("Getting products by brand via ADO and product name prefix");
                startTime = DateTime.Now;

                foreach (var brand in Brands)
                {
                    cmd.CommandText = $"select * from products where productName like '{brand}_%'";
                    var da = new SqlDataAdapter(cmd);
                    var dt = new DataTable();
                    da.Fill(dt);
                }

                Console.WriteLine($"Time taken: {(DateTime.Now - startTime).TotalMilliseconds}ms");
                Console.WriteLine("Getting products by brand via ADO and indexed brand column");
                startTime = DateTime.Now;

                foreach (var brand in Brands)
                {
                    cmd.CommandText = $"select * from products where brand='{brand}'";
                    var da = new SqlDataAdapter(cmd);
                    var dt = new DataTable();
                    da.Fill(dt);
                }

                Console.WriteLine($"Time taken: {(DateTime.Now - startTime).TotalMilliseconds}ms");
                con.Close();
            }

            var db = new SpeedTestEntities();
            Console.WriteLine("Getting products by brand via entity framework and product name prefix");
            startTime = DateTime.Now;

            foreach (var brand in Brands)
            {
                var products = db.Products.Where(p => p.ProductName.StartsWith(brand + "_")).ToList();
            }

            Console.WriteLine($"Time taken: {(DateTime.Now - startTime).TotalMilliseconds}ms");
            Console.WriteLine("Getting products by brand via entity framework and indexed brand column");
            startTime = DateTime.Now;

            foreach (var brand in Brands)
            {
                var products = db.Products.Where(p => p.Brand.Equals(brand, StringComparison.OrdinalIgnoreCase)).ToList();
            }

            Console.WriteLine($"Time taken: {(DateTime.Now - startTime).TotalMilliseconds}ms");
            Console.ReadLine();
        }
    }
}

Результаты Entity Framework были такими, как я ожидал, но результаты ADO показали, что поиск по индексированному столбцу был медленнее, чем по префиксу имени продукта, что, безусловно, не может быть правильным:

Creating 100,000 products
Getting products by brand via ADO and product name prefix
Time taken: 558.9306ms
Getting products by brand via ADO and indexed brand column
Time taken: 642.5258ms
Getting products by brand via entity framework and product name prefix
Time taken: 3266.8438ms
Getting products by brand via entity framework and indexed brand column
Time taken: 204.932ms

Должно быть, я где-то напутал, но не могу понять где. Моя демонстрация того, почему нам следует добавлять индексированные столбцы в таблицы базы данных, а не добавлять префиксы к другим строкам, идет плохо.

Может ли кто-нибудь спасти меня и посмотреть, что здесь происходит?

Обновлено: оказывается, что оба запроса ADO выполняют сканирование индекса PK_Products. Оба плана выполнения одинаковы. Это меня удивило, и я думал, что добавление индексированного столбца, безусловно, будет быстрее, но, видимо, нет.

План выполнения

Для оценки производительности укажите соответствующие планы выполнения.

The Impaler 24.05.2024 16:07

Вы делаете select *, поэтому при выполнении индекса бренда будет выполняться множество ключевых запросов, особенно если он не уникален, сервер sql может решить использовать PK для всех ваших поисков. Кроме того, это должен быть N'brand, или, что еще лучше, вы должны правильно параметризовать, если еще этого не сделали.

siggemannen 24.05.2024 16:13

Если вы хотите измерить время, которое занимает что-то, используйте Stopwatch, это гораздо точнее, чем DateTime.

JonasH 24.05.2024 16:16

^^ ... и если вы можете использовать Benchmark.Dotnet, но для тестов БД используйте инструменты БД.

Fildor 24.05.2024 16:17

Если я правильно понимаю, EF Core соответствует вашим ожиданиям. Итак, я думаю, это позволит составить лучший запрос? Вы можете посмотреть, что получится, и сравнить с вашим ADO sql.

Fildor 24.05.2024 16:19

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

Mark Henderson 24.05.2024 16:30

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

Fildor 24.05.2024 17:35
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
114
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

Когда вы выбираете по индексированному значению, БД должна просмотреть список идентификаторов строк и получить доступ к реальной таблице по этому идентификатору. У вас 6 брендов, то есть на каждый бренд приходится 17 % от общего числа строк в таблице. Если эти 17% означают 17000 строк, то БД должна обращаться к таблице 17000 раз.

При выборе без индекса БД выполняет полное сканирование таблицы и считывает все записи последовательно.

Чтение N записей последовательно происходит намного быстрее, чем чтение N записей «случайным образом», особенно если ваши записи имеют переменный размер, поскольку положение записи невозможно угадать.

Давайте приведем пример. Предположим, что:

  • в вашей таблице 100 000 записей
  • последовательное чтение записи занимает 1 нс
  • чтение одной случайной записи занимает в среднем 10 нс

Если вашему запросу необходимо прочитать 1 запись при полном сканировании, это займет 1 x 100 000 = 100 000 нс.

Если ваш запрос должен прочитать 1 запись по индексу, это займет 10 нс, что намного лучше.

Если ваш запрос должен прочитать 17 000 записей при полном сканировании, это займет 1 x 100 000 = 100 000 нс (так же, как 1 запись).

Если вашему запросу необходимо прочитать 17 000 записей по индексу, это займет 10 x 17 000 = 170 000, что хуже, чем полное сканирование.

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

Несколько советов:

  • попробуйте использовать типы фиксированного размера для всех полей (т.е. char вместо varchar).
  • постарайтесь увеличить количество брендов, чтобы у каждого бренда был меньший процент записей от общего числа.
  • попробуйте запустить каждый тест 2 раза и измерить только продолжительность второго запуска. Это обеспечит загрузку индекса в память (если он помещается в кеш) перед выполнением запроса (это необходимо, поскольку таблица воссоздается в начале вашего теста).

Если вы получите лучшие результаты, применив какой-либо из приведенных выше советов, сообщите об этом в комментариях.

Ответ принят как подходящий

Посмотрите на свои планы выполнения. У вас слишком много строк для каждого бренда, чтобы индекс был полезен. Таким образом, в обоих случаях вы получаете сканы таблицы.

Поскольку вы извлекаете все столбцы, план, использующий индекс, должен будет выполнять поиск для каждой строки. И сканирование выполняется быстрее, чем большое количество поисков в кластерном индексе.

А при удалении EF (который не влияет на производительность SQL Server) сканирование таблицы с помощью LIKE стало немного медленнее:

Creating 100,000 products
Getting products by brand via ADO and product name prefix
Time taken: 686.7718ms rows 100000
Getting products by brand via ADO and indexed brand field
Time taken: 548.8414ms rows 100000

Кроме того, вы в основном измеряете, сколько времени требуется для отправки 100 000 строк с SQL Server клиенту по сети. Если вы хотите измерить стоимость запросов, посмотрите на фактический план выполнения. В XML он сообщает вам стоимость процессора и все ожидания. ЭГ вот это

   <WaitStats>
      <Wait WaitType = "ASYNC_NETWORK_IO" WaitTimeMs = "257" WaitCount = "1565" />
    </WaitStats>
    <QueryTimeStats CpuTime = "39" ElapsedTime = "295" />

Это показывает, что этот запрос занял 295 мс затраченного времени, но всего 39 мс процессорного времени. Остальное было потрачено на отправку данных клиенту (ASYNC_NETWORK_IO).

Другие вопросы по теме