Mik*_*org 7 sql sql-server indexing performance query-optimization
我将要描述的是在以下硬件上运行:
SQL Server Management Studio (SSMS) 和 sql server 实例都在此服务器上运行。所以所有的查询都是在本地执行的。此外,在执行任何查询之前,我总是运行以下命令以确保没有数据访问缓存在内存中:
DBCC DROPCLEANBUFFERS
Run Code Online (Sandbox Code Playgroud)
我们有一个包含大约 11'600'000 行的 SQL Server 表。在大计划中,不是一张特别大的桌子,但它会随着时间的推移而大大增加。
该表具有以下结构:
CREATE TABLE [Trajectory](
[Id] [int] IDENTITY(1,1) NOT NULL,
[FlightDate] [date] NOT NULL,
[EntryTime] [datetime2] NOT NULL,
[ExitTime] [datetime2] NOT NULL,
[Geography] [geography] NOT NULL,
[GreatArcDistance] [real] NULL,
CONSTRAINT [PK_Trajectory] PRIMARY KEY CLUSTERED ([Id])
)
Run Code Online (Sandbox Code Playgroud)
(为简单起见,排除了一些列,但它们的数量和大小非常小)
虽然没有那么多行,但由于[Geography]列的原因,该表占用了相当多的磁盘空间。此列的内容是 LINESTRINGS,大约有 3000 个点(包括 Z 和 M 值)。
现在,假设我们只是在表的 Id 列上有一个聚集索引,它也表示主键约束,如上面的 DDL 中所述。
我们遇到的问题是,当我们查询日期范围和特定地理交叉点的表时,完成该查询需要相当长的时间。
我们正在查看的查询如下所示:
DEFINE @p1 = [...]
SELECT [Id], [Geography]--, (+ some other columns)
WHERE [FlightDate] BETWEEN '2018-09-04' AND '2018-09-12' AND [Geography].STIntersects(@p1) = 1
Run Code Online (Sandbox Code Playgroud)
这是一个相当简单的查询,使用我上面提到的两个过滤器。为了快速查询,我们尝试了几种不同类型的索引:
当我们查询表时,在添加了这样的索引之后,期望查询计划看起来像这样:
[Geography].STIntersects(@p1) = 1对返回的每一行执行地理过滤器这也正是它的作用。这是在 (SSMS) 中看到的实际查询执行计划的快照:
此查询需要很长时间才能完成(可以以分钟为单位,如上面的屏幕截图所示)。
--- 更新 1 开始 ---
附加查询计划信息(对于主要步骤,注意:与上面显示的查询执行不同,因此时间有所不同。此查询耗时 2:39):
SELECT.QueryTimeStats
CpuTime=12241msElapsedTime=157591msKey Lookup (97%)
Actual I/O Statistics
Actual Logical Reads=48165Actual Physical Reads=81Actual Time Statistics
Actual Elapsed CPU Time=144msActual Elapsed Time=266msIndex Seek (0%)
Actual I/O Statistics
Actual Logical Reads=85Actual Physical Reads=0Actual Read Aheads=73Actual Scans=21Filter (3%)
Actual Time Statistics
Actual Elapsed CPU Time=12156msActual Elapsed Time=157583ms对我来说,这个查询的所有时间或多或少都花在了 IO 上。为什么我无法解释。我还将添加以下感兴趣的内容:
EntryTime列的不同过滤器(未编入索引!),它大致返回相同的行数,那么查询时间将减少到大约 20 秒,尽管事实上我仍然选择相同的行数。我想对此的唯一解释是查询实际上不需要[Geography]在丢弃该行之前读取昂贵的列。--- 更新 1 结束 ---
该索引与其他索引的不同之处仅在于它与索引一起存储了大的 [Geography] 列。但是对查询计划的期望或多或少是相同的:
当我们查询表时,在添加了这样的索引之后,期望查询计划看起来像这样:
[Geography].STIntersects(@p1) = 1对返回的每一行执行地理过滤器此查询耗时不到 10 秒。这是在 SSMS 中看到的两个查询计划:
请注意,在上面的第 2 步和第 3 步中,与使用其他索引的查询相比,它进行了切换(意味着它只有在完全完成过滤后才执行查找,因此它只对主表进行了大约 1'000 次查找,而不是 50,000 次)。现在,这向我表明执行此查询时实际花费的时间是查找主表,而不是其他任何内容,例如 INDEX SEEK 或 FILTER。
现在维护这样的索引并不是我们想要做的理想的事情,因为当我们考虑[Geography]表中的列有多大以及它会增长多少时,它会使用相当多的空间。重建这样的索引需要几个小时。
--- 更新 2 开始 ---
附加查询计划信息:
SELECT.QueryTimeStats
CpuTime=11648msElapsedTime=7533msKey Lookup (88%)
Actual I/O Statistics
Actual Logical Reads=1191Actual Physical Reads=0Actual Time Statistics
Actual Elapsed CPU Time=0msActual Elapsed Time=0msIndex Seek (3%)
Actual I/O Statistics
Actual Logical Reads=7119Actual Physical Reads=4Actual Read Aheads=6678Actual Scans=21Actual Time Statistics
Actual Elapsed CPU Time=104msActual Elapsed Time=168msFilter (9%)
Actual Time Statistics
Actual Elapsed CPU Time=11535msActual Elapsed Time=6888ms关于统计的附加说明:
[Geography]列。但是由于在执行查找之前已经应用了过滤器,它显然必须少做很多。但即便如此,零 IO 还是让我感到困惑。--- 更新 2 结束 ---
现在,考虑到分区是我们考虑对表做的事情,我们还考虑更改聚集索引,使其包含 [FlightDate]。看看下面的 SQL DDL:
ALTER TABLE [Trajectory] DROP CONSTRAINT [PK_Trajectory]
ALTER TABLE [Trajectory] ADD CONSTRAINT [PK_Trajectory] PRIMARY KEY CLUSTERED ([FlightDate] ASC, [Id] ASC)
CREATE UNIQUE INDEX [AK_Trajectory] ON [Trajectory] ([Id] ASC)
Run Code Online (Sandbox Code Playgroud)
这会更改表,使其现在聚集在 [FlightDate] 上,然后是 [Id],以确保唯一性。另外我们在[Id]上添加了一个替代键约束,所以理论上它仍然可以用来引用表。
这 3 条 sql 语句需要几个小时才能完成,但这样做的一个额外好处是,将来可以非常轻松地在 [FlightDate] 上创建分区,从而允许对针对该表进行的所有查询进行分区消除。
当我们现在对表执行相同的查询时,期望查询计划如下所示:
[Geography].STIntersects(@p1) = 1对返回的每一行执行地理过滤器这是一个比前面示例中描述的更简单的查询计划,实际上它确实使用了这个计划,如下所示:
唯一的问题?大约需要一分钟才能完成。但是,如果我们仔细查看查询计划本身,它也会反驳先前的结论,即查询中实际花费时间的是对主表的查找,因为这里说大部分时间都花在了对 [Geography ] 柱子。
我对此有一个可能感兴趣的附加评论:即使我不删除我在上一节 ( [IX_Trajectory_FlightDate_Includes_Geography]) 中创建的索引,在像这样更改表结构后查询也会很慢。但是,如果我暗示查询编译器它应该使用上一节中的索引和我在这一步中刚刚创建的替代键 [AK_Trajectory] using,WITH (INDEX([AK_Trajectory], [IX_Trajectory_FlightDate_Includes_Geography])那么查询将具有与 (2) 中大致相同的性能。
所以SQL Server实际上主动决定使用较慢的查询计划,显然认为它更快。坦率地说,我不怪它。我也会这样做,因为该查询计划要简单得多。到底是怎么回事?
现在,您可能想知道我们是否考虑SPATIAL INDEX在[Geography]列中添加 a 。这已经是一个考虑。这种索引的问题(以及为什么它不能真正使用)有两个方面:
[FlightDate]索引能够过滤掉大量的[Trajectory]行。问题的关键在于,这种空间索引“SEEK”的结果会随着表的增长而线性增长,而 INDEX SEEK 的结果[FlightDate]则不会。--- 更新 3 开始 ---
附加查询计划信息(对于主要步骤,注意:与上面显示的查询执行不同,因此时间有所不同。此查询花费了 0:49):
SELECT.QueryTimeStats
CpuTime=11818msElapsedTime=48253msParallelism (7%)
Actual Time Statistics
Actual Elapsed CPU Time=7msActual Elapsed Time=47638msClustered Index Seek (25%)
Actual I/O Statistics
Actual Logical Reads=7403Actual Physical Reads=4Actual Read Aheads=6939Actual Scans=21Actual Time Statistics
Actual Elapsed CPU Time=107msActual Elapsed Time=57msFilter (69%)
Actual Time Statistics
Actual Elapsed CPU Time=11727msActual Elapsed Time=48250ms值得注意的是:
--- 更新 3 结束 ---
--- 更新 4 开始 ---
Lucky Brain 建议它可能会更慢,因为数据实际上存储在ROW_OVERFLOW_DATA页面中而不是IN_ROW_PAGES. 下面仔细看看数据是如何实际存储在表中的,使用以下查询进行查询:
SELECT
OBJECT_SCHEMA_NAME(p.object_id) table_schema,
OBJECT_NAME(p.object_id) table_name,
p.index_id,
p.partition_number,
au.allocation_unit_id,
au.type_desc,
au.total_pages,
au.used_pages,
au.data_pages
FROM sys.system_internals_allocation_units au
JOIN sys.partitions p
ON au.container_id = p.partition_id
WHERE OBJECT_NAME(p.object_id) = 'Trajectory'
ORDER BY table_schema, table_name, p.index_id, p.partition_number, au.type;
Run Code Online (Sandbox Code Playgroud)
这提供了有关如何为主表(聚集索引)和其他索引存储数据的信息。这样做的结果是:
Clustered Index
IN_ROW_DATA: total_pages=705137, used_pages=705137, data_pages=697811LOB_DATA: total_pages=10302796, used_pages=10248361, data_pages=0 ROW_OVERFLOW_DATA: total_pages=9, used_pages=2, data_pages=0 Index #2
IN_ROW_DATA: total_pages=497639, used_pages=494629, data_pages=496531LOB_DATA: total_pages=10219824, used_pages=10217546, data_pages=0 ROW_OVERFLOW_DATA: ------------------------------------------------------------由此可以看出,虽然数据没有完全存储在 中ROW_OVERFLOW_DATA,但也没有存储在IN_ROW_PAGES中。话虽这么说,我不认为有任何理由认为,从获取的数据LOB_DATA被认为是比快ROW_OVERFLOW_DATA。稍微阅读一下这些类型,很明显,LOB_DATA鉴于单个列通常超过 8kB 的最大值,必须存储这些数据ROW_OVERFLOW_DATA。
但是从上面也可以看出,主表(聚集索引)和索引 #2 都使用LOB_DATA页面,所以我不完全确定为什么索引 #2 会快得多,除非LOB_DATA意味着与索引不同的东西,与聚集索引相比。
但我觉得我所看到的一切都支持同样的结论:
LOB_DATA,该查找总是非常缓慢(即使它作为聚集索引上的 INDEX SEEK 的一部分执行)。基本上我所做的每个查询(快速或慢速)都表明了这一点。例如,考虑索引 #1:
UPDATE 1中解释),结果集保持大致相等(约 1'000 行),那么查询会突然花费大约 20 秒。这种更改意味着它只需要LOB_DATA在主表中查找实际结果集的页面,而不是在索引 #1 中查找所有 50'000 个条目。(这里的重要注意事项是它仍然必须对所有主表进行 Key Lookup,它只是不需要LOB_DATA为每个条目转到。)LOB_DATA某种方式显着减慢了查询速度。--- 更新 4 结束 ---
--- 更新 5 开始 ---
之前的更新包括两个索引的物理页面统计信息,这是第一个索引的相同统计信息:
Index #1
IN_ROW_DATA: total_pages=18705, used_pages=18698, data_pages=18659 LOB_DATA: ------------------------------------------------------------ROW_OVERFLOW_DATA: ------------------------------------------------------------显然,这不包括LOB_DATA或ROW_OVERFLOW_DATA。但更令人惊讶的是,与IN_ROW_DATA索引 #2 相比,使用的页面显着减少(大约 20-30 的数量级)。这表明,正如 Lucky Brain 所建议的那样,当索引中包含空间列时,SQL Server 可能会直接在 中存储有关该几何/地理的一些信息,例如边界框,IN_ROW_DATA以便快速执行几何操作。
这当然假设表不会为“正常”空间列执行此操作,当它是聚集索引的一部分时。
--- 更新 5 结束 ---
谁能回答这两个问题:
查看您的查询,首先要考虑的是您在 SELECT 列表中包含一个 .NET/CLR 数据类型的空间列,并且这些列存储在IN_ROW_DATA需要键查找的页面外部,除非该空间列包含在索引中它还可能在索引数据页中包含空间边界框,以加快过滤速度,从而节省大部分磁盘 I/O。我想说您发现了一种有效的技巧来加速空间列过滤而不需要空间索引。
为了证明我的观点,我建议您参阅原始 SQL 文档,我相信您已经了解了有关覆盖索引的内容,其中澄清了以下内容:“将非键列添加到leaf level非聚集索引以提高查询性能。这允许查询优化器从索引扫描中定位所有需要的信息;不访问表或聚集索引数据。 ”。最后一部分在这里非常重要,因此我假设边界框是空间列的“必需信息”的一部分,以帮助查询优化器避免访问IN_ROW_DATA.
结论:
IN_ROW_DATA需要更多磁盘 I/O 的页面之外。IN_ROW_DATA节省大部分磁盘 I/O;请记住,索引(3)仍然需要查找LOB_DATA.