如何使用单个命令有选择地[双]插入(T-SQL)?

时间:2012-05-22 03:56:42

标签: sql sql-server tsql

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

我有两个相关的表:LabsLabUsers

用户被分配到实验室,不会重复任何订单的整个组。

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

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

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

  3. 如果不允许现有实验室,请创建(INSERT)新实验室,然后插入@user行,不超过@maxLabs(例如{{} 1}})。

  4. 非常重要:服务器中有很多并发相同的请求,分裂为一秒,这可能会相互干扰。因此,一旦命令开始执行,在此命令结束之前不允许执行任何其他查询。

  5. 如果查询不能满足上述限制,则返回0,并返回插入行的@maxLabs=5

  6. [已编辑] 有多个实验室区域。区域是独立的。每个区域#labCount都以LabID为界。 @maxLabs对所有区域都相同,因此@maxLabs = Total_maxLabs x @maxLabs。对于示例#zonesCount(稍后在@zone=51)。 (相同的LabUsers可以使用没有限制的区域。区域彼此不“了解”)

  7. @zone=52, 53 etc.中的
  8. LabID是来自LabUsers的外键。

  9. 示例:

    这是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 或2,因为这些实验室中已经有4个用户。
    • 由于与LabID=1创建重复,因此不应将插入内容发送到LabID=3

    因此,由于LabID=1不是3(现有实验室),因此需要在@maxLabs中插入一个值为Labs的新行。

    LabZone=@zone=51会将新IDENTITY设置为4。

    现在是时候将LabID插入Paul,只需插入新实验室即可返回LabUsers

    如何解决这个问题?

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

    创建数据库的脚本是:

    LabID

3 个答案:

答案 0 :(得分:1)

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

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

  1. 获取要插入的实验室
    • 检查以确保用户不在实验室
    • 检查以确保实验室不在这里
    • 此处也包括新的实验室可能性
  2. 如果在插入用户后实验室已满,则按字母顺序创建每个实验室的所有实验室用户的列表
    • 标记了新的可能实验室
  3. 比较已标记的实验室列表与未标记的实验室列表,并选择与现有完整实验室列表不重复的最小labId
  4. 返回LabId插入或0作为输出
  5. 根据原始问题的起始数据并按以下顺序执行:

    1. 插入@userName =“Paul”,@ altZone = 51
      • Paul被添加到新创建的Lab 4
    2. 插入@userName =“Paul”,@ altZone = 51
      • Paul被添加到新创建的Lab 5
    3. 插入@userName =“Paul”,@ altZone = 51
      • 没有更多的新实验室,也没有现成的保罗实验室,所以返回0
    4. 插入@userName =“Rose”,@ tagZone = 51
      • Rose已添加到现有的Lab 3
    5. 插入@userName =“Rose”,@ tagZone = 51
      • Rose已添加到现有的Lab 4
    6. 插入@userName =“Rose”,@ tagZone = 51
      • Rose已添加到现有的Lab 5
    7. LabUsers上的事务中的tablockx应该可以防止并发事务造成破坏。

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

      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
      

答案 1 :(得分: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

答案 2 :(得分: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。使用参数和/或预先插入的数据来查看其他结果。