【问题标题】:Linq query with multiple joins and groups具有多个连接和组的 Linq 查询
【发布时间】:2020-11-09 07:31:28
【问题描述】:

我对 linq 表达式很陌生,正在尝试计算出这个 tsql 的等效 linq 版本:

SELECT 
    p.Name, 
    Count(a.Street) [Number of addresses], 
    c.ContractType, 
    COUNT(cv.ContractVersionId) Number, 
    MAX(cv.ChangedDate) [Last change date] 
From Person p 
LEFT JOIN Address a ON p.PersonId = a.PersonId AND a.ChangedDate IS NULL
LEFT JOIN Contract c ON p.PersonId = c.PersonId
LEFT JOIN ContractVersion cv ON c.ContractId = cv.ContractId

GROUP BY p.Name,  c.ContractType

这会产生输出:

Name    Number of addresses ContractType    Number  Last change date
Bob     1                   NULL            0       NULL
Allice  1                   Buy             1       2020-10-31 00:00:00.000
Karen   0                   Buy             3       2020-09-01 00:00:00.000
Peter   3                   Lease           3       2020-07-10 00:00:00.000
Tom     5                   Lease           5       2020-09-14 00:00:00.000
Allice  3                   Rent            3       2020-05-10 00:00:00.000

要重现的数据:

IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Person]') AND type in (N'U'))
DROP TABLE [dbo].Person
GO
IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Address]') AND type in (N'U'))
DROP TABLE [dbo].Address
GO
IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Contract]') AND type in (N'U'))
DROP TABLE [dbo].Contract
GO
IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[ContractVersion]') AND type in (N'U'))
DROP TABLE [dbo].ContractVersion
GO
CREATE TABLE Person (
        PersonId Uniqueidentifier NOT NULL,
        Name NVARCHAR(100) NOT NULL
    )
CREATE TABLE Address(
        AddressId Uniqueidentifier NOT NULL,
        PersonId Uniqueidentifier NOT NULL,
        Street NVARCHAR(100) NOT NULL,
        ChangedDate DATETIME NULL
    )
CREATE TABLE Contract (
        ContractId Uniqueidentifier NOT NULL,
        PersonId Uniqueidentifier NOT NULL,
        ContractType NVARCHAR(100) NOT NULL
    )
CREATE TABLE ContractVersion (
        ContractVersionId Uniqueidentifier NOT NULL,
        ContractId Uniqueidentifier NOT NULL,
        ContractText NVARCHAR(100) NOT NULL,
        ChangedDate DATETIME NULL
    )


INSERT INTO Person
VALUES
    ('{B8A97537-AF09-45FD-A723-1BF43796DADA}','Tom'),
    ('{124A34C0-1AB3-4449-AEE2-04ED2732B8ED}','Allice'),
    ('{E501CC32-C587-43C7-B0DD-096F04EE80AA}','Peter'),
    ('{9D1E37BE-D032-45CC-AA1D-06ECBB9C59EA}','Karen'),
    ('{049B5985-F4D0-448C-9391-8E08495DF61F}','Bob')
INSERT INTO Address
VALUES  
    ('{AF3BE020-6851-46E1-9D04-9959C665A80D}','{B8A97537-AF09-45FD-A723-1BF43796DADA}','Last address 1',NULL),
    ('{A0957F82-8922-452C-AA1B-4EA05A1B121B}','{124A34C0-1AB3-4449-AEE2-04ED2732B8ED}','Last address 2',NULL),
    ('{10D60EDC-75B3-412F-AA0B-962746EB72BD}','{E501CC32-C587-43C7-B0DD-096F04EE80AA}','Last address 3',NULL),
    ('{93A10053-5059-4127-8EA1-C0DF77760E84}','{049B5985-F4D0-448C-9391-8E08495DF61F}','Last address 4',NULL),
    ('{F1E838FE-7074-4AB3-8A4D-0895CEB98362}','{B8A97537-AF09-45FD-A723-1BF43796DADA}','D','2020-01-01'),
    ('{4CB0D80D-7F74-4755-9C1A-08A03EE88B4B}','{124A34C0-1AB3-4449-AEE2-04ED2732B8ED}','D','2020-01-01'),
    ('{B42DE51A-3EAD-4AAC-B5DC-75493FE214E5}','{E501CC32-C587-43C7-B0DD-096F04EE80AA}','D','2020-01-01')

INSERT INTO Contract
VALUES  
    ('{64989F02-793E-4313-9324-4916D6C5D610}','{B8A97537-AF09-45FD-A723-1BF43796DADA}','Lease'),
    ('{0101F8CD-E72E-4D9B-A8BD-A94BBDB39740}','{124A34C0-1AB3-4449-AEE2-04ED2732B8ED}','Rent'),
    ('{D1F94A9C-0718-4E18-B63F-B506C5E2C70E}','{E501CC32-C587-43C7-B0DD-096F04EE80AA}','Lease'),
    ('{A8397E6D-597A-4024-91DC-98EA0D9FF848}','{9D1E37BE-D032-45CC-AA1D-06ECBB9C59EA}','Buy'),
    ('{D47E82B3-2D41-4CCB-BD6F-003BD9C49BEA}','{B8A97537-AF09-45FD-A723-1BF43796DADA}','Lease'),
    ('{5EC4E588-00A5-442B-BD7B-0C9FD27E8076}','{124A34C0-1AB3-4449-AEE2-04ED2732B8ED}','Buy')
    
INSERT INTO ContractVersion
VALUES  
    ('{E3E26DE5-F00A-4BEB-9A48-72AE9EA29F2E}','{64989F02-793E-4313-9324-4916D6C5D610}','Created','2020-01-01'),
    ('{C5EFAB2E-EE41-4A10-A708-44CA19E505A9}','{0101F8CD-E72E-4D9B-A8BD-A94BBDB39740}','Created','2020-05-01'),
    ('{43EDFE20-B1EF-4236-8D4E-478F445EC327}','{D1F94A9C-0718-4E18-B63F-B506C5E2C70E}','Created','2020-06-03'),
    ('{16870733-0EC3-497B-BA51-770C921A245A}','{A8397E6D-597A-4024-91DC-98EA0D9FF848}','Created','2020-08-15'),
    ('{93CAEA22-D796-4237-A706-47C3D6AE8700}','{D47E82B3-2D41-4CCB-BD6F-003BD9C49BEA}','Created','2020-09-12'),
    ('{9D003A79-1376-4512-922F-88A33EC72CCB}','{5EC4E588-00A5-442B-BD7B-0C9FD27E8076}','Created','2020-10-31'),
    ('{8BD9E556-BB56-4404-A7C2-C6C06D321127}','{64989F02-793E-4313-9324-4916D6C5D610}','Reviewed','2020-01-05'),
    ('{E46D1C7E-94BF-43BC-883D-7960CE719C07}','{0101F8CD-E72E-4D9B-A8BD-A94BBDB39740}','Reviewed','2020-05-01'),
    ('{0B5B594C-78BF-49B7-834B-3E9A617A16F9}','{D1F94A9C-0718-4E18-B63F-B506C5E2C70E}','Reviewed','2020-06-08'),
    ('{931C3910-DFD6-4EB4-9826-9518B3C6BC54}','{A8397E6D-597A-4024-91DC-98EA0D9FF848}','Reviewed','2020-08-16'),
    ('{6F97D07E-9AAD-488E-87D3-D2A886547B1C}','{D47E82B3-2D41-4CCB-BD6F-003BD9C49BEA}','Reviewed','2020-09-14'),
    ('{BD3B3413-8853-4199-BC93-B23D49F77A9C}','{64989F02-793E-4313-9324-4916D6C5D610}','Finalized','2020-04-01'),
    ('{5DA560DE-69D7-4244-A320-AF3FE4135B22}','{0101F8CD-E72E-4D9B-A8BD-A94BBDB39740}','Finalized','2020-05-10'),
    ('{8392D528-438C-4F82-8BAC-69C1913CF610}','{D1F94A9C-0718-4E18-B63F-B506C5E2C70E}','Finalized','2020-07-10'),
    ('{0DC060B3-12F0-4927-8526-57E87C0150EA}','{A8397E6D-597A-4024-91DC-98EA0D9FF848}','Finalized','2020-09-01')
    

SELECT 
    p.Name, 
    Count(a.Street) [Number of addresses], 
    c.ContractType, 
    COUNT(cv.ContractVersionId) Number, 
    MAX(cv.ChangedDate) [Last change date] 
From Person p 
LEFT JOIN Address a ON p.PersonId = a.PersonId AND a.ChangedDate IS NULL
LEFT JOIN Contract c ON p.PersonId = c.PersonId
LEFT JOIN ContractVersion cv ON c.ContractId = cv.ContractId

GROUP BY p.Name,  c.ContractType

感谢所有帮助!

更新: 我可以生成这个 linq 表达式,但由于数据位于不同的表中,我应该如何进行分组。

from p in Persons
join a in Addresses on new { p.PersonId} equals new { a.PersonId} into addressGroup from a in addressGroup.DefaultIfEmpty()
join c  in Contracts on p.PersonId equals c.PersonId into contractGroups from c in contractGroups.DefaultIfEmpty()
join cv in ContractVersions on c.ContractId equals cv.ContractId

select new {p.Name, a.Street, c.ContractType, cv.ChangedDate}

更新 2:

我在下面尝试了@jdweng 查询,结果生成的sql 是这样的:

SELECT [t5].[Name] AS [name], (
    SELECT COUNT(*)
    FROM (
        SELECT NULL AS [EMPTY]
        FROM (
            SELECT [t7].[Street] AS [value], [t6].[Name], [t8].[ContractType]
            FROM [Person] AS [t6]
            LEFT OUTER JOIN [Address] AS [t7] ON [t6].[PersonId] = [t7].[PersonId]
            LEFT OUTER JOIN [Contract] AS [t8] ON [t6].[PersonId] = [t8].[PersonId]
            INNER JOIN [ContractVersion] AS [t9] ON [t8].[ContractId] = [t9].[ContractId]
            ) AS [t10]
        WHERE ([t5].[Name] = [t10].[Name]) AND ((([t5].[value] IS NULL) AND ([t10].[ContractType] IS NULL)) OR (([t5].[value] IS NOT NULL) AND ([t10].[ContractType] IS NOT NULL) AND ((([t5].[value] IS NULL) AND ([t10].[ContractType] IS NULL)) OR (([t5].[value] IS NOT NULL) AND ([t10].[ContractType] IS NOT NULL) AND ([t5].[value] = [t10].[ContractType])))))
        GROUP BY [t10].[value]
        ) AS [t11]
    ) AS [numberOfAddresses], [t5].[value] AS [contractType], (
    SELECT COUNT(*)
    FROM (
        SELECT NULL AS [EMPTY]
        FROM [Person] AS [t12]
        LEFT OUTER JOIN [Address] AS [t13] ON [t12].[PersonId] = [t13].[PersonId]
        LEFT OUTER JOIN [Contract] AS [t14] ON [t12].[PersonId] = [t14].[PersonId]
        INNER JOIN [ContractVersion] AS [t15] ON [t14].[ContractId] = [t15].[ContractId]
        WHERE ([t5].[Name] = [t12].[Name]) AND ((([t5].[value] IS NULL) AND ([t14].[ContractType] IS NULL)) OR (([t5].[value] IS NOT NULL) AND ([t14].[ContractType] IS NOT NULL) AND ((([t5].[value] IS NULL) AND ([t14].[ContractType] IS NULL)) OR (([t5].[value] IS NOT NULL) AND ([t14].[ContractType] IS NOT NULL) AND ([t5].[value] = [t14].[ContractType])))))
        GROUP BY [t15].[ContractVersionId]
        ) AS [t16]
    ) AS [number]
FROM (
    SELECT [t4].[Name], [t4].[value]
    FROM (
        SELECT [t0].[Name], [t2].[ContractType] AS [value]
        FROM [Person] AS [t0]
        LEFT OUTER JOIN [Address] AS [t1] ON [t0].[PersonId] = [t1].[PersonId]
        LEFT OUTER JOIN [Contract] AS [t2] ON [t0].[PersonId] = [t2].[PersonId]
        INNER JOIN [ContractVersion] AS [t3] ON [t2].[ContractId] = [t3].[ContractId]
        ) AS [t4]
    GROUP BY [t4].[Name], [t4].[value]
    ) AS [t5]

它几乎可以产生我想要的结果,并且花费的时间是原始 tsql 的 20 倍。

【问题讨论】:

  • 你在使用entity framework吗?如果是,是哪个版本? & 也分享相应的Models
  • 现在我只在 Linqpad 中使用 linq 表达式直接针对上面的表格工作,因此没有适用于它们的 EFcontexts 或模型。
  • 有,是Linqpad生成的。看看类属性,尤其是。导航属性并做一些初步尝试。这里没有真正的问题。你只说你想要的。
  • @GertArnold,我已经更新了未分组的 linq 表达式,但我不知道如何对数据进行分组
  • 正如经常发生的那样,您使用 SQL 思维攻击它并立即键入连接。首先设计一个具有主键和外键的适当数据库模型。然后,Linq-to-sql(Linqpad 使用)将生成具有导航属性的模型。如果你达到了这一点,请尝试使用这些属性编写一些查询并看看它们做了什么。然后你会发现你可以简单地做p.Addresses.Count()之类的事情。

标签: c# sql-server linq


【解决方案1】:

不确定 EF Core 如何支持 Count 的条件,所以只是模拟 Sum

var query =
   from p in Persons
   join a in Addresses on p.PersonId equals a.PersonId into addressGroup 
   from a in addressGroup.DefaultIfEmpty()
   join c in Contracts on p.PersonId equals c.PersonId into contractGroups 
   from c in contractGroups.DefaultIfEmpty()
   join cv in ContractVersions on c.ContractId equals cv.ContractId in versionsGroup
   from cv in versionsGroup.DefaultIfEmpty()
   group new { a, cv } by new { p.Name,  c.ContractType } into g
   select new 
   {
       g.Key.Name, 
       NumberOfAddresses = g.Sum(x => x.a.Street != null ? 1 : 0), 
       g.Key.ContractType, 
       Number = g.Sum(x => x.cv.ContractVersionId != null ? 1 : 0), 
       LastChangeDate = g.Max(x => x.cv.ChangedDate)
   }

【讨论】:

  • 不要认为你需要左外连接。
  • @jdweng 生成的SQL应该等于问题。
  • @SvyatoslavDanyliv,这会产生完全相同的执行计划!谢谢!
  • @SvyatoslavDanyliv :不! SQL 语言非常有限,并且在等效的 c# linq 中不需要复杂查询时,通常需要复杂查询。
  • @jdweng,那又怎样?此查询可通过 LINQ 轻松转换。而且我认为 EF Core 在生成所需的 SQL 方面非常有限,如果您不想将整个表抓取到客户端并进行后期处理,这一点很重要。
【解决方案2】:

尝试关注

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Context db = new Context();

            var results = (from p in db.Person
                           join a in db.Address on p.PersonId equals a.PersonId
                           join c in db.Contract on p.PersonId equals c.PersonId
                           join cv in db.ContractVersion on c.ContractId equals cv.ContractId
                           select new { p = p, a = a, c = c, cv = cv}
                           ).GroupBy(x => new { name = x.p.Name, contractType = x.c.ContractType })
                           .Select(x => new { 
                               name = x.Key.name, 
                               numberOfAddresses = x.GroupBy(y => y.a.Street).Count(),
                               contractType = x.Key.contractType,
                               number = x.GroupBy(y => y.cv.ContractVersionId).Count(),
                               lastChangeDate = x.OrderByDescending(y => y.cv.ChangedDate).FirstOrDefault().cv.ChangedDate
                           })
                           .ToList();
        }
    }
    public class Context
    {
        public List<Person> Person { get; set; }
        public List<Address> Address { get; set; }
        public List<Contract> Contract { get; set; }
        public List<ContractVersion> ContractVersion { get; set; }

    }
    public class Person
    {
        public string Name { get; set; }
        public string PersonId { get; set; }
    }
    public class Address
    {
        public string PersonId { get; set; }
        public string Street { get; set; }
        public DateTime ChangedDate { get; set; }

    }
    public class Contract
    {
        public string PersonId { get; set; }
        public string ContractType { get; set; }
        public string ContractId { get; set; }
    }
    public class ContractVersion
    {
        public string ContractId { get; set; }
        public DateTime ChangedDate { get; set; }
        public string ContractVersionId { get; set; }
    }
}

这是第二个查询,它可能运行得更快:

            var results =  db.Person.Select(p => new {
                Person = p,
                Address = db.Address.Where(a => p.PersonId == a.PersonId),
                Contract = db.Contract.Where(c => p.PersonId == c.PersonId),
                ContractVersion = db.ContractVersion.Where(cv => p.PersonId == cv.ContractId)
            }).GroupBy(x => new { name = x.Person.Name, contractType = x.Contract.FirstOrDefault().ContractType}) 
                           .Select(x => new
                           {
                               name = x.Key.name,
                               numberOfAddresses = x.Select(y => y.Address.SelectMany(z => z.Street)).Count(),
                               contractType = x.Key.contractType,
                               number = x.GroupBy(y => y.ContractVersion.SelectMany(z => z.ContractVersionId)).Count(),
                               lastChangeDate = x.SelectMany(y => y.ContractVersion.Select(z => z.ChangedDate)).OrderByDescending(y => y).FirstOrDefault()
                           })
                           .ToList();

【讨论】:

  • 谢谢!我在 var 结果语句中得到“值不能为空。(参数'外部')”。那是因为一个人的合同类型为空吗?
  • 可能是日期或合同类型。你可以注释掉找到哪一个。更有可能是日期,因为日期不能为空。
  • 这产生了一个有点接近的结果,但由此产生的 TSQL 一点也不好。它比我试图模仿的 SQL 慢大约 20 倍。 (您可以在我的更新 2 中看到查询)。有没有办法创建中间组并将它们加入到新表中?
  • 使用 SQL Server Management Studio 查询运行速度是否也很慢?需要查明是c#/sql接口导致的慢还是实际执行查询的时间。看起来连接可能需要使用 WHERE。
  • 是的,在现实世界的例子中,它们变得非常慢,因为结果就像“对于每一行,对另一个 id 匹配的表进行另一个选择”意味着第一个表中包含 100 行的查询将导致对数据库的 101 个“查询”
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-10
  • 2015-01-05
  • 2015-02-05
  • 1970-01-01
相关资源
最近更新 更多