Меня попросили продемонстрировать, насколько быстрее поиск по индексированному столбцу, чем поиск по строковому префиксу, поэтому я создал быстрый тест, но результаты оказались неожиданными, и я не понимаю, почему.
База данных состоит из одной таблицы (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. Оба плана выполнения одинаковы. Это меня удивило, и я думал, что добавление индексированного столбца, безусловно, будет быстрее, но, видимо, нет.
План выполнения
Вы делаете select *, поэтому при выполнении индекса бренда будет выполняться множество ключевых запросов, особенно если он не уникален, сервер sql может решить использовать PK для всех ваших поисков. Кроме того, это должен быть N'brand, или, что еще лучше, вы должны правильно параметризовать, если еще этого не сделали.
Если вы хотите измерить время, которое занимает что-то, используйте Stopwatch, это гораздо точнее, чем DateTime.
^^ ... и если вы можете использовать Benchmark.Dotnet, но для тестов БД используйте инструменты БД.
Если я правильно понимаю, EF Core соответствует вашим ожиданиям. Итак, я думаю, это позволит составить лучший запрос? Вы можете посмотреть, что получится, и сравнить с вашим ADO sql.
Я не думаю, что здесь важна точность синхронизации — я ожидал, что результаты будут такими же впечатляющими, как тесты платформы сущностей, при этом поиск по индексированному полю будет намного быстрее.
«Я думал, что добавление индексированного столбца, безусловно, будет быстрее» — это «конечно» — это слишком много уверенности. Я видел случаи добавления индексов, которые снижали производительность. Вам все равно нужно знать, что вы делаете.





Вы можете рассматривать индекс как своего рода таблицу, которая сопоставляет каждое уникальное индексированное значение со списком записей, имеющих это значение (записи представлены их внутренним уникальным идентификатором).
Когда вы выбираете по индексированному значению, БД должна просмотреть список идентификаторов строк и получить доступ к реальной таблице по этому идентификатору. У вас 6 брендов, то есть на каждый бренд приходится 17 % от общего числа строк в таблице. Если эти 17% означают 17000 строк, то БД должна обращаться к таблице 17000 раз.
При выборе без индекса БД выполняет полное сканирование таблицы и считывает все записи последовательно.
Чтение N записей последовательно происходит намного быстрее, чем чтение N записей «случайным образом», особенно если ваши записи имеют переменный размер, поскольку положение записи невозможно угадать.
Давайте приведем пример. Предположим, что:
Если вашему запросу необходимо прочитать 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, что хуже, чем полное сканирование.
БД должна быть достаточно умной, чтобы запускать полное сканирование вместо индексированного доступа, даже если ваш запрос может использовать индекс, но это зависит от реализации и точности статистики. Ведение таблиц можно использовать для улучшения статистики. Как предложено в комментариях, вам следует просмотреть план выполнения, чтобы проверить, как осуществляется доступ к таблице.
Несколько советов:
Если вы получите лучшие результаты, применив какой-либо из приведенных выше советов, сообщите об этом в комментариях.
Посмотрите на свои планы выполнения. У вас слишком много строк для каждого бренда, чтобы индекс был полезен. Таким образом, в обоих случаях вы получаете сканы таблицы.
Поскольку вы извлекаете все столбцы, план, использующий индекс, должен будет выполнять поиск для каждой строки. И сканирование выполняется быстрее, чем большое количество поисков в кластерном индексе.
А при удалении 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).
Для оценки производительности укажите соответствующие планы выполнения.