在 WHERE 子句中使用 and or and 结构优化 SQL 查询
Optimize SQL query with and or and structure in WHERE clause
我有一个函数必须根据从具有历史值的 table 检索到的兑换率来兑换货币。它有四个参数(@to_curr、@from_curr、@trans_date、@gl_cmp)和汇率returns。
table 结构是
currencypk int primary key
gl_crcnv_bdate datetime
gl_crcnv_edate datetime
gl_crcnv_rate float
gl_cmp_key char(30)
(我们有多家公司,每家都标识)
gl_CRNCY_TO char(30)
gl_CRNCY_FROM char(30)
下面是我的代码,当我将其插入函数时,它会导致执行时间大幅增加。我很清楚我的函数中的瓶颈在哪里,但我对如何重写它一筹莫展。我有一个带有 (and) 或 (and) 设置的丑陋 where 子句的原因是因为并非每个关系都是相反的。
例如,有从 USD 到 GBP 的转换率的记录,但根本不存在从 GBP 到 USD 的记录。该事实解释了 SELECT
中的 case 语句,以获取 table 中不存在的实际转换。 isnull
是为了防止 returns 什么都没有,所以它使用 1
非常感谢任何帮助。
SELECT
isnull((SELECT convert(decimal(5,4),
case
when @to_curr <> gl_CRNCY_TO
then (1/gl_crcnv_rate)
else gl_crcnv_rate
end)
FROM
[TABLE]
WHERE
gl_cmp_key = @gl_cmp
AND ((gl_CRNCY_TO = @from_curr AND gl_CRNCY_FROM = @to_curr)
OR (gl_CRNCY_TO = @to_curr AND gl_CRNCY_FROM = @from_curr))
AND @trans_date BETWEEN gl_crcnv_bdate AND gl_crcnv_edate), 1)
根据提供的信息,我只能想到这些
创建计算列,这样我们就不必为所有查询计算它。
ALTER TABLE [TABLE] ADD gl_crcnv_rate_invert AS (1/gl_crcnv_rate) PERSISTED;
创建索引
CREATE NONCLUSTERED INDEX ix_cmpkey_crncyto_crncyfrom_bdate_edate ON [TABLE] (gl_CRNCY_TO, gl_CRNCY_FROM, gl_crcnv_bdate, gl_crcnv_edate)
你可以试试:
SELECT isnull(SELECT convert(decimal(5,4), gl_crcnv_rate)
FROM (select 1/gl_crcnv_rate as gl_crcnv_rate
from [TABLE]
WHERE gl_cmp_key = @gl_cmp AND
gl_CRNCY_TO = @from_curr AND
gl_CRNCY_FROM = @to_curr AND
@trans_date BETWEEN gl_crcnv_bdate AND gl_crcnv_edate
UNION
select gl_crcnv_rate
from [TABLE]
WHERE gl_cmp_key = @gl_cmp AND
gl_CRNCY_TO = @to_curr AND
gl_CRNCY_FROM = @from_curr AND
@trans_date BETWEEN gl_crcnv_bdate AND gl_crcnv_edate
) sq,
1)
这假设如果两个可能的转换记录都存在,则转换因子将评估为相同的值,并且存在查询可以使用的索引。
Mark Bannisters 的回答已经向前迈进了一步,但我想更进一步并将代码更改为:
SELECT convert(decimal(5,4), Coalesce( -- direct lookup
(SELECT TOP 1 gl_crcnv_rate
FROM [TABLE]
WHERE gl_cmp_key = @gl_cmp
AND gl_CRNCY_FROM = @from_curr
AND gl_CRNCY_TO = @to_curr
AND @trans_date BETWEEN gl_crcnv_bdate AND gl_crcnv_edate),
-- reverse lookup
(SELECT TOP 1 1/gl_crcnv_rate
FROM [TABLE]
WHERE gl_cmp_key = @gl_cmp
AND gl_CRNCY_TO = @from_curr
AND gl_CRNCY_FROM = @to_curr
AND @trans_date BETWEEN gl_crcnv_bdate AND gl_crcnv_edate),
-- fallback
1))
COALESE()
足够聪明,如果它发现第一个块已经返回匹配项,则不会执行第二个块。它还使您免于 UNION
(首先应该是 UNION ALL
!)。
此外,您还需要确保拥有这样的索引:
CREATE INDEX idx ON [TABLE] (gl_cmp_key, gl_CRNCY_FROM, gl_CRNCY_TO, gl_crcnv_edate, gl_crcnv_bdate)
坦率地说,我会将当前的 PK 设为非集群,而将此设为集群,这样可以节省很多书签查找;唯一的缺点是你可能需要偶尔 rebuild/defragment 索引,因为添加新记录会随着时间的推移导致碎片化,因为新值被挤在现有记录之间......(侧记:你甚至需要 currencypk ?如果不需要,只需将 `gl_cmp_key、gl_CRNCY_FROM、gl_CRNCY_TO、gl_crcnv_edate、gl_crcnv_bdate、currencypk' 设为新的 PK...相当难看, 但在功能上同样有效,并且在使用 space 上节省了很多。
您希望在 bdate
之前显示 edate
的原因是您可能会查找最新的汇率。假设你有 20 个利率,它们的开始日期都在今天之前,但只有少数(好吧,除非你能预测未来,只有一个)的结束日期在今天之后。
一些进一步的建议:如果你想要性能,你应该远离标量函数。它们在 MSSQL 中的速度非常慢 =( 易于使用,但在基于集合的操作中使用时会产生大量开销;例如,一次更新几条 1000 条记录。
我假设你现在有一些类似的东西:
UPDATE [INVOICE]
SET target_amount = base_amount * dbo.fn_get_rate(gl_cmp, base_ccy, target_ccy, invoice_date)
WHERE blah = blah
如果你把它改到下面,它会非常快,虽然我同意它不太容易重用......我猜不能拥有它=)
UPDATE [INVOICE]
SET target_amount = Coalesce( -- direct lookup
(SELECT TOP 1 [INVOICE].base_amount * d.gl_crcnv_rate
FROM [TABLE] d
WHERE d.gl_cmp_key = [INVOICE].gl_cmp
AND d.gl_CRNCY_FROM = [INVOICE].base_ccy
AND d.gl_CRNCY_TO = [INVOICE].target_ccy
AND [INVOICE].invoice_date BETWEEN d.gl_crcnv_bdate AND d.gl_crcnv_edate),
-- reverse lookup
(SELECT TOP 1 [INVOICE].base_amount * r.gl_crcnv_rate
FROM [TABLE] r
WHERE r.gl_cmp_key = [INVOICE].gl_cmp
AND r.gl_CRNCY_TO = [INVOICE].target_ccy
AND r.gl_CRNCY_FROM = [INVOICE].base_ccy
AND [INVOICE].invoice_dateBETWEEN r.gl_crcnv_bdate AND r.gl_crcnv_edate),
-- fallback
[INVOICE].base_amount))
WHERE blah = blah
我有一个函数必须根据从具有历史值的 table 检索到的兑换率来兑换货币。它有四个参数(@to_curr、@from_curr、@trans_date、@gl_cmp)和汇率returns。
table 结构是
currencypk int primary key
gl_crcnv_bdate datetime
gl_crcnv_edate datetime
gl_crcnv_rate float
gl_cmp_key char(30)
(我们有多家公司,每家都标识)gl_CRNCY_TO char(30)
gl_CRNCY_FROM char(30)
下面是我的代码,当我将其插入函数时,它会导致执行时间大幅增加。我很清楚我的函数中的瓶颈在哪里,但我对如何重写它一筹莫展。我有一个带有 (and) 或 (and) 设置的丑陋 where 子句的原因是因为并非每个关系都是相反的。
例如,有从 USD 到 GBP 的转换率的记录,但根本不存在从 GBP 到 USD 的记录。该事实解释了 SELECT
中的 case 语句,以获取 table 中不存在的实际转换。 isnull
是为了防止 returns 什么都没有,所以它使用 1
非常感谢任何帮助。
SELECT
isnull((SELECT convert(decimal(5,4),
case
when @to_curr <> gl_CRNCY_TO
then (1/gl_crcnv_rate)
else gl_crcnv_rate
end)
FROM
[TABLE]
WHERE
gl_cmp_key = @gl_cmp
AND ((gl_CRNCY_TO = @from_curr AND gl_CRNCY_FROM = @to_curr)
OR (gl_CRNCY_TO = @to_curr AND gl_CRNCY_FROM = @from_curr))
AND @trans_date BETWEEN gl_crcnv_bdate AND gl_crcnv_edate), 1)
根据提供的信息,我只能想到这些
创建计算列,这样我们就不必为所有查询计算它。
ALTER TABLE [TABLE] ADD gl_crcnv_rate_invert AS (1/gl_crcnv_rate) PERSISTED;
创建索引
CREATE NONCLUSTERED INDEX ix_cmpkey_crncyto_crncyfrom_bdate_edate ON [TABLE] (gl_CRNCY_TO, gl_CRNCY_FROM, gl_crcnv_bdate, gl_crcnv_edate)
你可以试试:
SELECT isnull(SELECT convert(decimal(5,4), gl_crcnv_rate)
FROM (select 1/gl_crcnv_rate as gl_crcnv_rate
from [TABLE]
WHERE gl_cmp_key = @gl_cmp AND
gl_CRNCY_TO = @from_curr AND
gl_CRNCY_FROM = @to_curr AND
@trans_date BETWEEN gl_crcnv_bdate AND gl_crcnv_edate
UNION
select gl_crcnv_rate
from [TABLE]
WHERE gl_cmp_key = @gl_cmp AND
gl_CRNCY_TO = @to_curr AND
gl_CRNCY_FROM = @from_curr AND
@trans_date BETWEEN gl_crcnv_bdate AND gl_crcnv_edate
) sq,
1)
这假设如果两个可能的转换记录都存在,则转换因子将评估为相同的值,并且存在查询可以使用的索引。
Mark Bannisters 的回答已经向前迈进了一步,但我想更进一步并将代码更改为:
SELECT convert(decimal(5,4), Coalesce( -- direct lookup
(SELECT TOP 1 gl_crcnv_rate
FROM [TABLE]
WHERE gl_cmp_key = @gl_cmp
AND gl_CRNCY_FROM = @from_curr
AND gl_CRNCY_TO = @to_curr
AND @trans_date BETWEEN gl_crcnv_bdate AND gl_crcnv_edate),
-- reverse lookup
(SELECT TOP 1 1/gl_crcnv_rate
FROM [TABLE]
WHERE gl_cmp_key = @gl_cmp
AND gl_CRNCY_TO = @from_curr
AND gl_CRNCY_FROM = @to_curr
AND @trans_date BETWEEN gl_crcnv_bdate AND gl_crcnv_edate),
-- fallback
1))
COALESE()
足够聪明,如果它发现第一个块已经返回匹配项,则不会执行第二个块。它还使您免于 UNION
(首先应该是 UNION ALL
!)。
此外,您还需要确保拥有这样的索引:
CREATE INDEX idx ON [TABLE] (gl_cmp_key, gl_CRNCY_FROM, gl_CRNCY_TO, gl_crcnv_edate, gl_crcnv_bdate)
坦率地说,我会将当前的 PK 设为非集群,而将此设为集群,这样可以节省很多书签查找;唯一的缺点是你可能需要偶尔 rebuild/defragment 索引,因为添加新记录会随着时间的推移导致碎片化,因为新值被挤在现有记录之间......(侧记:你甚至需要 currencypk ?如果不需要,只需将 `gl_cmp_key、gl_CRNCY_FROM、gl_CRNCY_TO、gl_crcnv_edate、gl_crcnv_bdate、currencypk' 设为新的 PK...相当难看, 但在功能上同样有效,并且在使用 space 上节省了很多。
您希望在 bdate
之前显示 edate
的原因是您可能会查找最新的汇率。假设你有 20 个利率,它们的开始日期都在今天之前,但只有少数(好吧,除非你能预测未来,只有一个)的结束日期在今天之后。
一些进一步的建议:如果你想要性能,你应该远离标量函数。它们在 MSSQL 中的速度非常慢 =( 易于使用,但在基于集合的操作中使用时会产生大量开销;例如,一次更新几条 1000 条记录。
我假设你现在有一些类似的东西:
UPDATE [INVOICE]
SET target_amount = base_amount * dbo.fn_get_rate(gl_cmp, base_ccy, target_ccy, invoice_date)
WHERE blah = blah
如果你把它改到下面,它会非常快,虽然我同意它不太容易重用......我猜不能拥有它=)
UPDATE [INVOICE]
SET target_amount = Coalesce( -- direct lookup
(SELECT TOP 1 [INVOICE].base_amount * d.gl_crcnv_rate
FROM [TABLE] d
WHERE d.gl_cmp_key = [INVOICE].gl_cmp
AND d.gl_CRNCY_FROM = [INVOICE].base_ccy
AND d.gl_CRNCY_TO = [INVOICE].target_ccy
AND [INVOICE].invoice_date BETWEEN d.gl_crcnv_bdate AND d.gl_crcnv_edate),
-- reverse lookup
(SELECT TOP 1 [INVOICE].base_amount * r.gl_crcnv_rate
FROM [TABLE] r
WHERE r.gl_cmp_key = [INVOICE].gl_cmp
AND r.gl_CRNCY_TO = [INVOICE].target_ccy
AND r.gl_CRNCY_FROM = [INVOICE].base_ccy
AND [INVOICE].invoice_dateBETWEEN r.gl_crcnv_bdate AND r.gl_crcnv_edate),
-- fallback
[INVOICE].base_amount))
WHERE blah = blah