【问题标题】:How can a covering index satisfy more than one query?覆盖索引如何满足多个查询?
【发布时间】:2018-12-31 04:47:00
【问题描述】:

我继承了 Azure 中托管的 MS Sql 数据库。 寻找性能改进,我一直在阅读很多关于索引和覆盖索引的内容。 (也许这是我找到的最完整的阅读:https://www.red-gate.com/simple-talk/sql/learn-sql-server/using-covering-indexes-to-improve-query-performance/

但仍有一个疑问......

因此,例如,对于下面的计费表(大约有 800 万行),我发现查询的 where 子句中使用最多的字段是(是否在连接内): PAYMENT_DATE, DUE_DATE, CUSTOMER_ID, DELAY_DAYS, AMOUNT .

CREATE TABLE [dbo].[BILLING](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [CHANGED_DATE] [datetime] NULL,
    [INCLUDED_DATE] [datetime] NULL,
    [CHANGED_USER_ID] [int] NULL,
    [INCLUDED_USER_ID] [int] NULL,
    [BILL_CODE] [varchar](255) NOT NULL,
    [PAYMENT_DATE] [datetime] NULL,
    [DUE_DATE] [datetime] NOT NULL,
    [AMOUNT] [float] NOT NULL,
    [AMOUNT_PAYED] [float] NULL,
    [CUSTOMER_ID] [int] NOT NULL,
    [OUR_NUMBER] [varchar](200) NULL,
    [TYPE] [varchar](250) NULL,
    [BANK_ID] [int] NULL,
    [ISSUE_DATE] [datetime] NULL,
    [STATE] [varchar](20) NULL,
    [DUNNING_STATE_ID] [int] NULL,
    [OPEN_VALUE] [float] NULL,
    [ACCREDIT_VALUE] [float] NULL,
    [LOWER_VALUE] [float] NULL,
    [DISCCOUNT_VALUE] [float] NULL,
    [INTEREST_VALUE] [float] NULL,
    [FINE_VALUE] [float] NULL,
    [RECEIVED_AMOUNT] [float] NULL,
    [DELAY_DAYS] [int] NULL,
    [BRANCH_ID] [int] NULL,
    [FIELD1] [varchar](250) NULL,
    [FIELD2] [varchar](250) NULL,
    [FIELD3] [varchar](250) NULL,
    [FIELD4] [varchar](250) NULL,
    [FIELD5] [varchar](250) NULL,
    [OBS1] [varchar](250) NULL,
    [OBS2] [varchar](250) NULL,
    [OBS3] [varchar](250) NULL,
    [INTEREST_RATE] [float] NULL,
    [INTEREST_CALC] [float] NULL,
    [AGREEMENT_STATE] [varchar](20) NULL,
    [AGREEMENT_ID] [int] NULL,
PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)

此外,用于优化的目标查询对 select 子句进行计算: AMOUNT, DELAY_DAYS, COUNT(ID)。 例如:

SELECT
        T.CUSTOMER_ID AS CUSTOMER_ID
        , COUNT(T.ID) AS NUM_BILLS
        , COUNT(
            CASE
                WHEN T.DELAY_DAYS <= 0 THEN 1
                ELSE NULL
            END
        ) AS DEPOSITS
        , COUNT(
            CASE
                WHEN T.DELAY_DAYS > 0 THEN 1
                ELSE NULL
            END
        ) AS DEFAULTED
        , COUNT(
            CASE
                WHEN T.DELAY_DAYS BETWEEN 30 AND 60 THEN 1
                ELSE NULL
            END
        ) AS DEFAULTED_30
        , COUNT(
            CASE
                WHEN T.DELAY_DAYS BETWEEN 60 AND 90 THEN 1
                ELSE NULL
            END
        ) AS DEFAULTED_60
        , COUNT(
            CASE
                WHEN T.DELAY_DAYS > 90 THEN 1
                ELSE NULL
            END
        ) AS DEFAULTED_90
        , MAX(T.DELAY_DAYS) AS MAX_DEFAULTED_TIME
        , SUM(
            CASE
                WHEN T.DELAY_DAYS > 0 THEN T.DELAY_DAYS
                ELSE 0
            END
        ) AS SUM_DEFAULTED_TIME
        , SUM(T.AMOUNT) AS AMOUNT
        , SUM(
            CASE
                WHEN T.DELAY_DAYS > 0 THEN T.AMOUNT
                ELSE 0
            END
        ) AS DEFAULTED_AMOUNT
    FROM BILLING T
    WHERE
        T.DUE_DATE < GETDATE()
        AND T.AMOUNT > 0
    GROUP BY
        T.CUSTOMER_ID

因此,在我看来,以下索引可以解决我的所有问题:

CREATE NONCLUSTERED INDEX [ix_Titulo_main_fields] ON [dbo].[BILLING]
(
    [PAYMENT_DATE] ASC,
    [DUE_DATE] DESC,
    [AMOUNT] ASC,
    [CUSTOMER_ID] ASC,
    [STATE] ASC,
    [DELAY_DAYS] ASC,
    [BRANCH_ID] ASC,
    [AGREEMENT_ID] ASC
)
INCLUDE (   [BILLING_CODE],
    [AGREEMENT_STATE],
)
GO;

相比之下,当我在 Management Studio 上询问查询计划时,SQL Server 不使用此索引并建议我创建一个新索引:

CREATE NONCLUSTERED INDEX [ix_billing_due_date_amount] ON [dbo].[billing]
(
    [due_date] ASC,
    [amount] ASC
)
INCLUDE (   [customer_id],
    [delay_days])
GO

所以,疑问是:
覆盖索引是否需要正是 WHERE 子句搜索的内容?
如果是真的,覆盖索引如何满足多个查询?
否则为什么以前的索引不满足查询?

我真的不知道我错过了什么......

提前致谢!

【问题讨论】:

    标签: sql-server indexing covering-index


    【解决方案1】:

    订单很重要。由于您建议的索引以 [payment_date] 开头,但查询谓词不包括 [payment_date],因此索引不太可能比表扫描更有利。

    一个索引可以覆盖多个查询。索引的第一个列出的字段几乎总是需要在所有查询的谓词中。为了提高结果,也将此逻辑应用于第二个字段、第三个字段等。

    当一个职位有多个选择时,一个选择的表现可能比另一个好。

    旁注:Oracle 有一个称为“索引跳过扫描”的功能,即使前导列不在谓词中,也允许使用索引。当前导列的不同值很少(来自learningintheopen.org)时,它是有效的。

    【讨论】:

    • 谢谢,@crokusek!列数很重要吗?在这个例子中,我的列比谓词需要的多。在第 1 个索引为 due_date,在第 2 个索引为 amount,以及其他一些列使其与特定的索引一样有用(只有 due_dateamount)?
    • 位于所需谓词列之后的附加索引列预计具有可忽略的二阶影响。例如,有许多列或大列可能需要更多的 IO 读取/内存。
    【解决方案2】:

    对于任何特定查询,您当然可以创建专门的索引 - 优化器可以告诉您这一点。并且特定的查询将被超级提升,其他类似的查询或多或少会变得更快。但是,根据经验,我不专门为查询使用索引,也不喜欢多列索引,也不使用包含。可能有罕见的例外,但通常我不会。为什么?优化器会提示您在查询范围内需要什么索引 - 在您将 8-10 个左右的索引添加到同一个表之后,优化器将不再识别要使用的索引,更不用说插入/更新延迟(尽管即使在锁定时间插入/更新时,正确的索引也可以节省时间)。

    对于您的情况,我应该使用 8 个单一索引,每列一个,除非列已经是主键或 PK 的一部分。如果一列是唯一的,请检查您是否可以创建唯一索引而不是简单索引。这很有帮助。

    总的来说,对于最终将针对该表执行的所有 sql,在一个表上拥有 4-8 个单列索引是最好的解决方法。只要按照您描述的使用情况调查选择这些列,这就是有效的。

    这是因为真正最重要的是第一次过滤。在 3 秒内过滤掉 800 万行中的 10000 行是成功的 - 现在如何将 10000 过滤到 10 最终并不重要。也许那里也有一个索引,也许没有,但是您希望在哪里进行表扫描,800 万或 10000 过滤?

    根据我的经验,一组不错的单列索引可以帮助 99% 的查询快速响应,因为它们获得了一个索引列来搜索开始。

    有时查询会选择错误的索引 - 一些通用过滤器只过滤掉 5%,而忽略过滤掉 95% 的过滤器。这可能是导致执行计划不佳的统计数据或基数估计器。您可以通过查询提示来克服此问题,以确保您始终使用它或强制执行 2012 基数估计器。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-12-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-11
      • 1970-01-01
      相关资源
      最近更新 更多