SQL Server:聚集索引比等效的非聚集索引慢得多

7

安装

以下硬件可以运行我将要描述的内容:

  • 磁盘:6块2TB的HDD组成RAID5(带1个冗余驱动器)
  • CPU:Intel Xeon E5-2640 @ 2.4 GHz,6核
  • 内存:64 GB
  • SQL Server版本:SQL Server 2016 Developer

SQL Server Management Studio(SSMS)和sql server实例都在此服务器上运行。因此,所有查询都在本地执行。此外,在执行任何查询之前,我始终运行以下命令以确保没有数据访问被缓存在内存中:

DBCC DROPCLEANBUFFERS

问题

我们有一个大约有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])
)

为了简化,一些列被排除在外,但它们的数量和大小非常小。

虽然行数不是很多,但由于[Geography]列,表占用了相当大的磁盘空间。该列的内容是具有大约3000个点(包括Z和M值)的LINESTRINGS。

现在,我们只需考虑在表的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

这是一个相当简单的查询,具有我上面提到的两个过滤器。为了使查询速度快,我们尝试了几种不同类型的索引:
1. CREATE NONCLUSTERED INDEX [IX_Trajectory_FlightDate] ON [Trajectory] ([FlightDate] ASC)
当我们查询表时,在添加此类索引后,期望的查询计划如下所示:
  1. 在索引上执行INDEX SEEK操作(此操作将11,600,000行的数据筛选为大约50,000行)
  2. 查找主表以获取[Geography]列及任何其他选择的列
  3. 对返回的每一行执行地理筛选[Geography].STIntersects(@p1) = 1
这也正是它所做的。以下是在(SSMS)中看到的实际查询执行计划的快照:

Actual query execution plan with index on FlightDate

这个查询需要很长时间才能完成(可以用上面的截图来衡量,可能需要几分钟)。
--- 更新1开始 ---
其他查询计划信息(针对主要步骤,注意:与上面显示的查询不是同一次执行,因此时间会有所变化。这个查询花费了2分39秒):
  • SELECT.QueryTimeStats
    • CpuTime=12241毫秒
    • ElapsedTime=157591毫秒
  • Key Lookup (97%)
    • Actual I/O Statistics
      • Actual Logical Reads=48165
      • Actual Physical Reads=81
    • Actual Time Statistics
      • Actual Elapsed CPU Time=144毫秒
      • Actual Elapsed Time=266毫秒
  • Index Seek (0%)
    • Actual I/O Statistics
      • Actual Logical Reads=85
      • Actual Physical Reads=0
      • Actual Read Aheads=73
      • Actual Scans=21
  • Filter (3%)
    • Actual Time Statistics
      • Actual Elapsed CPU Time=12156毫秒
      • Actual Elapsed Time=157583毫秒
对我来说,这个查询的大部分时间都花在了IO上。为什么我无法解释。我还会添加以下有趣的内容:
  • 它表明,耗时/资源的3%步骤需要157583ms,而耗时/资源的97%步骤只需要266ms。我觉得这很奇怪。
  • 如果我用一个不使用索引的不同过滤器(使用EntryTime列代替),它大致返回相同数量的行,那么查询时间将缩短到大约20秒,尽管我仍然选择相同数量的行。我想唯一的解释是,在可以丢弃行之前,查询实际上并不需要读取昂贵的[Geography]列。

--- UPDATE 1 END ---

2. CREATE NONCLUSTERED INDEX [IX_Trajectory_FlightDate_Includes_Geography] ON [Trajectory] ([FlightDate] ASC) INCLUDE ([Geography])

这个索引唯一不同的地方就是它将大型的[Geography]列与索引一起存储。但是对于查询计划的期望基本相同:

当我们查询该表时,添加了如下索引后,期望的查询计划应该是这样的:
  1. 在索引上执行INDEX SEEK操作(此操作将把11600000行数据过滤到大约50000行)
  2. 对返回的每一行执行地理筛选器[Geography].STIntersects(@p1) = 1
  3. 查找主表以获得额外选择的列
此查询时间不超过10秒。以下是在SSMS中看到的两个查询计划:

Actual query execution plan with index on FlightDate including Geography

请注意,与使用其他索引的查询相比,上述步骤2和3被交换了(这意味着它仅在完成过滤后才执行查找操作,因此它只对主表进行大约1,000次查找,而不是50,000次)。现在,这告诉我执行此查询时实际上需要时间的是对主表的查找,而不是其他任何内容,例如INDEX SEEK或FILTER。

现在,维护这样的索引并不是我们想要做的理想情况,因为考虑到表中的[Geography]列有多大以及它将来会增长多少,它使用了相当多的空间。像这样重建索引需要几个小时。

--- UPDATE 2 START ---

其他查询计划信息:

  • SELECT.QueryTimeStats
    • CpuTime=11648ms
    • ElapsedTime=7533ms
  • Key Lookup (88%)
    • Actual I/O Statistics
      • Actual Logical Reads=1191
      • Actual Physical Reads=0
    • Actual Time Statistics
      • Actual Elapsed CPU Time=0ms
      • Actual Elapsed Time=0ms
  • Index Seek (3%)
    • Actual I/O Statistics
      • Actual Logical Reads=7119
      • Actual Physical Reads=4
      • Actual Read Aheads=6678
      • Actual Scans=21
    • Actual Time Statistics
      • Actual Elapsed CPU Time=104ms
      • Actual Elapsed Time=168ms
  • Filter (9%)
    • Actual Time Statistics
      • Actual Elapsed CPU Time=11535ms
      • Actual Elapsed Time=6888ms

关于统计数据的补充说明:

  • 当深入研究大多数这些数字时,它们在可用线程之间分配得非常好。
  • 我的猜测是,这些统计数据的主要收获是,在“键查找”期间,此查询花费的时间恰好为零,而其他查询则必须大量进行IO工作。我不确定为什么它在这方面要好得多,因为它仍然必须找到另外选择的列(我选择的那个不是[Geography]列)。但由于过滤器已经应用于查找之前,因此显然必须做得少得多。但即便如此,零IO也让我感到困惑。
  • 物理读取非常少。所有所需数据(包括[Geography]列)仅通过4个物理读取从索引搜索中读取。

--- 更新2结束 ---

3. 修改表格使其按([FlightDate] ASC,[Id] ASC)聚集

现在,考虑到我们已经考虑将表进行分区,我们还考虑更改聚集索引,使其包括[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)

这将更改表,使其现在基于[FlightDate]聚集,然后是[Id],确保唯一性。此外,我们在[Id]上添加了一个备用键约束,因此理论上仍然可以用于引用表格。
这三个SQL语句需要几个小时才能完成,但这样做的额外好处是,它非常容易在未来对[FlightDate]进行分区,从而允许在针对表格进行的所有查询中进行分区消除。
我们现在执行相同的查询到表时的预期是查询计划如下:
1.在索引上执行聚集索引查找(此操作将将11600000行过滤为大约50000)
2.在返回的每一行上执行地理筛选器[Geography].STIntersects(@p1)=1
这是比前面的示例中描述的查询计划更简单的查询计划,实际上它确实使用了此计划,如下所示:

Actual query execution plan with clustered index

唯一的问题是,完成此操作大约需要一分钟时间。但是,如果我们更仔细地查看查询计划本身,它还会反驳先前的结论,即查询中实际上花费时间的是对主表的查找,因为在这里它说大部分时间都花费在[Geography]列上的筛选上。
我有一个额外的评论可能会很有趣: 即使我不删除之前创建的索引([IX_Trajectory_FlightDate_Includes_Geography]),在像这样改变表结构后,查询也会很慢。但是,如果我提示查询编译器应该使用前一节中使用的替代键创建的索引[AK_Trajectory]和[IX_Trajectory_FlightDate_Includes_Geography],使用WITH (INDEX([AK_Trajectory], [IX_Trajectory_FlightDate_Includes_Geography])),那么查询的性能将与(2)中大致相同。
因此,SQL Server实际上积极决定使用较慢的查询计划,显然认为它更快。老实说,我不怪它。我会做同样的事情,因为那个查询计划简单得多。发生了什么事?
现在,您可能会有疑问,我们是否考虑将[Geography]列添加SPATIAL INDEX。这是一个考虑因素。这种索引的问题(以及为什么它实际上无法使用)有两个方面:

  1. [FlightDate]索引能够过滤掉比这种索引更多的[Trajectory]行。问题的关键在于,随着表的增长,这种 SPATIAL INDEX "SEEK" 的结果会线性增长,而[FlightDate]上的INDEX SEEK则不会。
  2. 维护这样的 SPATIAL INDEX 是昂贵的,并且插入操作会随着索引变得越来越大而变得越来越慢。

--- UPDATE 3 START ---

其他查询计划信息(主要步骤,注意:与上面显示的查询的执行不同,因此时间上有所变化。此查询花费了0:49):

  • SELECT.QueryTimeStats
    • CpuTime=11818ms
    • ElapsedTime=48253ms
  • Parallelism (7%)
    • Actual Time Statistics
      • Actual Elapsed CPU Time=7ms
      • Actual Elapsed Time=47638ms
  • Clustered Index Seek (25%)
    • Actual I/O Statistics
      • Actual Logical Reads=7403
      • Actual Physical Reads=4
      • Actual Read Aheads=6939
      • Actual Scans=21
    • Actual Time Statistics
      • Actual Elapsed CPU Time=107ms
      • Actual Elapsed Time=57ms
  • Filter (69%)
    • Actual Time Statistics
      • Actual Elapsed CPU Time=11727ms
      • Actual Elapsed Time=48250ms
值得注意的是:
  • 聚集索引中的逻辑读取在所有线程之间分散。
  • 预读取不会在线程之间分散(但其他索引也不会如此)。
  • 不确定这些内容如何解释为什么这比索引#2慢。
--- 更新3 结束 ---
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; 

这提供了有关如何存储主表(聚集索引)和每个其他索引的数据的信息。其结果为:
  • 聚集索引
    • IN_ROW_DATA: total_pages=705137, used_pages=705137, data_pages=697811
    • LOB_DATA: total_pages=10302796, used_pages=10248361, data_pages=0
    • ROW_OVERFLOW_DATA: total_pages=9, used_pages=2, data_pages=0
  • 索引 #2
    • IN_ROW_DATA: total_pages=497639, used_pages=494629, data_pages=496531
    • LOB_DATA: total_pages=10219824, used_pages=10217546, data_pages=0
    • ROW_OVERFLOW_DATA: ------------------------------------------------------------
从这里可以看出,虽然数据不是直接存储在ROW_OVERFLOW_DATA中,但也不是存储在IN_ROW_PAGES中。话虽如此,我认为没有理由认为从LOB_DATA检索数据应该比从ROW_OVERFLOW_DATA检索数据更快。通过对这些类型的阅读,显而易见的是,鉴于单个列通常超过ROW_OVERFLOW_DATA的8kB最大值,这些数据必须存储在LOB_DATA中。
但正如上面所述,主表(聚集索引)和索引#2都使用LOB_DATA页面,因此我不完全确定为什么索引#2会快得多,除非LOB_DATA在与聚集索引相比时对于索引意味着不同的东西。
但我觉得我看到的一切都支持同样的结论:
当查询需要在包含LOB_DATA数据的主表上进行查找时,即使它作为聚集索引的一部分执行索引搜索,该查找也总是非常缓慢。基本上我所做的每个查询(快或慢)都表明了这一点。例如,考虑索引#1:
  • 使用原始查询,它将不得不进行大约50000次键查找,需要近3分钟才能完成。
  • 如果我更改查询以便通过[EntryTime](示例已在UPDATE 1中解释)对其进行过滤,以使结果集保持大致相等(约1000行左右),则查询突然需要约20秒钟。这种更改意味着它只需针对实际结果集在主表中进行到LOB_DATA页面的查找,而不是在索引#1中寻找的所有50000个条目。 (重要的是,它仍然必须对所有内容进行主表的键查找,但不需要为每个条目转到LOB_DATA。)
  • 但是,虽然20秒比3分钟快得多,但它仍然不及使用索引#2执行的原始查询(必须触及所有50000个[Geometry]值!)。现在我感觉唯一合理的解释是,对主表的LOB_DATA进行查找会显着减慢查询速度。
  • 我认为这可以解释索引#1和索引#2之间性能差异的相当大的差异。而索引#2和聚集索引#3之间的差异则不是那么明显。

--- 更新 4 结束 ---

--- 更新 5 开始 ---

前一个更新包括两个索引的物理页面统计信息,这里是第一个索引的相同统计信息:

  • 索引 #1
    • IN_ROW_DATA:total_pages=18705,used_pages=18698,data_pages=18659
    • LOB_DATA:--------------------------------------------------
    • ROW_OVERFLOW_DATA:--------------------------------------------------

显然,这不包括LOB_DATAROW_OVERFLOW_DATA。但更令人惊讶的是,IN_ROW_DATA使用的页面数量比索引 #2少得多(大约20-30个数量级)。这表明,正如Lucky Brain所建议的那样,当在索引中包括空间列时,SQL Server可能会将有关该几何/地理信息的某些信息(例如边界框)直接存储在IN_ROW_DATA中,以便快速执行几何操作。

当然,这假设表在作为聚集索引的一部分时没有针对“常规”空间列执行此操作。

--- 更新 5 结束 ---

问题

有人能回答以下两个问题吗:

  • 是否可能简单的查找操作可以解释(1)和(2)中描述的索引之间性能差异?
  • 为什么(3)中描述的聚集索引比(2)中的索引慢得多?
  • 如果以上两个问题都无法回答,当比较1和2中描述的这两个索引时,我们应该看到如此大的性能差距吗?还是我们的设置可能出了其他问题?

4
这是我很长一段时间以来看到的写得最好的问题,给你点赞。 - Tim Biegeleisen
1
问题:您的基准测试在索引#2上为10秒,在索引#3上为1分钟。这个基准测试有多可重复?您是否曾经看到过这两个查询彼此更接近?如果它们之间相差不到2倍,我可能不会花太多时间解释它,而是选择最容易的方法。 - Tim Biegeleisen
1
我想知道潜在的问题是否与“地理”列的大小有关。给定行的数据大于单个页面。我不确定SQL Server何时决定需要获取额外的信息,而这种开销可能会大大增加查询时间。 - Gordon Linoff
1
@MikaelGuldborg,您可以尝试在没有DBCC DROPCLEANBUFFERS的情况下重复操作,以评估热缓存时间。存储引擎可能会在冷缓存的情况下表现不同。 - Dan Guzman
2
我并不总是信任时间长度。你能提供每个操作的 CPU 时间以及物理和逻辑读取次数吗?你能确保索引已经碎片整理并且统计信息是最新的吗? - Randy in Marin
显示剩余16条评论
3个回答

2
也许你可以添加一个边界框列,在对多边形中的点进行完整测试之前进行轻量级检查。如果使用边界框可以消除大多数记录,那么就可以节省时间。
DECLARE @g geography,
    @b_box geography,
    @pIn  geography,
    @pOut geography

SET @g = geography::STPolyFromText('POLYGON((-91.119987 40.705402, -91.129158 40.682148, -91.162498 40.656311, -91.214912 40.643818, -91.262062 40.639545, -91.375610 40.603439, -91.411118 40.572971, -91.412872 40.547993, -91.382103 40.528496, -91.374794 40.503654, -91.385399 40.447250, -91.372757 40.402988, -91.385757 40.392361, -91.418816 40.386875, -91.448593 40.371902, -91.476883 40.390968, -91.490158 40.390762, -91.500221 40.405117, -91.527534 40.410126, -91.529449 40.435043, -91.538689 40.441246, -91.533051 40.455399, -91.579224 40.463718, -91.585869 40.484478, -91.616699 40.504833, -91.622375 40.532864, -91.691917 40.551640, -91.689796 40.581165, -91.716812 40.593399, -91.741547 40.609749, -91.946198 40.608234, -92.192993 40.600060, -92.361328 40.599548, -92.646240 40.591438, -92.717621 40.589645, -93.100731 40.584335, -93.370056 40.580482, -93.562691 40.580807, -93.786079 40.578445, -94.017830 40.574024, -94.238159 40.570972, -94.484993 40.574215, -94.639633 40.575756, -94.920364 40.577229, -95.217171 40.581898, -95.382294 40.584335, -95.767204 40.589046, -95.757271 40.620903, -95.767723 40.643116, -95.876335 40.730434, -95.851509 40.792599, -95.846153 40.848331, -95.834114 40.870300, -95.836258 40.901108, -95.837318 40.974258, -95.860611 41.002651, -95.859253 41.035004, -95.878517 41.065872, -95.857986 41.109188, -95.876396 41.164204, -95.859512 41.166866, -95.858910 41.180538, -95.915810 41.194065, -95.921959 41.207855, -95.910690 41.225246, -95.929939 41.302059, -95.910912 41.308472, -95.897301 41.286865, -95.888817 41.301392, -95.942604 41.340080, -95.939766 41.394810, -95.934776 41.462387, -95.952896 41.472393, -96.006607 41.481960, -96.013161 41.493000, -95.996399 41.511524, -95.993675 41.528111, -96.004303 41.536671, -96.049881 41.524342, -96.085548 41.537529, -96.091644 41.563152, -96.080544 41.576008, -96.111015 41.599014, -96.099014 41.654690, -96.119972 41.684105, -96.121910 41.694923, -96.085266 41.704998, -96.099480 41.731575, -96.099030 41.752987, -96.076126 41.791481, -96.135330 41.862633, -96.159676 41.904163, -96.145576 41.924919, -96.147034 41.966267, -96.184921 41.980698, -96.202545 41.996628, -96.235794 42.001270, -96.238426 42.028450, -96.265182 42.048908, -96.284821 42.123463, -96.351860 42.168194, -96.363205 42.214050, -96.337402 42.229530, -96.332352 42.260315, -96.342575 42.282089, -96.368393 42.298031, -96.389473 42.328796, -96.423866 42.349285, -96.411453 42.380924, -96.417320 42.414783, -96.397583 42.441799, -96.395767 42.467407, -96.439087 42.489246, -96.479935 42.517136, -96.489029 42.564034, -96.500633 42.573891, -96.488190 42.580486, -96.512535 42.629761, -96.540855 42.662411, -96.562729 42.668518, -96.626228 42.708359, -96.640396 42.748608, -96.632668 42.776840, -96.600563 42.799564, -96.587334 42.835388, -96.572815 42.834354, -96.555901 42.846668, -96.537201 42.896915, -96.543953 42.913876, -96.514626 42.952393, -96.516838 42.986469, -96.498711 43.012062, -96.519699 43.051521, -96.479263 43.061897, -96.461784 43.075596, -96.460495 43.087887, -96.451195 43.126324, -96.472801 43.209099, -96.486931 43.217926, -96.558289 43.225506, -96.566673 43.239651, -96.559250 43.253281, -96.570404 43.263630, -96.578812 43.290092, -96.540245 43.307678, -96.522575 43.356987, -96.524734 43.384247, -96.557388 43.400749, -96.588791 43.435562, -96.583473 43.481945, -96.597992 43.499874, -96.460136 43.499744, -96.060738 43.498566, -95.866615 43.498978, -95.464493 43.499577, -95.396278 43.500370, -94.920197 43.499413, -94.859573 43.500072, -94.454987 43.498146, -94.246544 43.498993, -93.973717 43.500343, -93.653481 43.500809, -93.500618 43.500534, -93.054184 43.501495, -93.027016 43.501316, -92.557831 43.500294, -92.452995 43.499496, -92.077370 43.499187, -91.730217 43.499603, -91.610954 43.500656, -91.223434 43.500835, -91.235771 43.464710, -91.210785 43.424076, -91.198112 43.370537, -91.176918 43.353970, -91.078369 43.313320, -91.066299 43.280704, -91.068924 43.257919, -91.161224 43.147594, -91.168442 43.082905, -91.159622 43.081200, -91.152084 43.001331, -91.138992 42.925907, -91.093300 42.871452, -91.081902 42.783375, -91.066040 42.744923, -90.999054 42.707066, -90.919281 42.680683, -90.892418 42.678246, -90.745483 42.657005, -90.694664 42.637932, -90.664253 42.571392, -90.639091 42.555714, -90.625580 42.528561, -90.638329 42.509361, -90.651772 42.494698, -90.648346 42.475643, -90.605827 42.460560, -90.563583 42.421837, -90.491043 42.388783, -90.441597 42.360073, -90.427681 42.340633, -90.417984 42.263924, -90.407173 42.242645, -90.367729 42.210209, -90.323601 42.197319, -90.230934 42.159721, -90.191574 42.122688, -90.176086 42.120502, -90.166649 42.103745, -90.168098 42.061043, -90.150536 42.033428, -90.142670 41.983963, -90.154518 41.930775, -90.195839 41.806137, -90.255310 41.781738, -90.304886 41.756466, -90.326027 41.722736, -90.341133 41.649090, -90.339348 41.602798, -90.348366 41.586849, -90.423004 41.567272, -90.434967 41.543579, -90.454994 41.527546, -90.540840 41.525970, -90.600700 41.509586, -90.658791 41.462318, -90.708214 41.450062, -90.779900 41.449821, -90.844139 41.444622, -90.949654 41.421234, -91.000694 41.431084, -91.027489 41.423508, -91.055786 41.401379, -91.073280 41.334896, -91.102348 41.267818, -91.101524 41.231522, -91.056320 41.176258, -91.018257 41.165825, -90.990341 41.144371, -90.957787 41.104359, -90.954651 41.070362, -90.960709 40.950504, -90.983276 40.923927, -91.049210 40.879585, -91.088905 40.833729, -91.092751 40.761547, -91.119987 40.705402))', 4326);

SET @g = @g.ReorientObject()

DECLARE @g_flat_box geometry = geometry::STGeomFromWKB(@g.STAsBinary(), @g.STSrid).STEnvelope();
DECLARE @g_rnd_box geography = geography::STGeomFromWKB(@g_flat_box.STAsBinary(), @g_flat_box.STSrid);

SELECT @g as [poly], @g_rnd_box as [box]

SET @pIn = geography::STPointFromText('POINT( -90.6204165 41.5795478)',4326)
SET @pOut = geography::STPointFromText('POINT( -80.6204165 31.5795478)',4326)

SELECT STIntersectionIn    = @g_rnd_box.STIntersection( @pIn ).ToString(),
       STIntersectionOut   = @g_rnd_box.STIntersection( @pOut ).ToString(),
       STIntersectionIn    = @g.STIntersection( @pIn ).ToString(),
       STIntersectionOut   = @g.STIntersection( @pOut ).ToString()
GO

我从https://www.sqlservercentral.com/forums/topic/sql-geography-data-type-test-if-point-is-inside-a-closed-polygon获取了此示例的数据。

1
这绝对是一个有趣的想法,如果你从#3(聚集)索引的角度来看,并且接受在该查询中性能没有根本性问题,那么它可能会略微提高性能。然而,我不准备接受该性能没有根本性问题的说法。我所寻求的是一个解释,为什么我在#2和#3索引之间看到如此巨大的差异。(以及在#1和#2索引之间的较小差异) - Mikael Guldborg
事实证明,当SQL Server使用包含“地理”列的索引以避免访问“LOB_DATA”页面时,这可能正是它为您做的。谁会想到呢:P - Mikael Guldborg

1

看到您的查询,首要考虑的是在SELECT列表中包括一个特殊列,这是一个.NET/CLR数据类型,这些存储在IN_ROW_DATA页面之外,需要键查找,除非特殊列包含在索引中,该索引可能还包括索引数据页面中的特殊边界框,以加快过滤并节省大部分磁盘I/O。 我会说您发现了一种有效的技巧,可以加快特殊列过滤而无需使用空间索引。

为了证明我的观点,我向您提供原始的SQL文档,我相信您已经知道覆盖索引,其中澄清了以下内容:“将非键列添加到非聚集索引的叶级别以提高查询性能。这允许查询优化器从索引扫描中定位所有所需信息;不会访问表或聚集索引数据”。最后一部分在这里非常重要,因此我认为边界框是空间列的“所需信息”之一,可帮助查询优化器避免访问IN_ROW_DATA
结论:
  1. 简单的查找操作能否解释(1)和(2)中描述的索引之间性能差异的原因?我认为是可以的,因为在(1)中存储空间CLR数据类型被存储在IN_ROW_DATA页面之外,需要更多的磁盘I/O。
  2. (3)中描述的聚集索引为什么比(2)中的索引慢得多?同样的原因,包括在索引(2)中包含地理数据可以节省查找IN_ROW_DATA页面之外数据的时间,从而节省大部分磁盘I/O;请注意,索引(3)仍然需要在LOB_DATA中查找空间数据。
  3. 如果以上两个问题都无法回答,那么当比较问题1和2中描述的这两个指标时,我们是否应该看到如此大的性能差距,或者更有可能是设置出了其他问题?不适用。

1
你好。感谢回答!我已经更新了问题并添加了额外的信息“UPDATE 4”。你同意吗?简而言之,我肯定可以看出这解释了索引#1和#2之间的差异,但我对索引#2和聚集索引#3并不完全信服,因为它们都将[地理]数据存储在单独的页面中。 - Mikael Guldborg
1
谢谢,@MikaelGuldborg,分析得非常好!虽然我承认CLR数据不存储在ROW_OVERFLOW_DATA中,但它存储在LOB_DATA中,这意味着由于磁盘I/O而具有类似的性能。从另一个角度来看,由于关于如何将空间列包含在索引中的文档不多,我在本地进行了调查,并得出结论:边界框可以包含在索引数据页中(而不是索引树),这就是INDEX 2更快的原因,因为它不需要进行LOB_DATA查找。我会相应地修改我的答案。 - Lucky Brain
1
我认为你很可能是正确的。事实是,虽然只有[Geometry](默认情况下是聚集索引列Id)包含在索引中,但表示索引的页面数量几乎与表示主表的页面数量相同,这很奇怪,表明那些数据“IN_ROW_DATA”页面中存储了其他东西。实际上,主表要大得多(每行约50个字节)。 - Mikael Guldborg
1
刚刚尝试创建一个不包括[Geometry]的索引,以查看为该索引创建了多少个IN_ROW_DATA页面,结果只创建了大约18,698个页面,而包括[Geometry]时则创建了497,629个页面,这表明它甚至可能存储比较高级的内容,而不仅仅是一个边界框。这一定就是它了! - Mikael Guldborg
1
你能详细说明一下这里的“INSERT”性能退化是什么意思吗?我不确定我完全理解了。你是在谈论覆盖索引(#2)还是理论空间索引?(另外,我现在已经100%确信这一点了。对索引的理论页面计数进行了一些数学运算,结果与我预期的完全相符)。 - Mikael Guldborg
显示剩余4条评论

0

这一切都与磁盘访问有关。

如果没有几何索引,需要获取和测试50K个几何体。这是50K次磁盘访问。使用HDD,一个简单的经验法则是每秒100次。因此需要500秒。请参见您的#3:“对返回的每行执行地理筛选[Geography].STIntersects(@p1) = 1”。

为了加快速度,必须使用空间索引,并希望它比日期筛选更好。

您的#2说“包括几何图形”。那似乎会使索引变得过于庞大,以至于无法真正帮助。正如您所指出的“差不多相同”。

按(FlightDate,id)的顺序创建聚集索引,如果FlightDate是最佳起点,则可以减少磁盘访问次数。(我不知道几何索引是否更好。)

因此,有两个索引:聚集(FlightDate,id)和几何(geometry)。并希望优化器选择更好的那个。


嗨,感谢回答。空间索引作为查询的入口非常有问题,因为通过“寻找”得到的结果集会随着时间的推移而显著增长,而在给定时间段内的轨迹数量不会增加(我们在查询时还将始终具有日期范围)。此外,维护空间索引在开始增长时会意外地昂贵。我们发现它实际上会减慢数据库写入速度,以至于系统执行其所需功能的速度太慢了。事实是,虽然索引#2似乎臃肿,但非常快速。 - Mikael Guldborg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接