我遇到了奇怪的需求组合,这些组合会降低查询的性能。这是由 ESRI 的 ArcGIS 服务器在 SQL Server 中创建的表,查询来自对表中数据进行分页的要素服务。
这很慢(粘贴计划):
SELECT
OBJECTID,
Shape
FROM
dbo.PARCELS
WHERE
dbo.PARCELS.GDB_ARCHIVE_OID IN
(SELECT GDB_ARCHIVE_OID
FROM
(SELECT
GDB_ARCHIVE_OID,
ROW_NUMBER() OVER(PARTITION BY OBJECTID ORDER BY GDB_FROM_DATE DESC) rn_,
GDB_IS_DELETE
FROM
dbo.PARCELS
WHERE
((GDB_BRANCH_ID = 0 AND GDB_FROM_DATE <= '2023-12-22')
OR (GDB_BRANCH_ID = 1 AND GDB_FROM_DATE <= '2023-12-22'))) br__
WHERE
br__.rn_ = 1 AND br__.GDB_IS_DELETE = 0)
ORDER BY
OBJECTID ASC
OFFSET 2000 ROWS
FETCH NEXT 2000 ROWS ONLY
没有几何数据类型的形状字段,速度很快(粘贴计划):
SELECT
OBJECTID
--,Shape
FROM
dbo.PARCELS
WHERE
dbo.PARCELS.GDB_ARCHIVE_OID IN
(SELECT GDB_ARCHIVE_OID
FROM
(SELECT
GDB_ARCHIVE_OID,
ROW_NUMBER() OVER(PARTITION BY OBJECTID ORDER BY GDB_FROM_DATE DESC) rn_,
GDB_IS_DELETE
FROM
dbo.PARCELS
WHERE
((GDB_BRANCH_ID = 0 AND GDB_FROM_DATE <= '2023-12-22')
OR (GDB_BRANCH_ID = 1 AND GDB_FROM_DATE <= '2023-12-22'))) br__
WHERE
br__.rn_ = 1 AND br__.GDB_IS_DELETE = 0)
ORDER BY
OBJECTID ASC
OFFSET 2000 ROWS
FETCH NEXT 2000 ROWS ONLY
没有where子句,速度很快(粘贴计划):
SELECT
OBJECTID,
Shape
FROM
dbo.PARCELS
-- WHERE DBO.PARCELS.GDB_ARCHIVE_OID IN (
-- SELECT GDB_ARCHIVE_OID
-- FROM (
-- SELECT GDB_ARCHIVE_OID,ROW_NUMBER() OVER(PARTITION BY OBJECTID ORDER BY GDB_FROM_DATE DESC) rn_, GDB_IS_DELETE
-- FROM DBO.PARCELS
-- WHERE ((GDB_BRANCH_ID = 0 AND GDB_FROM_DATE <= '2023-12-22') OR (GDB_BRANCH_ID = 1 AND GDB_FROM_DATE <= '2023-12-22'))
-- ) br__
-- WHERE br__.rn_ = 1 AND br__.GDB_IS_DELETE = 0
-- )
ORDER BY
OBJECTID ASC
OFFSET 2000 ROWS
FETCH NEXT 2000 ROWS ONLY
不用分页就这么快(贴计划):
SELECT
OBJECTID,
Shape
FROM
dbo.PARCELS
WHERE
dbo.PARCELS.GDB_ARCHIVE_OID IN
(SELECT
GDB_ARCHIVE_OID
FROM
(SELECT
GDB_ARCHIVE_OID,
ROW_NUMBER() OVER(PARTITION BY OBJECTID ORDER BY GDB_FROM_DATE DESC) rn_,
GDB_IS_DELETE
FROM
dbo.PARCELS
WHERE
((GDB_BRANCH_ID = 0 AND GDB_FROM_DATE <= '2023-12-22')
OR (GDB_BRANCH_ID = 1 AND GDB_FROM_DATE <= '2023-12-22'))) br__
WHERE
br__.rn_ = 1 AND br__.GDB_IS_DELETE = 0)
ORDER BY
OBJECTID ASC
-- OFFSET 2000 ROWS
-- FETCH NEXT 2000 ROWS ONLY
几何列、分页和 where 子句这三者的组合杀死了查询。如果没有其中任何一个,执行时间将低于一秒。三者的执行时间均为 41 秒。
下面是最后带有索引的表定义:
CREATE TABLE [dbo].[PARCELS](
[OBJECTID] [int] NOT NULL,
[TXID_NMBR] [nvarchar](25) NULL,
[TAX_MAP] [nvarchar](10) NULL,
[CONTROL] [numeric](38, 8) NULL,
[PARCEL] [nvarchar](25) NULL,
[TIE_BACK] [nvarchar](25) NULL,
[LAST_NAME] [nvarchar](50) NULL,
[FIRST_NAME] [nvarchar](50) NULL,
[FULL_NAME] [nvarchar](255) NULL,
[OWNER_ADD] [nvarchar](50) NULL,
[OWNER_ADD2] [nvarchar](50) NULL,
[OWNER_CITY] [nvarchar](50) NULL,
[OWNER_ST] [nvarchar](2) NULL,
[OWNER_ZIP] [nvarchar](10) NULL,
[MUNIC] [nvarchar](25) NULL,
[SITUS] [nvarchar](255) NULL,
[SITUS_CITY] [nvarchar](50) NULL,
[SITUS_ST] [nvarchar](2) NULL,
[SITUS_ZIP] [nvarchar](10) NULL,
[USE_] [nvarchar](10) NULL,
[NBHD] [nvarchar](10) NULL,
[STORIES] [nvarchar](50) NULL,
[STYLE] [nvarchar](50) NULL,
[EXTERIOR] [nvarchar](50) NULL,
[YR_BUILT] [nvarchar](4) NULL,
[GRADE] [nvarchar](50) NULL,
[BEDROOMS] [nvarchar](2) NULL,
[BATHS] [nvarchar](2) NULL,
[HALF_BATHS] [nvarchar](2) NULL,
[BSMT] [int] NULL,
[FIN_BSMT] [int] NULL,
[HEAT] [nvarchar](50) NULL,
[CENT_AIR] [nvarchar](50) NULL,
[FIREPLACE] [nvarchar](5) NULL,
[TERRAIN] [nvarchar](10) NULL,
[SEWER] [nvarchar](50) NULL,
[WATER] [nvarchar](10) NULL,
[LIV_AREA] [int] NULL,
[GRANTOR] [nvarchar](255) NULL,
[DEED] [nvarchar](50) NULL,
[SALE_DATE] [datetime2](7) NULL,
[SALE_AMT] [int] NULL,
[VAL_2_SALE] [numeric](38, 8) NULL,
[PP_ACRE] [numeric](38, 8) NULL,
[PP_AC_ADJ] [numeric](38, 8) NULL,
[PP_SQFT] [numeric](38, 8) NULL,
[PP_SF_ADJ] [numeric](38, 8) NULL,
[C_AND_G] [nvarchar](1) NULL,
[DEED_ACRES] [numeric](38, 8) NULL,
[LAND_VAL] [int] NULL,
[BLDG_VAL] [int] NULL,
[OUT_VAL] [int] NULL,
[PROP_VAL] [int] NULL,
[LycoOnline] [nvarchar](255) NULL,
[TYPE] [nvarchar](10) NULL,
[MUNI_NAME] [nvarchar](25) NULL,
[MUNI_TYPE] [nvarchar](25) NULL,
[created_user] [nvarchar](255) NULL,
[created_date] [datetime2](7) NULL,
[last_edited_user] [nvarchar](255) NULL,
[last_edited_date] [datetime2](7) NULL,
[SALE_TYPE] [nvarchar](30) NULL,
[VALID_SALE] [nvarchar](1) NULL,
[Shape] [geometry] NULL,
[Calc_Acres] [numeric](38, 8) NULL,
[Notes] [nvarchar](150) NULL,
[GlobalID] [uniqueidentifier] NOT NULL,
[GDB_GEOMATTR_DATA] [varbinary](max) NULL,
[GDB_ARCHIVE_OID] [int] IDENTITY(1,1) NOT NULL,
[GDB_FROM_DATE] [datetime2](7) NOT NULL,
[GDB_IS_DELETE] [smallint] NOT NULL,
[GDB_BRANCH_ID] [int] NOT NULL,
[GDB_DELETED_AT] [datetime2](7) NULL,
[GDB_DELETED_BY] [nvarchar](255) NULL,
CONSTRAINT [R831_pk] PRIMARY KEY CLUSTERED
(
[GDB_ARCHIVE_OID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF, FILLFACTOR = 95, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
ALTER TABLE [dbo].[PARCELS] SET (LOCK_ESCALATION = DISABLE)
GO
ALTER TABLE [dbo].[PARCELS] ADD DEFAULT ('{00000000-0000-0000-0000-000000000000}') FOR [GlobalID]
GO
ALTER TABLE [dbo].[PARCELS] ADD CONSTRAINT [GDB_FROM_DATE831_def] DEFAULT (CONVERT([datetime2](3),getutcdate())) FOR [GDB_FROM_DATE]
GO
ALTER TABLE [dbo].[PARCELS] ADD CONSTRAINT [GDB_IS_DELETE831_def] DEFAULT ((0)) FOR [GDB_IS_DELETE]
GO
ALTER TABLE [dbo].[PARCELS] ADD CONSTRAINT [GDB_BRANCH_ID831_def] DEFAULT ((0)) FOR [GDB_BRANCH_ID]
GO
ALTER TABLE [dbo].[PARCELS] WITH NOCHECK ADD CONSTRAINT [g799_ck] CHECK (([SHAPE].[STSrid]=(2271)))
GO
ALTER TABLE [dbo].[PARCELS] NOCHECK CONSTRAINT [g799_ck]
GO
CREATE UNIQUE NONCLUSTERED INDEX [gdb_ct1_831] ON [dbo].[PARCELS]
(
[OBJECTID] ASC,
[GDB_FROM_DATE] ASC,
[GDB_BRANCH_ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF, FILLFACTOR = 75, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
CREATE UNIQUE NONCLUSTERED INDEX [gdb_ct2_831] ON [dbo].[PARCELS]
(
[GDB_BRANCH_ID] ASC,
[GDB_FROM_DATE] ASC,
[GDB_IS_DELETE] ASC,
[OBJECTID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF, FILLFACTOR = 75, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
ALTER TABLE [dbo].[PARCELS] ADD CONSTRAINT [R831_pk] PRIMARY KEY CLUSTERED
(
[GDB_ARCHIVE_OID] 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 = OFF, FILLFACTOR = 95, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [R831_SDE_ROWID_UK] ON [dbo].[PARCELS]
(
[OBJECTID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF, FILLFACTOR = 75, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
CREATE SPATIAL INDEX [S799_idx] ON [dbo].[PARCELS]
(
[Shape]
)USING GEOMETRY_GRID
WITH (BOUNDING_BOX =(2009646.876269, 328920.783953, 2326307.240322, 522100.77817), GRIDS =(LEVEL_1 = MEDIUM,LEVEL_2 = MEDIUM,LEVEL_3 = MEDIUM,LEVEL_4 = MEDIUM),
CELLS_PER_OBJECT = 16, PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF, FILLFACTOR = 95) ON [PRIMARY]
GO
CREATE UNIQUE NONCLUSTERED INDEX [UUID_831] ON [dbo].[PARCELS]
(
[GlobalID] ASC,
[GDB_BRANCH_ID] ASC,
[GDB_FROM_DATE] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
有什么想法吗?
您的查询可以得到很大的改进,您的索引也可以。
您的主要问题基本上是没有可以支持整个查询的索引。不必要的自连接使它变得更加复杂。
首先,可以重写查询以删除自连接,这是不必要的。您可以直接从
ROW_NUMBER
查询中提取所有列。
此外,我强烈建议您考虑Keyset Pagination。这意味着您无需读取 2000 行并丢弃它们,而是跳转到索引中所需的确切位置。
所以查询可以简化如下:
SELECT TOP (2000)
br.OBJECTID,
br.Shape
FROM (
SELECT
p.*,
ROW_NUMBER() OVER (PARTITION BY p.OBJECTID ORDER BY p.GDB_FROM_DATE DESC) rn
FROM
dbo.PARCELS p
WHERE p.GDB_BRANCH_ID BETWEEN 0 AND 1
AND p.GDB_FROM_DATE <= '2023-12-22'
AND p.OBJECTID > @previousMaxID -- remove this line for the first page
) br
WHERE
br.rn = 1
AND br.GDB_IS_DELETE = 0
ORDER BY
OBJECTID ASC;
注意前一个 Max ID 的使用,以便分页到下一个键。删除第一页最后的
WHERE
子句。
对于索引,键集分页通常需要具有支持索引的确定性排序。
ROW_NUMBER
也可以,因此使用相同的是有意义的。考虑到行编号,我们不需要担心额外的唯一符列,因为保证每个OBJECTID
只有一行。
我建议使用以下索引,它将支持所有
WHERE
子句,以及 ROW_NUMBER
和键集分页的排序要求:
CREATE NONCLUSTERED INDEX [R831_SDE_ROWID_UK] ON dbo.PARCELS
( OBJECTID, GDB_FROM_DATE DESC)
INCLUDE ( GDB_BRANCH_ID, GDB_IS_DELETE, Shape );
如果
p.GDB_BRANCH_ID BETWEEN 0 AND 1
大大减少了行数,那么使用不同的索引可能是有意义的,可能与上面类似,但在 GDB_BRANCH_ID
上使用过滤器。
如您所见,查询计划非常整洁。