59

I'd like to update a set of rows based on a simple criteria and get the list of PKs that were changed. I thought I could just do something like this but am worried about possible concurrency problems:

SELECT Id FROM Table1 WHERE AlertDate IS NULL;
UPDATE Table1 SET AlertDate = getutcdate() WHERE AlertDate IS NULL;

If that is wrapped in a transaction are there any concurrency issues that can occur? Or is there a better way to do this?

Cade Roux
  • 88,164
  • 40
  • 182
  • 265
DavGarcia
  • 18,540
  • 14
  • 58
  • 96
  • I think this is the best solution so far https://stackoverflow.com/questions/22066397/how-to-select-and-update-selected-rows-in-a-single-sql-query/22067150#22067150 – Damitha Nov 20 '21 at 00:59

9 Answers9

88

Consider looking at the OUTPUT clause:

USE AdventureWorks2012;  
GO  

DECLARE @MyTableVar table(  
    EmpID int NOT NULL,  
    OldVacationHours int,  
    NewVacationHours int,  
    ModifiedDate datetime);  

UPDATE TOP (10) HumanResources.Employee  
SET VacationHours = VacationHours * 1.25,  
    ModifiedDate = GETDATE()   
OUTPUT inserted.BusinessEntityID,  
       deleted.VacationHours,  
       inserted.VacationHours,  
       inserted.ModifiedDate  
INTO @MyTableVar;  

--Display the result set of the table variable.  
SELECT EmpID, OldVacationHours, NewVacationHours, ModifiedDate  
FROM @MyTableVar;  
GO  
--Display the result set of the table.  
SELECT TOP (10) BusinessEntityID, VacationHours, ModifiedDate  
FROM HumanResources.Employee;  
GO 
Mark Canlas
  • 9,385
  • 5
  • 41
  • 63
  • Is it possible to omit the "INTO" part in case this statement is at the end of a procedure ? Like you would with a simple SELECT and then be able to retrieve data from the procedure ? – Axel Samyn Dec 06 '22 at 09:43
  • To answer my own question (previous comment), it is possible indeed – Axel Samyn Dec 06 '22 at 11:17
20

Many years later...

The accepted answer of using the OUTPUT clause is good. I had to dig up the actual syntax, so here it is:

DECLARE @UpdatedIDs table (ID int)
UPDATE 
    Table1 
SET 
    AlertDate = getutcdate() 
OUTPUT
    inserted.Id
INTO
    @UpdatedIDs
WHERE 
    AlertDate IS NULL;

ADDED SEP 14, 2015:

"Can I use a scalar variable instead of a table variable?" one may ask... Sorry, but no you can't. You'll have to SELECT @SomeID = ID from @UpdatedIDs if you need a single ID.

K. R.
  • 1,220
  • 17
  • 20
20

One way to handle this is to do it in a transaction, and make your SELECT query take an update lock on the rows selected until the transaction completes.

BEGIN TRAN

SELECT Id FROM Table1 WITH (UPDLOCK)
WHERE AlertDate IS NULL;

UPDATE Table1 SET AlertDate = getutcdate() 
WHERE AlertDate IS NULL;

COMMIT TRAN 

This eliminates the possibility that a concurrent client updates the rows selected in the moment between your SELECT and your UPDATE.

When you commit the transaction, the update locks will be released.

Another way to handle this is to declare a cursor for your SELECT with the FOR UPDATE option. Then UPDATE WHERE CURRENT OF CURSOR. The following is not tested, but should give you the basic idea:

DECLARE cur1 CURSOR FOR
  SELECT AlertDate FROM Table1 
  WHERE AlertDate IS NULL
  FOR UPDATE;

DECLARE @UpdateTime DATETIME

SET @UpdateTime = GETUTCDATE()

OPEN cur1;

FETCH NEXT FROM cur1;

WHILE @@FETCH_STATUS = 0
BEGIN

  UPDATE Table1 
  SET AlertDate = @UpdateTime  --set value
  WHERE CURRENT OF cur1;

  FETCH NEXT FROM cur1;
  
END
Justin
  • 954
  • 4
  • 22
  • 44
Bill Karwin
  • 538,548
  • 86
  • 673
  • 828
  • 5
    +1 for UPDLOCK, its a correct solution to this problem and in short transactions will not lead to deadlocks – Sam Saffron Jan 31 '09 at 23:49
  • Will this result in all rows getting the same datetime value (as you would in a single SQL call)? If not, you should get the time before the loop into a variable and just set AltertDate to the variable. If it's an issue, please edit this in. – Thorsten Feb 01 '09 at 17:18
11

It'd be easier to do your UPDATE first and then run 'SELECT ID FROM INSERTED'.

Take a look at SQL Tips for more info and examples.

Kevin Fairchild
  • 10,891
  • 6
  • 33
  • 52
6

Perhaps something more like this?

declare @UpdateTime datetime

set @UpdateTime = getutcdate()

update Table1 set AlertDate = @UpdateTime where AlertDate is null

select ID from Table1 where AlertDate = @UpdateTime
Gordon Bell
  • 13,337
  • 3
  • 45
  • 64
  • 2
    This has the nice advantage that, even if you don't do this within a transaction, it will be *very* difficult that you'd get results some other process altered. – Joe Pineda Jan 31 '09 at 05:09
3

I have faced the same issue; I have to update the credit amount, and have to get modified time, along with credit details from DB. It is basically

SYNCHRONOUSLY/ATOMICALLY perform (UPDATE then GET) in MYSQL

I have tried many options and found one that solved my issue.

1) OPTION_1 SELECT FOR UPDATE

This is maintaining the lock till update (SYNC from GET to UPDATE), but i need lock after update till the GET.

2) OPTION_2 Stored procedure

Stored procedure will not execute synchronously like redis lua, So there also we need sync code to perform that.

3) OPTION_3 Transaction

I have used JPA entityManager like below, thought that before commit no one can update, and before commit i will get the updated object along with modified time (from DB). But i didn't get the latest object. Only commit i got the latest.

    try {
        entityManager.getTransaction().begin();
        //entityManager.persist(object);
        int upsert = entityManager.createNativeQuery(
        "update com.bill.Credit c set c.balance = c.balance - ?1
          where c.accountId = ?2 and c.balance >= ?1").executeUpdate(); 
             //c.balance >= ? for limit check
        Credit newCredit = entityManager.find(Credit.class, "id");
        entityManager.refresh(newCredit); //SHOULD GET LATEST BUT NOT
        entityManager.getTransaction().commit();
    } finally {     
        entityManager.unwrap(Session.class).close();
    } 

4) OPTION_4 LOCK solved the issue, so before update i acquired the lock; then after GET i have released the lock.

private Object getLock(final EntityManager entityManager, final String Id){

    entityManager.getTransaction().begin();
    Object obj_acquire = entityManager.createNativeQuery("SELECT GET_LOCK('" + Id + "', 10)").getSingleResult();
    entityManager.getTransaction().commit();
    return obj_acquire;
}


private Object releaseLock(final EntityManager entityManager, final String Id){

    entityManager.getTransaction().begin();
    Object obj_release = entityManager.createNativeQuery("SELECT RELEASE_LOCK('" + Id + "')").getSingleResult();
    entityManager.getTransaction().commit();
    return obj_release;
}
Kanagavelu Sugumar
  • 18,766
  • 20
  • 94
  • 101
1

Edit: my bad, you wanted the select to show results after the update, not update from a select.

Have you tried a sub-select?

update mytable set mydate = sysdate 
where mydate in (select mydate from mytable where mydate is null);
Jeff B
  • 8,572
  • 17
  • 61
  • 140
0

if it's inside the transaction, the database locking system will take care of concurrency issues. of course, if you use one (the mssql default is that it uses lock, so it states if you don't override that)

zappan
  • 3,668
  • 4
  • 29
  • 24
0

in SQL 2008 a new TSQL statement "MERGE" is introduced which performs insert, update, or delete operations on a target table based on the results of a join with a source table. You can synchronize two tables by inserting, updating, or deleting rows in one table based on differences found in the other table.

http://blogs.msdn.com/ajaiman/archive/2008/06/25/tsql-merge-statement-sql-2008.aspx http://msdn.microsoft.com/en-us/library/bb510625.aspx