在 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 结构是

下面是我的代码,当我将其插入函数时,它会导致执行时间大幅增加。我很清楚我的函数中的瓶颈在哪里,但我对如何重写它一筹莫展。我有一个带有 (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 Ba​​nnisters 的回答已经向前迈进了一步,但我想更进一步并将代码更改为:

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