【问题标题】:How to return parent records, only if there is a child record, with a max date that falls between a date range?仅当存在子记录且最大日期介于日期范围之间时,如何返回父记录?
【发布时间】:2019-09-13 23:29:14
【问题描述】:

当我的 PARENTS 表中所有已完成的父项在某个日期范围内完成时,我需要返回它们。针对父级的所有操作都存储在子表 PARENT_ACTIONS 中。我遇到的困难是,完成操作后可能还有更多操作,甚至为该 PARENT_ID 存储了多个已完成操作,我只需要返回父级,如果最大操作是已完成的操作,它在用户选择的日期范围内。

我尝试过搜索论坛,但我发现的大部分内容都是“如何找到父记录的子记录的最大日期”,这对如何做最大值很有帮助,但对我的确切的问题。

我不记得我尝试过的所有内容,但我目前正在尝试这段代码,它确实返回了正确的结果(11 行),由于 SQL 效率极低,它只需要 10 分钟。

var parents = (from p in db.PARENTs
               where p.PARENT_ACTION
                        .Any(pa => pa.ACTION_ID == 99 
                                && pa.ACTION_DATE >= beginDate
                                && pa.ACTION_DATE <= endDate
                                && pa.ACTION_DATE == p.PARENT_ACTION.Max(pam => pam.ACTION_DATE))
              select p);

我希望能够编写与下面的 SQL 非常相似的东西,它会在

select * from parent p
 where p.STATUS = 99
   and exists (select 'x' from parent_action pa
                where pa.PARENT_ID = p.ID
                  and pa.ACTION_ID = 99
                  and pa.ACTION_DATE = (select max(pam.action_date)
                                          from parent_action pam
                                         where pam.parent_id = p.id)
                  and pa.action_date between to_date('04/10/2019', 'MM/DD/YYYY') and to_date('04/23/2019', 'MM/DD/YYYY'));

*****更新 1*****

我正在使用 System.Data.Entity 查询 Oracle 12c 数据库

Sample data:
STATUS
ID, DESC
1, ENTERED
2, SUBMITTED
99, COMPLETED

ACTIONS
ID, DESC
1, ENTER
2, SUBMIT
99, COMPLETE

PARENT
ID, STATUS
1, 99
2, 1
3, 99
4, 99

PARENT_ACTIONS
ID, PARENT_ID, ACTION_ID, ACTION_DATE
1, 1, 1, 04/01/2019
2, 1, 2, 04/05/2019
3, 1, 99, 04/11/2019
4, 2, 1, 04/11/2019
5, 3, 1, 04/15/2019
6, 3, 2, 04/16/2019
7, 3, 99, 04/17/2019
8, 3, 2, 04/18/2019 --Parent sent back to submitted status
9, 4, 1, 04/01/2019
10, 4, 2, 04/11/2019
11, 4, 99, 04/15/2019
12, 4, 99, 04/24/2019 --Completion details updated by customer, business rules require a new complete action be written

基于此示例数据,日期范围为 2019 年 4 月 10 日至 2019 年 4 月 23 日,我想写一些仅返回父 ID 1 的内容,因为这是唯一具有最大操作日期的父是 1) 一个 COMPLETE 操作 (99) 和 2) 在指定日期范围之间。我希望将 Parent 4 排除在外,因为最大操作日期不在指定的日期范围内。

***** 更新 2 *****

看起来我确实有正确的代码来返回所需的结果;但是,我认为我做错了什么,因为查询执行超过了 10 分钟。经过进一步调查,我似乎是生成的 SQL 中出现的“CAST AS”的另一个受害者,并大大降低了性能。在我的情况下,它将我在 SQL 中的 ID 列转换为数字 (10,0),当我的表中的 ID 列是整数时,在我的 EF 模型中,ID 列是 Int32。我不太确定如何解决这个问题,或者我是否应该为这个特定的数据需求编写自己的 SQL。

【问题讨论】:

  • 您能否提供表架构、示例数据和预期输出?还有你使用的是什么 dbms(MSSQL、MySQL 等)?

标签: c# entity-framework linq-to-entities


【解决方案1】:

编辑: 按日期降序排列操作,然后执行第一个(最新)操作。此操作的状态必须为 99,并且它的日期应在要求的范围内:

    var prs = (from p in parents
           where p.PARENT_ACTION
                .OrderByDescending(pa => pa.ACTION_DATE)
               .Take(1)
               .Any(pa => 
                    pa.ACTION_ID == 99
                    && pa.ACTION_DATE >= beginDate
                    && pa.ACTION_DATE <= endDate)
          select p);

编辑(根据下面的链接,这段代码应该相当于T-SQL的排序函数,应该更高效:https://smehrozalam.wordpress.com/2009/12/29/linq-how-to-get-the-latest-last-record-with-a-group-by-clause/

    var parentIds = new HashSet<int>(from a in PARENT_ACTIONS
            group a by a.PARENT_ID into grp
            let maxDate = grp.Max (g => g.ACTION_DATE)
            from p in grp
            where p.ACTION_DATE == maxDate 
                && p.ACTION_ID == 99
                && p.ACTION_DATE >= beginDate
                && p.ACTION_DATE <= endDate
            select p.PARENT_ID);
    var prs = parents.Where(p => parentIds.Contains(p.PARENT_ID));

【讨论】:

  • 感谢您的回复;然而,这不是我所要求的。如果最长行动日期是针对已完成的行动,并且该最长行动日期介于日期范围之间,我需要返回父母。我将用一些示例数据编辑我的问题,希望能澄清一下。
  • 这不会返回所需的结果。我得到了 153 个项目,而我应该得到 9 个。在查看它返回的额外 144 个项目时,我发现这些是带有错误数据的旧记录 - 它们在操作表中没有操作,但它们仍然包含在其中代码。此查询也需要 10 多分钟才能完成。看起来我在我的问题中的代码可能是正确的代码,它只是执行得非常糟糕,我还没有弄清楚为什么。
  • 我们的代码似乎都存在“选择 N+1 问题”。我找到了一个可能提供更好解决方案的帖子,请参阅我编辑的答案。
  • 这似乎适用于一小部分数据,但是如果扩展日期参数,我最终会得到一个包含 25k 项的 HashSet,这不能很好地转换为 sql where 子句使用 parendIds.Contains()。我不认为它受到“选择 N+1 问题的困扰,因为它正在生成一个返回所有我需要的信息的查询。看来我的问题是生成的 SQL 有很多 CAST AS Number(10,0) 代替 Integer 数据库列。如果我从 sql 中删除 CAST AS,它的性能与我编写的 sql 一样。
  • 我刚刚修改了记事本中的 .edmx 文件,将列映射类型从“数字”更改为“int”。一旦我这样做了,生成的 SQL 就不再具有所有的 CAST 函数,并且执行得很快。令人沮丧的是,在创建数据库优先模型时,我无法修改 UI 中的列映射,并且它将 Oracle Integer 列映射到数字类型。
【解决方案2】:

如果最大操作是已完成的操作,并且它在用户选择的日期范围内,我只需要返回父级。

因此,如果父项的状态已完成 (99) 并且此父项的最后一个 ParentAction 已完成,并且日期介于 beginDate 和 endDate 之间,那么您需要父项。或者更准确地说:此 Parent 的最后一个 ParentAction 有一个 ActionId Complete (99) 和一个介于 beginDate 和 endDate 之间的 ActionDate。

当您想要“项目及其子项目”时,例如“学校及其学生”、“客户及其订单”、“产品及其产品部件”,请考虑使用 Queryable.GroupBy

var parentsThatCompletedTheirActionsInTime = dbContext.Parents
    .Where(parent => parent.Status == 99) // = completed
    .GroupJoin(dbContext.ParentActions,   // GroupJoin with the ParentActions
        parent => parent.Id,              // from every Parent take the primary key
    parentAction => parentAction.Id,      // from every ParentAction the foreign key

    // ResultSelector: take the parent and all its matching parentActions
    // to make one new object
    (parent, actionsOfThisParent) => new
    {
        Parent = parent,
        LastParentAction = actionsOfThisParent
           .Select(action => new
           {
               IsCompleted = action.ActionId == 99,
               ActionDate = action.ActionDate,
           })
           .OrderByDescending(action => action.ActionDate)               
           .FirstOrDefault(),
    })

    // Keep only those parents where the Last Action was completed in time
    .Where(joinResult => joinResult.LastParentAction.IsCompleted
        && joinResult.LastParentAction.ActionDate >= beginDate
        && joinResult.LastParentAction.ActionDate <= endDate)

    // finally: keep only the Parent:
    .Select(joinResult => joinResult.Parent);

【讨论】:

  • 感谢您的回复。这似乎确实返回了正确的结果,但它仍然非常慢(14 分钟后取消,删除了所有 CAST 函数并在
【解决方案3】:

事实证明,我最初拥有的代码确实返回了我想要的,而且这似乎是一种相对有效的方式。我问这个问题的最初原因是看看 1) 我的代码是否正确,可以返回我想要的内容,更重要的是 2) 是否有更有效的方法来检索我想要的数据,因为我的代码需要 10 多分钟返回 - 我确信我一定做错了什么。

在尝试了所提供的所有精彩代码(非常感谢 Metheny 和 Harald)并且仍然遇到 10 行的 10 多分钟返回时间后,我偶然发现了一些帖子表明 Linq to Entity 在生成 SQL 时可能会出现糟糕的性能包含“CAST(FIELD AS TYPE)”,这正是我所经历的。

以下代码返回我的问题所需的结果。

var parents = (from p in db.PARENTs
               where p.PARENT_ACTION
                        .Any(pa => pa.ACTION_ID == 99 
                                && pa.ACTION_DATE >= beginDate
                                && pa.ACTION_DATE <= endDate
                                && pa.ACTION_DATE == p.PARENT_ACTION.Max(pam => pam.ACTION_DATE))
              select p);

除了此代码之外,要从生成的 SQL 中删除“CAST AS”,我必须在记事本中修改我的 .edmx 文件,并将所有 Oracle INTEGER 列的类型从“number”更改为“int” .

最后,让我声明,我不太确定如何最好地处理结束我的问题。我不确定,因为我在问题中的原始代码返回了正确的结果,Harald 的代码返回了正确的结果,而 Metheny 的代码也返回了正确的结果(尽管我确实担心是否有大量父 ID)。看来我最初的问题并不是解决问题所需的问题,只是当时我不知道真正的问题是什么。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-26
    • 2012-01-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-20
    相关资源
    最近更新 更多