How to prevent the deadlock in the below situation? - mariadb

I have a table, which should hold rows for OrderGroups. Basically, when a client creates an Order, his Order should be put inside a group based on his client id, until an administrator can verify the order. The OrderGroups tables structure is the following:
OrderGroupId | IsClosed | clientId
-------------------------------------------------
INT PRIMARY (AutoIncrement) | BOOLEAN | INT
My code should work in the following way: when a client creates a new order, we should check if he already has a NOT cloesd order group. If he has, we should attach that order group to his order. If he has none, we should create a new order group for him, and attach his order to the newly created group.
In the past, no locking was used when fetching/creating the order group, which resulted in naturally, that some clients, when inserting multiple orders concurently, ended up with multiple open order groups. I've modified my order group fetching query, to the following:
BEGIN TRANSACTION;
SELECT * FROM OrderGroups WHERE clientId = {id} AND IsClosed = 0 FOR UPDATE;
// if no order groups are returned, insert a new group and use that one
// if an order group is returned, use the returned order group
END TRANSACTION;
This prevents the appeareance of multiple OrderGroups, but it sometimes results in a deadlock. I presume that the reason for this, is how MariaDB is locking the rows, when they are not present. Basically, if a result would be returned by the query in question, all subsequent calls requesting the same row, should wait, until the transaction that was first requesting it for update, commits or rolls back. But this is not the case, if a non-existent row gets locked this way. The insersions are still prevented (that is why I am getting the deadlock), but the select queries are processed.
Basically, this is what happens:
C1 -> BEGIN TRANSACTION;
C1 -> SELECT OrderGroups WHERE clientId = 1 AND IsClosed = 0 FOR UPDATE; // returns no rows
C2 -> BEGIN TRANSACTION;
C2 -> SELECT OrderGroups WHERE clientId = 1 AND IsClosed = 0 FOR UPDATE; // returns no rows, instead of waiting for C1 to commit or rollback the transaction
C1 -> INSERT INTO OrderGroups SET clientId = 1, IsClosed = 0; // holds, because C2 has a for update lock on the row? being inserted
C2 -> INSERT INTO OrderGroups SET clientId = 1, IsClosed = 0; // holds, because C1 has a for update lock on the row
MARIADB -> randomly kills C1 or C2 because of the deadlock, while the other may finish
How could I avoid this deadlock situation, and still maintain the single open group policy for the OrderGroups table?

For the moment, I managed to resolve this in a kind of hackish way, by inserting a new, unique column inside the table.
I've inserted the UniqId (VARCHAR(20)) column as a unique in the database. When I am trying to fetch or create a new row in the table, instead of selecting and then inserting if nothing is found, I just simply make an INSERT IGNORE INTO ... using c{clientId} for the UniqId column. Whenever the order group is closed, I update the UniqId column to the primary key of the table.
This way, when performing the INSERT IGNORE, if the row already exists, it gets locked, and I will be able to select it inside the next SELECT statement, and if the row does not exist yet, it gets inserted and also gets locked, and I am able to select it in the next select statement.
This is a rather hacky way to solve my exact situation, but I am still open to suggestions for the possibility, to somehow get an exclusive lock for a row in the table, that does not exist yet.

Related

How do I make a trigger with an increment for the values in a column?

I have two tables TRIP and DRIVER. When a new set of values in inserted into TRIP (to indicate a new trip being made), the values in the column TOTALTRIPMADE (which is currently empty) in the table DRIVER will increase by one. The trigger should recognise which row to update with the select statement I've made.
This is the trigger I've made:
CREATE OR REPLACE TRIGGER updatetotaltripmade
AFTER INSERT ON trip
FOR EACH ROW
ENABLE
BEGIN
UPDATE DRIVER
SET TOTALTRIPMADE := OLD.TOTALTRIPMADE+1
WHERE (SELECT L#
FROM TRIP
INNER JOIN DRIVER
ON TRIP.L# = DRIVER.L#;)
END;
/
However I get this error:
ORA-04098: trigger 'CSCI235.UPDATETOTALTRIPMADE' is invalid and failed re-validation
What should I edit in my code so that my trigger works? Thanks!
One error you made is in trying to reference OLD.TOTALTRIPMADE in your SET clause since no alias OLD exists, and unless the table TRIP contains a TOTALTRIPMADE column then the :OLD record won't contain a TOTALTRIPMADE column either (note that since this is an insert trigger the :OLD record either won't exist or won't contain any meaningful data anyway). Another error is in your WHERE clause where you are selecting L# from TRIP joined to DRIVER, but you aren't linking it back to the DRIVER table that you are attempting to update. Instead just update DRIVER where L# is equal the :NEW value of L# from the trip table. The final error I noticed is your use of , the := assignment operator which is for PLSQL code, however you are using it within SQL so just use = without the colon:
CREATE OR REPLACE TRIGGER updatetotaltripmade
AFTER INSERT ON trip
FOR EACH ROW
ENABLE
BEGIN
UPDATE DRIVER
SET TOTALTRIPMADE = nvl(TOTALTRIPMADE,0)+1
WHERE L# = :NEW.L#;
END;
/
Your code has syntax error due to which the trigger is not compiling,I have modified the trigger and it should get compiled successfully with desired results.Please check and feedback.
Please find below the script to create table and compile the trigger,
drop table trip;
create table trip (trip_id number(10),L# varchar2(10));
drop table driver;
create table driver(driver_id number(10),TOTALTRIPMADE number(10),L# varchar2(10));
drop trigger updatetotaltripmade;
CREATE OR REPLACE TRIGGER updatetotaltripmade
AFTER INSERT ON trip
FOR EACH ROW
ENABLE
DECLARE
BEGIN
UPDATE DRIVER
SET TOTALTRIPMADE = nvl(TOTALTRIPMADE,0) + 1
WHERE DRIVER.L# = :new.L#;
END;
/
select * from ALL_OBJECTS where object_type ='TRIGGER';
Output is below from the tests i did on https://livesql.oracle.com/apex/
There are no issues in the code.The trigger is compiled successfully and is valid.

how could mvcc works when the primary key is changed?

In MVCC document, it says when "select query" finds the records, it will compare the transactionId with its own id to judge if the data can be seen and if history records should be reconstructed from redo log. My question is what if it cannot find the original records, and how could it maintain consistent reading?
Consider the following example:
create table tb_a (id bigtint not null primary key auto_increment, name varchar(100) not null default "");
// isolation level is RR
// transaction 1
select * from tb_a where id = 1; // it returns (1, "a")
// transaction 2
// another trx update the first line with its primary key
update tb_a set id = 3 where id = 1;
commit;
// transaction 1
select * from tb_a where id = 1; // still gets (1, "a")
the primary key with the filter id = 1 cannot find the row since the history records are in redo log, and updates in innodb happen inplace. So how does innodb treat this kind of thing and still maintains consistency?
Changing the PK column is probably a "delete" of the old record and an "insert" of the new row. I think this implies that something is left in the table, but marked as deleted (until the cleanup after Commits from both transactions).
Similarly for UNIQUE key changes. Other transactions need to be able to see the deleted row to check for dup key.
Each version of each row (old/new) has a transaction id. So...
Repeatable Read:
When a transaction starts, it is assigned a "transaction id". This is a monotonically increasing sequence number for identifying rows that might be modified. For transaction isolation = RR, the queries can only "see" rows that have that trx id (or older). This explains why your final SELECT sees what it does. And, note, that query is actually (as far as I know) re-executed.
Your other txn had a higher trx id. It created a 'newer' copy of the row. So, there were at least two copies of that row floating around. The isolation mode, plus the trx id, controls which row each transaction can "see".

SQLite trigger Issue

I have a trigger on a table which gets triggered on any new insert...Here is the code for the trigger. This trigger has to calculate duration in seconds and insert the record in the table.
CREATE TRIGGER duration_trigger
BEFORE INSERT on StudentData
BEGIN
UPDATE StudentData set duration = (select cast( (julianday(enddatetime) - julianday(startdatetime) ) *24 * 60 *60 as integer) from StudentData);
END
In StudentsData table Duration column is defined as INTEGER,
StartDatetime and EndDatetime are defined as TEXT
Here comes my issue.
Trigger gets triggered, but the value in Duration column is always 7
When I execute the same select query that is in the the trigger in a SQL tool, it gives me correct duration in seconds. Trigger on the database is not producing the same result...what could be the issue?
I am also attaching screenshots of the trigger data in the table and select query results from same table.
Table results after trigger.
Select Query results
Basically you are updating all rows as you are not specifying a WHERE clause for the update. So the very last successful update will apply the value to all rows, hence why they are all 7.
Furthermore before you have inserted a row what is there to update? I don't think this can be done an analogy would be; Before you build the wall paint the wall.
Now you could UPDATE after the insert, but care needs to be taken when using UPDATE i.e. if you want to update anything other than all rows then you need to restrict the update to the required rows. A WHERE clause can do this.
As such if you were to ensure that an inserted column were set to an invalid value (as far as your view of the data e.g. a duration of -1 would only suit Dr. Who (apologies to any other Time Travellers)).
Null could also be used.
However, I prefer using a value that is specifically set. Assuming that the row is inserted with duration being given a value of -1 (e.g. duartion INTEGER DEFAULT -1) Then :-
CREATE TRIGGER duration_trigger001
AFTER INSERT on StudentData
BEGIN
UPDATE StudentData SET duration = ((julianday(enddatetime) - julianday(startdatetime)) * 24 * 60 * 60) WHERE duration = -1;
END;
Would work e.g. :-
Notes
The first two rows were added before the trigger was created.
Row 10 was deleted because I used . instead of : as a separator it did nothing.
I didn't cast to INT for simplicity/laziness.

How to get number of rows updated by a query in .net

In my database, i have a trigger which insert the change log entries when a row in Table tblA is updated.
Now, in my code i have to update it through a plain Sql query like
int count = DBContext.ExecuteStoreCommand("<sql query to update records>");
This count variable contains the number of rows affected(no of rows updated + no of rows inserted) due to query.
So my question is, How do i can get only the number of updated rows?
Currently i'm using Entity framework 4. I have looked for solution through connected or disconnected model but couldn't help myself.
int count = DBContext.ExecuteStoreCommand("");
I think you hv to change this to return Select result set
then do this,
<sql query to update>
Select ##RowCount rowcountAffected
Or
suppose your update is
update table1 set col1='foo' where id=2
select count(*) rowcountAffected from table1 where id=2
The most efficient way to return row affected can be
i) Assuming you only update (don't refresh any record after that)
Put Set Nocount ON
Declare #Output parameter inside proc

How do you write a good stored procedure for update?

I want to write a stored procedure (SQL server 2008r2), say I have a table:
person
Columns:
Id int (pk)
Date_of_birth date not null
Phone int allow null
Address int allow null
Name nvarchat(50) not null
Sample data:
Id=1,Date_of_birth=01/01/1987,phone=88888888,address=null,name='Steve'
Update statement in Stored procedure, assume
The parameters are already declare:
Update person set
Date_of_birth=#dob,phone=#phone,address=#address,name=#name where id=#id
The table has a trigger to log any changes.
Now I have an asp.net update page for updating the above person table
The question is, if user just want to update address='apple street' , the above update statement will update all the fields but not check if the original value = new value, then ignore this field and then check the next field. So my log table will log all the event even the columns are not going to be updated.
At this point, my solutions
Select all the value by id and store them into local variables.
Using if-else check and generate the update statement. At last,
dynamically run the generated SQL (sp_executesql)
Select all the value by id and store them into local variables.
Using if-else check and update each field seperately:
If #dob <> #ori_dob
Begin
Update person set date_of_birth=#dob where id=#id
End
May be this is a stupid question but please advice me if you have better idea, thanks!
This is an answer to a comment by the OP and does not address the original question. It would, however, be a rather ugly comment.
You can use a statement like this to find the changes to Address within an UPDATE trigger:
select i.Id, d.Address as OldAddress, i.Address as NewAddress
from inserted as i inner join
deleted as d on d.Id = i.Id
where d.Address <> i.Address
One such statement would be needed for each column that you want to log.
You could accumulate the results of the SELECTs into a single table variable, then summarize the results for each Id. Or you can use INSERT/SELECT to save the results directly to your log table.

Resources