如何将关联连接从 SQL 重写为 LINQ

Posted

技术标签:

【中文标题】如何将关联连接从 SQL 重写为 LINQ【英文标题】:How to rewrite correlated join from SQL to LINQ 【发布时间】:2021-09-12 22:45:59 【问题描述】:

我正在尝试将以下 SQL 查询重写为 LINQ:

SELECT `i`.`symbol`, `i`.`id`, `t0`.`close`, `t`.`close`, `t`.`close` - `t0`.`close`, (`t`.`close` - `t0`.`close`) / `t0`.`close`
FROM `investment` AS `i`
LEFT JOIN `investment_record` AS `t0` ON `t0`.id = (
    SELECT `i0`.id
    FROM `investment_record` AS `i0`
    WHERE (`i0`.`date` <= @dateFrom) AND i.id = i0.investment_id
    ORDER BY `i0`.`date` DESC
    LIMIT 1
)
LEFT JOIN `investment_record` AS `t` ON `t`.id =(
    SELECT `i0`.id
    FROM `investment_record` AS `i0`
    WHERE (`i0`.`date` <= @dateTo) AND i.id = i0.investment_id
    ORDER BY `i0`.`date` DESC
    LIMIT 1
) 

WHERE `i`.`id` IN (@id0, @id1, ....)

我的主要问题是 JOIN 的 AND i.id = i0.investment_idLIMIT 1 部分。

目前我能做到的最好的是:

from inv in _context.Investment
join recTo in _context.InvestmentRecord on inv.Id equals recTo.InvestmentId into recToColl
from recToNullable in recToColl.Where(x => x.Date <= dateTo).OrderByDescending(x => x.Date).Take(1).DefaultIfEmpty()
join recFrom in _context.InvestmentRecord on inv.Id equals recFrom.InvestmentId into recFromColl
from recFromNullable in recFromColl.Where(x => x.Date <= dateFrom).OrderByDescending(x => x.Date).Take(1).DefaultIfEmpty()
where investmentIds.Contains(inv.Id)
let amountFrom = recFromNullable.Close
let amountTo = recToNullable.Close
select new InvestmentPerformance(
  inv.Symbol,
  inv.Id,
  amountFrom,
  amountTo,
  amountTo - amountFrom,
  (amountTo - amountFrom) / amountFrom
);

但问题是它不起作用。

它给出了表达式无法翻译的异常:

System.InvalidOperationException:LINQ 表达式 '数据库集() .GroupJoin( 内部:DbSet(), outerKeySelector: inv => inv.Id, innerKeySelector: recTo => recTo.InvestmentId, resultSelector: (inv, recToColl) => new inv = inv, recToColl = recToColl )' 无法翻译。要么以可翻译的形式重写查询,要么显式切换到客户端评估 通过插入对“A sEnumerable”、“AsAsyncEnumerable”、“ToList”的调用, 或“ToListAsync”。见https://go.microsoft.com/fwlink/?linkid=2101038 了解更多信息。

这个丑陋的 SQL(和 LINQ)的要点是计算给定时间间隔的投资性能。用户可以指定从到日期。问题是有时用户可以指定没有任何记录的日期(例如银行假期)。所以对于给定的日期,我想使用最接近的先前记录(这就是 &lt;= @dateFrom 条件和 ORDER BY date DESC LIMIT 1 部分 SQL 的原因。

我用不同形式的连接尝试了许多 LINQ 变体,但没有一个能满足我的需要 :(

我正在使用 EF.Core 5 和 mysql 数据库。

【问题讨论】:

为什么不直接查找最接近的有效日期作为主查询的前兆?顺便说一句,它基本上是select x order by x desc limit 1 select max 如果您已经在 SQL 中使用了查询,我强烈建议您将该查询保存为存储过程或视图并在 EF Core 中使用。每当我的查询逻辑变得复杂时,我都会使用存储过程 另外,我不熟悉您的架构或您正在尝试做什么,但是您正在执行子查询和连接限制 1 的事实让我想知道如果你应该考虑做一个 OUTER APPLY 我明白你的意思,我在工作中经常使用 MS SQL,我也会使用类似的东西,但我认为 MySQL 不支持 OUTER APPLY :( 这绝对应该是@​​987654330@,或者LEFT JOINROW_NUMBER,反正当前代码是错误的/效率低下 【参考方案1】:

原始 SQL 查询对我来说似乎很复杂。我已经使用OUTER APPLY 重写了它,而不是子连接查询。

SELECT `i`.`symbol`, `i`.`id`, `t0`.`close`, `t`.`close`, `t`.`close` - `t0`.`close`, (`t`.`close` - `t0`.`close`) / `t0`.`close`
FROM `investment` AS `i`
OUTER APPLY (
  SELECT `i0`.id
  FROM `investment_record` AS `i0`
  WHERE (`i0`.`date` <= @dateFrom) AND i.id = i0.investment_id
  ORDER BY `i0`.`date` DESC
  LIMIT 1
) AS t0
OUTER APPLY (
  SELECT `i0`.id
  FROM `investment_record` AS `i0`
  WHERE (`i0`.`date` <= @dateTo) AND i.id = i0.investment_id
  ORDER BY `i0`.`date` DESC
  LIMIT 1
) AS t

WHERE `i`.`id` IN (@id0, @id1, ....)

然后我会用 EF 的方式来翻译这个,写成OUTER APPLY。这个SO post 可能会有所帮助。

看起来像这样:

from inv in _context.Investments
from rec1 in _context.InvestmentsRecords.Where(ir => ir.InvestmentId = inv.InvestmentId).Where(ir => ir.Date <= DateFrom).OrderByDescending().Take(1)
from rec1 in _context.InvestmentsRecords.Where(ir => ir.InvestmentId = inv.InvestmentId).Where(ir => ir.Date <= DateTo).OrderByDescending().Take(1)
...

【讨论】:

我正在使用 MySQL,但我认为它不支持 OUTER APPLY。 我的错,然后lateral?

以上是关于如何将关联连接从 SQL 重写为 LINQ的主要内容,如果未能解决你的问题,请参考以下文章

将 Linq 中的 NHibernate 应用程序重写为 SQL

将涉及多个表的左外连接从 Informix 重写为 Oracle

使用 UNPIVOT 将代码从 sql 重写为 redshift

如何重写具有连接子查询的 SQL 查询

我如何将 SQL 原始查询重写为 Laravel 查询生成器

将多个右连接重写为左连接