SQL Server无法在简单双射上使用索引


11

这是另一个查询优化器难题。

也许我只是高估了查询优化器,或者我错过了一些东西,所以我把它放在了那儿。

我有一张简单的桌子

CREATE TABLE [dbo].[MyEntities](
  [Id] [uniqueidentifier] NOT NULL,
  [Number] [int] NOT NULL,
  CONSTRAINT [PK_dbo.MyEntities] PRIMARY KEY CLUSTERED ([Id])
)

CREATE NONCLUSTERED INDEX [IX_Number] ON [dbo].[MyEntities] ([Number])

带有一个索引和几千行,Number均匀分布在值0、1和2中。

现在这个查询:

SELECT * FROM
    (SELECT
        [Extent1].[Number] AS [Number],
        CASE
        WHEN (0 = [Extent1].[Number]) THEN 'one'
        WHEN (1 = [Extent1].[Number]) THEN 'two'
        WHEN (2 = [Extent1].[Number]) THEN 'three'
        ELSE '?'
        END AS [Name]
        FROM [dbo].[MyEntities] AS [Extent1]
        ) P
WHERE P.Number = 0;

是否IX_Number会像人们期望的那样寻找索引。

如果where子句是

WHERE P.Name = 'one';

但是,它变成了扫描。

案例显然是一个双射,因此从理论上讲,应该有可能进行优化以从第二个查询中扣除第一个查询计划。

这也不是纯粹的学术性:查询是通过将枚举值转换为它们各自的友好名称而得到启发的。

我想听听知道查询优化器(特别是Sql Server中的查询优化器)的期望的人:我只是期望太多了吗?

我问的是,在某些情况下,查询的一些细微变化会使优化突然出现。

我正在使用Sql Server 2016开发人员版。

Answers:


18

我只是期待太多了吗?

是。至少在该产品的当前版本中。

SQL Server将不挑开的CASE声明和逆向工程它来发现,如果计算列的结果'one'[Extent1].[Number]必须是0

您需要确保您编写的谓词是可引用的。几乎总是涉及到它的形式。basetable_column_name comparison_operator expression

即使很小的偏差也会破坏可燃性。

WHERE P.Number + 0 = 0;

即使它比CASE表达式更简单,也不会使用索引查找。

如果要搜索字符串名称并获取编号,则需要一个具有名称和数字的映射表并在查询中加入该表,那么该计划可能会在映射表上进行搜索,然后再执行相关的搜索在[dbo].[MyEntities]与返回的数量第一个寻求。


6

不要将您的枚举设计为个案陈述。将其投影为派生表,如下所示:

SELECT * FROM
   (SELECT
      [Extent1].[Number] AS [Number],
      enum.Name
   FROM
      [dbo].[MyEntities] AS [Extent1]
      LEFT JOIN (VALUES
         (0, 'one'),
         (1, 'two'),
         (2, 'three')
      ) enum (Number, Name)
         ON Extent1.Number = enum.Number
   ) P
WHERE
   P.Name = 'one';

我怀疑您会得到更好的结果。(?丢失时,我没有将Name转换为,因为这可能会干扰性能提升。但是,您可以将WHERE外部子句中的子句移到enum表中,以便将谓词放到表上,或者可以从内部查询,一个用于谓词,一个用于显示,其中一个谓词是NULL在没有匹配的枚举值时。)

但是,我猜测由于其中的原因[Extent1],您正在使用诸如实体框架或Linq-To-SQL之类的ORM。我无法指导您如何本地完成这种投影,但是您可以使用其他技术。

在我的一个项目中,我通过自定义构建类将枚举值合并到数据库中,从而在数据库的实际表中反映了代码枚举值。(您必须遵守以下规则:必须明确列出您的枚举值,不能在不检查表的情况下删除任何枚举值,也永远不能更改它们,尽管您已经必须在当前设置中至少观察到其中的一些规则) 。

现在,我正在使用Identifier具有许多不同具体子类的基类的枚举,但是没有理由用普通的香草枚举无法完成。这是一个示例用法:

new EnumOrIdentifierProjector<CodeClassOrEnum, PrivateDbDtoObject>(
   _sqlConnector.Connection,
   "dbo.TableName",
   "PrimaryKeyId",
   "NameColumnName",
   dtoObject => dtoObject.PrimaryKeyId,
   dtoObject => dtoObject.NameField,
   EnumerableOfIdentifierOrTypeOfEnum
)
   .Populate();

您可以看到我传递了所有必要的信息,以便同时写入和读取数据库值。(我遇到的情况是当前请求可能不包含所有现有值,因此需要从数据库以及当前加载的集合中返回任何其他值。我还让数据库分配ID,尽管对于枚举,您可能不会想要那个。)

这个想法是,一旦您拥有一个在启动时只能读取/写入一次的表,并且该表将可靠地具有所有枚举值,则您只需像其他任何表一样将其加入,并且性能应该很好。

我希望这些想法足以使您有所改进。


是的,我使用EntityFramework,并且在真正应该存在最佳解决方案的地方。在此之前,您的建议是我认为最好的解决方法之一。
约翰

5

我认为这个问题是因为您通常对优化器感兴趣,但是对SQL Server却特别感兴趣。我使用db2 LUW V11.1测试了您的方案:

]$ db2 "create table myentities ( id int not null, number int not null )"
]$ db2 "create index ix_number on myentities (number)"
]$ db2 "insert into myentities (id, number) with t(n) as ( values 0 union all select n+1 from t where n<10000) select n, mod(n,3) from t"

DB2中的优化器将第二个查询重写为第一个查询:

Original Statement:
------------------
SELECT 
  * 
FROM 
  (SELECT 
     number,

   CASE 
   WHEN (0 = Number) 
   THEN 'one' 
   WHEN (1 = Number) 
   THEN 'two' 
   WHEN (2 = Number) 
   THEN 'three' 
   ELSE '?' END AS Name 
   FROM 
     MyEntities
  ) P 
WHERE 
  P.name = 'one'


Optimized Statement:
-------------------
SELECT 
  Q1.NUMBER AS "NUMBER",

CASE 
WHEN (0 = Q1.NUMBER) 
THEN 'one' 
WHEN (1 = Q1.NUMBER) 
THEN 'two' 
WHEN (2 = Q1.NUMBER) 
THEN 'three' 
ELSE '?' END AS "NAME" 
FROM 
  LELLE.MYENTITIES AS Q1 
WHERE 
  (0 = Q1.NUMBER)

该计划如下所示:

Access Plan:
-----------
        Total Cost:             33.5483
        Query Degree:           1


      Rows 
     RETURN
     (   1)
      Cost 
       I/O 
       |
      3334 
     IXSCAN
     (   2)
     33.1861 
     4.66713 
       |
      10001 
 INDEX: LELLE   
    IX_NUMBER
       Q1

我对其他优化器了解不多,但是我感到即使在竞争对手中,DB2优化器也被认为是相当不错的。


真令人兴奋。您能否阐明“优化语句”的来源?db2本身会把它还给您吗?-另外,我在阅读计划时遇到了麻烦。我认为“ IXSCAN” 在这种情况下并不意味着索引扫描?
约翰

1
您可以告诉DB2为您解释一条语句。收集的信息存储在一组表中,您可以使用直观说明,也可以在这种情况下使用实用程序db2exfmt(或创建自己的实用程序)。另外,您可以监视语句并将计划中的估计基数与实际计划进行比较。在该计划中,我们可以看到它确实是一个索引扫描(IXSCAN),并且该运算符的估计输出为3334行。这在SQL Server中不好吗?它知道开始键和停止键,因此仅扫描DB2中的相关行。
Lennart

因此,它所说的扫描确实涉及寻找,老实说,Sql Server的等效计划说明有时也称某事确实涉及寻找的扫描,而有时它称为寻找。我总是需要查看行数以了解什么。由于db2的输出中显然有3334,所以可以确定我想要的是什么。很有意思。
约翰

是的,有时我也会感到困惑。必须查看每个操作员的更详细信息,才能真正了解正在发生的情况。
Lennart

0

在此特定查询中,甚至有一个 CASE语句。您正在过滤一种特殊情况!也许这只是您所提供的特定示例查询的详细信息,但是如果没有,您可以编写此查询以获取等效的结果:

SELECT
    [Extent1].[Number] AS [Number],
    'one' AS [Name]
FROM [dbo].[MyEntities] AS [Extent1]
WHERE [Extent1].[Number] = 0;

这将为您提供完全相同的结果集,并且由于CASE无论如何您已经在语句中对值进行硬编码,因此在此不会失去任何可维护性。


1
我认为您没有抓住重点-这是从后端代码库生成的SQL,该后端代码库通过枚举的字符串表示形式与枚举配合使用。投影SQL的代码正在对查询施加影响。我敢肯定,问问提问者,如果他自己编写SQL,将能够编写更好的查询。因此,根本没有CASE声明很傻,因为ORM会做这种事情。愚蠢的是,您没有意识到问题的这些简单方面……(间接称为“无脑”又是怎么回事?)
ErikE

@ErikE还是很愚蠢的,因为无论如何假设C#都可以使用枚举的数值。(考虑到我们正在谈论SQL Server,这是一个相当安全的假设。)
jpmc26,2016年

但是您不知道用例是什么。切换到数字值可能会是一个巨大的变化。枚举可能已改型为现有的巨型代码库。没有知识的批评是荒谬的。
ErikE

@ErikE如果太荒谬了,那为什么要这么做呢?=)我仅回答指出,如果用例与问题中的示例一样简单(在我的答案的序言中已明确指定),则CASE可以完全消除该语句而没有缺点。的过程中可能存在未知因素,但他们是不确定的。
jpmc26 2013年

我不反对您回答的事实部分,仅是主观表征的部分。至于我是否在没有知识的情况下进行批评,我会全力以赴地理解我未能使用审慎的逻辑或做出明显错误的假设的任何方式……
ErikE
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.