This question already has an answer here:
Limit same data entries
(1 answer)
Closed 1 year ago.
I have two tables in postgresql:
table A: has the definitions for a certain object
table B: has the instances of the objects defined in table A
Table A has columns total_instances and per_player_instances, which can both be null, and which I need to prevent the instance count in table B from going above if they're set. The code handles most cases, but I was getting duplicates from concurrent inserts.
Table A does not need to be locked as it rarely changes, and if we do it we can do it in planned downtime when no inserts will be happening in table B.
I wrote a trigger to count the existing instances and return an error if the count has gone over, but I must have gotten the locking wrong as now I am getting deadlock_detected errors. My trigger function is this:
CREATE OR REPLACE FUNCTION ri_constraints_func() RETURNS trigger AS $$
DECLARE
max_total INTEGER := NULL;
max_per_player INTEGER := NULL;
total_count INTEGER := 0;
per_player_count INTEGER := 0;
BEGIN
-- prevent concurrent inserts from multiple transactions
SELECT INTO max_total, max_per_player
awardable_total, awardable_per_player
FROM t1
WHERE id = NEW.t1_id;
LOCK TABLE t2 IN EXCLUSIVE MODE;
IF max_total IS NOT NULL THEN
SELECT INTO total_count
count(1)
FROM t2
WHERE t1_id = NEW.t1_id;
IF total_count >= max_total THEN
RAISE EXCEPTION 'awardable_total_exceeded';
END IF;
END IF;
IF max_per_player IS NOT NULL THEN
SELECT INTO per_player_count
count(1)
FROM t2
WHERE t1_id = NEW.t1_id
AND awarded_to_player_id = NEW.awarded_to_player_id;
IF per_player_count >= max_per_player THEN
RAISE EXCEPTION 'awardable_per_player_exceeded';
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Basically what I need to do is prevent inserts into the table between counting the instances and doing the insert. I thought that using LOCK TABLE t2 IN EXCLUSIVE MODE; would accomplish that. I'm doing more research into table locking, but if anyone knows what locking level to use to accomplish this I'd appreciate hearing it.
Also, I'm not married to this particular approach, so if getting this working requires re-writing this function, I'm open to that too.
Postgresql version is 11.
I believe that what you are doing is unnecessary and it can probably be accomplished by using proper transaction management.
For the transaction that needs to look the entire table, set the isolation level to SERIALIZABLE. This will disallow ghosts which is why you are trying to lock the entire table.
You don't need to do locks manually.
This might help: https://youtu.be/oI2mxnZbSiA
I recommend you read about isolation levels in postgresql.
Related
In my web application, I store a counter value in the database table, which I need to increment or reset at each transaction (which are highly concurrent). Do I need to explicitly lock the row to avoid lost updates? Read committed transaction isolation level is being used at the connection level. The following statement updates the counter
UPDATE Counter c SET value =
CASE
WHEN c.last_updated = SYSDATE THEN c.value+1
ELSE 1
END,
last_updated = SYSDATE
WHERE c.counter_id = 123;
The statement is atomic and read committed isolation level implicitly locks the rows for update statements, as far as I know. Does this render the use of explicit locking redundant in this case?
You're talking optimistic locking vs pessimistic locking ("explicit lock").
If you go with pessimistic locking, you're guaranteed to have no lost updates. However, the approach comes at a price:
It doesn't scale well - you're essentially serializing access to the row being updated, and if the client running the first transaction hangs for some reason - everyone is stuck.
Given the nature of the usually multi-tier web apps, it may be difficult (or impossible) to implement, as the explicit lock needs to be run in the same database connection as the update itself, which your middle tier may or may not guarantee.
So you can go with optimistic locking instead. Assume the following table:
create table t (key int, value int, version int);
insert into t (1, 1, 1);
Basically, the logic would be like this (PL/SQL code):
declare
current_version t.version%type;
current_value t.value%type;
new_value t.value%type;
begin
-- read current version of a row
select version, value
into current_version, current_value
from t where id = 1;
-- calculate new value; while we're doing this,
-- someone else may update the row, changing its version
new_value = func_calculate_new_value(current_value);
-- update the row...
update t
set
value = new_value,
version = version + 1
where 1 = 1
and id = 1
-- but ONLY if the version of the row is the one we read
-- otherwise there would be a lost update
and version = current_version
;
if sql%rowcount = 0 then
-- 0 updated rows means the version is different
-- we're not updating because we don't want lost updates
-- and we throw to let the caller know
raise_application_error(-20000, 'Row version has changed');
rollback;
end if;
end;
I've already seen a dozen such questions but most of them get answers that doesn't apply to my case.
First off - the database is am trying to get the data from has a very slow network and is connected to using VPN.
I am accessing it through a database link.
I have full write/read access on my schema tables but I don't have DBA rights so I can't create dumps and I don't have grants for creation new tables etc.
I've been trying to get the database locally and all is well except for one table.
It has 6.5 million records and 16 columns.
There was no problem getting 14 of them but the remaining two are Clobs with huge XML in them.
The data transfer is so slow it is painful.
I tried
insert based on select
insert all 14 then update the other 2
create table as
insert based on select conditional so I get only so many records and manually commit
The issue is mainly that the connection is lost before the transaction finishes (or power loss or VPN drops or random error etc) and all the GBs that have been downloaded are discarded.
As I said I tried putting conditionals so I get a few records but even this is a bit random and requires focus from me.
Something like :
Insert into TableA
Select * from TableA#DB_RemoteDB1
WHERE CREATION_DATE BETWEEN to_date('01-Jan-2016') AND to_date('31-DEC-2016')
Sometimes it works sometimes it doesn't. Just after a few GBs Toad is stuck running but when I look at its throughput it is 0KB/s or a few Bytes/s.
What I am looking for is a loop or a cursor that can be used to get maybe 100000 or a 1000000 at a time - commit it then go for the rest until it is done.
This is a one time operation that I am doing as we need the data locally for testing - so I don't care if it is inefficient as long as the data is brought in in chunks and a commit saves me from retrieving it again.
I can count already about 15GBs of failed downloads I've done over the last 3 days and my local table still has 0 records as all my attempts have failed.
Server: Oracle 11g
Local: Oracle 11g
Attempted Clients: Toad/Sql Dev/dbForge Studio
Thanks.
You could do something like:
begin
loop
insert into tablea
select * from tablea#DB_RemoteDB1 a_remote
where not exists (select null from tablea where id = a_remote.id)
and rownum <= 100000; -- or whatever number makes sense for you
exit when sql%rowcount = 0;
commit;
end loop;
end;
/
This assumes that there is a primary/unique key you can use to check if a row int he remote table already exists in the local one - in this example I've used a vague ID column, but replace that with your actual key column(s).
For each iteration of the loop it will identify rows in the remote table which do not exist in the local table - which may be slow, but you've said performance isn't a priority here - and then, via rownum, limit the number of rows being inserted to a manageable subset.
The loop then terminates when no rows are inserted, which means there are no rows left in the remote table that don't exist locally.
This should be restartable, due to the commit and where not exists check. This isn't usually a good approach - as it kind of breaks normal transaction handling - but as a one off and with your network issues/constraints it may be necessary.
Toad is right, using bulk collect would be (probably significantly) faster in general as the query isn't repeated each time around the loop:
declare
cursor l_cur is
select * from tablea#dblink3 a_remote
where not exists (select null from tablea where id = a_remote.id);
type t_tab is table of l_cur%rowtype;
l_tab t_tab;
begin
open l_cur;
loop
fetch l_cur bulk collect into l_tab limit 100000;
forall i in 1..l_tab.count
insert into tablea values l_tab(i);
commit;
exit when l_cur%notfound;
end loop;
close l_cur;
end;
/
This time you would change the limit 100000 to whatever number you think sensible. There is a trade-off here though, as the PL/SQL table will consume memory, so you may need to experiment a bit to pick that value - you could get errors or affect other users if it's too high. Lower is less of a problem here, except the bulk inserts become slightly less efficient.
But because you have a CLOB column (holding your XML) this won't work for you, as #BobC pointed out; the insert ... select is supported over a DB link, but the collection version will get an error from the fetch:
ORA-22992: cannot use LOB locators selected from remote tables
ORA-06512: at line 10
22992. 00000 - "cannot use LOB locators selected from remote tables"
*Cause: A remote LOB column cannot be referenced.
*Action: Remove references to LOBs in remote tables.
I have a column in DB table which has to be increment when let's say some item is selected. But it can be selected parallel and for any records it has to start from 0. My solution is to increment the value from DB procedure, but can I be sure that the first procedure manages to increment the value before another procedure want to load the value to increment? I mean:
t0 Value is 10
t1 Procedure1 valueToInc = Value
t2 Procedure2 valueToInc = Value
t3 Procedure1 valueToInc ++
t4 Procedure2 valueToInc ++
t5 Value = 11
t6 Value = 11
Value written back from Procedure1 is 11 but from Procedure2 is obviously also 11 (need to secure 12 there).
I have also checked identity (property) and sequence (Transact-SQL) but nothing seems to be suitable for me.
Edit
What I´m trying to solve is that I have a console application - TCP server and MSSQL database, where I have a User table. Each time the single user wants to login, I have to increment users loginCount field. Any parallelization here should not be possible or is manageable from code, I know, but it was told me that I have to hande parallel acces by database, so not just to use update query. I have it as job interview project...
I wanted to make understanding easier by my first explanation, but it won´t work.
You can just use
UPDATE Users
SET LoginCount = ISNULL(LoginCount,0) + 1
WHERE UserId = #UserId
This is entirely safe under conditions of concurrency.
Use a transaction with transaction isolation level equal to SERIALIZABLE.
SERIALIZABLE
Statements cannot read data that has been modified but not yet committed by other transactions.
No other transactions can modify data that has been read by the current transaction until the current transaction completes.
Other transactions cannot insert new rows with key values that would fall in the range of keys read by any statements in the current transaction until the current transaction completes.
Don't load the Value to increment it: increment it, then select it (within the transaction). This will lock the table/row (depending) from updates/selects of other transactions.
My C# program is doing the following (pseudo code):
START TRANSACTION READ COMMITTED
Select isOriginal, * from myTable where tnr = x;
//Record found?
Yes:
//isOriginal?
Yes:
update myTable set is_active = 0 where tnr = x;
No:
delete from myTable where tnr = x;
//Do some simple logic on the values
Insert into myTable (newvalues)
No:
return record_not_found;
END TRANSACTION
However, when I start two instances of my program and both edit the same record at the same time two records are inserted as they both find the record in the select query.
What should happen is that the first transaction finds the record and inserts a new row while the second transaction returns a record not found.
How can I fix this? Put my transaction to serializable? Check the return value of update/delete? What's the best way?
Edit:
It should work on Sybase, Oracle & SQL Server.
without knowing what db you are using you could setup a lock field in the db.
where each concurrent thread has a pid or thread id or at least a unique timestamp
just do a
update myTable set lock = <pid> where pid = null limit 1;
select isOriginal, * from myTable where lock = <pid>
If you are using MS SQL You will need to look at NOLOCK and ROWLOCK
NOLOCK tells SQL Server to ignore any type of locks and read directly from the actual tables. The pro is it has great performance, the con is in this way you are circumventing a locked system. ROWLOCK on the other hand asks SQL Server to use row-level locks. Performance does hinder with rowlock so you need to determine if you need to lock on UPDATES / DELETES
In your case SELECT isOriginal, * FROM myTable WITH (NOLOCK) WHERE tnr=x
Then UPDATE myTable WITH (ROWLOCK) SET is_active=0 WHERE tnr=x
I have an application that uses incident numbers (amongst other types of numbers). These numbers are stored in a table called "Number_Setup", which contains the current value of the counter.
When the app generates a new incident, it number_setup table and gets the required number counter row (counters can be reset daily, weekly, etc and are stored as int's). It then incremenets the counter and updates the row with the new value.
The application is multiuser (approximately 100 users at any one time, as well as sql jobs that run and grab 100's of incident records and request incident numbers for each). The incident table has some duplicate incident numbers where they should not be duplicate.
A stored proc is used to retrieve the next counter.
SELECT #Counter = counter, #ShareId=share_id, #Id=id
FROM Number_Setup
WHERE LinkTo_ID=#LinkToId
AND Counter_Type='I'
IF isnull(#ShareId,0) > 0
BEGIN
-- use parent counter
SELECT #Counter = counter, #ID=id
FROM Number_Setup
WHERE Id=#ShareID
END
SELECT #NewCounter = #Counter + 1
UPDATE Number_Setup SET Counter = #NewCounter
WHERE id=#Id
I've now surrounded that block with a transaction, but I'm not entirely sure it' will 100% fix the problem, as I think there's still shared locks, so the counter can be read anyway.
Perhaps I can check that the counter hasn't been updated, in the update statement
UPDATE Number_Setup SET Counter = #NewCounter
WHERE Counter = #Counter
IF ##ERROR = 0 AND ##ROWCOUNT > 0
COMMIT TRANSACTION
ELSE
ROLLBACK TRANSACTION
I'm sure this is a common problem with invoice numbers in financial apps etc.
I cannot put the logic in code either and use locking at that level.
I've also locked at HOLDLOCK but I'm not sure of it's application. Should it be put on the two SELECT statements?
How can I ensure no duplicates are created?
The trick is to do the counter update and read in a single atomic operation:
UPDATE Number_Setup SET Counter = Counter+1
OUTPUT INSERTED.Counter
WHERE id=#Id;
This though does not assign the new counter to #NewCounter, but instead returns it as a result set to the client. If you have to assign it, use an intermediate table variable to output the new counter INTO:
declare #NewCounter int;
declare #tabCounter table (NewCounter int);
UPDATE Number_Setup SET Counter = Counter+1
OUTPUT INSERTED.Counter INTO #tabCounter (NewCounter)
WHERE id=#Id
SELECT #NewCounter = NewCounter FROM #tabCounter;
This solves the problem of making the Counter increment atomic. You still have other race conditions in your procedure because the LinkTo_Id and share_id can still be updated after the first select so you can increment the counter of the wrong link-to item, but that cannot be solved just from this code sample as it depends also on the code that actualy updates the shared_id and/or LinkTo_Id.
BTW you should get into the habbit of name your fields with consistent case. If they are named consistently then you must use the exact match case in T-SQL code. Your scripts run fine now just because you have a case insensitive collation server, if you deploy on a case sensitive collation server and your scripts don't match the exact case of the field/tables names errors will follow galore.
have you tried using GUIDs instead of autoincrements as your unique identifier?
If you have the ablity to modify your job that gets mutiple records, I would change the thinking so that your counter is an identity column. Then when you get the next record you can just do an insert and get the ##identity of the table. That would ensure that you get the biggest number. You would also have to do a dbccReseed to reset the counter instead of just updating the table when you want to reset the identity. The only issue is that you'd have to do 100 or so inserts as part of your sql job to get a group of identities. That may be too much overhead but using an identity column is a guarenteed way to get unique numbers.
I might be missing something, but it seems like you are trying to reinvent technology that has already been solved by most databases.
instead of reading and updating from the 'Counter' column in the Number_Setup table, why don't you just use an autoincrementing primary key for your counter? You'll never have a duplicate value for a primary key.