Joh*_*ohn 11 sql-server execution-plan sql-server-2016
这是另一个查询优化器难题。
也许我只是高估了查询优化器,或者我遗漏了一些东西 - 所以我把它放在那里。
我有一张简单的桌子
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])
Run Code Online (Sandbox Code Playgroud)
有一个索引和几千行,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;
Run Code Online (Sandbox Code Playgroud)
是否IX_Number
按预期进行索引查找。
如果 where 子句是
WHERE P.Name = 'one';
Run Code Online (Sandbox Code Playgroud)
然而,它变成了扫描。
case-clause 显然是一个双射,所以理论上应该可以优化从第二个查询中扣除第一个查询计划。
这也不是纯粹的学术:查询的灵感来自将枚举值转换为它们各自的友好名称。
我想听听了解查询优化器(尤其是 Sql Server 中的优化器)的期望的人的意见:我只是期望过高吗?
我在问,因为我之前有过这样的情况,查询的一些细微变化会使优化突然曝光。
我正在使用 Sql Server 2016 开发人员版。
Mar*_*ith 18
是我期望太高了吗?
是的。至少在产品的当前版本中。
SQL Server 不会拆分该CASE
语句并对其进行逆向工程以发现如果计算列的结果是 ,'one'
则[Extent1].[Number]
必须是0
.
您需要确保将谓词编写为可讨论的。这几乎总是涉及它的形式。basetable_column_name comparison_operator expression
.
即使是微小的偏差也会破坏 sargability。
WHERE P.Number + 0 = 0;
Run Code Online (Sandbox Code Playgroud)
即使简化比CASE
表达式更简单,也不会使用索引查找。
如果您想搜索字符串名称并获得数字搜索,您需要一个包含名称和数字的映射表并在查询中加入它,然后计划可能在映射表上搜索,然后是相关搜索继续[dbo].[MyEntities]
从第一次搜索返回的数字。
不要将您的枚举投影为 case 语句。将其投影为派生表,如下所示:
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';
Run Code Online (Sandbox Code Playgroud)
我怀疑你会得到更好的结果。(我没有将 Name 转换为?
丢失时,因为这可能会干扰可能的性能提升。但是,您可以将WHERE
子句移动到外部查询中以便将谓词放在enum
表上,或者您可以从内部查询,一个用于谓词,一个用于显示,NULL
当没有匹配的枚举值时,谓词之一。)
不过,我猜测,由于[Extent1]
那里的原因,您正在使用 ORM,例如实体框架或 Linq-To-SQL。我无法指导您如何以本机方式完成这样的投影,但是,您可以使用不同的技术。
在我的一个项目中,我通过将枚举值合并到数据库中的自定义构建类反映了数据库中真实表中的代码枚举值。(您必须遵守必须明确列出枚举值的规则,在不查看表的情况下永远不能删除任何值,并且永远不能更改它们,尽管您已经必须在当前设置中至少观察到其中的一些) .
现在,我正在使用Identifier
具有许多不同具体子类的基类的枚举,但是没有理由不能使用普通的香草枚举来完成。这是一个使用示例:
new EnumOrIdentifierProjector<CodeClassOrEnum, PrivateDbDtoObject>(
_sqlConnector.Connection,
"dbo.TableName",
"PrimaryKeyId",
"NameColumnName",
dtoObject => dtoObject.PrimaryKeyId,
dtoObject => dtoObject.NameField,
EnumerableOfIdentifierOrTypeOfEnum
)
.Populate();
Run Code Online (Sandbox Code Playgroud)
您可以看到我传递了所有必要的信息,以便写入和读取数据库值。(我遇到了当前请求可能不包含所有现存值的情况,因此需要从数据库以及当前加载的集合返回任何额外的值。我也让数据库分配 ID,但对于枚举你可能不会想要那个。)
这个想法是,一旦你有一个在启动时只读/写一次的表,它可以可靠地拥有所有枚举值,你只需像任何其他表一样加入它,性能应该很好。
我希望这些想法足以让你做出改进。
我将这个问题解释为您通常对优化器感兴趣,但对 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"
Run Code Online (Sandbox Code Playgroud)
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)
Run Code Online (Sandbox Code Playgroud)
该计划如下所示:
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
Run Code Online (Sandbox Code Playgroud)
我不太了解其他优化器,但我感觉 DB2 优化器即使在竞争对手中也被认为是相当不错的。