So I have a cursor called open_loop that does a query and now I want to loop through it, check if the value it holds exists in the database, if so I need to update that entry, if not insert it.
The problem is, the way I quit the loop is by waiting for a state 02000 which is: Success, but no rows found. Unfortunately, that also occurs if there is no entry in the user table, so the loop is cut right then! How can I make it not trigger for that event, but still trigger if loop_cur runs out of entries, or perhaps find a different way of exiting the loop?
My code is here:
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=1;
OPEN loop_cur;
REPEAT
FETCH loop_cur
INTO user_id, user_info
SELECT Id INTO user_id FROM users WHERE Id = user_id;
IF user_id IS NOT NULL THEN
UPDATE users
SET info = user_info
WHERE Id = user_id;
ELSE
INSERT INTO users (user_info);
END IF;
UNTIL done END REPEAT;
CLOSE loop_cur;
Please not that this is simplified since I'm really inserting and fetching many more values and the query for loop_cur is pretty complicated so I didn't include that, but it does work.
Finally, I cannot use INSERT ON DUPLICATE KEY UPDATE, what I'm checking isn't really a user_id, it's a different column, I used user_id for simplicity.
Use a boolean variable that is set to true within the loop.
Irrelevant code removed for simplicity:
DECLARE found boolean default false;
...
REPEAT
FETCH loop_cur ...
SET found = true;
...
UNTIL done END REPEAT;
-- found is now true if you got some rows, false otherwise
Related
Hello in a test database i have only one row in the table customer.I have created a stored procedure to do some work. Every time i execute this query
select count(*) from Customer where Email= ....
in my database i get a count 0 but in my stored procedure the result is always 1 without any error.This is the code inside my stored procedure.
BEGIN
START TRANSACTION;
SET #var=(select count(*) from Customer where Email=email);
select #var;
if (#var>0) then
update Customer Set Password=temppass,TemporaryPassword=temppass where
Email=email;
COMMIT;
ELSE
ROLLBACK;
END IF;
END
Any ideas please?Thanks!
The problem is your condition Email = email. This is always going to be true -- because both references to email are referring to the same thing, the email column in the table.
Call the variable by a different name. I often use the suffix v_, so where customer.email = v_email.
That said, I think you can just do:
begin
update Customer c
set c.Password = v_temppass,
c.TemporaryPassword = v_temppass
where c.email = v_email;
end;
I don't see what value the transaction gives.
I assume that you are not storing passwords as clear-text. That is definitely a no-no if you care at all about security and access.
I was trying to create trigger which can update value of column user_count of table user_details using value of u_count of table user_info.
CREATE TRIGGER `test`
AFTER INSERT ON `user_details` FOR EACH ROW
BEGIN
DECLARE default_user_count int(11);
SELECT u_count INTO #default_user_count FROM
user_info WHERE user_info.id= user_details.id_c;
IF user_details.user_count= 0
THEN UPDATE user_details SET
user_count = default_user_count
WHERE user_details.id_c = user_info.id;
END IF;
END
Trigger saved successfully but when i tried to insert value in both table it is preventing to insert record into user_details means no row inserted in 2 this table, if we delete trigger then its working.
Can anyone let me know wrong with this trigger?
THanks,
M.
It's not really clear what you're trying to accomplish, but it seems like it's something like what we have below.
There are numerous errors in and ambiguities in your trigger.
Confusion on variables -- DECLARE default_user_count INT(11); does not declare the user-defined variable #default_user_count. It declares the program variable default_user_count. The # prefix references an entirely different variable scope and namespace.
SELECT and UPDATE from the table which invoked the trigger doesn't usually make sense (SELECT) or is completely invalid (UPDATE).
With in a trigger, you are operating FOR EACH ROW -- that is, for each row included in the statement that invoked the trigger. Inside an INSERT trigger, the NEW values for the row are in a pseudo-table/pseudo-row accessible via the alias NEW. For UPDATE triggers, there are NEW and OLD row values, and for DELETE triggers, just OLD.
AFTER INSERT doesn't seem to make sense. I think you're looking for BEFORE INSERT -- that is, while processing an INSERT INTO ... query, before the newly-inserted row actually gets written into the table, modify its values accordingly. The resulting row contains the original values except where the trigger has modified them.
SELECT ... INTO a variable is a practice you should not get into the habit of, because it can bite you in a way a scalar subquery can't, by leaving a variable unexpectedly unaltered instead of setting it to NULL as would be expected. In this case, it would have made no difference, but it's still a caution worth mentioning... and in this case, I've eliminated that intermediate variable altogether, so the subquery is the only option.
If you are trying to set a value in this table using a value found in another table, all you need to do is SET NEW.column_name equal to the value you want used in the row instead of the value provided with the insert statement.
CREATE TRIGGER `test`
BEFORE INSERT ON `user_details` FOR EACH ROW
BEGIN
IF NEW.user_count = 0 /* maybe also >> */ OR NEW.user_count IS NULL /* << this */ THEN
SET NEW.user_count = (SELECT ui.u_count
FROM user_info ui
WHERE ui.id = NEW.id_c);
END IF;
END
Again, it's unclear how the two tables are connected based on the content of the original question, but this appears to do what you're trying to accomplish.
I am trying to write a stored procedure in MySQL which will perform a somewhat simple select query, and then loop over the results in order to decide whether to perform additional queries, data transformations, or discard the data altogether. Effectively, I want to implement this:
$result = mysql_query("SELECT something FROM somewhere WHERE some stuff");
while ($row = mysql_fetch_assoc($result)) {
// check values of certain fields, decide to perform more queries, or not
// tack it all into the returning result set
}
Only, I want it only in MySQL, so it can be called as a procedure. I know that for triggers, there is the FOR EACH ROW ... syntax, but I can't find mention of anything like this for use outside of the CREATE TRIGGER ... syntax. I have read through some of the looping mechanisms in MySQL, but so far all I can imagine is that I would be implementing something like this:
SET #S = 1;
LOOP
SELECT * FROM somewhere WHERE some_conditions LIMIT #S, 1
-- IF NO RESULTS THEN
LEAVE
-- DO SOMETHING
SET #S = #S + 1;
END LOOP
Although even this is somewhat hazy in my mind.
For reference, though I don't think it's necessarily relevant, the initial query will be joining four tables together to form a model of hierarchal permissions, and then based on how high up the chain a specific permission is, it will retrieve additional information about the children to which that permission should be inherited.
Something like this should do the trick (However, read after the snippet for more info)
CREATE PROCEDURE GetFilteredData()
BEGIN
DECLARE bDone INT;
DECLARE var1 CHAR(16); -- or approriate type
DECLARE var2 INT;
DECLARE var3 VARCHAR(50);
DECLARE curs CURSOR FOR SELECT something FROM somewhere WHERE some stuff;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET bDone = 1;
DROP TEMPORARY TABLE IF EXISTS tblResults;
CREATE TEMPORARY TABLE IF NOT EXISTS tblResults (
--Fld1 type,
--Fld2 type,
--...
);
OPEN curs;
SET bDone = 0;
REPEAT
FETCH curs INTO var1, var2, var3;
IF whatever_filtering_desired
-- here for whatever_transformation_may_be_desired
INSERT INTO tblResults VALUES (var1, var2, var3);
END IF;
UNTIL bDone END REPEAT;
CLOSE curs;
SELECT * FROM tblResults;
END
A few things to consider...
Concerning the snippet above:
may want to pass part of the query to the Stored Procedure, maybe particularly the search criteria, to make it more generic.
If this method is to be called by multiple sessions etc. may want to pass a Session ID of sort to create a unique temporary table name (actually unnecessary concern since different sessions do not share the same temporary file namespace; see comment by Gruber, below)
A few parts such as the variable declarations, the SELECT query etc. need to be properly specified
More generally: trying to avoid needing a cursor.
I purposely named the cursor variable curs[e], because cursors are a mixed blessing. They can help us implement complicated business rules that may be difficult to express in the declarative form of SQL, but it then brings us to use the procedural (imperative) form of SQL, which is a general feature of SQL which is neither very friendly/expressive, programming-wise, and often less efficient performance-wise.
Maybe you can look into expressing the transformation and filtering desired in the context of a "plain" (declarative) SQL query.
Use cursors.
A cursor can be thought of like a buffered reader, when reading through a document. If you think of each row as a line in a document, then you would read the next line, perform your operations, and then advance the cursor.
Using a cursor within a stored procedure.
Prepare the SQL Query
SELECT id FROM employee where department_id = 1;
Create the cursor which will hold the result set returned by the SQL Query.
DECLARE BonusDistributionCursor CURSOR FOR SELECT id FROM employee where department_id = 1;
To have a safe exit when fetching a row from cursor does not return any result then declare a handler called NOT FOUND and set value to a declared variable
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
Open the Cursor before you can fetch the next row from the cursor.
OPEN BonusDistributionCursor;
Fetch the next row pointed by the cursor and move the cursor to next row after that.
FETCH BonusDistributionCursor INTO employeeId;
Run the desired business logic according to the usecase required.
DELIMITER $$
CREATE PROCEDURE distributeYearlyBonus (IN departmentId VARCHAR(2))
BEGIN
DECLARE finished INTEGER DEFAULT 0;
DECLARE empId VARCHAR(TEXT) DEFAULT "";
DECLARE BonusDistributionCursor CURSOR FOR SELECT id FROM employee where department_id = departmentId;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
OPEN BonusDistributionCursor;
addBonus: LOOP
FETCH BonusDistributionCursor INTO empId;
IF finished = 1 THEN
LEAVE addBonus;
END IF;
INSERT INTO `bonus_paid_details` (`employee_id`, `year`, `datetime`) VALUES (empId, YEAR(CURDATE());, now());
END LOOP addBonus;
CLOSE BonusDistributionCursor;
END$$
DELIMITER ;
Execute the above script and you will find a new Stored Procedure created.
Call or Invoke the Stored Procedure by inputing the departmentId which will receive the bonus amount.
CALL BonusDistributionCursor(1);
Hope this explains "How to iterate using Cursors used within Stored Procedure"
I’ve got a table of bookings with start and end times, and no two bookings can overlap.
I need to check that a new booking won’t overlap with any existing bookings. However we’ve got very high load so there’s a race condition: two overlapping bookings can be both successfully inserted because the first booking was inserted after the second booking checked for overlaps.
I’m trying to solve this by taking a lock on a related resource using a BEFORE INSERT database trigger.
DELIMITER //
CREATE TRIGGER booking_resource_double_booking_guard BEFORE INSERT ON booking_resource
FOR EACH ROW BEGIN
DECLARE overlapping_booking_resource_id INT DEFAULT NULL;
DECLARE msg VARCHAR(255);
-- Take an exclusive lock on the resource in question for the duration of the current
-- transaction. This will prevent double bookings.
DECLARE ignored INT DEFAULT NULL;
SELECT resource_id INTO ignored
FROM resource
WHERE resource_id = NEW.resource_id
FOR UPDATE;
-- Now we have the lock, check for optimistic locking conflicts:
SELECT booking_resource_id INTO overlapping_booking_resource_id
FROM booking_resource other
WHERE other.booking_from < NEW.booking_to
AND other.booking_to > NEW.booking_from
AND other.resource_id = NEW.resource_id
LIMIT 1;
IF overlapping_booking_resource_id IS NOT NULL THEN
SET msg = CONCAT('The inserted times overlap with booking_resource_id: ', overlapping_booking_resource_id);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
END IF;
END
//
If I put this trigger in the database and insert two bookings asynchronously from the command line, this trigger successfully blocks the overlapping booking. I’ve tried this with putting a SLEEP before the last IF statement in the trigger, to make sure that the lock has really been taken out.
However, I have a load testing environment in Jenkins which runs a lot of bookings concurrently using jMeter. When I put this trigger there and run the load tests, no overlapping bookings are caught, i.e. double bookings are made.
Some checks I’ve done:
I’ve logged out the SQL queries that the load test script generates when creating a booking, and it is the same as the SQL I use in the command line.
The trigger is definitely being triggered in the load test environment, and it is definitely not catching any overlapping bookings. I ascertained this by inserting the “overlapping_booking_resource_id” variable from the trigger into another table. All the values were null.
The trigger works in the load test environment when inserting bookings from the command line, i.e. it prevents the overlapping booking from being inserted.
If I make the constraint for what a “double booking” is slightly too strict, i.e. adjacent bookings count as double bookings, then I do see things being caught by the trigger – that is, the apache log records several errors with the message ‘The inserted times overlap with booking_resource_id:’
I’m wondering if maybe the lock is only taken out until the end of the trigger, and there is still a race condition between the end of the trigger and actually inserting into the table. However this doesn’t explain why none of the overlapping bookings are ever caught.
I’m really stuck now. Does anyone have any ideas as to what I have done wrong?
A less elegant but more robust method would be to use a table made for locking records accross the system.
DELIMITER //
CREATE TRIGGER booking_resource_double_booking_guard BEFORE INSERT ON booking_resource
FOR EACH ROW BEGIN
DECLARE overlapping_booking_resource_id INT DEFAULT NULL;
DECLARE msg VARCHAR(255);
-- Take an exclusive lock on the resource in question for the duration of the current
-- transaction. This will prevent double bookings.
---CHANGED HERE
REPEAT
BEGIN
DECLARE CONTINUE HANDLER FOR SQLWARNING
BEGIN
SET locked = FALSE;
END;
locked=TRUE;
INSERT INTO lockresource values(NEW.resource_id);
END;
UNTIL LOCKED END REPEAT;
---TIL HERE
-- Now we have the lock, check for optimistic locking conflicts:
SELECT booking_resource_id INTO overlapping_booking_resource_id
FROM booking_resource other
WHERE other.booking_from < NEW.booking_to
AND other.booking_to > NEW.booking_from
AND other.resource_id = NEW.resource_id
LIMIT 1;
IF overlapping_booking_resource_id IS NOT NULL THEN
SET msg = CONCAT('The inserted times overlap with booking_resource_id: ', overlapping_booking_resource_id);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
END IF;
END
//
---ADDED FROM HERE
DELIMITER //
CREATE TRIGGER booking_resource_double_booking_guard_after AFTER INSERT ON booking_resource
FOR EACH ROW BEGIN
DECLARE CONTINUE HANDLER FOR SQLWARNING BEGIN END;
delete from lockresource where lockid=NEW.resource_id;
END
//
Anyway, that'd be the idea and would certainly prevent any loss of lock until completion of your validation.
This stored procedure that I'm working on errors out some times. I am getting a Result consisted of more than one row error, but only for certain JOB_ID_INPUT values. I understand what causes this error, and so I have tried to be really careful to make sure that my return values are scalar when they should be. Its tough to see into the stored procedure, so I'm not sure where the error could be generated. Since the error is thrown conditionally, it has me thinking memory could be an issue, or cursor reuse. I don't work with cursors that often so I'm not sure. Thank you to anyone who helps.
DROP PROCEDURE IF EXISTS export_job_candidates;
DELIMITER $$
CREATE PROCEDURE export_job_candidates (IN JOB_ID_INPUT INT(11))
BEGIN
DECLARE candidate_count INT(11) DEFAULT 0;
DECLARE candidate_id INT(11) DEFAULT 0;
# these are the ib variables
DECLARE _overall_score DECIMAL(5, 2) DEFAULT 0.0;
# declare the cursor that will be needed for this SP
DECLARE curs CURSOR FOR SELECT user_id FROM job_application WHERE job_id = JOB_ID_INPUT;
# this table stores all of the data that will be returned from the various tables that will be joined together to build the final export
CREATE TEMPORARY TABLE IF NOT EXISTS candidate_stats_temp_table (
overall_score_ib DECIMAL(5, 2) DEFAULT 0.0
) engine = memory;
SELECT COUNT(job_application.id) INTO candidate_count FROM job_application WHERE job_id = JOB_ID_INPUT;
OPEN curs;
# loop controlling the insert of data into the temp table that is retuned by this function
insert_loop: LOOP
# end the loop if there is no more computation that needs to be done
IF candidate_count = 0 THEN
LEAVE insert_loop;
END IF;
FETCH curs INTO candidate_id;
# get the ib data that may exist for this user
SELECT
tests.overall_score
INTO
_overall_score
FROM
tests
WHERE
user_id = candidate_id;
#build the insert for the table that is being constructed via this loop
INSERT INTO candidate_stats_temp_table (
overall_score
) VALUES (
_overall_score
);
SET candidate_count = candidate_count - 1;
END LOOP;
CLOSE curs;
SELECT * FROM candidate_stats_temp_table WHERE 1;
END $$
DELIMITER ;
The WHERE 1 (as pointed out by #cdonner) definitely doesn't look right, but I'm pretty sure this error is happening because one of your SELECT ... INTO commands is returning more than one row.
This one should be OK because it's an aggregate without a GROUP BY, which always returns one row:
SELECT COUNT(job_application.id) INTO candidate_count
FROM job_application WHERE job_id = JOB_ID_INPUT;
So it's probably this one:
# get the ib data that may exist for this user
SELECT
tests.overall_score
INTO
_overall_score
FROM
tests
WHERE
user_id = candidate_id;
Try to figure out if it's possible for this query to return more than one row, and if so, how do you work around it. One way might be to MAX the overall score:
SELECT MAX(tests.overall_sore) INTO _overall_score
FROM tests
WHERE user_id = candidate_id
I think you want to use
LIMIT 1
in your select, not
WHERE 1
Aside from using this safety net, you should understand your data to figure out why you are getting multiple results. Without seeing the data, it is difficult for me to take a guess.