sql-server - 键集分页 - 按跨多列的搜索词过滤

标签 sql-server pagination seek keyset

我正试图远离 OFFSET/FETCH分页到 Keyset分页(也称为搜索方法)。因为我刚刚开始,所以我有很多问题,但这是我尝试与过滤器一起正确分页的众多问题之一。
所以我有2张 table

  • aspnet_users

  • 有列
    PK
    UserId uniquidentifier
    
    字段
    UserName NVARCHAR(256) NOT NULL, 
    AffiliateTag varchar(50) NULL
    .....other fields
    
  • aspnet_membership

  • 有列
    PK+FK
    UserId uniquidentifier
    
    字段
    Email NVARCHAR(256) NOT NULL
    .....other fields
    
    索引
  • Non Clustered表索引 aspnet_users (用户名)
  • Non Clustered表索引 aspnet_users (AffiliateTag)
  • Non Clustered表索引 aspnet_membership (邮箱)

  • 我有一个页面将列出用户(基于 search term),页面大小设置为 20。我想跨多个列进行搜索,而不是执行 OR我发现每个都有一个单独的查询,然后 Union他们将使索引正确使用。
    所以有将采取 search term 的存储过程和可选 UserNameUserId下一页的最后一条记录。
    Create proc [dbo].[sp_searchuser]
    @take int,
    @searchTerm nvarchar(max) NULL,
    @lastUserName nvarchar(256)=NULL,
    @lastUserId nvarchar(256)=NULL
    AS
    
    IF(@lastUserName IS NOT NULL AND @lastUserId IS NOT NULL)
    Begin
        select top (@take) *
        from
        (
            select  u.UserId, u.UserName, u.AffiliateTag, m.Email
            from aspnet_Users as u
            inner join aspnet_Membership as m
            on u.UserId=m.UserId
            where u.UserName like @searchTerm
    
            UNION
    
            select  u.UserId, u.UserName, u.AffiliateTag, m.Email
            from aspnet_Users as u
            inner join aspnet_Membership as m
            on u.UserId=m.UserId
            where u.AffiliateTag like convert(varchar(50), @searchTerm)
        ) as u1
        where u1.UserName > @lastUserName
            OR (u1.UserName=@lastUserName And u1.UserId > convert(uniqueidentifier, @lastUserId))
        order by u1.UserName
    End
    
    Else
    Begin
    
        select top (@take) *
        from
        (
            select  u.UserId, u.UserName, u.AffiliateTag, m.Email
            from aspnet_Users as u
            inner join aspnet_Membership as m
            on u.UserId=m.UserId
            where u.UserName like @searchTerm
    
            UNION
    
            select  u.UserId, u.UserName, u.AffiliateTag, m.Email
            from aspnet_Users as u
            inner join aspnet_Membership as m
            on u.UserId=m.UserId
            where u.AffiliateTag like convert(varchar(50), @searchTerm)
        ) as u1
        
        order by u1.UserName
    End
    
    现在使用搜索词 mua 获取第一页的结果
    exec [sp_searchuser] 20, 'mua%'
    
    它使用为 UserName 列创建的索引和为 AffiliateTag 列创建的另一个索引,这很好
    但问题是我发现内部联合查询返回所有匹配的行
    就像在这种情况下,执行计划显示
    UserName Like SubQuery
    Number of Rows Read= 5
    Actual Number of Rows= 4
    
    AffiliateTag Like SubQuery
    Number of Rows Read= 465
    Actual Number of Rows= 465
    
    所以总共内部查询返回 469匹配行
    然后外部查询取出 20 用于最终结果重置。所以真的要读取比需要更多的数据。
    当进入下一页
    exec [sp_searchuser] 20, 'mua%', 'lastUserName', 'lastUserId'
    
    执行计划显示
    UserName Like SubQuery
    Number of Rows Read= 5
    Actual Number of Rows= 4
    
    AffiliateTag Like SubQuery
    Number of Rows Read= 465
    Actual Number of Rows= 445
    
    总共内部查询返回 449匹配行
    所以无论有没有分页,它都会读取比需要更多的数据。
    我的期望是以某种方式限制内部查询,因此它不会返回所有匹配的行。

    最佳答案

    您可能对 Logical Processing Order 感兴趣,这决定了在一个步骤中定义的对象何时可用于后续步骤中的子句。 Logical Processing Order步骤是:

  • 发件人
  • 开启
  • 加入
  • 哪里
  • GROUP BY
  • WITH CUBE 或 WITH ROLLUP
  • 选择
  • 区别
  • 订购者
  • 首页

  • 当然,正如文档所述:

    The actual physical execution of the statement is determined by the query processor and the order may vary from this list.


    这意味着有时某些语句可以在上一个完成之前开始。
    在您的情况下,您的查询如下所示:
  • 部分数据提取
  • user_name 排序
  • 获得TOP记录

  • 没有办法减少数据提取部分中的行以获得确定性结果(我们实际上可能需要按 user_name, user_id 排序以获得这样的结果)我们需要获取所有匹配的行,对它们进行排序,然后获取所需的行.
    例如,想象第一个查询返回 20 个以“Z”开头的名字。第二个查询只返回一个以“A”开头的名字。如果您以某种方式停止执行并跳过第二个查询,您将得到错误的结果 - 20 个名称以“Z”开头,而不是一个以“A”开头和 19 个以“Z”开头。
    在这种情况下,我更喜欢使用动态 T-SQL 语句以获得更好的执行时间并减少代码长度。你是说:

    And I want to search across multiple columns so instead of doing OR I find out having a separate query for each and then Union them will make the index use correctly.


    当您使用时 UNION您正在对表执行双重读取。在您的情况下,您正在阅读 aspnet_Membership表两次和 aspnet_Users两次(是的,这里您使用了两个不同的索引,但我相信它们不是 covering 并且您最终执行查找以提取用户 nameemail
    我猜你已经开始覆盖索引,如下例所示:
    DROP TABLE IF EXISTS [dbo].[StackOverflow];
    
    CREATE TABLE [dbo].[StackOverflow]
    (
        [UserID] INT PRIMARY KEY
       ,[UserName] NVARCHAR(128)
       ,[AffiliateTag] NVARCHAR(128)
       ,[UserEmail] NVARCHAR(128)
       ,[a] INT
       ,[b] INT
       ,[c] INT
       ,[z] INT
    );
    
    CREATE INDEX IX_StackOverflow_UserID_UserName_AffiliateTag_I_UserEmail ON [dbo].[StackOverflow]
    (
        [UserID]
       ,[UserName]
       ,[AffiliateTag]
    )
    INCLUDE ([UserEmail]);
    
    GO
    
    INSERT INTO [dbo].[StackOverflow] ([UserID], [UserName], [AffiliateTag], [UserEmail])
    SELECT TOP (1000000) ROW_NUMBER() OVER(ORDER BY t1.number)
                        ,CONCAT('UserName',ROW_NUMBER() OVER(ORDER BY t1.number))
                        ,CONCAT('AffiliateTag', ROW_NUMBER() OVER(ORDER BY t1.number))
                        ,CONCAT('UserEmail', ROW_NUMBER() OVER(ORDER BY t1.number))
    FROM master..spt_values t1 
    CROSS JOIN master..spt_values t2;
    
    
    GO
    
    因此,对于以下查询:
    SELECT TOP 20 [UserID]
                 ,[UserName]
                 ,[AffiliateTag]
                 ,[UserEmail]
    FROM [dbo].[StackOverflow]
    WHERE [UserName] LIKE 'UserName200%'
        OR [AffiliateTag] LIKE 'UserName200%'
    ORDER BY [UserName];
    
    
    GO
    
    这里的问题是我们正在读取所有行,即使我们正在使用索引。
    enter image description here
    好处是索引正在覆盖并且我们没有执行查找。根据搜索条件,它的性能可能比您的方法更好。
    如果性能不好,我们可以使用触发器到UNPIVOT原始数据和记录在单独的表中。它可能看起来像这样(最好使用 attribute_id 而不是像我这样的文本):
    DROP TABLE IF EXISTS [dbo].[StackOverflowAttributes];
    
    CREATE TABLE [dbo].[StackOverflowAttributes]
    (
        [UserID] INT
       ,[AttributeName] NVARCHAR(128)
       ,[AttributeValue] NVARCHAR(128)
       ,PRIMARY KEY([UserID], [AttributeName], [AttributeValue])
    );
    
    GO
    
    CREATE INDEX IX_StackOverflowAttributes_AttributeValue ON [dbo].[StackOverflowAttributes]
    (
        [AttributeValue]
    )
    
    INSERT INTO [dbo].[StackOverflowAttributes] ([UserID], [AttributeName], [AttributeValue])
    SELECT [UserID]
          ,'Name'
          ,[UserName]
    FROM [dbo].[StackOverflow]
    UNION 
    SELECT [UserID]
          ,'AffiliateTag'
          ,[AffiliateTag]
    FROM [dbo].[StackOverflow];
    
    之前的查询将如下所示:
    SELECT TOP 20 U.[UserID]
                 ,U.[UserName]
                 ,U.[AffiliateTag]
                 ,U.[UserEmail]
    FROM [dbo].[StackOverflowAttributes] A
    INNER JOIN [dbo].[StackOverflow] U
        ON A.[UserID] = U.[UserID]
    WHERE A.[AttributeValue] LIKE 'UserName200%'
    ORDER BY U.[UserName];
    
    enter image description here
    现在,我们只读取索引行的一部分,然后执行查找。
    为了比较性能,最好使用:
    SET STATISTICS IO, TIME ON; 
    
    因为它将为您提供如何从索引中读取页面。结果可以可视化here .

    关于sql-server - 键集分页 - 按跨多列的搜索词过滤,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63316273/

    相关文章:

    sql - 是否可以为 "Get largest 2"编写具有 O(n) 运行时复杂度的 T-SQL 查询?

    sql - 如何替换SQL表中的NULL值?

    delphi - Delphi-搜索();程序。不兼容类型

    sql - 错误: Login failed for user 'domain\username' using SqlCredential object

    SQL麻烦!需要 IF/CASE 帮助

    javascript - ReactJS 对表格进行分页

    grails - 在 grails 中创建具有自定义样式的自定义分页按钮

    java - 如何在 Lucene 中实现数百万条记录的正确分页

    ios - 平移寻找AVPlayer

    compression - 任何可查找的压缩库?