79

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.

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Dereleased
  • 9,939
  • 3
  • 35
  • 51

3 Answers3

109

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.

PatPeter
  • 394
  • 2
  • 17
mjv
  • 73,152
  • 14
  • 113
  • 156
  • 1
    So **that's** what a cursor is for... it never made sense to me until now, as I've only rather recently started using procedures, triggers and so-on. This is more helpful than I could've hoped for, thank you very much. – Dereleased Nov 16 '09 at 22:51
  • 1
    Glad I could help! Again, use [cursors] in moderation. A said SQL is a very powerful with set algebra using DDL an DML (the "core" functions of SQL), but less with for imperative language construct. – mjv Nov 16 '09 at 22:59
  • Typo: `tblResults` vs `tblResult` – Merlyn Morgan-Graham May 22 '12 at 02:45
  • 1
    According to [this answer](http://stackoverflow.com/questions/8179600/are-mysql-in-memory-tables-global/8179616#8179616), temporary tables are not shared between sessions, so you shouldn't need to deal with unique ids etc. – Gruber Jul 02 '15 at 08:41
  • 4
    This repeat-until loop needs also a check "IF not bDone" because it will be executed one more time after the last record is hit. – Marinos An Sep 09 '15 at 10:18
  • @Dereleased yea, cursor is bascially something similar to iterator in C-like languages, BTW: I think this is great when you are inserting rows from old table to new table as multiple rows :) – jave.web Mar 22 '17 at 19:42
  • 2
    FETCH curs INTO var1,, b; What is the "b" on this line? – Yves Gonzaga Feb 25 '18 at 15:48
  • @YvesGonzaga the variables are not precisely written there. "b" may have been a previous thought to use a boolean for the sake of the example. If we want to stick to the var's declared above, "FETCH" should happen "INTO var1, Var2, Var3". – terminus.technicus Nov 24 '19 at 12:25
6

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.

Flexo
  • 87,323
  • 22
  • 191
  • 272
AlishahNovin
  • 1,904
  • 3
  • 20
  • 37
2

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"

Ishaq Khan
  • 929
  • 9
  • 7