SQL Server:返回最小行数

时间:2018-01-25 21:09:28

标签: sql sql-server tsql

我正在寻找快速解决方案,以便在我们构建新版本时解决遗留系统的问题。

概述

核对清单页面检查数据库是否为每个用户输入了一定数量的条目。然后,页面根据登录用户的类型,检查每个参考表的count个条目,其中包含所需条目数(“RequiredRows”)。然后页面将迭代RequiredRows的用户条目。

问题

显然,当返回的条目数小于RequiredRows时,这会失败。

明显

我知道大多数人,就像我一样,会说要更新页面,不要以这种方式进行迭代。不幸的是,在这个时刻,如果没有详细说明,这是不可能的,而且更加复杂。

我需要什么

我需要一个可以根据RequiredCount返回相同数量的记录或更多记录的查询。

以下是一个例子:

  • User1以公共用户身份登录
  • User2以私人用户身份登录
  • 公共用户需要提交3个条目
  • 私人用户必须提交2个条目

方案

  1. 如果User1有0个条目,则查询应该不返回任何内容。
  2. 如果User1有1个条目,则查询应返回包含数据的1行和包含NULL的2行。
  3. 如果User1有2个条目,则查询应返回包含数据的2行,以及包含NULL的1行。
  4. 如果User1有3个条目,则查询应返回包含数据的3行。
  5. 如果User2有0个条目,则查询应该不返回任何行。
  6. 如果User2有1个条目,则查询应返回包含数据的1行和包含NULL的1行。
  7. 如果User2有2个条目,则查询应返回2行数据。
  8. 如果User2有3个条目,则查询应返回包含数据的3行。
  9. 数据库结构

    Id    User    Type
    -------------------
    1     User1   1
    2     User2   2
    
    Id    UserType    RequiredRows
    --------------------------------
    1     Public      3
    2     Private     2
    
    Id    UserId    Entry
    ----------------------
    1     1         Test
    

    期望的结果

    查询 User1 的条目数时,我需要以下结果。

    EntryId
    --------
    1
    null
    null
    

    查询 User2 时:

    EntryId
    --------
    null
    null
    

    考虑

    虽然这只是一个小样本数据,但所需条目的数量在0-50之间变化,具体取决于不同的用户类型。

2 个答案:

答案 0 :(得分:1)

这可行。

DECLARE @User TABLE(UserID INT,UserName NVARCHAR(20), UserTypeID INT)
DECLARE @UserType TABLE(UserTypeID INT, UserTypeName NVARCHAR(50), RequiredEntries INT)
DECLARE @UserEntry TABLE(UserEntryID INT, UserID INT, EntryName NVARCHAR(50))
INSERT @User SELECT 1 , 'User1', 1
INSERT @User SELECT 2 , 'User2', 2
INSERT @UserType SELECT 1, 'Public', 3
INSERT @UserType SELECT 2, 'Private', 2
INSERT @UserEntry SELECT 1,1,'Test'


DECLARE  @UserID INT = 1
;
WITH RequiredEntries AS(
   SELECT UserTypeID, RowNumber=1, RequiredEntries FROM @UserType UT
   UNION ALL
   SELECT UserTypeID,RowNumber=RowNumber + 1,RequiredEntries FROM RequiredEntries IR  WHERE IR.RowNumber < IR.RequiredEntries
),
UserEntries AS(
    SELECT UserID,EntryNumber=COUNT(*)
    FROM @UserEntry UE
    GROUP BY UserID,EntryName
),
UserTotals AS(
    SELECT UserID,TotalEntries=COUNT(*)
    FROM @UserEntry UE
    GROUP BY UserID
)
SELECT 
    EntryNumber
FROM   
    @User U 
    INNER JOIN RequiredEntries RE ON RE.UserTypeID=U.UserTypeID
    LEFT OUTER JOIN UserEntries UE ON UE.UserID=U.UserID AND EntryNumber=RE.RowNumber
    INNER JOIN UserTotals UT ON UT.UserID=U.UserID AND UT.TotalEntries > 0 --1 and 5
WHERE
    U.UserID=@UserID
ORDER BY 
    RE.RowNumber

答案 1 :(得分:1)

这是一种较少关系但冗长的方法:

DECLARE @User TABLE (
Id INT, [User] VARCHAR(255), [UserTypeId] INT
)
INSERT INTO @User
VALUES (1,'User1',1),(2,'User2',1),(3,'User3',2),(4,'User4',1),(5,'User5',2),(6,'User6',3)

DECLARE @UserType TABLE (
Id INT, UserType VARCHAR(20), RequiredRows INT
)
INSERT INTO @UserType
VALUES (1,'Public',3),(2,'Private',2),(3,'Untrusted',50)

DECLARE @Entries TABLE (
    Id INT IDENTITY(1,1), UserId INT,  [Entry] VARCHAR(255)
)
INSERT INTO @Entries
VALUES (1,'Test'),(2,'Test'),(2,'Test 2'),(3,'Test')
, (5,'MoreTests1'),(5,'Test2'),(5,'Test 3'),(5,'Test 4')
, (6,'SomeTest1'),(6,'SomeTest2'),(6,'SomeTest3'),(6,'SomeTest4'),(6,'SomeTest5')
, (6,'SomeTest6'),(6,'SomeTest7'),(6,'SomeTest8'),(6,'SomeTest9'),(6,'SomeTest10')

DECLARE @UserId INT = 1

DECLARE @RequiredRows INT = (
    SELECT RequiredRows
    FROM @User u
    INNER JOIN @UserType ut
    ON u.UserTypeId = ut.Id
    WHERE u.Id = @UserId)

DECLARE @ExistingRows INT = (SELECT COUNT(*) FROM @Entries WHERE UserId = @UserId)
DECLARE @MissingRows INT = (SELECT CASE WHEN @RequiredRows < @ExistingRows OR @ExistingRows = 0 THEN 0 ELSE @RequiredRows - @ExistingRows END)

;WITH fill AS (
    SELECT EmptyId FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) gen(EmptyId)
)
    SELECT Id as EntryId
    FROM @Entries WHERE UserId = @UserId
    UNION ALL
    SELECT TOP(@MissingRows) a.EmptyId
    FROM fill a, fill b, fill c

基本上它确定了所需的行,如果没有条目和现有行,则确定0。其余的是选择条目并填写,如有必要,使用空元组。

您可以针对方案 2 @UserId更改为1,针对方案 3 更新@UserId = 2,针对方案@UserId = 3方案 1 > 6 和@UserId = 4,但您可以轻松添加数据以涵盖其他测试用例。

编辑:添加了一些测试数据并重写了查询以最大限度地减少数据读取量。

<强> EDIT2: 这是一个紧凑的recursive cte,它使用ROW_NUMBER来枚举条目,并确保它具有所需的行数。

DECLARE @User TABLE (
Id INT, [User] VARCHAR(255), [UserTypeId] INT
)
INSERT INTO @User
VALUES (1,'User1',1),(2,'User2',1),(3,'User3',2),(4,'User4',1),(5,'User5',2),(6,'User6',3)

DECLARE @UserType TABLE (
Id INT, UserType VARCHAR(20), RequiredRows INT
)
INSERT INTO @UserType
VALUES (1,'Public',3),(2,'Private',2),(3,'Untrusted',50)

DECLARE @Entries TABLE (
    Id INT IDENTITY(1,1), UserId INT,  [Entry] VARCHAR(255)
)
INSERT INTO @Entries
VALUES (1,'Test'),(2,'Test'),(2,'Test 2'),(3,'Test')
, (5,'MoreTests1'),(5,'Test2'),(5,'Test 3'),(5,'Test 4')
, (6,'SomeTest1'),(6,'SomeTest2'),(6,'SomeTest3'),(6,'SomeTest4'),(6,'SomeTest5')
, (6,'SomeTest6'),(6,'SomeTest7'),(6,'SomeTest8'),(6,'SomeTest9'),(6,'SomeTest10')

DECLARE @UserId INT = 1

;WITH rcte AS (
    SELECT ROW_NUMBER() OVER (ORDER BY e.Id) AS RN
        , e.Id
        , ut.RequiredRows
    FROM @Entries e 
    INNER JOIN @User AS u ON e.UserId = u.Id
    INNER JOIN @UserType AS ut ON u.UserTypeId = ut.Id
    WHERE UserId = @UserId
    UNION ALL
    SELECT RN + 1, NULL, RequiredRows
    FROM rcte r
    WHERE r.RN + 1 <= RequiredRows
)
SELECT RN, MAX(Id) AS EntryId
FROM rcte 
GROUP BY RN