修改 MS SQL Server 中的 IO 成本计算公式以反映可忽略不计的 SSD 寻道时间?

问题描述

MS sql Server 的查询优化器似乎使用固定公式来估算 IO 成本。这个公式似乎是基于一个固定值,反映了磁硬盘的寻道时间,加上根据连续读取的块数量而变化的成本。 如果只读取一两个块,则计算出的查找固定成本远高于读取块的计算成本。

Joe Chang 在 2002 年的这篇文章中对成本结构进行了研究:http://www.qdpma.com/CBO/s2kCBO_2a.html

Paul White 于 2010 年在此处研究了该主题https://www.sql.kiwi/2010/09/inside-the-optimizer-plan-costing.html

在发生足够多的随机访问的情况下,此成本计算导致查询优化器从索引搜索切换到某些查询的索引扫描。如果基数合适,优化器会计算出磁盘查找将花费大量时间,因此顺序读取整个表(或表或索引的大范围)会更快。

在 SSD 上运行 sql Server 时,由于 SSD 的寻道时间几乎可以忽略不计(微秒而不是毫秒),IO 成本计算变得不切实际。我创建了一个简单的环境来展示这一点。 In 创建三个表和一个查询,导致对键值的访问足够稀疏。您可以在名为“reprod”的数据库中运行第一个 sql(如下)一次,然后在 where 子句 (... and T1.id1<X) 上使用不同的上限多次运行第二个 sql。请注意不要使用参数修改查询,否则由于参数嗅探,无法重现行为 - 执行计划中的更改 - 如下所述。

在我的机器上,结果如下:

执行时间(毫秒) 限制 子树总成本 表格提示
32 id1<89 15,08132
382 id1<90 15,1062
35 id1<90 15,2106 强制搜索

数字显示执行时间从 89 到 90 显着增加。检查分析器输出显示,对于 id1

我的解释是查询优化器假设磁盘查找时间基于磁硬盘,如本问题介绍中所述。

SSD vs HD seek and read times

与现实(绿色)相比,随机读取一个块会导致 SSD 的读取时间被大大高估(红色 - 查询优化器)。跳转在上表中可见。

在我们的环境中,有很多糟糕的执行计划涉及全表或索引扫描。我们确信(通过测试)使用索引查找会大大减少执行时间(最多 1/100,意味着 100 毫秒而不是 10 秒)。

现在的问题是:如何让 sql Server 正确反映 SSD 可忽略不计的查找时间,以及还有哪些其他解决方法(下面列出了一些)?

可能的解决方法

  • 使用计划指南(但是会产生更多的工作,存在的表越大,针对每个表运行的不同类型的查询就越多)
  • 使用 forceseek(在我们的工作环境中,当来自实体框架时需要注入,可能使用 DbCommandInterceptor - 也许还有其他方法?)
  • 可以使用“dbcc setioweight”,但对我没有影响,因为我猜它只会扩展整个 IO 成本估算,并且会导致对查询的权衡,包括更多的 cpu 时间,而不是不同的方式访问索引。

我猜其他人也受到这种行为的影响 - 甚至可能没有注意到,因为 SSD 通常还具有更高的顺序读取率。如果对问题有足够的认识,可能会引入一个可配置的选项,以便我们可以在 IO 成本计算/估计中为固定成本组件设置另一个值。

    -- reprod-1-create.sql

    use reprod;
    drop table T3;
    drop table T2;
    drop table T1;
    go
    create table T1(id1 int identity(1,1) primary key,crc int);
    create table T2(id2 int identity(1,id1 int not null,crc int,foreign key(id1) references T1(id1) )
    create table T3(id3 int identity(1,id2 int not null,foreign key(id2) references T2(id2) )
    create index IX_id1 on T2(id1) include(crc); alter index IX_id1 on T2 disABLE;
    create index IX_id2 on T3(id2) include(crc); alter index IX_id2 on T3 disABLE;
    go
    insert into T1(crc) values(1);
    insert into T1(crc) select crc from T1
    insert into T1(crc) select crc from T1
    insert into T1(crc) select crc from T1
    insert into T1(crc) select crc from T1
    insert into T1(crc) select crc from T1
    insert into T1(crc) select crc from T1
    insert into T1(crc) select crc from T1
    insert into T1(crc) select crc from T1
    insert into T1(crc) select crc from T1
    insert into T1(crc) select crc from T1
    go
    update T1 set crc = checksum(str(id1)) where id1 > 0;
    go
    
    insert into T2(id1,crc) ( select id1,checksum(str(id1)+str(crc)) from T1 )
    insert into T2(id1,checksum(str(id1)+str(id2)+str(crc)) from T2 )
    insert into T2(id1,checksum(str(id1)+str(id2)+str(crc)) from T2 )
    
    insert into T3(id2,crc) ( select id2,checksum(str(id2)+str(crc)) from T2 )
    insert into T3(id2,checksum(str(id2)+str(id3)+str(crc)) from T3 )
    insert into T3(id2,checksum(str(id2)+str(id3)+str(crc)) from T3 )
    
    alter index IX_id1 on T2 rebuild;
    alter index IX_id2 on T3 rebuild;
    select count(*) as NrRowsT1 from T1;
    select count(*) as NrRowsT2 from T2;
    select count(*) as NrRowsT3 from T3;
    -- reprod-2-execute.sql
    use reprod; set statistics time on; set statistics profile on;
    select  count(T3.id3) from T1
    inner join T2 on T2.id1 = T1.id1
    
    inner join T3 --with(forceseek)--with(index(IX_id2))
    
    on T3.id2 = T2.id2
    where abs(T3.crc) < 2147483--6--4--7
    
    and T1.id1<90 -- 89 = index seek,90=index scan
    
    set statistics profile off; set statistics time off;

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)