120

I have a table A and there is one primary key ID.

Now I want to go through all rows in A.

I found something like 'for each record in A', but this seems to be not how you do it in MySQL.

Thing is for each row I want to take a field and transform it, insert it into another table and then update some of the row's fields. I can put the select part and the insert into one statement, but I don't know how to get the update in there as well. So I want to loop. And for practice I don't want to use anything else than MySQL.

edit

I would appreciate an example.

And a solution which does not need to be put into a procedure.

edit 2

okay think of this scenario:

Table A and B, each with fields ID and VAL.

Now this is the pseudo code for what I want to do:

for(each row in A as rowA)
{
  insert into B(ID, VAL) values(rowA[ID], rowA[VAL]);
}

basically copying content of A into B using a loop.

(this is just a simplified example, of course you wouldn't use a loop for this.) }

Raffael
  • 19,547
  • 15
  • 82
  • 160
  • Hi Rafeel for me its giving error as : My SQL> for (each row in wp_mobiune_ecommune_user as x){ insert into wp_mobiune_ecommune_user (gender_new) values (x[gender])}; RROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'for (each row in wp_mobiune_ecommune_user as x){ insert into wp_mobiune_ecommune' at line 1 – dinesh kandpal Sep 15 '17 at 16:05

5 Answers5

174

Since the suggestion of a loop implies the request for a procedure type solution. Here is mine.

Any query which works on any single record taken from a table can be wrapped in a procedure to make it run through each row of a table like so:

First delete any existing procedure with the same name, and change the delimiter so your SQL doesn't try to run each line as you're trying to write the procedure.

DROP PROCEDURE IF EXISTS ROWPERROW;
DELIMITER ;;

Then here's the procedure as per your example (table_A and table_B used for clarity)

CREATE PROCEDURE ROWPERROW()
BEGIN
DECLARE n INT DEFAULT 0;
DECLARE i INT DEFAULT 0;
SELECT COUNT(*) FROM table_A INTO n;
SET i=0;
WHILE i<n DO 
  INSERT INTO table_B(ID, VAL) SELECT (ID, VAL) FROM table_A LIMIT i,1;
  SET i = i + 1;
END WHILE;
End;
;;

Then dont forget to reset the delimiter

DELIMITER ;

And run the new procedure

CALL ROWPERROW();

You can do whatever you like at the "INSERT INTO" line which I simply copied from your example request.

Note CAREFULLY that the "INSERT INTO" line used here mirrors the line in the question. As per the comments to this answer you need to ensure that your query is syntactically correct for which ever version of SQL you are running.

In the simple case where your ID field is incremented and starts at 1 the line in the example could become:

INSERT INTO table_B(ID, VAL) VALUES(ID, VAL) FROM table_A WHERE ID=i;

Replacing the "SELECT COUNT" line with

SET n=10;

Will let you test your query on the first 10 record in table_A only.

One last thing. This process is also very easy to nest across different tables and was the only way I could carry out a process on one table which dynamically inserted different numbers of records into a new table from each row of a parent table.

If you need it to run faster then sure try to make it set based, if not then this is fine. You could also rewrite the above in cursor form but it may not improve performance. eg:

DROP PROCEDURE IF EXISTS cursor_ROWPERROW;
DELIMITER ;;

CREATE PROCEDURE cursor_ROWPERROW()
BEGIN
  DECLARE cursor_ID INT;
  DECLARE cursor_VAL VARCHAR;
  DECLARE done INT DEFAULT FALSE;
  DECLARE cursor_i CURSOR FOR SELECT ID,VAL FROM table_A;
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
  OPEN cursor_i;
  read_loop: LOOP
    FETCH cursor_i INTO cursor_ID, cursor_VAL;
    IF done THEN
      LEAVE read_loop;
    END IF;
    INSERT INTO table_B(ID, VAL) VALUES(cursor_ID, cursor_VAL);
  END LOOP;
  CLOSE cursor_i;
END;
;;

Remember to declare the variables you will use as the same type as those from the queried tables.

My advise is to go with setbased queries when you can, and only use simple loops or cursors if you have to.

Mr Purple
  • 2,325
  • 1
  • 18
  • 15
  • 2
    INSERT INTO table_B(ID, VAL) VALUES(ID, VAL) FROM table_A LIMIT i,1; gives a syntax error. – Jonathan Dec 14 '13 at 19:40
  • 1
    Seems to be missing 'DECLARE done INT DEFAULT FALSE;' after cursor_i declaration – ErikL May 02 '14 at 11:32
  • 1
    Where is done property set to true, i should place it or is a reserved word that is used automaticly by mysql cursor? – IgniteCoders Oct 29 '15 at 12:22
  • 1
    The INSERT INTO call throws a syntax error as of SQL 5.5, the correct call is INSERT INTO table_B(ID, VAL) SELECT (ID, VAL) FROM table_A WHERE ID=i; – Ambar Aug 31 '16 at 18:50
  • This will take a lifetime, so try to increase the batch size to 1000 by changing the limit and increment: `LIMIT i,1000;` and `set i = i + 1000`; – Alan Deep Oct 29 '18 at 19:27
  • I needed to remove parentheses from `SELECT (ID, VAL)` to `SELECT id, val` due to `Operand should contain 1 column(s)` error. – jean d'arme Jul 03 '19 at 14:19
  • will this work if another client is inserting values into the table? – Tsonglew Nov 12 '19 at 07:54
  • Great answer. It worked for me. Just struggled with **dbeaver** which was throwing stupid error. It seems that it does not support procedures.. Deceptive – redochka Sep 25 '20 at 17:53
  • Can somebody expand this answer to explain why the delimiter is changed ? – beppe9000 Jan 24 '21 at 14:48
25

You should really use a set based solution involving two queries (basic insert):

INSERT INTO TableB (Id2Column, Column33, Column44)
SELECT id, column1, column2 FROM TableA

UPDATE TableA SET column1 = column2 * column3

And for your transform:

INSERT INTO TableB (Id2Column, Column33, Column44)
SELECT 
    id, 
    column1 * column4 * 100, 
    (column2 / column12) 
FROM TableA

UPDATE TableA SET column1 = column2 * column3

Now if your transform is more complicated than that and involved multiple tables, post another question with the details.

Raj More
  • 47,048
  • 33
  • 131
  • 198
  • +1 for the example. though I doubt this is applicable in my situ b/c the update afterwards is related to the insert and especially what selected rows causes which specific insert. This is why I suggested a loop, so I can perfom select/insert/update for each row individually. – Raffael Apr 28 '11 at 11:13
  • 2
    @Raffael1984: edit your question to add the conditions for "specific rows causing specific inserts" and we can help with that. You really don't want to go the cursor / loop route - it is extremely inefficient. – Raj More Apr 28 '11 at 11:24
  • oh well ... you know I'd be more than happy to go the set based query way! but efficience is no motivation here as my question is more of academic interest. the table is small enough I could do it by hand. I would really appreciate a loop-version. I might post that problem again providing more details to allow somebody to help me with set based style. – Raffael Apr 28 '11 at 11:28
  • agree with @RajMore, cursor/loop is inefficient, below are 2 links about cursor performance for references: 1. http://rpbouman.blogspot.tw/2006/09/refactoring-mysql-cursors.html 2. http://stackoverflow.com/questions/11549567/procedure-mysql-with-cursor-is-too-slow-why – Browny Lin Dec 26 '13 at 00:47
  • @RajMore Can you help me in my [question](https://stackoverflow.com/questions/62611169/how-to-write-procedure-to-fetch-data-from-other-table?noredirect=1#comment110722972_62611169) – Nurav Jun 28 '20 at 04:16
4

CURSORS are an option here, but generally frowned upon as they often do not make best use of the query engine. Consider investigating 'SET Based Queries' to see if you can achieve what it is you want to do without using a CURSOR.

Ron Weston
  • 280
  • 2
  • 16
  • I cannot find much information about set based queries. but I guess you mean an select into kind of statement. the problem is that I also need to have an update executed. – Raffael Apr 28 '11 at 11:08
2

Mr Purple's example I used in mysql trigger like that,

begin
DECLARE n INT DEFAULT 0;
DECLARE i INT DEFAULT 0;
Select COUNT(*) from user where deleted_at is null INTO n;
SET i=0;
WHILE i<n DO 
  INSERT INTO user_notification(notification_id,status,userId)values(new.notification_id,1,(Select userId FROM user LIMIT i,1)) ;
  SET i = i + 1;
END WHILE;
end
Erkan RUA
  • 21
  • 3
-32
    Use this:

    $stmt = $user->runQuery("SELECT * FROM tbl WHERE ID=:id");
    $stmt->bindparam(":id",$id);
    $stmt->execute();

        $stmt->bindColumn("a_b",$xx);
        $stmt->bindColumn("c_d",$yy);


    while($rows = $stmt->fetch(PDO::FETCH_BOUND))
    {
        //---insert into new tble
    }   
utee
  • 11
  • 1