在 MySQL 中使用动态 SQL 创建游标

Posted

技术标签:

【中文标题】在 MySQL 中使用动态 SQL 创建游标【英文标题】:Creating cursor with Dynamic SQL in MySQL 【发布时间】:2021-11-22 14:02:46 【问题描述】:

我正在编写一个存储过程,它打开一个表的游标,然后遍历所有记录。在迭代过程中,我根据第一个游标的结果创建了一个动态查询。

我需要在动态 SQL 上打开游标,但 mysql 不允许我这样做。根据 MySQL 的官方文档:“游标必须在声明处理程序之前声明。变量和条件必须在声明游标或处理程序之前声明”

这是脚本:

DELIMITER $$

DROP PROCEDURE IF EXISTS sp_test$$

CREATE PROCEDURE `sp_test`()
BEGIN
    -- Declarations
    
    DECLARE prepared_sql VARCHAR(1000);
    DECLARE index_count INT;

    -- Cursors
    DECLARE cursor1 CURSOR FOR SELECT * from table1;
    -- Continue Handler for Cursor
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET no_more_rows = TRUE;
    -- Open cursors
    OPEN cursor1;

    -- Business Logic
    all_alerts_loop: LOOP
        -- Fetch record from cursor1 and create a dynamic sql
                
        -- Check if cursor has reached to end than leave the loop
        IF no_more_rows THEN
            LEAVE all_alerts_loop;
        END IF;
        
        
        WHILE @some_other_variable <> 0
        DO
                              -- I want to open cursor 2 on this sql
            -- set @prepared_sql =  'create dynamic sql here';  
                    END WHILE;
        
                    -- This works fine
        PREPARE stmt FROM @prepared_sql;
        EXECUTE stmt;

                    -- But can't define cursor here? so what is the solution
                    -- Gives syntax error, I have tried with @prepared_sql also rather than stmt
        DECLARE cursor2 CURSOR FOR stmt;
        
    END LOOP;
    
    -- closing cursors
    CLOSE cursor1;
    END$$

DELIMITER ;

关于如何为动态查询创建游标的任何想法?在 MySQL 中?

【问题讨论】:

【参考方案1】:

我们可以考虑使用stmt 来解决这种情况:

    统计查询将返回的所有记录

    循环并浏览查询返回的每条记录,为此使用限制。

请看下面的例子:

CREATE PROCEDURE `proc_example`(IN p_where text)
BEGIN

    DECLARE v_where text default "";
    DECLARE v_cont integer default 0;
    
    #build a dynamic where
    set v_where = p_where;
    
    #Count query records 
    set @v_sqlSelect_count = 'select count(*) into @v_total ';
    set @v_sqlSelect_count = concat(@v_sqlSelect_count,'from table ');
    set @v_sqlSelect_count = concat(@v_sqlSelect_count,'where ');
    set @v_sqlSelect_count = concat(@v_sqlSelect_count,v_where);
    
    #Executa query
    PREPARE stmt_total FROM @v_sqlSelect_count;
    EXECUTE stmt_total;
    DEALLOCATE PREPARE stmt_total;
    
    #if exists records
    if (@v_total > 0) then
        
        set v_cont = 0;
        
        navRecords:loop
        
            if (v_cont > (@v_total - 1)) then
                leave getAgend;
            end if;
            
            #build select
            set @v_sqlSelect = 'select id,name ';
            set @v_sqlSelect = concat(@v_sqlSelect,'into @id,@name ');
            set @v_sqlSelect = concat(@v_sqlSelect,'from table ');
            set @v_sqlSelect = concat(@v_sqlSelect,'where ');
            set @v_sqlSelect = concat(@v_sqlSelect,v_where);
            set @v_sqlSelect = concat(@v_sqlSelect,' order by id asc limit ',v_cont,',1'); 
            
            #Execute query
            PREPARE stmt_select FROM @v_sqlSelect;
            EXECUTE stmt_select;
            DEALLOCATE PREPARE stmt_select;   
            
            #Do anything with the data @id, @name
            update table1 set desc1 = @name where id1 = @id;
            
            #Next record
            set v_cont = v_cont + 1;
            
        end loop navRecords;
        
    end if;

END

【讨论】:

【参考方案2】:

这个帖子对我帮助很大,所以这是我关于如何使用一个表中的值通过查询迭代它并存储到第二个表或视图的答案。

DROP PROCEDURE IF EXISTS my_dynamic_proc;

DELIMITER //
CREATE PROCEDURE my_dynamic_proc()
BEGIN
    DECLARE Surname varchar(255);
    DECLARE done BOOLEAN DEFAULT FALSE;
    -- Cursor definition
    DECLARE cur1 CURSOR FOR SELECT distinct Surname_values FROM some_clientdata_table;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
    -- Open cursor
    OPEN cur1;
    read_loop: LOOP
      FETCH cur1 INTO Surname;
      IF done THEN
        LEAVE read_loop;
      END IF;
      -- Use every result in a dynamic update
    INSERT into new_table (column1, column2, column3) 
    -- here is jus an example of query that I am using to loop Surname through. Query calculates days between orders
    SELECT T.Surname, T.Date, IFNULL(datediff(T.Date,(select MAX(TT.Date) as days from some_invoicedata_table TT where TT.Date < T.Date and TT.Surname = Surname)),0) from some_invoicedata_table T where T.Surname = Surname;
    -- Note how Surname value is used in WHERE statement to iterate each value through query in stored procedure.
    END LOOP;
  CLOSE cur1;
END//
DELIMITER ;

call my_dynamic_proc();

【讨论】:

【参考方案3】:

不允许使用 DEFINE cur CURSOR FORprepared_statement,您必须定义有效的 SQL 语句。好消息是您可以在稍后可以动态创建的视图上定义光标。比如……

DROP PROCEDURE IF EXISTS my_dynamic_proc;
DELIMITER //
CREATE PROCEDURE my_dynamic_proc(tablename varchar(64), fieldname varchar(64), country VARCHAR(64))
BEGIN
    DECLARE adr_value varchar(500);
    DECLARE done BOOLEAN DEFAULT FALSE;
    -- Cursor definition
    DECLARE cur1 CURSOR FOR SELECT address FROM tmp_view_address;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
    -- Dynamic view definition and creation
    SET @v = concat('CREATE OR REPLACE VIEW tmp_view_address as SELECT `',fieldname,'` as address FROM ',tablename,' WHERE country_name = "',country,'" group by 1 order by count(1) desc');
    PREPARE stm FROM @v;
    EXECUTE stm;
    DEALLOCATE PREPARE stm;
    -- Open cursor
    OPEN cur1;
    read_loop: LOOP
      FETCH cur1 INTO adr_value;
      IF done THEN
        LEAVE read_loop;
      END IF;
      -- Basic output result
      SELECT concat("My address is ",adr_value);
      -- Use every result in a dynamic update
      SET @u = concat('update ',tablename,' set new_field_address = "',adr_value,'" where country_name = "',country,'" and new_field_address is null');
      PREPARE stm FROM @u;
      EXECUTE stm;
      DEALLOCATE PREPARE stm;
  END LOOP;
  CLOSE cur1;
END//
DELIMITER ;

【讨论】:

它确实是一个救生员。非常感谢您提供 100% 的我想要的东西,这就像一个魅力,在任何地方都没有任何麻烦!干杯! :)【参考方案4】:

因为您不能对游标使用动态查询,因为您不能在 DECLARE 之前使用 SET。您也不能将存储过程CALLCURSOR FOR 一起使用

DECLARE cursor_name CURSOR FOR select_statement

CALL 不是 select_statement

作为一种解决方法: 您应该创建 3 个程序,而不是只创建 1 个。

    临时表/视图生成器 编写一个存储过程来为您的动态查询生成临时表或视图。 计算结果 您当前的过程将使用 CURSOR FOR SELECT FROM 临时表。但是您应该确保首先运行临时表/视图过程 - 以获得更新的结果。而且您不能在DECLARE 光标之前使用CALL 过程。这就是您需要第三步的原因。 一起跑步 CALL 生成临时表/视图的过程的最终存储过程,然后 CALL 用于计算结果的预期过程。最后,您应该使用最后一个过程作为执行结果的过程。

【讨论】:

【参考方案5】:

karni 的方法不那么麻烦。创建两个或多个 SP 以满足每个条件分支(每个条件分支都需要动态 sql)。创建一个包装器 SP 并将此 SP 的调用扇出到“分支”SP。

“准备好的视图”方法的替代方法在运行过程时需要更多的 CPU 周期和内存以及额外的磁盘空间。

【讨论】:

【参考方案6】:

我在您的脚本中发现 2 个可能的问题:

1) "DECLARE cursor2 CURSOR FOR stmt;"可能需要与所有其他声明一起移动到过程的顶部,在任何可执行语句之前。

2) 游标不能基于动态 SQL(即我认为您不能在准备好的语句上构建它)。要解决此限制,您可以基于视图声明游标,然后在打开游标之前使用动态 SQL 创建视图。这种方法的问题是视图是公共的——游标声明必须有一个固定的视图名称,因此多个并发用户可能会无意中看到其他人动态定义的视图。我的解决方法是检查视图是否存在并延迟执行过程,直到视图被删除。这意味着为了在繁忙的环境中可行,您应该创建视图,遍历光标,然后尽快放下视图。在技​​术上并不优雅,但这种方法在我的低流量情况下有效,并且避免了临时表的开销。或者,正如其他人所建议的那样,临时表是线程安全的,但可能会影响性能。

【讨论】:

【参考方案7】:

创建另一个过程并在这个新过程中编写游标的代码,然后从你想要声明游标的地方调用过程......

【讨论】:

这真的是最好的解决方案吗?太麻烦了!

以上是关于在 MySQL 中使用动态 SQL 创建游标的主要内容,如果未能解决你的问题,请参考以下文章

动态 SQL 和游标相关问题

oracle游标的使用

Oracle 游标简介

在 PL/SQL 过程中打开动态表名的游标

从动态 SQL 打开游标时出错

Mysql从入门到入魔——9. 游标高级SQL特性