同事問我MySQL怎么遞歸查詢,我懵逼了 - zbs666 - 博客園 (cnblogs.com)
MySQL 遞歸查詢
可以看到,Oracle 實現遞歸查詢非常的方便。但是,在 MySQL 中並沒有幫我們處理,因此需要我們自己手動實現遞歸查詢。為了方便,我們創建一個部門表,並插入幾條可以形成遞歸關系的數據。|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
`id` varchar(
10
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`name` varchar(
255
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`pid` varchar(
10
) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1000'
,
'總公司'
, NULL);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1001'
,
'北京分公司'
,
'1000'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1002'
,
'上海分公司'
,
'1000'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1003'
,
'北京研發部'
,
'1001'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1004'
,
'北京財務部'
,
'1001'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1005'
,
'北京市場部'
,
'1001'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1006'
,
'北京研發一部'
,
'1003'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1007'
,
'北京研發二部'
,
'1003'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1008'
,
'北京研發一部一小組'
,
'1006'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1009'
,
'北京研發一部二小組'
,
'1006'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1010'
,
'北京研發二部一小組'
,
'1007'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1011'
,
'北京研發二部二小組'
,
'1007'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1012'
,
'北京市場一部'
,
'1005'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1013'
,
'上海研發部'
,
'1002'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1014'
,
'上海研發一部'
,
'1013'
);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES (
'1015'
,
'上海研發二部'
,
'1013'
);
|
沒錯,剛才 Oracle 遞歸,就是用的這張表。
另外,在這之前,我們需要復習一下幾個 MYSQL中的函數,后續會用到。
find_in_set 函數
函數語法:find_in_set(str,strlist)str 代表要查詢的字符串 , strlist 是一個以逗號分隔的字符串,如 ('a,b,c')。此函數用於查找 str 字符串在字符串 strlist 中的位置,返回結果為 1 ~ n 。若沒有找到,則返回0。舉個栗子:|
1
|
select FIND_IN_SET(
'b'
,
'a,b,c,d'
);
|
結果返回 2 。因為 b 所在位置為第二個子串位置。
此外,在對表數據進行查詢時,它還有一種用法,如下:
|
1
|
select * from dept where FIND_IN_SET(id,
'1000,1001,1002'
);
|
結果返回所有 id 在 strlist 中的記錄,即 id = '1000' ,id = '1001' ,id = '1002' 三條記錄。
看到這,對於我們要解決的遞歸查詢,不知道你有什么啟發沒。以向下遞歸查詢所有子節點為例。我想,是不是可以找到一個包含當前節點和所有子節點的以逗號拼接的字符串 strlist,傳進 find_in_set 函數。就可以查詢出所有需要的遞歸數據了。那么,現在問題就轉化為怎樣構造這樣的一個字符串 strlist 。這就需要用到以下字符串拼接函數了。
concat,concat_ws,group_concat 函數
一、字符串拼接函數中,最基本的就是 concat 了。它用於連接N個字符串,如,|
1
|
select CONCAT(
'M'
,
'Y'
,
'S'
,
'Q'
,
'L'
) from dual;
|
結果為 'MYSQL' 字符串。
二、concat 是以逗號為默認的分隔符,而 concat_ws 則可以指定分隔符,第一個參數傳入分隔符,如以下划線分隔。
三、group_concat 函數更強大,可以分組的同時,把字段以特定分隔符拼接成字符串。用法:group_concat( [distinct] 要連接的字段 [order by 排序字段 asc/desc ] [separator '分隔符'] )可以看到有可選參數,可以對將要拼接的字段值去重,也可以排序,指定分隔符。若沒有指定,默認以逗號分隔。對於 dept 表,我們可以把表中的所有 id 以逗號拼接。(這里沒有用到 group by 分組字段,則可以認為只有一組)
MySQL 自定義函數,實現遞歸查詢
可以發現以上已經把字符串拼接的問題也解決了。那么,問題就變成怎樣構造有遞歸關系的字符串了。我們可以自定義一個函數,通過傳入根節點id,找到它的所有子節點。 以向下遞歸為例。 (講解自定義函數寫法的同時,講解遞歸邏輯)|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
delimiter $$
drop function
if
exists get_child_list$$
create function get_child_list(in_id varchar(
10
)) returns varchar(
1000
)
begin
declare ids varchar(
1000
)
default
''
;
declare tempids varchar(
1000
);
set tempids = in_id;
while
tempids is not
null
do
set ids = CONCAT_WS(
','
,ids,tempids);
select GROUP_CONCAT(id) into tempids from dept where FIND_IN_SET(pid,tempids)>
0
;
end
while
;
return
ids;
end
$$
delimiter ;
|
(2)drop function if exists get_child_list$$ 。若函數 get_child_list 已經存在了,則先刪除它。注意這里需要用 當前自定義的結束符 $$ 來結束並執行語句。 因為,這里需要和下邊的函數體單獨區分開來。
(3)create function get_child_list 創建函數。並且參數傳入一個根節點的子節點id,需要注意一定要注明參數的類型和長度,如這里是 varchar(10)。returns varchar(1000) 用來定義返回值參數類型。
(4)begin 和 end 中間包圍的就是函數體。用來寫具體的邏輯。
(5)declare 用來聲明變量,並且可以用 default 設置默認值。這里定義的 ids 即作為整個函數的返回值,是用來拼接成最終我們需要的以逗號分隔的遞歸串的。而 tempids 是為了記錄下邊 while 循環中臨時生成的所有子節點以逗號拼接成的字符串。
(6) set 用來給變量賦值。此處把傳進來的根節點賦值給 tempids 。
(7) while do ... end while; 循環語句,循環邏輯包含在內。注意,end while 末尾需要加上分號。循環體內,先用 CONCAT_WS 函數把最終結果 ids 和 臨時生成的 tempids 用逗號拼接起來。然后以 FIND_IN_SET(pid,tempids)>0 為條件,遍歷在 tempids 中的所有 pid ,尋找以此為父節點的所有子節點 id ,並且通過 GROUP_CONCAT(id) into tempids 把這些子節點 id 都用逗號拼接起來,並覆蓋更新 tempids 。等下次循環進來時,就會再次拼接 ids ,並再次查找所有子節點的所有子節點。循環往復,一層一層的向下遞歸遍歷子節點。直到判斷 tempids 為空,說明所有子節點都已經遍歷完了,就結束整個循環。這里,用 '1000' 來舉例,即是:(參看圖1的表數據關系)
(8)return ids; 用於把 ids 作為函數返回值返回。
|
1
2
3
4
5
6
7
8
9
10
|
第一次循環:
tempids=
1000
ids=
1000
tempids=
1001
,
1002
(
1000
的所有子節點)
第二次循環:
tempids=
1001
,
1002
ids=
1000
,
1001
,
1002
tempids=
1003
,
1004
,
1005
,
1013
(
1001
和
1002
的所有子節點)
第三次循環:
tempids=
1003
,
1004
,
1005
,
1013
ids=
1000
,
1001
,
1002
,
1003
,
1004
,
1005
,
1013
tempids=
1003
和
1004
和
1005
及
1013
的所有子節點
...
最后一次循環,因找不到子節點,tempids=
null
,就結束循環。
|
(9)函數體結束以后,記得用結束符 $$ 來結束整個邏輯,並執行。
(10)最后別忘了,把結束符重新設置為默認的結束符分號 。自定義函數做好之后,我們就可以用它來遞歸查詢我們需要的數據了。如,我查詢北京研發部的所有子節點。
以上是向下遞歸查詢所有子節點的,並且包括了當前節點,也可以修改邏輯為不包含當前節點,我就不演示了。
查找北京研發二部一小組,以及它的遞歸父節點,如下:
注意,單位是字節,不是字符。在 MySQL 中,單個字母占1個字節,而我們平時用的 utf-8下,一個漢字占3個字節。這個對於遞歸查詢還是非常致命的。因為一般遞歸的話,關系層級都比較深,很有可能超過最大長度。(盡管一般拼接的都是數字字符串,即單字節)所以,我們有兩種方法解決這個問題:
手動實現遞歸查詢(向上遞歸)
相對於向下遞歸來說,向上遞歸比較簡單。因為向下遞歸時,每一層遞歸一個父節點都對應多個子節點。而向上遞歸時,每一層遞歸一個子節點只對應一個父節點,關系比較單一。同樣的,我們可以定義一個函數 get_parent_list 來獲取根節點的所有父節點。|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
delimiter $$
drop function
if
exists get_parent_list$$
create function get_parent_list(in_id varchar(
10
)) returns varchar(
1000
)
begin
declare ids varchar(
1000
);
declare tempid varchar(
10
);
set tempid = in_id;
while
tempid is not
null
do
set ids = CONCAT_WS(
','
,ids,tempid);
select pid into tempid from dept where id=tempid;
end
while
;
return
ids;
end
$$
delimiter ;
|
注意事項
我們用到了 group_concat 函數來拼接字符串。但是,需要注意它是有長度限制的,默認為 1024 字節。可以通過show variables like "group_concat_max_len"; 來查看。
注意,單位是字節,不是字符。在 MySQL 中,單個字母占1個字節,而我們平時用的 utf-8下,一個漢字占3個字節。這個對於遞歸查詢還是非常致命的。因為一般遞歸的話,關系層級都比較深,很有可能超過最大長度。(盡管一般拼接的都是數字字符串,即單字節)所以,我們有兩種方法解決這個問題:
- 修改 MySQL 配置文件 my.cnf ,增加
group_concat_max_len = 102400 #你要的最大長度。 - 執行以下任意一個語句。
SET GLOBAL group_concat_max_len=102400;或者SET SESSION group_concat_max_len=102400;他們的區別在於,global是全局的,任意打開一個新的會話都會生效,但是注意,已經打開的當前會話並不會生效。而 session 是只會在當前會話生效,其他會話不生效。共同點是,它們都會在 MySQL 重啟之后失效,以配置文件中的配置為准。所以,建議直接修改配置文件。102400 的長度一般也夠用了。假設一個id的長度為10個字節,也能拼上一萬個id了。
本來只想查5條數據來拼接,現在不生效了。不過,如果需要的話,可以通過子查詢來實現,
