4

这是一个 SQL 专家的问题。我正在使用 SQL Server 2008 R2

我有两个相关的表:LabsLabUsers.

用户被分配到实验室,没有任何顺序的整个组的重复。

目标是插入一个@userName(例如@user = "Paul")来LabUsers满足以下所有限制:

  1. 不超过@maxUsers一个组(例如@maxUsers=4

  2. 没有完整组的重复(完整实验室)。组中用户的顺序并不重要。[编辑]

  3. 如果不允许现有实验室,则创建 ( INSERT) 一个新实验室,然后插入 的行@user,不超过@maxLabs(例如@maxLabs=5)。

  4. 非常重要:一秒钟内有许多来自服务器的并发相同请求,这可能会相互干扰。因此,该命令一开始执行,就不允许执行其他查询,直到该命令结束。

  5. 如果不能满足上述限制,查询应返回 0,并返回LabID插入行的 。

  6. [编辑]有几个实验室的区域。区域是独立的。每个区域 #labCount 由@maxLabs. 所有区域的@maxLabs都相等,因此Total_maxLabs= @maxLabsx #zonesCount。例如@zone=51(稍后@zone=52, 53 etc.)。(相同的 LabUser 可以不受限制地使用区域。区域之间不“了解”彼此)

  7. LabIDinLabUsers是来自 的外键Labs

这个例子:

这是Labs表格:

LabID   LabName     LabZone
-----   -------     -------
1       North       51  
2       North East  51
3       South West  51

而且LabUsers是:

LabUserID   LabUserName LabID
---------   ----------- -----
1           Diana       3
2           Julia       2
3           Paula       2
4           Romeo       1
5           Julia       3
6           Rose        2
7           Diana       1
8           Diana       2
9           Julia       1
10          Romeo       3
11          Paul        1

在示例中,用户的分配方式如下:

LabID   LabName     LabZone LabUsers (ordered LTR a>z)
-----   -------     ------- --------
1       North       51      Diana•Julia•Paul•Romeo
2       North East  51      Diana•Julia•Paula•Rose
3       South West  51      Diana•Julia•Romeo
  • 插入不应是 intoLabID=1或 2,因为这些实验室中已经有 4 个用户。
  • 由于LabID=3使用LabID=1.

因此,因为@maxLabs不是 3(现有实验室),所以需要插入一个新行Labs,其值为LabZone=@zone=51

IDENTITY将为新行设置4 LabID

现在是插入新实验室的Paul时候了LabUsersLabID

如何解决这个问题呢?

使用什么方法来确保命令作为一个整体执行而不受干扰?

创建数据库的脚本是:

CREATE DATABASE [Allocation]
GO

USE [Allocation]
GO

CREATE TABLE [dbo].[LabUsers](
    [LabUserID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED ,
    [LabUserName] [nvarchar](50) NOT NULL,
    [LabID] [int] NOT NULL)
GO

SET IDENTITY_INSERT [dbo].[LabUsers] ON
INSERT [dbo].[LabUsers] ([LabUserID], [LabUserName], [LabID]) VALUES (1, N'Diana', 3)
INSERT [dbo].[LabUsers] ([LabUserID], [LabUserName], [LabID]) VALUES (2, N'Julia', 2)
INSERT [dbo].[LabUsers] ([LabUserID], [LabUserName], [LabID]) VALUES (3, N'Paula', 2)
INSERT [dbo].[LabUsers] ([LabUserID], [LabUserName], [LabID]) VALUES (4, N'Romeo', 1)
INSERT [dbo].[LabUsers] ([LabUserID], [LabUserName], [LabID]) VALUES (5, N'Julia', 3)
INSERT [dbo].[LabUsers] ([LabUserID], [LabUserName], [LabID]) VALUES (6, N'Rose', 2)
INSERT [dbo].[LabUsers] ([LabUserID], [LabUserName], [LabID]) VALUES (7, N'Diana', 1)
INSERT [dbo].[LabUsers] ([LabUserID], [LabUserName], [LabID]) VALUES (8, N'Diana', 2)
INSERT [dbo].[LabUsers] ([LabUserID], [LabUserName], [LabID]) VALUES (9, N'Julia', 1)
INSERT [dbo].[LabUsers] ([LabUserID], [LabUserName], [LabID]) VALUES (10, N'Romeo', 3)
INSERT [dbo].[LabUsers] ([LabUserID], [LabUserName], [LabID]) VALUES (11, N'Paul', 1)
SET IDENTITY_INSERT [dbo].[LabUsers] OFF

CREATE TABLE [dbo].[Labs](
    [LabID] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED ,
    [LabName] [nvarchar](50) NULL,
    [LabZone] [int] NOT NULL)
GO

SET IDENTITY_INSERT [dbo].[Labs] ON
INSERT [dbo].[Labs] ([LabID], [LabName], [LabZone]) VALUES (1, N'North', 51)
INSERT [dbo].[Labs] ([LabID], [LabName], [LabZone]) VALUES (2, N'North East', 51)
INSERT [dbo].[Labs] ([LabID], [LabName], [LabZone]) VALUES (3, N'South West', 51)
SET IDENTITY_INSERT [dbo].[Labs] OFF
4

3 回答 3

1

我背弃了 dradu 的变量并实施了一个类似但不同的解决方案。它确实假设新实验室将比当前最大可用实验室多 1 个。我还假设实验室不会删除用户。

此解决方案的目标是查看用户插入的最终结果是什么样的,并对其运行检查以查看哪个最终结果是有效的。逻辑如下:

  1. 获取可用的实验室以插入
    • 检查以确保用户不在实验室中
    • 检查以确保此处的实验室未满
    • 在这里也包括新的实验室可能性
  2. 如果在插入用户后实验室将满员,则创建按字母顺序排列的每个实验室的所有实验室用户列表
    • 新的可能实验室被标记
  3. 比较标记的实验室列表与未标记的实验室列表,并选择不与现有完整实验室列表重复的最小 labId
  4. 返回插入的 LabId 或 0 作为输出

给定原始问题的起始数据并按以下顺序执行:

  1. 插入@userName =“保罗”,@labZone = 51
    • 保罗被添加到新创建的实验室 4
  2. 插入@userName =“保罗”,@labZone = 51
    • 保罗被添加到新创建的实验室 5
  3. 插入@userName =“保罗”,@labZone = 51
    • 没有新的实验室,也没有可供 Paul 去的现有实验室,所以返回 0
  4. 插入@userName =“玫瑰”,@labZone = 51
    • Rose 被添加到现有的 Lab 3
  5. 插入@userName =“玫瑰”,@labZone = 51
    • Rose 被添加到现有的 Lab 4
  6. 插入@userName =“玫瑰”,@labZone = 51
    • Rose 被添加到现有的 Lab 5

LabUsers 事务中的 tabblockx 应防止并发事务造成破坏。

此外,在调试公用表表达式时,将它们替换为临时表会很有帮助,这样您就可以一路查看每个步骤的结果。

BEGIN TRAN

DECLARE @maxUsers INT
DECLARE @maxLabs INT
DECLARE @userName VARCHAR(50)
DECLARE @labZone INT
DECLARE @labID INT

SET @maxUsers = 4
SET @maxLabs = 5

SET @userName = 'Paul'
SET @labZone = 52
SET @labID = NULL

declare @currentLabCount int

-- get current number of labs
select @currentLabCount = count(*)
from Labs l
/*
-- uncomment this if the max labs applies individual lab zones rather than across all lab zones
where LabZone = @labZone
*/  

;with availableLabs as ( -- get available labs to insert into
    -- check existing labs for valid spots
    select
        lu.LabID
    ,   count(*) + 1 as LabUserCount -- need this to see when we're at max users
    from LabUsers lu with (tablockx) -- ensures blocking until this completes (serialization)
      inner join Labs l with (tablockx) -- might as well lock this too
        on l.LabId = lu.LabID
        and l.LabZone = @labZone -- check Lab Zone
    where not exists( -- make sure lab user isn't already in this lab
        select 1
        from LabUsers lu2
        where lu2.LabId = lu.LabId
        and lu2.LabUserName = @userName
    )
    group by lu.LabID
    having count(*) < @maxUsers -- make sure lab isn't full
    union all
    -- create new lab if not at limit
    select
        max(LabId) + 1 as LabId
    ,   1 as LabUserCount
    from Labs -- check all labs
    where @currentLabCount < @maxLabs -- don't bother checking new labs if going to exceed max allowable labs
)
-- only do this check if lab is going to be filled
, dupeCheck as( -- generates a lab user list sorted alphabetically by lab user name per lab
    select
        y.LabId
    ,   max(y.newLabFlag) as newLabFlag -- if existing lab getting new lab user, then 1, if new lab with new lab user, then 1 else 0
    ,   replace(replace(replace(stuff( -- cool way to comma concatenate without looping/recursion taking advantage of "XML path"
            (
                select
                    ',' + x.LabUserName + '' -- lab users
                from (
                    select
                        LabId
                    ,   @userName as LabUserName 
                    from availableLabs -- the new user and his/her potential labs
                    union all
                    select
                        lu.LabId
                    ,   lu.LabUserName
                    from LabUsers lu -- the current lab users and the labs they belong to
                ) x
                where x.LabID = y.LabId -- make sure the LabId's match
                and max(y.LabUserCount) = @maxUsers -- don't generate this list if lab is not full
                order by x.LabUserName -- sorted alphabetically
                for xml path('')
            ), 1, 1, ''
        )
        , '&lt;', '<'), '&gt;', '>'), '&amp;', '&') as LabUserList
    from (
        -- get list of old labs and flag them as such
        select
            lu.LabId
        ,   convert(tinyint,0) as newLabFlag
        ,   count(*) as LabUserCount -- need the current lab user count
        from LabUsers lu
        /*
            -- uncomment this if full labs can be duplicated across lab zones
            inner join Labs l
                on l.LabId = lu.LabId
                and l.LabZone = @labZone
        */
        group by lu.LabId
        union all
        -- get list of potential candidate labs for lab user and flag them as such
        select
            al.LabId
        ,   convert(tinyint,1) as newLabFlag
        ,   al.LabUserCount -- new lab user count if we were to insert the new user
        from availableLabs al
    ) y
    group by y.LabId
)
select
    @labID = min(dc.LabID)
from dupeCheck dc
where dc.newLabFlag = 1
-- make sure the same list of users does not already exist at an existing lab
and not exists(
    select 1
    from dupeCheck dupe
    where dupe.LabUserList = dc.LabUserList
    and dupe.newLabFlag = 0
)

-- insert new lab if doesn't exist
insert into Labs(LabName, LabZone) -- always better to be clearer
select
    'New Lab' as LabName
,   @labZone as LabZone
where @currentLabCount < @maxLabs -- make sure we can't have more than max labs
and not exists(
    select 1
    from Labs
    where LabId = @labId
)

-- insert lab users
insert into LabUsers(LabUserName, LabId)
select
    @userName as LabUserName
,   @labId as LabId
where @labId is not null

-- return labId
select isnull(@labId,0)
commit tran
于 2012-05-23T15:50:52.730 回答
0

由于MERGE不能使用,所以需要几个语句。我想不出一个更简单的解决方案,抱歉。我相信专家可以找到更好的解决方案。

首先,我根据指定的规则寻找潜在的实验室。为避免重复组,我在可能插入之前和之后比较了每个实验室的用户。如果有任何实验室可用,请插入用户。如果没有,请插入实验室,然后插入用户。为了在事务完成之前锁定表,我使用isolation level serializable. 这是代码:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE -- Range locks until transaction completes

BEGIN TRAN

DECLARE @maxUsers INT
DECLARE @maxLabs INT
DECLARE @userName VARCHAR(50)
DECLARE @labZone INT
DECLARE @labID INT

SET @maxUsers = 4
SET @maxLabs = 5

SET @userName = 'Paul'
SET @labZone = 51
SET @labID = NULL

--Check potential spots
;WITH U1(LabID, UserName) AS(
    SELECT LabID, LabUserName FROM dbo.LabUsers WHERE LabID IN (
        SELECT LabID 
        FROM dbo.LabUsers 
        WHERE LabUserName <> @userName 
        GROUP BY LabID 
        HAVING COUNT(LabUserName) < @maxUsers
    )
)
, U2(LabID, UserName) AS(
    SELECT LabID, LabUserName FROM dbo.LabUsers WHERE LabID IN (
        SELECT LabID 
        FROM dbo.LabUsers 
        GROUP BY LabID 
        HAVING COUNT(LabUserName) = @maxUsers
    )
)
--Get the first potential LabID
SELECT @labID = (
SELECT TOP 1 potential.LabID FROM (
SELECT DISTINCT LabID, SUBSTRING((SELECT ',' + UserName FROM (
    SELECT LabID, UserName FROM U1
    UNION SELECT LabID, @userName FROM U1
) t WHERE LabID = lu.LabID ORDER BY UserName FOR XML PATH('')
), 2, 50) AS AfterUsers, SUBSTRING((SELECT ',|' + UserName + '|' FROM (
    SELECT LabID, UserName FROM U1
) t WHERE LabID = lu.LabID ORDER BY UserName FOR XML PATH('')
), 2, 50) AS BeforeUsers
FROM U1 lu) potential
LEFT OUTER JOIN (
SELECT DISTINCT LabID, SUBSTRING((SELECT ',' + UserName FROM (
    SELECT LabID, UserName FROM U2
    UNION SELECT LabID, @userName FROM U2
) t WHERE LabID = lu.LabID ORDER BY UserName FOR XML PATH('')
), 2, 50) AS Users
FROM U2 lu) allocated
     ON potential.AfterUsers = allocated.Users
WHERE allocated.Users IS NULL 
    AND potential.BeforeUsers NOT LIKE '%|' + @userName + '|%'
ORDER BY 1
)

IF @labID IS NULL --No existing lab available
BEGIN

    --Insert Lab
    INSERT INTO dbo.Labs(LabName, LabZone) 
        SELECT 'New Lab', @labZone 
        WHERE (SELECT COUNT(*) FROM dbo.Labs) < @maxLabs
    IF @@ROWCOUNT = 1
    BEGIN
        SET @labID = SCOPE_IDENTITY() --Get the new LabID
        --Insert Lab user
        INSERT INTO dbo.LabUsers(LabUserName, LabID) 
            SELECT @userName, @labID
    END

END
ELSE --Lab exists, insert user if possible
BEGIN

    INSERT INTO dbo.LabUsers(LabUserName, LabID) 
        SELECT @userName, @labID
        WHERE NOT EXISTS(SELECT * FROM dbo.LabUsers WHERE LabID = @labID AND LabUserName = @userName)

END

--A quick select to check the results
SELECT * FROM dbo.Labs
SELECT DISTINCT LabID, SUBSTRING((SELECT ',' + LabUserName FROM (
    SELECT LabID, LabUserName FROM dbo.LabUsers
) t WHERE LabID = lu.LabID ORDER BY LabUserName FOR XML PATH('')
), 2, 50) AS Users
FROM dbo.LabUsers lu

COMMIT TRAN

SET TRANSACTION ISOLATION LEVEL READ COMMITTED --Restore isolation level to default
于 2012-05-22T11:03:38.947 回答
0

这是尝试使用MERGE. 作为工作的一部分,该解决方案构建有序的 CSV 列表并进行比较,因此可能效率不高。尽管如此,在我的测试中,它似乎已经满足了所有其他要求。

首先,模式,用原始帖子中的示例完成:

CREATE TABLE Labs
    (LabID int IDENTITY, LabName varchar(50), LabZone int);

SET IDENTITY_INSERT Labs ON;
INSERT INTO Labs
    (LabID, LabName, LabZone)
VALUES
    (1, 'North'     , 51),
    (2, 'North East', 51),
    (3, 'South West', 51);
SET IDENTITY_INSERT Labs OFF;

CREATE TABLE LabUsers
    (LabUserID int IDENTITY, LabUserName varchar(50), LabID int);

SET IDENTITY_INSERT LabUsers ON;
INSERT INTO LabUsers
    (LabUserID, LabUserName, LabID)
VALUES
    ( 1, 'Diana', 3),
    ( 2, 'Julia', 2),
    ( 3, 'Paula', 2),
    ( 4, 'Romeo', 1),
    ( 5, 'Julia', 3),
    ( 6, 'Rose' , 2),
    ( 7, 'Diana', 1),
    ( 8, 'Diana', 2),
    ( 9, 'Julia', 1),
    (10, 'Romeo', 3),
    (11, 'Paul' , 1);
SET IDENTITY_INSERT LabUsers OFF;

脚本,注释,参数预先初始化了一些值:

/* script parameters */
DECLARE @zone     int         = 51;
DECLARE @maxLabs  int         = 3;
DECLARE @maxUsers int         = 4;
DECLARE @userName varchar(50) = 'Paul';

/* auxiliary variables */
DECLARE @defLabName varchar(50) = 'New Lab';
DECLARE @SelectedLab table (LabID int);

/* the main part begins */
WITH ZoneLabs AS (
  /* get labs for the specified @zone */
  SELECT LabID
  FROM Labs
  WHERE LabZone = @zone
)
, IncompleteLabs AS (
  /* get labs with the number of users < @maxUsers */
  SELECT LabID
  FROM LabUsers
  WHERE LabID IN (SELECT LabID FROM ZoneLabs)
  GROUP BY LabID
  HAVING COUNT(*) < @maxUsers
  UNION ALL
  /* …and add a new lab if the number of labs < @maxLabs */
  SELECT 0
  FROM ZoneLabs
  HAVING COUNT(*) < @maxLabs
)
, LabUsersAdjusted AS (
  /* get all existing users */
  SELECT LabUserID, LabUserName, LabID, 0 AS IsNew
  FROM LabUsers
  WHERE LabID IN (SELECT LabID FROM ZoneLabs)
  UNION ALL
  /* …and add the new user as a member of every incomplete lab
     unless the user is already a member */
  SELECT 0        , @userName  , LabID, 1
  FROM IncompleteLabs
  WHERE LabID NOT IN (SELECT LabID FROM LabUsers WHERE LabUserName = @userName)
)
, UsersGrouped AS (
  /* get labs along with their CSV-lists of users */
  SELECT
    LabID,
    OldUserCount = COUNT(NULLIF(IsNew, 1)),
    NewUserCount = SUM(IsNew),
    LabUsers = SUBSTRING(
      (
        SELECT ',' + LabUserName
        FROM LabUsersAdjusted
        WHERE LabID = lu.LabID
        ORDER BY LabUserName
        FOR XML PATH('')
      ),
      2,
      2147483647
    )
  FROM LabUsersAdjusted lu
  GROUP BY LabID
)
, SelectedLab AS (
  /* (the crucial part) get one of the (currently) incomplete labs
     where the new user is being added:
     - exclude every lab whose set of users is going to match that
       of any existing full lab;
     - prioritise remaining labs by:
       1) the number of users: more users = higher priority;
       2) the order of addition: older labs (those with lower IDs)
          = higher priority;
  */
  SELECT TOP 1 LabID
  FROM UsersGrouped new
  WHERE NewUserCount = 1
    AND NOT EXISTS (
      SELECT *
      FROM UsersGrouped old
      WHERE new.LabUsers = old.LabUsers
        AND old.OldUserCount = @maxUsers
    )
  ORDER BY
    OldUserCount DESC,
    LabID        ASC
)
/* merge the selected lab into the existing lab set */
MERGE INTO Labs
USING SelectedLab s ON (Labs.LabID = s.LabID)
WHEN MATCHED THEN  /* if there's a match, just do nothing */
  UPDATE SET @zone = @zone
WHEN NOT MATCHED THEN  /* when no match, add a new lab */
  INSERT (LabName, LabZone) VALUES (@defLabName, @zone)
/* in any event, remember the final LabID */
OUTPUT INSERTED.LabID INTO @SelectedLab (LabID)
;
/* add the new user as a member of the stored LabID;
   if no LabID was OUTPUT by MERGE, then @SelectedLab
   contains no rows and, consequently, no user gets inserted */
INSERT INTO LabUsers (LabUserName, LabID)
SELECT @userName, LabID FROM @SelectedLab
;
/* return the remembered LabID or 0 */
SELECT ISNULL((SELECT LabID FROM @SelectedLab), 0) AS Result;

对于上面的示例和指定的参数值,脚本返回0. 使用参数和/或预先插入的数据来查看其他结果。

于 2012-05-25T08:53:20.717 回答