I've got this query which used to take minutes to run and now takes like 6 seconds but it runs a thousands of time a day so I'd like to make it faster.
https://www.brentozar.com/pastetheplan/?id=SJMLjJOWm
It seems that over 99% of the I/O during this query is happening on one clustered index scan.
Is this normal ? This query is used enough to justify adding any extra indexes just for it so I'd like to know if I'm missing something obvious here.
The dbo.GROUP_CONCAT function comes from this github assembly project https://github.com/orlando-colamatteo/ms-sql-server-group-concat-sqlclr
SpecsProd table definition:
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
The clustered index that uses 99% I/O is [PK_SpecsProd] (the first one). Other indexes are also there.
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
One of the guiness-record contenders for the table with the most columns (specs table)
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
product
index is almost certainly redundant; I believe any query that could use it would be able to useIX_ProdID_SpecID
as effectively. The same may be true ofspecId_inc_prodID_specvalue1
. From an index perspective,IX_SpecID_ProductID
would be as effective, but if you frequently have queries that use the three columns in the first query and notSpecValue_2
, and ifSpecValue_2
tends to be large, then the smaller index may be more efficient. Finally, you don't need toinclude
theID
column - the clustered index key is included automatically.