【问题标题】:Linq to SQL - Joins and Skip+TakeLinq to SQL - 连接和跳过+执行
【发布时间】:2012-02-28 02:13:38
【问题描述】:

我有可以处理多对多关系的 Linq-to-SQL 代码,但请注意,关系本身有自己的一组属性(在这种情况下,产品属于许多类别,并且每个产品在-category 关系有自己的 SortOrder 属性)。

我有一个 Linq-to-SQL 块,它返回具有类别成员资格信息的匹配产品。当我执行代码时,它会生成优化的 T-SQL 代码,如下所示:

exec sp_executesql N'SELECT [t0].[ProductId], [t0].[Name], [t1].[ProductId] AS [ProductId2], [t1].[CategoryId], [t1].[SortOrder] AS [SortOrder2], [t2].[CategoryId] AS [CategoryId2], [t2].[Name] AS [Name2] (
SELECT COUNT(*)
FROM [dbo].[ProductsInCategories] AS [t3]
INNER JOIN [dbo].[Categories] AS [t4] ON [t4].[CategoryId] = [t3].[CategoryId]
WHERE [t3].[ProductId] = [t0].[ProductId]
) AS [value]
FROM [dbo].[Products] AS [t0]
LEFT OUTER JOIN ([dbo].[ProductsInCategories] AS [t1]
INNER JOIN [dbo].[Categories] AS [t2] ON [t2].[CategoryId] = [t1].[CategoryId]) ON [t1].[ProductId] = [t0].[ProductId]
WHERE (([t0].[OwnerId]) = @p0) AND ([t0].[Visible] = 1)
ORDER BY [t0].[SortOrder], [t0].[Name], [t0].[ProductId], [t1].[CategoryId]',N'@p0 bigint',@p0=3

但是,当我向 Linq 表达式添加分页指令(即“.Skip(0).Take(50)”)时,生成的 SQL 变为:

exec sp_executesql N'SELECT TOP (50) [t0].[ProductId], [t0].[Name]
FROM [dbo].[Products] AS [t0]
WHERE (([t0].[OwnerId]) = @p0) AND ([t0].[Visible] = 1)
ORDER BY [t0].[SortOrder], [t0].[Name]',N'@p0 bigint',@p0=3

这意味着不再加载类别成员信息,因此 Linq-to-SQL 然后将手动加载代码执行 50 次(返回集中的每个成员一个):

exec sp_executesql N'SELECT [t0].[ProductId], [t0].[CategoryId], [t0].[SortOrder], [t1].[CategoryId] AS [CategoryId2], [t1].[Name]
FROM [dbo].[ProductsInCategories] AS [t0]
INNER JOIN [dbo].[Categories] AS [t1] ON [t1].[CategoryId] = [t0].[CategoryId]
WHERE [t0].[ProductId] = @x1',N'@x1 bigint',@x1=1141

(显然,“@x1”ID 参数因原始查询的每个结果而异)。

很明显,Linq 分页会中断查询并导致它单独加载数据。有没有办法解决这个问题,或者我应该在自己的软件中进行分页?

...幸运的是,数据库中的产品数量足够少(可能有数万种产品,而这只是这不是一个好的查询。

编辑:

这是我的 Linq:

DataLoadOptions dlo = new DataLoadOptions();
dlo.LoadWith<Product>( p => p.ProductsInCategories );
dlo.LoadWith<ProductsInCategory>( pic => pic.Category );
this.LoadOptions = dlo;

query = from p in this.Products
select p;

// The lines below are added conditionally:
query = query.OrderBy( p => p.SortOrder ).ThenBy( p => p.Name );
query = query.Where( p => p.Visible );
query = query.Where( p => p.Name.Contains( filter ) || p.Description.Contains( filter ) );
query = query.Where( p => p.OwnerId == siteId );

skip/take 行是可选添加的,并且是导致不同 T-SQL 生成的唯一差异(据我所知):

IQueryable<Product> query = GetProducts( siteId, category, filter, showHidden, sortBySortOrder );

///////////////////////////////////

total = query.Count();

var pagedProducts = query.Skip( pageIndex * pageSize ).Take( pageSize );
return pagedProducts;

【问题讨论】:

    标签: c# sql linq linq-to-sql


    【解决方案1】:

    另一种答案是先对产品进行分页,然后在父子结构中选择产品和类别,如下所示:

    var filter = "a";
    var pageSize = 2;
    var pageIndex = 1;
    
    // get the correct products
    var query = Products.AsQueryable();
    
    query = query.Where (q => q.Name.Contains(filter));
    query = query.OrderBy (q => q.SortOrder).ThenBy(q => q.Name);
    
    // do paging
    query = query.Skip(pageSize*pageIndex).Take(pageSize);
    
    // now get products + categories as tree structure
    var query2 = query.Select(
        q=>new 
        {
            q.Name, 
            Categories=q.ProductsInCategories.Select (pic => pic.Category)
        });
    

    生成单个 SQL 语句

    -- Region Parameters
    DECLARE @p0 NVarChar(1000) = '%a%'
    DECLARE @p1 Int = 2
    DECLARE @p2 Int = 2
    -- EndRegion
    SELECT [t2].[Name], [t4].[CategoryId], [t4].[Name] AS [Name2], [t4].[Visible], (
        SELECT COUNT(*)
        FROM (
            SELECT [t5].[CategoryId]
            FROM [ProductsInCategories] AS [t5]
            WHERE [t5].[ProductId] = [t2].[ProductId]
            ) AS [t6]
        INNER JOIN [Categories] AS [t7] ON [t7].[CategoryId] = [t6].[CategoryId]
        ) AS [value]
    FROM (
        SELECT [t1].[ProductId], [t1].[Name], [t1].[ROW_NUMBER]
        FROM (
            SELECT ROW_NUMBER() OVER (ORDER BY [t0].[SortOrder], [t0].[Name], [t0].[ProductId]) AS [ROW_NUMBER], [t0].[ProductId], [t0].[Name]
            FROM [Products] AS [t0]
            WHERE [t0].[Name] LIKE @p0
            ) AS [t1]
        WHERE [t1].[ROW_NUMBER] BETWEEN @p1 + 1 AND @p1 + @p2
        ) AS [t2]
    LEFT OUTER JOIN ([ProductsInCategories] AS [t3]
        INNER JOIN [Categories] AS [t4] ON [t4].[CategoryId] = [t3].[CategoryId]) ON [t3].[ProductId] = [t2].[ProductId]
    ORDER BY [t2].[ROW_NUMBER], [t3].[CategoryId], [t3].[ProductId]
    

    【讨论】:

      【解决方案2】:

      这是一种解决方法:您应该根据所有条件构建查询,在那里执行排序,但只选择Product 表上的主键(假设这是ProductId 列)。

      下一步是取总计数(计算行应该被跳过和取), 最后一步是从Product 表中选择ProductIds 在query 中的所有记录(注意:SkipTake 扩展方法应应用于query,而不是应用于新选择本身)。

      这将为您提供与您的(来自第一个示例)类似的带有相关实体的 SELECT 语句。

      编辑: 刚刚创建了一个类似的数据库结构(根据问题中的原始SQL):

      然后使用:

      using (var db = new TestDataContext())
      {
          DataLoadOptions options = new DataLoadOptions();
          options.LoadWith<Product>(p => p.ProductsInCategories);
          options.LoadWith<ProductsInCategory>(pic => pic.Category);
          db.LoadOptions = options;
      
          var filter = "product";
          var pageIndex = 1;
          var pageSize = 10;
      
          var query = db.Products
              .OrderBy(p => p.SortOrder)
              .ThenBy(p => p.Name)
              .Where(p => p.Name.Contains(filter) || p.Description.Contains(filter))
              .Select(p => p.ProductId);
      
          var total = query.Count();
      
          var products = db.Products
              .Where(p => query.Skip(pageIndex * pageSize).Take(pageSize).Contains(p.ProductId))
              .ToList();
      }
      

      .ToList() 调用之后,products 变量持有产品与产品类别与类别。这也产生了 2 个 SQL 语句,一个 - for .Count() 语句:

      exec sp_executesql N'SELECT COUNT(*) AS [value]
      FROM [dbo].[Products] AS [t0]
      WHERE ([t0].[Name] LIKE @p0) OR ([t0].[Description] LIKE @p1)',N'@p0 nvarchar(4000),@p1 nvarchar(4000)',@p0=N'%product%',@p1=N'%product%'
      

      另一个是.ToList():

      exec sp_executesql N'SELECT [t0].[ProductId], [t0].[Name], [t0].[Description], [t0].[SortOrder], [t1].[ProductId] AS [ProductId2], [t1].[CategoryId], [t1].[SortOrder] AS [SortOrder2], [t2].[CategoryId] AS [CategoryId2], [t2].[Name] AS [Name2], (
          SELECT COUNT(*)
          FROM (
              SELECT NULL AS [EMPTY]
              FROM [dbo].[ProductsInCategories] AS [t6]
              INNER JOIN [dbo].[Category] AS [t7] ON [t7].[CategoryId] = [t6].[CategoryId]
              WHERE [t6].[ProductId] = [t0].[ProductId]
              ) AS [t8]
          ) AS [value]
      FROM [dbo].[Products] AS [t0]
      LEFT OUTER JOIN ([dbo].[ProductsInCategories] AS [t1]
          INNER JOIN [dbo].[Category] AS [t2] ON [t2].[CategoryId] = [t1].[CategoryId]) ON [t1].[ProductId] = [t0].[ProductId]
      WHERE EXISTS(
          SELECT NULL AS [EMPTY]
          FROM (
              SELECT [t4].[ProductId]
              FROM (
                  SELECT ROW_NUMBER() OVER (ORDER BY [t3].[SortOrder], [t3].[Name], [t3].[ProductId]) AS [ROW_NUMBER], [t3].[ProductId]
                  FROM [dbo].[Products] AS [t3]
                  WHERE ([t3].[Name] LIKE @p0) OR ([t3].[Description] LIKE @p1)
                  ) AS [t4]
              WHERE [t4].[ROW_NUMBER] BETWEEN @p2 + 1 AND @p2 + @p3
              ) AS [t5]
          WHERE [t5].[ProductId] = [t0].[ProductId]
          )
      ORDER BY [t0].[ProductId], [t1].[CategoryId]',N'@p0 nvarchar(4000),@p1 nvarchar(4000),@p2 int,@p3 int',@p0=N'%product%',@p1=N'%product%',@p2=10,@p3=10
      

      不再有额外的查询(如 SQL Server Profiler 所说)。

      【讨论】:

      • DataLoadOptions 设置为为每个产品加载 ProductCategories 和 Categories。这会产生这 50 个额外的查询。我们如何摆脱它们并仍然获得 ProductCategories 和 Categories?
      • @Adrian,没有。此解决方案对所有产品使用一个查询(老实说,另一个查询Count),并且仍应获得与Product 相关的ProductCategoriesCategories。我用于测试目的的数据库具有类似的结构Article - ArticleTag - Tag。我在那里使用了类似的方法,并在一个查询中获取所有相关实体。
      猜你喜欢
      • 2010-10-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-24
      • 2011-01-13
      • 2011-04-21
      • 2011-07-18
      相关资源
      最近更新 更多