# 游标

在本教程中，您将学习如何在存储过程中使用MySQL游标来遍历`SELECT`语句返回的结果集。

## MySQL游标简介

要处理[存储过程](http://www.yiibai.com/mysql/stored-procedure.html)中的结果集，请使用游标。游标允许您迭代查询返回的一组行，并相应地处理每行。

MySQL游标为只读，不可滚动和敏感。

* **只读**：无法通过光标更新基础表中的数据。
* **不可滚动**：只能按照[SELECT](http://www.yiibai.com/mysql/select-statement-query-data.html)语句确定的顺序获取行。不能以相反的顺序获取行。 此外，不能跳过行或跳转到结果集中的特定行。
* **敏感**：有两种游标：敏感游标和不敏感游标。敏感游标指向实际数据，不敏感游标使用数据的临时副本。敏感游标比一个不敏感的游标执行得更快，因为它不需要临时拷贝数据。但是，对其他连接的数据所做的任何更改都将影响由敏感游标使用的数据，因此，如果不更新敏感游标所使用的数据，则更安全。 MySQL游标是敏感的。

您可以在[存储过程](http://www.yiibai.com/mysql/stored-procedure.html)，[存储函数](http://www.yiibai.com/mysql/stored-function.html)和[触发器](http://www.yiibai.com/mysql/triggers.html)中使用MySQL游标。

## 使用MySQL游标

首先，必须使用`DECLARE`语句声明游标：

```sql
DECLARE cursor_name CURSOR FOR SELECT_statement;
```

游标声明必须在[变量](http://www.yiibai.com/mysql/variables-in-stored-procedures.html)声明之后。如果在变量声明之前声明游标，MySQL将会发出一个错误。游标必须始终与`SELECT`语句相关联。

接下来，使用`OPEN`语句打开游标。`OPEN`语句初始化游标的结果集，因此您必须在从结果集中提取行之前调用`OPEN`语句。

```sql
OPEN cursor_name;
```

然后，使用`FETCH`语句来检索光标指向的下一行，并将光标移动到结果集中的下一行。

```sql
FETCH cursor_name INTO variables list;
```

之后，可以检查是否有任何行记录可用，然后再提取它。

最后，调用`CLOSE`语句来停用光标并释放与之关联的内存，如下所示：

```sql
CLOSE cursor_name;
```

当光标不再使用时，应该关闭它。

当使用MySQL游标时，还必须声明一个`NOT FOUND`处理程序来处理当游标找不到任何行时的情况。 因为每次调用`FETCH`语句时，游标会尝试读取结果集中的下一行。 当光标到达结果集的末尾时，它将无法获得数据，并且会产生一个条件。 处理程序用于处理这种情况。

要声明一个`NOT FOUND`处理程序，参考以下语法：

```sql
DECLARE CONTINUE HANDLER FOR NOT FOUND SET finished = 1;
```

`finished`是一个变量，指示光标到达结果集的结尾。请注意，处理程序声明必须出现在存储过程中的变量和游标声明之后。

下图说明了MySQL游标如何工作。

![img](https://704912985-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LyNuWjP9GN-Sj39sEjc%2F-LyNuhnSZm-HQxKHgcTJ%2F-LyNv2CMxtO2bExMdfr7%2F764170849_82012.png?generation=1578818632511630\&alt=media)

## MySQL游标示例

为了更好地演示，我们将开发一个存储过程，来获取MySQL[示例数据库(yiibaidb)](http://www.yiibai.com/mysql/sample-database.html)中`employees`表中所有员工的电子邮件列表。

首先，声明一些变量，一个用于循环员工电子邮件的游标和一个`NOT FOUND`处理程序：

```sql
DECLARE finished INTEGER DEFAULT 0;
DECLARE email varchar(255) DEFAULT "";

-- declare cursor for employee email
DEClARE email_cursor CURSOR FOR 
 SELECT email FROM employees;

-- declare NOT FOUND handler
DECLARE CONTINUE HANDLER 
FOR NOT FOUND SET finished = 1;
```

接下来，使用`OPEN`语句打开`email_cursor`：

```sql
OPEN email_cursor;
```

然后，迭代电子邮件列表，并使用分隔符(`;`)[连接](http://www.yiibai.com/mysql/sql-concat-in-mysql.html)每个电子邮件：

```sql
get_email: LOOP
 FETCH email_cursor INTO v_email;
 IF v_finished = 1 THEN 
 LEAVE get_email;
 END IF;
 -- build email list
 SET email_list = CONCAT(v_email,";",email_list);
END LOOP get_email;
```

之后，在循环中，使用`v_finished`变量来检查列表中是否有任何电子邮件来终止循环。

最后，使用`CLOSE`语句关闭游标：

```sql
CLOSE email_cursor;
```

`build_email_list`存储过程所有代码如下：

```sql
DELIMITER $$

CREATE PROCEDURE build_email_list (INOUT email_list varchar(4000))
BEGIN

 DECLARE v_finished INTEGER DEFAULT 0;
        DECLARE v_email varchar(100) DEFAULT "";

 -- declare cursor for employee email
 DEClARE email_cursor CURSOR FOR 
 SELECT email FROM employees;

 -- declare NOT FOUND handler
 DECLARE CONTINUE HANDLER 
        FOR NOT FOUND SET v_finished = 1;

 OPEN email_cursor;

 get_email: LOOP

 FETCH email_cursor INTO v_email;

 IF v_finished = 1 THEN 
 LEAVE get_email;
 END IF;

 -- build email list
 SET email_list = CONCAT(v_email,";",email_list);

 END LOOP get_email;

 CLOSE email_cursor;

END$$

DELIMITER ;
```

可以使用以下脚本测试`build_email_list`存储过程：

```sql
SET @email_list = "";
CALL build_email_list(@email_list);
SELECT @email_list;
```

> 注：由于内容比较长，这里就不放上输出结果了。

在本教程中，我们向您展示了如何使用MySQL游标来迭代结果集并相应地处理每一行。
