索引上的高 IO,是否有更高性能的解决方案?

A_V*_*A_V 6 sql-server t-sql execution-plan sql-server-2016

我有这个查询,它过去需要几分钟才能运行,现在需要 6 秒,但它每天运行数千次,所以我想让它更快。

https://www.brentozar.com/pastetheplan/?id=SJMLjJOWm

似乎在此查询期间超过 99% 的 I/O 发生在一次聚集索引扫描上。

这正常吗?这个查询足以证明为它添加任何额外的索引是合理的,所以我想知道我是否在这里遗漏了一些明显的东西。

dbo.GROUP_CONCAT 函数来自这个 github 组装项目 https://github.com/orlando-colamatteo/ms-sql-server-group-concat-sqlclr

SpecsProd 表定义:

CREATE TABLE [dbo].[SpecsProd](
    [ID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
    [specsID_1] [int] NULL,
    [specsID_2] [int] NULL,
    [specID] [int] NOT NULL,
    [productID] [int] NOT NULL,
    [SpecValue_1] [varchar](1000) NULL,
    [SpecValue_2] [varchar](1000) NULL,
    [Flock] [bit] NULL,
    [SpecValue_1a] [varchar](2000) NULL,
    [SpecValue_2a] [varchar](2000) NULL,
 CONSTRAINT [PK_SpecsProd] 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 = 90) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[SpecsProd] ADD  CONSTRAINT [DF_SpecsProd_Flock]  DEFAULT ((0)) FOR [Flock]
GO

ALTER TABLE [dbo].[SpecsProd]  WITH NOCHECK ADD  CONSTRAINT [FK_SpecsProd_Products] FOREIGN KEY([productID])
REFERENCES [dbo].[Products] ([Id_product])
NOT FOR REPLICATION 
GO

ALTER TABLE [dbo].[SpecsProd] CHECK CONSTRAINT [FK_SpecsProd_Products]
GO
Run Code Online (Sandbox Code Playgroud)

使用 99% I/O 的聚集索引是 [PK_SpecsProd](第一个)。其他索引也在那里。

    ALTER TABLE [dbo].[SpecsProd] ADD  CONSTRAINT [PK_SpecsProd] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_ProdID_SpecID] ON [dbo].[SpecsProd]
(
    [productID] ASC,
    [specID] ASC
)
INCLUDE (   [ID],
    [SpecValue_1],
    [SpecValue_2]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_SpecID_ProductID] ON [dbo].[SpecsProd]
(
    [specID] ASC,
    [productID] ASC
)
INCLUDE (   [SpecValue_1],
    [SpecValue_2]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [product] ON [dbo].[SpecsProd]
(
    [productID] ASC
)
INCLUDE (   [specID],
    [SpecValue_1],
    [SpecValue_2]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [specId_inc_prodID_specvalue1] ON [dbo].[SpecsProd]
(
    [specID] ASC
)
INCLUDE (   [productID],
    [SpecValue_1]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
GO

CREATE NONCLUSTERED INDEX [specsID] ON [dbo].[SpecsProd]
(
    [specsID_1] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
GO
Run Code Online (Sandbox Code Playgroud)

具有最多列的表格的吉尼斯记录竞争者之一(规格表)

CREATE TABLE [dbo].[Specs](
    [ID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
    [Id_spec] [int] NOT NULL,
    [CatId] [int] NOT NULL,
    [sect] [varchar](50) NULL,
    [spec] [varchar](75) NOT NULL,
    [format] [varchar](50) NULL,
    [unit] [varchar](20) NULL,
    [definition] [ntext] NULL,
    [ordre] [int] NOT NULL,
    [Id_langue] [int] NOT NULL,
    [FormField] [varchar](50) NULL,
    [List] [varchar](max) NULL,
    [filterField] [bit] NOT NULL,
    [isFilter] [bit] NOT NULL,
    [isCollectionFilter] [bit] NOT NULL,
    [quickViewSubcats] [varchar](250) NULL,
    [width] [bit] NOT NULL,
    [height] [bit] NOT NULL,
    [depth] [bit] NOT NULL,
    [weight] [bit] NOT NULL,
    [DateCreation] [datetime] NOT NULL,
    [DateModification] [datetime] NOT NULL,
    [quickView] [bit] NOT NULL,
    [Visible] [bit] NULL,
    [compare] [bit] NOT NULL,
    [priceTag] [bit] NOT NULL,
    [ConvertionRate] [varchar](20) NULL,
    [ConvertionUnit] [varchar](20) NULL,
    [searchableLabel] [bit] NULL,
    [searchableValue] [bit] NULL,
 CONSTRAINT [PK_Specs] 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 = 90) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

ALTER TABLE [dbo].[Specs] ADD  CONSTRAINT [DF_Specs_filter]  DEFAULT ((0)) FOR [filterField]
GO

ALTER TABLE [dbo].[Specs] ADD  CONSTRAINT [DF_Specs_isFilter]  DEFAULT ((0)) FOR [isFilter]
GO

ALTER TABLE [dbo].[Specs] ADD  CONSTRAINT [DF_Specs_isCollectionFilter]  DEFAULT ((0)) FOR [isCollectionFilter]
GO

ALTER TABLE [dbo].[Specs] ADD  CONSTRAINT [DF_Specs_width]  DEFAULT ((0)) FOR [width]
GO

ALTER TABLE [dbo].[Specs] ADD  CONSTRAINT [DF_Specs_height]  DEFAULT ((0)) FOR [height]
GO

ALTER TABLE [dbo].[Specs] ADD  CONSTRAINT [DF_Specs_depth]  DEFAULT ((0)) FOR [depth]
GO

ALTER TABLE [dbo].[Specs] ADD  CONSTRAINT [DF_Specs_weight]  DEFAULT ((0)) FOR [weight]
GO

ALTER TABLE [dbo].[Specs] ADD  CONSTRAINT [DF_Specs_DateCreation]  DEFAULT (getdate()) FOR [DateCreation]
GO

ALTER TABLE [dbo].[Specs] ADD  CONSTRAINT [DF_Specs_DateModification]  DEFAULT (getdate()) FOR [DateModification]
GO

ALTER TABLE [dbo].[Specs] ADD  CONSTRAINT [DF_Specs_quickView]  DEFAULT ((0)) FOR [quickView]
GO

ALTER TABLE [dbo].[Specs] ADD  CONSTRAINT [DF_Specs_compare]  DEFAULT ((0)) FOR [compare]
GO

ALTER TABLE [dbo].[Specs] ADD  CONSTRAINT [DF_Specs_priceTag]  DEFAULT ((0)) FOR [priceTag]
GO
Run Code Online (Sandbox Code Playgroud)

Joe*_*ish 4

  1. 等待统计数据表明ASYNC_NETWORK_IO是瓶颈。

在您的实际计划中,大约 87% 的时间都花在将结果发送给客户上。您可能需要更改应用程序代码或减少发回的数据量。

<WaitStats>
  <Wait WaitType="CMEMTHREAD" WaitTimeMs="33" WaitCount="86" />
  <Wait WaitType="SESSION_WAIT_STATS_CHILDREN" WaitTimeMs="1029" WaitCount="55" />
  <Wait WaitType="LATCH_EX" WaitTimeMs="2796" WaitCount="308" />
  <Wait WaitType="ASYNC_NETWORK_IO" WaitTimeMs="3083" WaitCount="144" />
</WaitStats>
<QueryTimeStats ElapsedTime="3542" CpuTime="7247" />
Run Code Online (Sandbox Code Playgroud)
  1. MAXDOP 22对于查询来说相当高SELECT

您是否使用不同的MAXDOP值测试了此查询以验证 22 确实是最佳选择?如果我不得不猜测的话,我会说您有一个两个套接字服务器,每个套接字有 12 个核心。也许MAXDOP出于某种原因在实例级别设置为 22。我基于计划中的线程信息:

<ThreadStat Branches="4" UsedThreads="88">
  <ThreadReservation NodeId="0" ReservedThreads="24" />
  <ThreadReservation NodeId="1" ReservedThreads="24" />
  <ThreadReservation NodeId="2" ReservedThreads="16" />
  <ThreadReservation NodeId="3" ReservedThreads="24" />
</ThreadStat>
Run Code Online (Sandbox Code Playgroud)

NUMA 节点 2 的调度程序比其他节点少,服务器有 2 个套接字,每个套接字有 12 个调度程序,服务器有 4 个套接字,每个套接字有 6 个调度程序,或者设置了手动软 NUMA。对于 MAXDOP 的常见建议是使用小于每个硬 NUMA 节点的物理核心数量的值。

综上所述,您无需了解所有技术细节即可进行测试。尝试使用不同的 MAXDOP 进行测试(确保多次运行测试),看看是否有帮助。

  1. 如果需要,您可以减少计划中所有三个表的 IO。

请注意,我只是查看了查询计划,并没有查看您已有的索引定义。SpecsProd看起来是你最大的桌子。您可以仅在需要的列上定义覆盖索引。您无法对其进行查找,但会执行更少的 IO,因为仅包含查询中使用的三列的非聚集索引将小于聚集索引。

Products表具有第二高的 IO 成本。您已经按聚集索引进行排序,这很好,但聚集索引的大小是表中所有数据的大小。您可以仅在聚集键列上创建非聚集索引以减少 IO。您甚至可以创建它以DESC使扫描符合并行性,但我不知道这是否会由于表达式而在实践中产生影响TOP

您已经在 上有一个覆盖索引Specs,但过滤器是在谓词而不是搜索谓词中计算的。如果您更改索引或以正确的顺序创建一个包含键列的新索引,您应该能够执行查找而不是扫描。