【问题标题】:Tough T-SQL To Left Join?难以离开 T-SQL 连接?
【发布时间】:2011-07-27 09:41:31
【问题描述】:

我有一张 ExchangeRates 表,其中包含一个 countryid 和一个 exchangerateate 的东西:

ExchangeRateID   Country   ToUSD      ExchangeRateDate
1                  Euro     .7400     2/14/2011
2                  JAP      80.1900   2/14/2011
3                  Euro     .7700      7/20/2011

请注意,同一国家/地区可能会根据日期使用不同的汇率...例如,上面的欧元是 2011 年 2 月 14 日的 0.7400,现在是 2011 年 7 月 20 日的 0.7700。

我有另一个订单项表,用于根据国家/地区列出项目。在此表中,每个订单项都有一个与之关联的日期。行项目日期应使用基于汇率的相应日期和国家。因此,如果我在 2011 年 2 月 16 日有一个带有国家欧元的订单项,则使用上述数据,它应该使用 2011 年 2 月 14 日的欧元值,而不是 2011 年 7 月 20 日的值,因为日期(条件 er. ExchangeRateDate

SELECT     
    er.ExchangeRateID, 
    er.CountryID AS Expr1, 
    er.ExchangeRateDate, 
    er.ToUSD, 
    erli.ExpenseReportLineItemID, 
    erli.ExpenseReportID, 
    erli.LineItemDate
FROM         
    dbo.ExpenseReportLineItem AS erli 
LEFT JOIN
    dbo.ExchangeRate AS er 
ON er.CountryID = erli.CountryID 
AND DATEADD(d, DATEDIFF(d, 0, er.ExchangeRateDate), 0) <= DATEADD(d, DATEDIFF(d, 0, 
                      erli.LineItemDate), 0)
WHERE     (erli.ExpenseReportID = 196)

这个左连接的问题...是因为日期是

LineItem 表有多条记录,每条记录都可以有自己的 CountryID:

Item            Country      ParentID    LineItemDate
Line Item 1      Euro           1           2/14/2011
Line Item 2      US             1           2/14/2011
Line Item3       Euro           1           2/15/2011

所以 ParentID (ExpenseReportID) = 1 有 3 条记录。然后我获取这些记录并加入 ExchangeRate 表,其中我的行项目表中的 Country = 汇率表的国家/地区(这部分很简单)但是我要做的第二个条件是:

  AND DATEADD(d, DATEDIFF(d, 0, er.ExchangeRateDate), 0) <= DATEADD(d, DATEDIFF(d, 0, 
                          erli.LineItemDate), 0)

但这就是问题所在,因为这将返回我的汇率表中的多行,因为欧元被列出了两次。

【问题讨论】:

  • 我在这里为 t-sql 专家祈祷......

标签: sql-server-2005 tsql


【解决方案1】:

我可能在这里遗漏了一些东西,但据我了解,解决您的问题的“愚蠢”解决方案是使用 A ROW_NUMBER 函数和外部过滤器与您现有的“返回太多条目”查询(这也可以通过CTE,但对于像这样的简单情况,我更喜欢派生表语法):

SELECT *
FROM (
    SELECT     
        er.ExchangeRateID, 
        er.CountryID AS Expr1, 
        er.ExchangeRateDate, 
        er.ToUSD, 
        erli.ExpenseReportLineItemID, 
        erli.ExpenseReportID, 
        erli.LineItemDate,
        ROW_NUMBER() OVER (PARTITION BY ExpenseReportID, ExpenseReportLineItemID ORDER BY ExchangeRateDate DESC) AS ExchangeRateOrderID
    FROM dbo.ExpenseReportLineItem AS erli 
    LEFT JOIN dbo.ExchangeRate AS er 
        ON er.CountryID = erli.CountryID 
            AND DATEADD(d, DATEDIFF(d, 0, er.ExchangeRateDate), 0) 
                <= DATEADD(d, DATEDIFF(d, 0, erli.LineItemDate), 0)
    WHERE (erli.ExpenseReportID = 196)
        --For reasonable performance, it would be VERY nice to put a filter
        -- on how far back the exchange rates can go here:
        --AND er.ExchangeRateDate > DateAdd(Day, -7, GetDate())
) As FullData
WHERE ExchangeRateOrderID = 1

如果我误解了,请见谅,否则希望对您有所帮助!

【讨论】:

  • 这似乎没有返回任何东西。
  • 很公平 - 分区上有错字,但不确定是否可以解释 - 仍然不应该返回任何内容:)
  • 这看起来是最适合我的解决方案。我认为 Lieven 的解决方案很有趣,但是,他的 = 条件是不显示任何行...所以这确实有效!
【解决方案2】:

如果您可以在 ExchangeRates 表中添加一个名为(类似)的附加列,您的生活会轻松很多

ExchangeRateToDate

一个单独的进程可以在添加新条目时更新之前的条目。

然后,您可以只查询 LineItemDate >= ExhangeRateDate 和

(将最后一个,可能是 null ExchangeRateToDate 视为特殊情况)。

【讨论】:

    【解决方案3】:

    我将创建一个内存表,并使用 ExchangeRateDates From & To 创建一个 ExchangeRate 表。
    之后剩下要做的就是在查询中加入此 CTE 而不是 ExchangeRate 表,并添加日期为 betweenthe date from/to 的条件。

    SQL 语句

    ;WITH er AS (
        SELECT  rn = ROW_NUMBER() OVER (PARTITION BY er1.ExchangeRateID ORDER BY er2.ExchangeRateDate DESC)
                , er1.ExchangeRateID
                , er1.Country
                , ExchangeRateDateFrom = ISNULL(DATEADD(d, 1, er2.ExchangeRateDate), 0)
                , ExchangeRateDateTo = er1.ExchangeRateDate
                , er1.ToUSD
        FROM    @ExchangeRate er1
                LEFT OUTER JOIN @ExchangeRate er2
                    ON  er1.Country = er2.Country
                        AND er1.ExchangeRateDate >= er2.ExchangeRateDate
                        AND er1.ExchangeRateID > er2.ExchangeRateID     
    )
    SELECT  er.ExchangeRateID, 
            er.CountryID AS Expr1, 
            er.ExchangeRateDateTo, 
            er.ToUSD, 
            erli.ExpenseReportLineItemID, 
            erli.ExpenseReportID, 
            erli.LineItemDate
    FROM    dbo.ExpenseReportLineItem AS erli 
            LEFT JOIN er ON er.CountryID = erli.CountryID 
                            AND DATEADD(d, DATEDIFF(d, 0, er.ExchangeRateDateTo), 0) <= DATEADD(d, DATEDIFF(d, 0, erli.LineItemDate), 0)
                            AND DATEADD(d, DATEDIFF(d, 0, er.ExchangeRateDateFrom), 0) >= DATEADD(d, DATEDIFF(d, 0, erli.LineItemDate), 0)
    WHERE   (erli.ExpenseReportID = 196)
            and er.rn = 1
    

    测试脚本

    DECLARE @ExchangeRate TABLE (
        ExchangeRateID INTEGER
        , Country VARCHAR(32)
        , ToUSD FLOAT
        , ExchangeRateDate DATETIME
    )   
    
    INSERT INTO @ExchangeRate 
    VALUES  (1, 'Euro', 0.7400, '02/14/2011')
            , (2, 'JAP', 80.1900, '02/14/2011')
            , (3, 'Euro', 0.7700, '07/20/2011')     
            , (4, 'Euro', 0.7800, '07/25/2011')     
    
    ;WITH er AS (
        SELECT  rn = ROW_NUMBER() OVER (PARTITION BY er1.ExchangeRateID ORDER BY er2.ExchangeRateDate DESC)
                , er1.ExchangeRateID
                , er1.Country
                , ExchangeRateDateFrom = ISNULL(DATEADD(d, 1, er2.ExchangeRateDate), 0)
                , ExchangeRateDateTo = er1.ExchangeRateDate
                , ToUSD = er1.ToUSD
        FROM    @ExchangeRate er1
                LEFT OUTER JOIN @ExchangeRate er2
                    ON  er1.Country = er2.Country
                        AND er1.ExchangeRateDate >= er2.ExchangeRateDate
                        AND er1.ExchangeRateID > er2.ExchangeRateID     
    )
    SELECT  *
    FROM    er
    WHERE   rn = 1
    

    【讨论】:

    • 酷 - 我猜当您选择大量费用报告行项目时,这可能比 ROW_NUMBER 或相关子查询解决方案更有效?
    • 嗯,它抱怨两个无效的列名 ExchangeRateDate 和 ToUSD...
    • @oJM86o - 抱歉,离开了。缺少的列已添加,您是否仍然感兴趣。
    • @Tao - 在一个大集合上,它确实会更有效率。
    【解决方案4】:

    也许您可以尝试使用table expression 来获得您的TOP 1,然后加入表表达式。那有意义吗?希望这可以帮助。

    【讨论】:

    • 我不能这样做,因为在我的 lineitem 表中我可能有多个订单项,每个订单项都有不同的国家,所以如果我在 CTE 中使用 TOP 1,那么我会失去在缺少的国家/地区的加入。 ..
    【解决方案5】:

    这可以通过使用一个或多个 CTE 来解决。这个较早的 SO 问题应该具有所需的构建块: How can you use SQL to return values for a specified date or closest date < specified date?

    请注意,您必须将其修改为您自己的架构,并过滤掉更接近但在未来的结果。 我希望这会有所帮助,但如果还不够,我相信我可以发布更详细的答案。

    【讨论】:

      【解决方案6】:

      如果我没有误解你想要做什么,你可以使用外部申请来获得最新的汇率。

      select *
      from ExpenseReportLineItem erli
        outer apply (select top 1 *
                     from ExchangeRates as er1
                     where er1.Country = erli.Country and
                           er1.ExchangeRateDate <= erli.LineItemDate 
                     order by er1.ExchangeRateDate desc) as er
      

      【讨论】:

      • 这和相关子查询有什么区别(除了它对我的冒犯更少:))?
      • @Tao 我真的不知道。这个查询至少对它的作用是诚实的。 “获取每条线路的最新费率”。但是查询计划并不是那么愚蠢。至少对于我使用的非常少量的样本数据来说不是这样。我很乐意将它留给其他人(也许是 OP)来找出/测试哪个版本最有效。
      【解决方案7】:

      您可以将其用作相关子查询,它会为您提供一个表格,其中包含给定日期的最新交换值(在评论中指出):

      SELECT * 
      FROM er
          INNER JOIN
          (
              SELECT CountryID, MAX(ExchangeRateDate) AS ExchangeRateDate
              FROM er
              WHERE ExchangeRateDate <= '9/1/2011' 
                  -- the above is the date you will need to correlate with the main query...
              GROUP BY Country
          ) iq
          ON iq.Country = er.Country AND er.ExchangeRateDate = iq.ExchangeRateDate
      

      所以完整的查询应该是这样的:

      SELECT     
          iq2.ExchangeRateID, 
          iq2.CountryID AS Expr1, 
          iq2.ExchangeRateDate, 
          iq2.ToUSD, 
          erli.ExpenseReportLineItemID, 
          erli.ExpenseReportID, 
          erli.LineItemDate
      FROM dbo.ExpenseReportLineItem AS erli 
          LEFT JOIN
          (
              SELECT * 
              FROM ExchangeRate er
                  INNER JOIN
                  (
                      SELECT CountryID, MAX(ExchangeRateDate) AS ExchangeRateDate
                      FROM ExchangeRate er
                      WHERE ExchangeRateDate <= erli.LineItemDate 
                      -- the above is where the correlation occurs...
                      GROUP BY Country
                  ) iq
                  ON iq.Country = er.Country AND er.ExchangeRateDate = iq.ExchangeRateDate
          ) iq2
          ON er.CountryID = erli.CountryID 
              AND DATEADD(d, DATEDIFF(d, 0, iq2.ExchangeRateDate), 0) <= DATEADD(d, DATEDIFF(d, 0, erli.LineItemDate), 0)
          WHERE     (erli.ExpenseReportID = 196)
      

      【讨论】:

      • 我很想知道 Row_Number 解决方案或相关子查询是否更有效......我猜你的为 SQL Server 提供了更多优化选项,但相关子查询感觉如此......错误的! :)
      • 这真的取决于数据拓扑。我让ROW_NUMBER 工作得非常好,而且也确实出轨了。如果您不喜欢相关子查询,您可以随时尝试等效的 CTE。至少它看起来会更干净!
      • 我可以理解这一点,但是它有一大堆错误......多部分标识符等......现在正在尝试修复它,所以我不会打折你的答案。
      • 有趣。我看到了几个问题,我将对其进行编辑以修复。我的内部SELECT 中的“*”也有可能在您的架构中拾取未包含在您的示例数据中的列。如果您将其指定为从ExchangeRate 中选择的实际需要的字段,它也可能会有所帮助。
      • @mwigdahl 我试图修复你的问题,但看看你的内部条件ExchangeRateDate &lt;= erli.LineItemDate,如果你把它放在管理工作室并尝试运行它,你应该得到一个The multi-part identifier erli.lineitemdate could not be bound
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-24
      • 1970-01-01
      • 1970-01-01
      • 2022-11-17
      • 1970-01-01
      相关资源
      最近更新 更多