索引是否有任何理由包含主键?

dat*_*ess 4 sql-server index-tuning

我继承了一个使用SQL Server 2005的项目,最近出现了一些性能问题,我开始查看索引,发现

_dta_index_survey_25_135059617__K1_K10_K19:
    id ASC, sent_date ASC, group_id ASC
Run Code Online (Sandbox Code Playgroud)

_dta_index_survey_21_364632442__K18_K1_K2_K9:
    group_id ASC, id ASC, campaign_id ASC, sent_date ASC
Run Code Online (Sandbox Code Playgroud)

这似乎是由某种自动性能调整工具生成的。我认为 SQL Server 带有类似的东西,但我不熟悉它。

无论如何,id是主键,我在这里试图理解的是将主键包含在二级索引中可能有什么好处......因为您必须拥有最左边的信息才能使用索引和如果你知道,id那么无论如何你已经得到了这一行。

有人可以解释为什么工具会生成这些索引,以及保留它们是否有任何意义?

仅仅因为索引被使用并不意味着它们是一个好的设计。SQL Server最近使用了错误的索引,我正在尝试确定原因。更具体地说,对一个月前以毫秒为单位运行的此表的查询现在需要数秒。这张桌子很大(400 万行),而且还在稳步增长,但多年来一直以同样的速度发展。

几乎每个查询都通过group_idor连接campaign_id。往往两者兼而有之。该应用程序几乎从不专门按 PK 进行选择或排序,但我可以看到ID在比较或合并ID数据库引擎中的s列表时如何有用。...但id也是一个标识列,并且一个索引包含行 id 的列表,因此ID当它也在索引“存储桶”中时,在键中使用它似乎仍然是多余的。并且额外的字段仍然可以被索引覆盖而不是键的一部分。

我发现(id, sent_date, group_id)几乎没有人读过。同时(group_id, id, campaign_id, sent_date)是最热门的指数之一。接下来我发现一个我认为非常合理的索引在表中的每一个其他列中(campaign_id, sent_date, group_id, id)也有一个index_columns_include,并且比主聚集索引占用更多的空间id!(但它也很热......)我当然可以用一个简单的(campaign_id, sent_date, group_id)?

Joe*_*ish 8

您对问题中列出的两个索引表示怀疑是正确的,但是有一些狭窄的用例可以将索引中的主键作为键列包含在内。默认情况下,SQL Server 会将您的主键转换为聚集键,这对于该问题的性能而言是更重要的概念,因此如果参考主键来回答,我将改为参考聚集键。

有关指数idsent_date以及group_id,查询只选择那些列可能受益,因为该指数将成为该查询覆盖指标。你说如果你知道id那么你已经得到了行,但你真正拥有的是一种非常有效的方法来读取该行数据页中的所有数据。假设您的(行存储)表在id列上只有一个聚集索引。考虑以下查询:

SELECT id, sent_date, group_id
FROM your_table
ORDER BY id;
Run Code Online (Sandbox Code Playgroud)

SQL Server 可能会执行聚集索引扫描以满足该数据。将读取数据页中的所有数据,其中包括查询未引用的表中的列。

创建_dta_index_survey_25_135059617__K1_K10_K19索引后,SQL Server 将能够通过对新索引的索引扫描来满足该查询。索引可能会比表占用更少的页面,因此从 IO 的角度来看可能会更有效。例如,假设您有一VARCHAR(2000)列填充了大量数据。该列将包含在聚集索引的数据页中,但不会包含在非聚集索引中。这意味着使用非聚集索引满足查询可能会导致更少的逻辑读取。

请注意,sent_dategroup_id不能用作此示例的键列。它们可以是INCLUDED具有相同好处的列。请注意,如果没有ORDER BY子句,则 just sent_dateand上的索引group_id也将是覆盖索引,因此包含该id列对于没有ORDER BY.

如果您的聚集键被定义为id DESC(id ASC是默认值),那么该索引也可能有点用处。SQL Server 无法并行执行向后聚集索引扫描,但以下查询可以与扫描并行运行_dta_index_survey_25_135059617__K1_K10_K19

SELECT id
FROM your_table
ORDER BY id DESC;
Run Code Online (Sandbox Code Playgroud)

同样,键列 onsent_dategroup_id对这个查询没有用,但查询优化器可以使用定义索引的第一列。我想不出可以从所有三个关键列中受益的查询。

这是更难以认为会从索引中获益的查询group_ididcampaign_id,和sent_date。假设聚集键被定义为id ASC,我认为,ORDER BY group_id, id, campaign_id, sent_date可以通过指数仅仅满足group_id,因为id是一个独特的聚集键和索引将被创建group_id,然后id顺序。和以前一样,如果 id 上的索引与 上的聚集键的排序顺序不同,您可能会受益id

我怀疑还有一些数据分布可以_dta_index_survey_21_364632442__K18_K1_K2_K9在过滤 group_id 和 id 时从索引中受益,即使 id 是唯一的聚集键。假设表中的每个数据页包含 2 行,并且表中的一半行有group_id = 1。理论上,SQL Server 可以使用该索引来满足以下查询:

SELECT group_id, id, campaign_id, sent_date
FROM your_table
WHERE group_id = 1 AND id IN (1,3,5,7,9,...);
Run Code Online (Sandbox Code Playgroud)

并且可能比使用聚集索引执行更少的 IO。但是,我不知道查询优化器是否真的会这样做。

总而言之,有一些包含索引和排序顺序的极端情况可以从您在问题中列出的索引中受益。但是,根据我的经验,这些索引不太可能为您的工作负载提供显着(甚至任何)好处。如果这些索引确实提供了好处,很可能会有较小的索引可以提供相同的好处。


小智 2

调整工具使用霰弹枪方法。他们把所有的指标组合作为假设指标。假设索引不会创建任何索引,但某种类型的假设确实会根据发生的读取和写入次数来保存索引的统计信息。然后,调整工具会获取具有最高统计数据的假设,并将其提供给您并销毁所有假设。

因此,有人从调优工具中放入索引,但我更愿意获取自上次 SQL Server 重新启动以来保留的实际统计信息。以下是获取统计信息的示例代码。查看总读取次数并与总写入次数进行比较。写入多于读取是不好的。如果我发现一个读取次数为零的索引,我就会将其丢弃,并且不必将它们放回去。

  1. 什么是聚集索引?
  2. 有多少索引?任何超过 5 个索引的值通常就很多了。

SELECT distinct
  DB_Name(DB_id()) as DbName
  , oo.name AS object_name
  , stat.user_updates AS [Total Writes]
  , stat.user_seeks + stat.user_scans + stat.user_lookups AS [Total Reads]
  , stat.user_updates - (stat.user_seeks + stat.user_scans + stat.user_lookups) AS [Difference]
  , partition1.Rows
  , partition1.SizeMB
  , iib.name AS index_name
  , iib.type_desc
  , FilegroupName = fg.groupname
  , ISNULL(IndexColumns.index_columns_key, '---') AS index_columns_key
  , ISNULL(IndexColumns.index_columns_include, '---') AS index_columns_include
  , iib.is_primary_key
  , iib.is_unique
  , iib.is_unique_constraint
  , iib.is_hypothetical
  , STATS_DATE ( oo.[object_id] , iib.index_id ) as IndexCreatedDate
  , iib.fill_factor
FROM sys.objects                      AS oo  WITH (NOLOCK)
JOIN sys.schemas                      AS ss  WITH (NOLOCK) ON oo.schema_id=ss.schema_id
JOIN sys.indexes                      AS iib WITH (NOLOCK) ON oo.object_id=iib.object_id
left JOIN sys.dm_db_index_usage_stats AS stat WITH (NOLOCK) ON stat.[object_id] = iib.[object_id] AND iib.index_id = stat.index_id
left JOIN sys.partitions              AS pp WITH (NOLOCK) ON iib.object_id = pp.OBJECT_ID AND iib.index_id = pp.index_id
left JOIN sys.allocation_units        AS aa WITH (NOLOCK) ON pp.partition_id = aa.container_id
left JOIN sys.sysfilegroups           AS fg WITH (NOLOCK) ON fg.groupid = aa.data_space_id
left JOIN (
 SELECT
   object_id, index_id, SUM(row_count) AS Rows,
   CONVERT(numeric(19,3), CONVERT(numeric(19,3), SUM(in_row_reserved_page_count+lob_reserved_page_count+row_overflow_reserved_page_count))/CONVERT(numeric(19,3), 128)) AS SizeMB
 FROM sys.dm_db_partition_stats WITH (NOLOCK)
 GROUP BY object_id, index_id
 ) AS partition1 ON iib.object_id=partition1.object_id AND iib.index_id=partition1.index_id
 CROSS APPLY
 (
    SELECT
    (
      SELECT sys.columns.name + ', '
      FROM sys.index_columns WITH (NOLOCK)
      JOIN sys.columns WITH (NOLOCK) ON sys.index_columns.column_id=sys.columns.column_id
                       AND sys.index_columns.object_id=sys.columns.object_id
      WHERE sys.index_columns.is_included_column=0
      AND iib.object_id=sys.index_columns.object_id AND iib.index_id=sys.index_columns.index_id
      ORDER BY key_ordinal
      FOR XML PATH('')
    ) AS index_columns_key,
    (
      SELECT sys.columns.name + ', '
      FROM sys.index_columns WITH (NOLOCK)
      JOIN sys.columns WITH (NOLOCK) ON sys.index_columns.column_id=sys.columns.column_id
                       AND sys.index_columns.object_id=sys.columns.object_id
      WHERE sys.index_columns.is_included_column=1
      AND iib.object_id=sys.index_columns.object_id AND iib.index_id=sys.index_columns.index_id
      ORDER BY index_column_id
      FOR XML PATH('')
    ) AS index_columns_include
 ) AS IndexColumns
WHERE stat.database_id = DB_ID()
and oo.is_ms_shipped = 0
and oo.name not in ( 'sysdiagrams' )
and iib.is_hypothetical = 0
ORDER BY oo.name, iib.name
Run Code Online (Sandbox Code Playgroud)

你提到的索引可能会起作用。但我更喜欢将索引作为实验,并使用上面的脚本来获得关于索引有多好的事实测量。