【问题标题】:Pagination in a SQL Server stored procedure with duplicated data具有重复数据的 SQL Server 存储过程中的分页
【发布时间】:2018-08-18 00:18:58
【问题描述】:

我在 SQL Server 中有一个存储过程,它根据多个表中的多个过滤器(例如 DateOfBirthDisplayName、...)获取联系人。我需要更改存储过程以包括分页和总计数,因为分页是在后端完成的。 PartyId 是唯一键。需要注意的是,一个人可以有多个电子邮件和电话,假设我们搜索DisplayName = "Sarah",查询将返回以下内容:

TotalCount  PartyId     DisplayName EmailAddress      PhoneNumber   
-----------------------------------------------------------------
3           1           Sarah       sarah@gmail.com   1
3           1           Sarah       sarah2@gmail.com  1
3           1           Sarah       sarah@gmail.com   2

这大致是存储过程的作用,CurrentPagePageSize 的分配值以及底部的 ORDER BY OFFSET 我包括用于测试分页:

DECLARE @CurrentPage int = 1
DECLARE @PageSize int = 1000

SELECT 
    COUNT(*) OVER () as TotalCount,
    p.Id AS PartyId,
    e.EmailAddress,
    pn.PhoneNumber
    etc.....                            
FROM 
    [dbo].[Party] AS p WITH(NOLOCK) 
INNER JOIN 
    [dbo].[Email] AS e WITH(NOLOCK) ON p.[Id] = e.[PartyID]
INNER JOIN 
    [dbo].[PhoneNumber] AS pn WITH(NOLOCK) ON p.[Id] = pn.[PartyID]    
    etc.....
WHERE 
    p.PartyType = 1 /*Individual*/ 
GROUP BY 
    p.Id, e.EmailAddress, pn.PhoneNumber etc...  
ORDER BY 
    p.Id 
    OFFSET (@CurrentPage - 1) * @PageSize ROWS 
    FETCH NEXT @PageSize ROWS ONLY

这就是我们在后台按PartyId分组并分配相应的电子邮件和电话。

var responseModel = unitOfWork.PartyRepository.SearchContacts(model);

if (responseModel != null && responseModel.Count == 0)
{
    return null;
}

// get multiple phones/emails for a party
var emailAddresses = responseModel.GroupBy(p => new { p.PartyId, p.EmailAddress })
                            .Select(x => new {
                                    x.Key.PartyId,
                                    x.Key.EmailAddress
                            });

var phoneNumbers = responseModel.GroupBy(p => new { p.PartyId, p.PhoneNumber, p.PhoneNumberCreateDate })
                            .Select(x => new {
                                    x.Key.PartyId,
                                    x.Key.PhoneNumber,
                                    x.Key.PhoneNumberCreateDate
                            }).OrderByDescending(p => p.PhoneNumberCreateDate);

// group by in order to avoid multiple records with different email/phones
responseModel = responseModel.GroupBy(x => x.PartyId)
                   .Select(grp => grp.First())
                   .ToList();

var list = Mapper.Map<List<SearchContactResponseModelData>>(responseModel);

// add all phones/emails to respective party
list = list.Select(x =>
                    {
                        x.EmailAddresses = new List<string>();
                        x.EmailAddresses.AddRange(emailAddresses.Where(y => y.PartyId == x.PartyId).Select(y => y.EmailAddress));

                        x.PhoneNumbers = new List<string>();
                        x.PhoneNumbers.AddRange(phoneNumbers.Where(y => y.PartyId == x.PartyId).Select(y => y.PhoneNumber));
                        return x;
                    }).ToList();

var sorted = SortAndPagination(model, model.SortBy, list);

SearchContactResponseModel result = new SearchContactResponseModel()
            {
                Data = sorted,
                TotalCount = list.Count
            };

return result;

响应将是:

{
  "TotalCount": 1,
  "Data": [
    {
      "PartyId": 1,
      "DisplayName": "SARAH",
      "EmailAddresses": [
        "sarah@gmail.com",
        "sarah2@gmail.com"
      ],
      "PhoneNumbers": [
        "1",
        "2"
      ]
    }
  ]
}

从存储过程返回的 TotalCount 显然不是真实的,在后端代码(我们分配电子邮件/电话和按 id 分组的地方)之后,我们得到真实的 totalCount,它是 1 而不是 3。

如果我们有 3 个名为 Sarah 的人,由于有多个电话/电子邮件,存储过程中的 totalCount 将是 9,实际计数将为 3,如果我执行存储过程以将人员从 1 变为2、因为有9条记录,所以分页不起作用。

如何在上述场景中实现分页?

【问题讨论】:

  • count() over with group by 对我来说没有意义
  • 你能解释一下原因吗?

标签: c# sql-server tsql stored-procedures pagination


【解决方案1】:

您可以尝试使用 CTE 将查询与 Party 表隔离开来。这将允许您提取正确的行数(以及正确的总行数),而不必担心电子邮件和电话号码的扩展。

看起来像这样(重新排列上面的查询):

DECLARE @CurrentPage int = 1;
DECLARE @PageSize int = 1000;

WITH PartyList AS (
    SELECT 
        COUNT(*) OVER () as TotalCount,
        p.Id AS PartyId         
    FROM 
        [dbo].[Party] AS p WITH(NOLOCK) 
    WHERE 
        p.PartyType = 1 /*Individual*/ 
    GROUP BY -- You might not need this now depending on your data
        p.Id
    ORDER BY 
        p.Id 
        OFFSET (@CurrentPage - 1) * @PageSize ROWS 
        FETCH NEXT @PageSize ROWS ONLY
)
SELECT
    pl.TotalCount,
    pl.PartyId,
    e.EmailAddress,
    pn.PhoneNumber   
FROM PartyList AS pl
    INNER JOIN 
        [dbo].[Email] AS e WITH(NOLOCK) ON pl.[PartyId] = e.[PartyID]
    INNER JOIN 
        [dbo].[PhoneNumber] AS pn WITH(NOLOCK) ON pl.[PartyId] = pn.[PartyID];

请注意,CTE 要求前面的语句以分号结尾。

【讨论】:

  • @OldGodOfAsgard 我的猜测是它们会进行类似的优化,但仅通过查看查询很难判断。如果您担心性能特征,我会比较两者之间的执行计划。
  • 谢谢,我试过了,速度没有明显变化。还有一个问题,我如何订购让我们说 CTE 中的电子邮件,因为如果分页有效,让我说用 EmailAddress desc 给我 10 条记录,我需要通过电子邮件在 CTE 中订购,这样我才能得到正确的 PartyIds,但是它说“列“Email.EmailAddress”在 ORDER BY 子句中无效,因为它不包含在聚合函数或 GROUP BY 子句中。”反过来我需要将它添加到 group by 子句中,但如果我添加它,它不会让我得到正确的partyIds。提前致谢!
  • @OldGodOfAsgard 抱歉,我不确定这里是否有足够的信息来帮助解决这个问题。由于每一方都可以有多个电子邮件地址,我无法想象排序将如何应用和显示给用户。例如,如果一方有两封电子邮件,一封以“a”开头,另一封以“z”开头,那将如何排序?我的想法是,这从根本上改变了显示和查询。也许那将是一个完全不同的观点(通过电子邮件地址)。
  • 非常感谢您的帮助!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-14
  • 2023-03-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多