4

これは SQL の専門家への質問です。SQL Server 2008 R2 を使用しています

関連する 2 つのテーブルがあります:LabsLabUsers.

ユーザーは、任意の順序でグループ全体を繰り返すことなく、ラボに割り当てられます。

目標は、次のすべての制限を満たす@userNameために (例では@user = "Paul")を挿入することです。LabUsers

  1. グループ@maxUsers内のみ (例@maxUsers=4)

  2. 完全なグループ (完全なラボ)の重複はありません。グループ内のユーザーの順序は重要ではありません。[編集]

  3. 既存のラボが許可されていない場合は、新しいラボを作成 ( ) してから、超過しないようにINSERTの行を挿入します (例)。@user@maxLabs@maxLabs=5

  4. 非常に重要: 1 秒間にサーバーから多数の同じ要求が同時に発生し、相互に干渉する可能性があります。したがって、コマンドの実行が開始されるとすぐに、このコマンドが終了するまで他のクエリを実行できなくなります。

  5. 上記の制限を満たさない場合、クエリは 0 を返しLabID、挿入された行の を返す必要があります。

  6. [編集済み]いくつかのラボのゾーンがあります。ゾーンは独立しています。各ゾーン #labCount は によって境界付けられます@maxLabs。は@maxLabsすべてのゾーンで等しいため、Total_maxLabs= @maxLabsx#zonesCountです。例については@zone=51(後述@zone=52, 53 etc.)。(同じ LabUsers は制限なしでゾーンを使用できます。ゾーンはお互いを「認識」しません)

  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
  • LabID=1これらのラボにはすでに 4 人のユーザーがいるため、挿入を 2 つにすることはできません。
  • LabID=3との複製が作成されているため、 への挿入は行われませんLabID=1

したがって、 は 3 ではないため(既存のラボ)、値を持つ に@maxLabs新しい行を挿入する必要があります。LabsLabZone=@zone=51

は、新しい行の を 4 に設定IDENTITYします。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 = "Paul"、@labZone = 51 を挿入
    • 新しく作成された Lab 4 に Paul が追加されます
  2. @userName = "Paul"、@labZone = 51 を挿入
    • 新しく作成された Lab 5 に Paul が追加されます
  3. @userName = "Paul"、@labZone = 51 を挿入
    • ポールが行く新しいラボも既存のラボもないため、0 を返します。
  4. @userName = "Rose"、@labZone = 51 を挿入
    • Rose が既存の Lab 3 に追加されます
  5. @userName = "Rose"、@labZone = 51 を挿入
    • Rose が既存の Lab 4 に追加されます
  6. @userName = "Rose"、@labZone = 51 を挿入
    • Rose は既存の Lab 5 に追加されます

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
于 2012-05-23T15:50:52.730 に答える
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 に答える
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 に答える