今日は、RECURSIVEで再帰の罠にかかったの日。
目次
とある日
RECURSIVEを使ってみたかった。
ただそれだけの理由。
RECURSIVEとは
RECURSIVEとはを知る前に、WITH句について知る必要があります。
このWITH句は便利なので多用してます。
WITH(共通テーブル式)
共通テーブル式(CTE)は、単一のステートメントのスコープ内に存在し、後でそのステートメント内で、場合によっては複数回参照できる、名前付きの一時的な結果セットです。次の説明では、CTEを使用するステートメントの記述方法について説明します。
WITH cte1 AS (SELECT a, b FROM table1), cte2 AS (SELECT c, d FROM table2) SELECT b, d FROM cte1 JOIN cte2 WHERE cte1.a = cte2.c;
FROM句
でテーブルを指定するときにごちゃごちゃせずに事前に必要テーブルを指定できる。
RECURSIVE(再帰共通テーブル式)
WITH RECURSIVE cte (n) AS ( SELECT 1 UNION ALL SELECT n + 1 FROM cte WHERE n < 5 ) SELECT * FROM cte;
結果↓
+------+ | n | +------+ | 1 | | 2 | | 3 | | 4 | | 5 | +------+
UNIONでしたのクエリではWITH句で指定したcte
を指定しています。
このように、再帰的に呼び出すことができる。
これで何ができるかというと、木構造になっているものを検索することができるのです。
実践
OracleのデフォルトテーブルのEmployees
を使って実験したいと思います。
型など不要な制約は少し変更しています。
テーブル定義
お好きなだけデータを生成してください。
CREATE TABLE employees ( employee_id int , first_name VARCHAR(20) , last_name VARCHAR(25) NOT NULL , email VARCHAR(25) NOT NULL , phone_number VARCHAR(20) , hire_date DATE NOT NULL , job_id VARCHAR(10) NOT NULL , salary int , commission_pct int , manager_id int , department_id int ) ; ALTER TABLE employees ADD ( CONSTRAINT emp_emp_id_pk PRIMARY KEY (employee_id) ) ; INSERT INTO employees VALUES ( 100 , 'Steven' , 'King' , 'SKING' , '515.123.4567' , str_str_to_date('17-JUN-1987', '%d-%b-%Y') , 'AD_PRES' , 24000 , NULL , NULL , 90 ); INSERT INTO employees VALUES ( 101 , 'Neena' , 'Kochhar' , 'NKOCHHAR' , '515.123.4568' , str_to_date('21-SEP-1989', '%d-%b-%Y') , 'AD_VP' , 17000 , NULL , 100 , 90 ); INSERT INTO employees VALUES ( 102 , 'Lex' , 'De Haan' , 'LDEHAAN' , '515.123.4569' , str_to_date('13-JAN-1993', '%d-%b-%Y') , 'AD_VP' , 17000 , NULL , 100 , 90 ); INSERT INTO employees VALUES ( 103 , 'Alexander' , 'Hunold' , 'AHUNOLD' , '590.423.4567' , str_to_date('03-JAN-1990', '%d-%b-%Y') , 'IT_PROG' , 9000 , NULL , 102 , 60 ); INSERT INTO employees VALUES ( 104 , 'Bruce' , 'Ernst' , 'BERNST' , '590.423.4568' , str_to_date('21-MAY-1991', '%d-%b-%Y') , 'IT_PROG' , 6000 , NULL , 103 , 60 ); INSERT INTO employees VALUES ( 105 , 'David' , 'Austin' , 'DAUSTIN' , '590.423.4569' , str_to_date('25-JUN-1997', '%d-%b-%Y') , 'IT_PROG' , 4800 , NULL , 103 , 60 ); INSERT INTO employees VALUES ( 106 , 'Valli' , 'Pataballa' , 'VPATABAL' , '590.423.4560' , str_to_date('05-FEB-1998', '%d-%b-%Y') , 'IT_PROG' , 4800 , NULL , 103 , 60 ); INSERT INTO employees VALUES ( 107 , 'Diana' , 'Lorentz' , 'DLORENTZ' , '590.423.5567' , str_to_date('07-FEB-1999', '%d-%b-%Y') , 'IT_PROG' , 4200 , NULL , 103 , 60 ); INSERT INTO employees VALUES ( 108 , 'Nancy' , 'Greenberg' , 'NGREENBE' , '515.124.4569' , str_to_date('17-AUG-1994', '%d-%b-%Y') , 'FI_MGR' , 12000 , NULL , 101 , 100 ); INSERT INTO employees VALUES ( 109 , 'Daniel' , 'Faviet' , 'DFAVIET' , '515.124.4169' , str_to_date('16-AUG-1994', '%d-%b-%Y') , 'FI_ACCOUNT' , 9000 , NULL , 108 , 100 ); INSERT INTO employees VALUES ( 110 , 'John' , 'Chen' , 'JCHEN' , '515.124.4269' , str_to_date('28-SEP-1997', '%d-%b-%Y') , 'FI_ACCOUNT' , 8200 , NULL , 108 , 100 );
mysql> show create table employees\G; *************************** 1. row *************************** Table: employees Create Table: CREATE TABLE `employees` ( `employee_id` int NOT NULL, `first_name` varchar(20) DEFAULT NULL, `last_name` varchar(25) NOT NULL, `email` varchar(25) NOT NULL, `phone_number` varchar(20) DEFAULT NULL, `hire_date` date NOT NULL, `job_id` varchar(10) NOT NULL, `salary` int DEFAULT NULL, `commission_pct` int DEFAULT NULL, `manager_id` int DEFAULT NULL, `department_id` int DEFAULT NULL, PRIMARY KEY (`employee_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci 1 row in set (0.00 sec)
再帰共通テーブル
RECURSIVEを使って何をするかというと、特定のemployee_id
を指定してそれに付随する部下を取得するクエリを作成します。
employee_id
が自分のID
そして、manager_id
が上司のIDとなっている。
先程の再帰共通テーブルの例に基づいて、UNION
の上側では基底となるemployee_id
を指定する。
下側では、cte
を再帰に呼び出してWHERE句
で比較する。
WITH RECURSIVE cte AS ( SELECT employee_id, first_name, manager_id FROM employees WHERE employee_id = 100 UNION ALL SELECT t1.employee_id, t1.first_name, t1.manager_id FROM employees as t1, cte WHERE cte.employee_id = t1.employee_id ) SELECT * FROM cte;
SQLエラー [3636] 発生
先程のクエリを実行するとエラー発生した。
SQLエラー [3636] [HY000]: Recursive query aborted after 2 iterations. Try increasing @@cte_max_recursion_depth to a larger value.
だがしかし、発生理由は把握済みである。
詳しくは、下記の記事をご覧ください。
簡潔に内容を述べると、再帰呼び出しに制限が存在する。
それを、超えてしまったのが原因。
ってことで、MAX値を更新。
set @@session.cte_max_recursion_depth = 10000000000; SELECT @@session.cte_max_recursion_depth; @@session.cte_max_recursion_depth| ---------------------------------| 4294967295|
比較IDミス
そして再度実行。
すると、永遠と終わらない。
変だと思い、SQLを見直したら・・・。
WITH RECURSIVE cte AS ( SELECT employee_id, first_name, manager_id FROM employees WHERE employee_id = 108 UNION ALL SELECT t1.employee_id, t1.first_name, t1.manager_id FROM employees as t1, cte WHERE cte.employee_id = t1.employee_id ) SELECT * FROM cte;
上司をするはずの下側SELECT句
のWHERE句
にt1.employee_id
を指定していた。
つまり、ずっと同じIDを呼び出しては比較してを繰り返していると思われる。
なので、cte_max_recursion_depth
の上限に引っかかりエラーが発生した。
SQL修正
比較IDをmanager_id
に修正した。
WITH RECURSIVE cte AS ( SELECT employee_id, first_name, manager_id FROM employees WHERE employee_id = 108 UNION ALL SELECT t1.employee_id, t1.first_name, t1.manager_id FROM employees as t1, cte WHERE cte.employee_id = t1.manager_id ) SELECT * FROM cte;
結果↓
+-------------+------------+------------+ | employee_id | first_name | manager_id | +-------------+------------+------------+ | 108 | Nancy | 101 | | 109 | Daniel | 108 | | 110 | John | 108 | +-------------+------------+------------+ 3 rows in set (0.00 sec)
これで意図した部下の社員IDを取得できた。
〆
便利な世の中になりましたね。
再帰呼び出しでよくやりがちなミスを犯してしまった。
5分ぐらい待ったが終わらないのでおかしいと思った。
今度からは気をつけようと思った。
他にも使ったことのない機能などを試していきMySQLに詳しくなりたいと思う今日このごろ。
参考記事
http://www.matsubarasystems.com/mysql/mysql-to_char
https://download.oracle.com/oll/tutorials/DBXETutorial/html/module2/les02_load_data_sql.htm
https://dev.mysql.com/doc/refman/8.0/en/with.html
https://yoku0825.blogspot.com/2018/04/mysql-80ctewith-recursive1000.html