这是SQL专家的问题。我正在使用SQL Server 2008 R2
我有两个相关的表:Labs
和LabUsers
。
用户被分配到实验室,不会重复任何订单的整个组。
目标是将@userName
(例如@user = "Paul"
)插入LabUsers
,以满足以下所有限制:
组中不超过@maxUsers
(例如@maxUsers=4
)
没有重复完整组(完整实验室)。组中用户的顺序并不重要。的 [编辑]
如果不允许现有实验室,请创建(INSERT
)新实验室,然后插入@user
行,不超过@maxLabs
(例如{{} 1}})。
非常重要:服务器中有很多并发相同的请求,分裂为一秒,这可能会相互干扰。因此,一旦命令开始执行,在此命令结束之前不允许执行任何其他查询。
如果查询不能满足上述限制,则返回0,并返回插入行的@maxLabs=5
。
[已编辑] 有多个实验室区域。区域是独立的。每个区域#labCount都以LabID
为界。 @maxLabs
对所有区域都相同,因此@maxLabs
= Total_maxLabs
x @maxLabs
。对于示例#zonesCount
(稍后在@zone=51
)。 (相同的LabUsers可以使用没有限制的区域。区域彼此不“了解”)
@zone=52, 53 etc.
中的 LabID
是来自LabUsers
的外键。
示例:
这是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
答案 0 :(得分:1)
我捎带了dradu的变量并实现了类似但不同的解决方案。它确实假设新实验室将比最大可用当前实验室多1个。我也假设实验室不会删除用户。
此解决方案的目标是查看用户插入的最终结果是什么样的,并对其进行检查以查看哪个最终结果有效。逻辑如下:
根据原始问题的起始数据并按以下顺序执行:
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, ''
)
, '<', '<'), '>', '>'), '&', '&') 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
。使用参数和/或预先插入的数据来查看其他结果。