• Interesting problem, but you may have missed something. Shouldn't your PU table also include the quantity of product a particular user has on loan?

    The code below should handle your case #1 OK by using the UPDLOCK hint within a transaction to ensure that the first user grabs the associated product record and holds it until the PU record is inserted. Note that if you need to know whether a loan was rejected or not, grab the @@ROWCOUNT right after the INSERT - if 0 it was a reject (otherwise it should always be 1).

    CREATE TABLE #Product(

    [p_ID] [int] IDENTITY(1,1) NOT NULL,

    [p_Quantity] [smallint] NULL,

    CONSTRAINT [PK_Product] PRIMARY KEY CLUSTERED

    (

    [p_ID] ASC

    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

    ) ON [PRIMARY]

    CREATE TABLE #ProductUser(

    [pu_ID] [int] IDENTITY(1,1) NOT NULL,

    [pu_ProductID] [int] NOT NULL,

    [pu_UserID] [smallint] NOT NULL,

    [pu_Active] [bit] NOT NULL,

    [pu_Quantity] [int] NOT NULL,

    CONSTRAINT [PK_ProductUser] PRIMARY KEY CLUSTERED

    (

    [pu_ID] ASC

    )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

    ) ON [PRIMARY]

    INSERT INTO #Product

    SELECT 100 UNION ALL SELECT 50 UNION ALL SELECT 25

    DECLARE @User SMALLINT = 5

    ,@ProductID INT = 3

    ,@Quantity INT = 50

    -- User 5 wants 50 of product 3 but quantity is only 25 - reject

    BEGIN TRAN T1;

    ;WITH QtyRemaining AS (

    SELECT Quantity=SUM(p_quantity)

    FROM (

    SELECT p_quantity

    FROM #Product WITH(UPDLOCK)

    WHERE p_ID = @ProductID

    UNION ALL

    SELECT -pu_Quantity

    FROM #ProductUser

    WHERE @ProductID = pu_ProductID AND pu_active = 1) a)

    INSERT INTO #ProductUser

    SELECT @ProductID, @User, 1, @Quantity

    FROM QtyRemaining

    WHERE @Quantity <= Quantity

    COMMIT TRAN T1;

    SELECT * FROM #ProductUser

    -- User 5 wants 50 of product 1 and available quantity is 100 - allowed

    SELECT @ProductID = 1

    BEGIN TRAN T1;

    ;WITH QtyRemaining AS (

    SELECT Quantity=SUM(p_quantity)

    FROM (

    SELECT p_quantity

    FROM #Product WITH(UPDLOCK)

    WHERE p_ID = @ProductID

    UNION ALL

    SELECT -pu_Quantity

    FROM #ProductUser

    WHERE @ProductID = pu_ProductID AND pu_active = 1) a)

    INSERT INTO #ProductUser

    SELECT @ProductID, @User, 1, @Quantity

    FROM QtyRemaining

    WHERE @Quantity <= Quantity

    COMMIT TRAN T1;

    SELECT * FROM #ProductUser

    -- User 3 wants to return 30 of product 1

    SELECT @ProductID = 1

    ,@Quantity = 30

    BEGIN TRAN T1;

    UPDATE pu

    SET pu_Active = CASE WHEN @Quantity < pu_Quantity THEN 1 ELSE 0 END

    ,pu_Quantity = CASE WHEN @Quantity < pu_Quantity THEN pu_Quantity - @Quantity

    ELSE 0 END

    FROM #ProductUser pu

    WHERE pu_UserID = @User AND pu_ProductID = @ProductID

    COMMIT TRAN T1;

    SELECT * FROM #ProductUser

    DROP TABLE #Product, #ProductUser

    The issue here is the case of returns. The code I wrote handles returns OK unless the same user has taken out more than one active instance of the same product. This will be a bit of a challenge - to split the return quantity, if it doesn't exactly match one of the PU records, across multiple PU records. The reason I didn't finish out this case is you'll need to decide: 1) do you return the products in the order they were taken out (you could use the created_date column I took out of this table for this or 2) do you want to do a fancy matching, so that if the user took out 50 and returned 50 it matches first on a PU record that is exactly 50, otherwise it spreads it out. Both may be doable by what is known as a "Quirky Update."

    The transaction around the 3rd case should still be what you need to prevent case #2 from being an issue.

    The code is designed to get you thinking about what you'll need to do. Not be the final solution.


    My mantra: No loops! No CURSORs! No RBAR! Hoo-uh![/I]

    My thought question: Have you ever been told that your query runs too fast?

    My advice:
    INDEXing a poor-performing query is like putting sugar on cat food. Yeah, it probably tastes better but are you sure you want to eat it?
    The path of least resistance can be a slippery slope. Take care that fixing your fixes of fixes doesn't snowball and end up costing you more than fixing the root cause would have in the first place.

    Need to UNPIVOT? Why not CROSS APPLY VALUES instead?[/url]
    Since random numbers are too important to be left to chance, let's generate some![/url]
    Learn to understand recursive CTEs by example.[/url]
    [url url=http://www.sqlservercentral.com/articles/St