有人要求我演示如何在索引列上搜索比搜索字符串前缀更快,因此我创建了一个快速测试,但结果令人惊讶,我不明白为什么。
数据库由一张表 (
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();
}
}
}
实体框架结果符合我的预期,但 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
上进行索引扫描。两个执行计划是相同的。这让我感到惊讶,我认为添加索引列肯定会更快,但显然不是。
看看你的执行计划。 每个品牌的行数太多,索引无法发挥作用。 所以在这两种情况下你都会得到表扫描。
删除 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
您可以将索引视为一种表,它将每个唯一索引值映射到具有该值的记录列表(记录由其内部唯一标识符表示)。
当您通过索引值进行选择时,数据库必须扫描行 ID 列表,并通过该 ID 访问真实表。 您有 6 个品牌,这意味着每个品牌占表中总行数的 17%。 如果这个 17% 意味着 17000 行,那么数据库必须访问该表 17000 次。
当您选择不带索引时,数据库会对表进行全扫描,因此它会按顺序读取所有记录。
按顺序读取 N 条记录比“随机”读取 N 条记录要快得多,特别是如果您的记录大小可变,因为无法猜测记录位置。
我们举个例子。假设:
如果您的查询必须通过完整扫描读取 1 条记录,则需要 1 x 100.000 = 100.000 ns。
如果您的查询必须通过索引读取 1 条记录,则需要 10 ns,这要好得多。
如果您的查询必须通过完整扫描读取 17.000 条记录,则需要 1 x 100.000 = 100.000 ns(与 1 条记录相同)。
如果您的查询必须按索引读取 17.000 条记录,则需要 10 x 17.000 = 170.000,这比完整扫描更糟糕。
即使您的查询可以使用索引,数据库也应该足够智能来运行完整扫描而不是索引访问,但这取决于统计的实现和准确性。表维护可用于改进统计数据。正如评论中所建议的,您应该查看执行计划来检查表的访问方式。
一些提示:
如果您通过应用上述任何技巧获得更好的结果,请在评论中报告。