返回

MySQL Error 2014 解决:存储过程 Commands out of sync 详解

mysql

解决 MySQL Error #2014:“Commands out of sync” 调用存储过程时出错

写存储过程的时候,有时会碰到些奇奇怪怪的问题。这次我们来聊聊一个比较常见的报错:MySQL error #2014 - Commands out of sync; you can't run this command now。特别是在用 phpMyAdmin 这类工具调用存储过程时,更容易踩到这个坑。

问题来了:调用存储过程,MySQL 报错 #2014

假设你写了个存储过程,类似下面这样(这是用户在原始问题中提供的代码):

-- 定义新的分隔符
DELIMITER //

CREATE PROCEDURE CreateUpdateCarrierRange (
    IN idCarrier INT,
    IN idZone INT,
    IN iWeightMin DECIMAL,
    IN iWeightMax DECIMAL,
    IN price DECIMAL,
    IN display INT
)
BEGIN
    -- 声明并初始化一个用户变量
    SET @iCount :=  0;
    
    -- 【问题点 1】这里有一个 SELECT 语句,会产生一个结果集
    SELECT @iCount, idCarrier, iWeightMin, iWeightMax; 
    
    -- 查询指定条件下 range_weight 表中的记录数,并将结果赋给 @iCount
    -- 这个 SELECT ... INTO 不会直接给客户端返回结果集
    SELECT @iCount := COUNT(RANGE_WEIGHT.id_carrier)
    FROM range_weight as RANGE_WEIGHT
    WHERE
        RANGE_WEIGHT.id_carrier = idCarrier 
        AND RANGE_WEIGHT.delimiter1 = iWeightMin 
        AND RANGE_WEIGHT.delimiter2 = iWeightMax;
    
    -- 如果 display 参数为 1,则显示 @iCount 的值
    -- 【问题点 2】这里可能有另一个 SELECT 语句,会产生第二个结果集
    IF display = 1 THEN
        SELECT @iCount as 'Nombre enregistrements range_weight';
    END IF;

END //

-- 恢复默认分隔符
DELIMITER ;

然后,你在 phpMyAdmin 里执行 CALL 语句来调用它:

CALL CreateUpdateCarrierRange(128, 1, 4.000, 5.000, 10.75, 1);

结果,duang!报错了:

Static Analysis :

1 error found during the analysis

Missing expression. (near "ON" at position 25)
SET FOREIGN_KEY_CHECKS = ON;
#2014 - Commands out of sync; you can't run this command now

看到这个报错信息,尤其是那个 SET FOREIGN_KEY_CHECKS = ON;,你可能会一脸懵逼:我的存储过程里根本没写这句啊?这是啥情况?而且,"Commands out of sync" 又是什么意思?

别急,我们来慢慢分析。

刨根问底:为啥会出现 'Commands out of sync'?

这个错误的核心在于 MySQL 客户端和服务器之间的通信协议。简单来说,当你通过一个连接向 MySQL 服务器发送一条命令(比如 SELECTINSERT 或者 CALL)后,你通常需要先完全处理 这条命令返回的所有结果,然后才能发送下一条命令。

如果你发送了一条命令,服务器也开始给你返回数据了,但你还没接收完所有数据(或者还没告诉服务器你已经处理完了),就又急着发送下一条命令,服务器就会拒绝,并抛出 Commands out of sync 错误。因为它觉得:“兄弟,上一单还没结呢,你这又来一单,我处理不过来啊!”

那么,这跟我们上面的存储过程有什么关系呢?

关键就在于存储过程可以返回多个结果集 (Result Sets)

回头看我们的 CreateUpdateCarrierRange 存储过程:

  1. SELECT @iCount, idCarrier, iWeightMin, iWeightMax; 这句会立即产生并返回第一个结果集 给客户端。
  2. IF display = 1 THEN SELECT @iCount ... END IF; 如果 display 参数是 1,这句会产生并返回第二个结果集

当你通过 CALL CreateUpdateCarrierRange(..., 1); 调用时,这个存储过程实际上向客户端发送了两个 结果集。

现在问题来了:很多简单的 MySQL 客户端工具或库(包括 phpMyAdmin 的标准 SQL 执行界面,以及一些编程语言中基础的数据库操作函数)默认只期望或只处理 CALL 语句返回的第一个 结果集。当它们处理完第一个结果集后,可能没有正确地获取或忽略后续的结果集。

就在这个时候,如果客户端(或者像 phpMyAdmin 这样的工具,它在执行完你的 SQL 后可能想自动执行一些清理命令,比如 SET FOREIGN_KEY_CHECKS = ON; 来恢复某些状态)试图发送新的命令,而连接因为之前的存储过程还有未处理的第二个结果集而处于“忙碌”状态,Commands out of sync 错误就出现了。

那个报错信息里提到的 SET FOREIGN_KEY_CHECKS = ON; 很可能就是 phpMyAdmin 在你的 CALL 语句之后,尝试执行的内部命令,结果正好撞上了这个“out of sync”的状态。所以,它本身不是你存储过程的问题,而是这个错误的“受害者”或者说是一个指示信号。

对症下药:搞定 #2014 错误的几种姿势

知道了原因,解决起来就思路清晰了。核心目标是:避免让客户端在未准备好的情况下收到多个结果集,或者让客户端能够正确处理多个结果集。

方案一:只返回最终需要的结果 (推荐)

这是最推荐、也是最符合存储过程设计初衷的方法:让存储过程在内部完成所有计算和逻辑,只在最后返回一个单一的结果集,或者根本不返回结果集(而是通过 OUT/INOUT 参数返回值)

原理与作用:
修改存储过程,移除那些中间调试或过程性的 SELECT 语句,确保整个过程执行完毕后,最多只有一个 SELECT 语句会把结果发送给客户端。

修改后的代码示例:

假设我们最终只想知道那个 @iCount 的值,并且只在 display 为 1 时才需要。

DELIMITER //

DROP PROCEDURE IF EXISTS CreateUpdateCarrierRange_Fixed; -- 如果存在先删除,方便测试
CREATE PROCEDURE CreateUpdateCarrierRange_Fixed (
    IN idCarrier INT,
    IN idZone INT,        -- 虽然过程没用,但保留接口一致性
    IN iWeightMin DECIMAL,
    IN iWeightMax DECIMAL,
    IN price DECIMAL,     -- 同样没用,保留接口
    IN display INT,
    OUT recordCount INT   -- 使用 OUT 参数返回计数
)
BEGIN
    DECLARE iCount INT DEFAULT 0; -- 使用局部变量代替用户变量更规范

    -- 查询记录数并存入局部变量
    SELECT COUNT(RANGE_WEIGHT.id_carrier) INTO iCount
    FROM range_weight as RANGE_WEIGHT
    WHERE
        RANGE_WEIGHT.id_carrier = idCarrier 
        AND RANGE_WEIGHT.delimiter1 = iWeightMin 
        AND RANGE_WEIGHT.delimiter2 = iWeightMax;

    -- 将计算结果赋给 OUT 参数
    SET recordCount = iCount;
    
    -- 如果需要基于 display 参数返回一个结果集,可以这样做:
    -- 注意:即使有 OUT 参数,仍然可以 SELECT 一个结果集。
    -- 但要确保整个过程最多只有一个 SELECT 返回给客户端。
    -- 这里移除了之前的第一个 SELECT,只保留了这个条件 SELECT。
    IF display = 1 THEN
        -- 直接返回计数,列名可以自定义
        SELECT iCount as 'Nombre_enregistrements_range_weight'; 
    END IF;
    
    -- 如果你总是需要计数,但又不想要结果集,可以完全依赖 OUT 参数,
    -- 并注释掉上面的 IF display ... END IF; 部分。

END //

DELIMITER ;

如何调用带 OUT 参数的过程(在 SQL 客户端或 phpMyAdmin):

-- 调用存储过程,@finalCount 会接收 OUT 参数的值
CALL CreateUpdateCarrierRange_Fixed(128, 1, 4.000, 5.000, 10.75, 1, @finalCount);

-- 查看 OUT 参数的值 (如果过程中没有 SELECT 返回结果集)
-- SELECT @finalCount; 

-- 如果过程中 IF display = 1 THEN ... SELECT iCount ... END IF; 被执行了,
-- 那么 CALL 语句本身会显示那个 SELECT 的结果集。
-- 此时 SELECT @finalCount; 仍然可以获取 OUT 参数的值。

解释:
上面修改后的版本 CreateUpdateCarrierRange_Fixed

  1. 移除了第一个无条件的 SELECT @iCount, ... 语句,它会产生不必要的第一个结果集。
  2. 使用了局部变量 iCount 代替用户变量 @iCount,这是存储过程中更推荐的做法。
  3. 保留了根据 display 参数决定是否 SELECT 最终计数的逻辑。这样,最多只有一个结果集被返回。
  4. 还增加了一个 OUT 参数 recordCount。这是一种不需要结果集就能从存储过程获取单个值的方式。调用者可以通过 SELECT @varName 来获取这个值。

这样修改后,CALL 语句要么返回零个结果集(如果 display 不为 1),要么返回一个结果集(如果 display 为 1)。无论哪种情况,都不会有多结果集的问题,Commands out of sync 错误通常就能解决。

进阶使用技巧:
如果你只是想从存储过程获取几个计算好的值,而不是一个表格形式的结果集,那么大量使用 OUTINOUT 参数是更好的选择。这样调用者可以直接获取这些值,而无需处理结果集的读取,代码通常更简洁。

方案二:客户端处理多个结果集

如果修改存储过程不可行(比如它是第三方提供的,或者你有意设计它返回多个结果集),那么就得在调用它的客户端 代码(比如 PHP、Python、Java 等应用代码)中正确处理多个结果集。

原理与作用:
客户端程序需要使用特定的函数或方法来告知数据库驱动库:“嘿,刚才那个命令可能不止一个结果集,你帮我看看还有没有下一个?有的话就给我。”

代码示例 (PHP - 使用 mysqli 扩展):

<?php
$mysqli = new mysqli("hostname", "username", "password", "database");

if ($mysqli->connect_errno) {
    echo "Failed to connect to MySQL: " . $mysqli->connect_error;
    exit();
}

$idCarrier = 128;
$idZone = 1;
$iWeightMin = 4.000;
$iWeightMax = 5.000;
$price = 10.75;
$display = 1;

// 注意:调用存储过程最好使用预处理语句,这里为了简化演示用了 multi_query
// 对于仅调用存储过程,mysqli_query 通常也能工作,但处理多结果集更复杂
$query = "CALL CreateUpdateCarrierRange($idCarrier, $idZone, $iWeightMin, $iWeightMax, $price, $display)";

// 使用 multi_query 执行可能产生多个结果集的语句
if ($mysqli->multi_query($query)) {
    $resultIndex = 0;
    do {
        $resultIndex++;
        echo "--- Result Set #{$resultIndex} ---<br>";
        
        /* 获取第一个结果集 */
        if ($result = $mysqli->store_result()) {
            while ($row = $result->fetch_assoc()) {
                // 处理每一行数据
                print_r($row);
                echo "<br>";
            }
            $result->free(); // 释放结果集内存
        } else {
             // 如果 store_result() 返回 false,检查是否有错误
             if ($mysqli->errno) {
                 echo "Store result error: " . $mysqli->error . "<br>";
             } else {
                 // 可能是没有返回结果集的操作(如 UPDATE, INSERT,或 SELECT INTO)
                 // 或者是一个空的 SELECT 结果
                 echo "No rows in this result set or not a SELECT statement.<br>";
             }
        }
        
        /* 检查是否还有更多的结果集 */
        // more_results() 检查是否还有后续结果集
        // next_result() 移动到下一个结果集,必须调用它才能继续处理
    } while ($mysqli->more_results() && $mysqli->next_result()); 
} else {
     echo "Multi query failed: (" . $mysqli->errno . ") " . $mysqli->error;
}

// 检查 multi_query 之后是否有遗留错误
if ($mysqli->errno) {
    echo "Error after multi_query: (" . $mysqli->errno . ") " . $mysqli->error;
    // 这里很可能会捕捉到 Commands out of sync 如果 next_result 没被正确调用
}

$mysqli->close();

?>

解释:
这段 PHP 代码演示了如何使用 mysqli 扩展来处理可能返回多个结果集的存储过程调用。
关键在于:

  1. 使用 multi_query() (或者 mysqli_query() 结合后续循环处理)。
  2. 在一个 do...while 循环中:
    • 使用 store_result()use_result() 获取当前结果集。
    • 处理完当前结果集(包括遍历所有行并释放 free())。
    • 调用 more_results() 检查是否还有更多结果集。
    • 如果 more_results() 返回 true,必须调用 next_result() 来准备读取下一个结果集。忘记调用 next_result() 是导致在应用代码中出现 Commands out of sync 的常见原因!

重要提示:
这个方案要求你修改的是调用存储过程的应用程序代码 ,而不是在 phpMyAdmin 里直接执行 CALL 语句。如果你主要是在 phpMyAdmin 里遇到问题,这个方案帮助不大,应该优先考虑方案一。

方案三:理解 phpMyAdmin 与多结果集

在 phpMyAdmin 的标准 SQL 输入窗口执行 CALL 语句时,它可能并不擅长优雅地处理存储过程返回的多个结果集。它的设计更偏向于执行单条 SQL 语句并显示其(第一个)结果。

原理与局限:
当你的存储过程返回多个结果集时,phpMyAdmin 可能只显示了第一个,并且没有自动去消耗(读取并丢弃)后续的结果集。这使得 MySQL 连接处于未完成状态。此时,如果 phpMyAdmin 尝试执行任何后续操作(即便是内部状态管理命令),就会触发 Commands out of sync

你能做什么?

  1. 首选方案一: 修改你的存储过程,让它只返回最多一个结果集。这是最根本的解决办法。
  2. 测试环境: 如果你只是想在 phpMyAdmin 里测试存储过程,并且遇到了这个问题,考虑暂时注释掉产生多余结果集的 SELECT 语句,或者像方案一那样修改它。
  3. 接受局限: 认识到 phpMyAdmin 的 SQL 窗口可能不是测试这类返回多结果集的存储过程的最佳场所。真正的调用场景是在你的应用程序代码中(如 PHP、Python 等),应该在应用代码层面使用方案二的方法来正确处理。

基本上,不要指望在 phpMyAdmin 的标准 SQL 执行界面找到一个神奇的设置来完美处理任意存储过程的多结果集问题。问题根源在于存储过程本身的设计或客户端如何处理其返回结果。

好了,关于 MySQL Error #2014 - Commands out of sync 在调用存储过程时出现的原因和解决办法就聊到这里。希望这能帮你搞定这个有点“坑爹”的错误!