วิธีการสร้างแบบสอบถามแบบเรียกซ้ำ MySQL ลำดับชั้น


271

ฉันมีตาราง MySQL ซึ่งเป็นดังนี้:

id | name        | parent_id
19 | category1   | 0
20 | category2   | 19
21 | category3   | 20
22 | category4   | 21
......

ตอนนี้ฉันต้องการมีแบบสอบถาม MySQL เดียวที่ฉันเพียงแค่ใส่รหัส [ตัวอย่างเช่นพูดว่า 'id = 19'] จากนั้นฉันควรจะได้รับรหัสลูกทั้งหมดของมัน ... นอกจากนี้ลำดับชั้นของเด็ก ๆ ยังไม่เป็นที่ทราบว่าสามารถเปลี่ยนแปลงได้

นอกจากนี้ฉันมีวิธีการใช้สำหรับวนลูป ..... ให้ฉันรู้วิธีการบรรลุเดียวกันโดยใช้แบบสอบถาม MySQL เดียวถ้าเป็นไปได้


สมมติว่าลำดับชั้นลึก 7 ระดับ คุณคาดหวังว่าตารางผลลัพธ์จะเป็นอย่างไร
Jonathan Leffler

1
MySQL (ยัง) ไม่สนับสนุนการสืบค้นแบบลำดับชั้น (เช่น DBMS ที่ทันสมัยอื่น ๆ ) คุณจะต้องเขียนขั้นตอนการจัดเก็บหรือใช้รูปแบบที่แตกต่างกัน
a_horse_with_no_name


1
MYSQL 8.0 จะสนับสนุนการสืบค้นแบบเรียกซ้ำโดยใช้ CTE (Common Table Expressions)
3712320

สิ่งที่เกี่ยวกับการรับรายการเต็มของโพสต์เริ่มต้นจาก id ความคิดเห็นล่าสุด? หรือลูกคนสุดท้าย?
โจ

คำตอบ:


392

สำหรับMySQL 8+:ใช้withไวยากรณ์ซ้ำ
สำหรับMySQL 5.x:ใช้ตัวแปรอินไลน์, ID พา ธ หรือการรวมตัวเอง

MySQL 8+

with recursive cte (id, name, parent_id) as (
  select     id,
             name,
             parent_id
  from       products
  where      parent_id = 19
  union all
  select     p.id,
             p.name,
             p.parent_id
  from       products p
  inner join cte
          on p.parent_id = cte.id
)
select * from cte;

parent_id = 19ควรระบุค่าที่ระบุในเป็นidของพาเรนต์ที่คุณต้องการเลือกการสืบทอดทั้งหมด

MySQL 5.x

สำหรับเวอร์ชัน MySQL ที่ไม่สนับสนุน Common Table Expressions (จนถึงรุ่น 5.7) คุณจะได้รับสิ่งนี้โดยใช้แบบสอบถามต่อไปนี้:

select  id,
        name,
        parent_id 
from    (select * from products
         order by parent_id, id) products_sorted,
        (select @pv := '19') initialisation
where   find_in_set(parent_id, @pv)
and     length(@pv := concat(@pv, ',', id))

นี่คือไวโอลิน

ในที่นี้ค่าที่ระบุใน@pv := '19'ควรถูกตั้งค่าเป็นidของพาเรนต์ที่คุณต้องการเลือกการสืบทอดทั้งหมดของ

วิธีนี้จะใช้ได้หากผู้ปกครองมีลูกหลายคน อย่างไรก็ตามจำเป็นต้องให้แต่ละเรคคอร์ดเป็นไปตามเงื่อนไขparent_id < idมิฉะนั้นผลลัพธ์จะไม่สมบูรณ์

การมอบหมายตัวแปรภายในแบบสอบถาม

แบบสอบถามนี้ใช้ไวยากรณ์ MySQL ที่เฉพาะเจาะจง: ตัวแปรถูกกำหนดและแก้ไขระหว่างการดำเนินการ สมมติฐานบางอย่างเกี่ยวกับลำดับของการดำเนินการ:

  • fromประโยคแรกที่ได้รับการประเมิน นั่นคือที่ที่@pvจะเริ่มต้นได้
  • whereข้อคือการประเมินผลสำหรับแต่ละระเบียนในคำสั่งของการดึงจากที่fromนามแฝง ดังนั้นนี่คือที่ที่เงื่อนไขถูกใส่เพื่อรวมเฉพาะเร็กคอร์ดที่พาเรนต์ถูกระบุว่าอยู่ในทรีลูกหลาน (ลูกหลานของพาเรนต์หลักจะถูกเพิ่มไป@pvเรื่อย ๆ )
  • เงื่อนไขในwhereข้อนี้มีการประเมินตามลำดับและการประเมินผลจะถูกขัดจังหวะเมื่อผลรวมแน่นอน ดังนั้นเงื่อนไขที่สองจะต้องอยู่ในตำแหน่งที่สองเนื่องจากมันจะเพิ่มidลงในรายการพาเรนต์และสิ่งนี้จะเกิดขึ้นก็ต่อเมื่อidผ่านเงื่อนไขแรก lengthฟังก์ชั่นที่เรียกว่าเพียงเพื่อให้แน่ใจว่าสภาพเช่นนี้เป็นความจริงเสมอแม้ว่าpvสตริงจะด้วยเหตุผลบางอย่างให้ผลคุ้มค่า falsy

ทั้งหมดในทุกคนอาจพบว่าสมมติฐานเหล่านี้เสี่ยงเกินไปที่จะพึ่งพา เอกสารเตือน:

คุณอาจได้รับผลลัพธ์ตามที่คาดหวัง แต่สิ่งนี้ไม่รับประกัน [... ] ลำดับการประเมินผลสำหรับนิพจน์ที่เกี่ยวข้องกับตัวแปรผู้ใช้นั้นไม่ได้กำหนดไว้

ดังนั้นแม้ว่าจะใช้งานได้อย่างสอดคล้องกับแบบสอบถามด้านบนลำดับการประเมินอาจยังเปลี่ยนแปลงเช่นเมื่อคุณเพิ่มเงื่อนไขหรือใช้แบบสอบถามนี้เป็นมุมมองหรือแบบสอบถามย่อยในแบบสอบถามที่มีขนาดใหญ่ขึ้น มันเป็น "คุณสมบัติ" ที่จะถูกลบออกในรุ่น MySQL ในอนาคต :

รุ่นก่อนหน้านี้ของ MySQL ทำให้มันเป็นไปได้ที่จะกำหนดค่าให้กับตัวแปรของผู้ใช้ในงบอื่น ๆ SETกว่า ฟังก์ชั่นนี้รองรับใน MySQL 8.0 สำหรับความเข้ากันได้แบบย้อนหลัง แต่ขึ้นอยู่กับการลบใน MySQL รุ่นใหม่ในอนาคต

ตามที่ระบุไว้ข้างต้นจาก MySQL 8.0 เป็นต้นไปคุณควรใช้withไวยากรณ์ซ้ำ

อย่างมีประสิทธิภาพ

สำหรับชุดข้อมูลที่มีขนาดใหญ่มากโซลูชันนี้อาจช้าลงเนื่องจากการfind_in_setดำเนินการไม่ใช่วิธีที่ดีที่สุดในการค้นหาตัวเลขในรายการแน่นอนไม่อยู่ในรายการที่มีขนาดเท่ากับลำดับความสำคัญเท่ากับจำนวนระเบียนที่ส่งคืน

ทางเลือกที่ 1: with recursive,connect by

ฐานข้อมูลมากขึ้นใช้SQL มาตรฐาน ISO WITH [RECURSIVE]ไวยากรณ์สำหรับการสอบถามซ้ำ (เช่นPostgres 8.4+ , SQL Server 2005+ , DB2 , Oracle 11gR2 + , SQLite 3.8.4+ , Firebird 2.1+ , H2 , HyperSQL 2.1.0+ , Teradata , MariaDB 10.2.2+ ) และในเวอร์ชั่น 8.0 นั้น MySQL ก็รองรับเช่นกัน ดูด้านบนของคำตอบนี้สำหรับไวยากรณ์ที่จะใช้

ฐานข้อมูลบางตัวมีไวยากรณ์ที่เป็นทางเลือกและไม่ใช่มาตรฐานสำหรับการค้นหาแบบลำดับชั้นเช่นส่วนCONNECT BYคำสั่งที่มีอยู่ในOracle , DB2 , Informix , CUBRIDและฐานข้อมูลอื่น ๆ

MySQL เวอร์ชั่น 5.7 ไม่มีคุณสมบัติดังกล่าว เมื่อเอ็นจิ้นฐานข้อมูลของคุณมีไวยากรณ์นี้หรือคุณสามารถโยกย้ายไปยังที่ทำอยู่นั่นเป็นทางเลือกที่ดีที่สุด ถ้าไม่เช่นนั้นให้พิจารณาทางเลือกต่อไปนี้

ทางเลือก 2: ตัวระบุลักษณะเส้นทาง

สิ่งต่าง ๆ จะกลายเป็นเรื่องง่ายขึ้นถ้าคุณจะกำหนดidค่าที่มีข้อมูลลำดับชั้น: พา ธ ตัวอย่างเช่นในกรณีของคุณสิ่งนี้อาจมีลักษณะเช่นนี้:

ID       | NAME
19       | category1   
19/1     | category2  
19/1/1   | category3  
19/1/1/1 | category4  

จากนั้นคุณselectจะมีลักษณะเช่นนี้:

select  id,
        name 
from    products
where   id like '19/%'

ทางเลือก 3: การรวมตัวเองซ้ำแล้วซ้ำอีก

หากคุณรู้ว่าขีด จำกัด สูงสุดของต้นไม้ลำดับชั้นของคุณลึกแค่ไหนคุณสามารถใช้sqlแบบสอบถามมาตรฐานเช่นนี้:

select      p6.parent_id as parent6_id,
            p5.parent_id as parent5_id,
            p4.parent_id as parent4_id,
            p3.parent_id as parent3_id,
            p2.parent_id as parent2_id,
            p1.parent_id as parent_id,
            p1.id as product_id,
            p1.name
from        products p1
left join   products p2 on p2.id = p1.parent_id 
left join   products p3 on p3.id = p2.parent_id 
left join   products p4 on p4.id = p3.parent_id  
left join   products p5 on p5.id = p4.parent_id  
left join   products p6 on p6.id = p5.parent_id
where       19 in (p1.parent_id, 
                   p2.parent_id, 
                   p3.parent_id, 
                   p4.parent_id, 
                   p5.parent_id, 
                   p6.parent_id) 
order       by 1, 2, 3, 4, 5, 6, 7;

ดูซอนี้

whereสภาพระบุซึ่งผู้ปกครองคุณต้องการที่จะดึงลูกหลานของ คุณสามารถขยายแบบสอบถามนี้ด้วยระดับที่มากขึ้นตามต้องการ


26
ฉันชอบคำอธิบายของคุณ มันไม่เพียงให้คำตอบอธิบายว่าทำไมมันแก้ปัญหาเพื่อให้เราสามารถเรียนรู้จากมันได้ แก้ไข:นอกจากนี้ยังเป็นเรื่องดีที่ไม่ต้องพึ่งพาการรู้จำนวนระดับล่วงหน้า
Byson

1
@ กระทิงฉันรู้สึกขอบคุณมากกับการย้อนกลับมาของคุณ ขอบคุณ!
trincot

2
@ Aviónไม่ใช่สิ่งที่คุณต้องใส่ที่ไหนสักแห่งมันเป็นข้อกำหนดที่ว่าสำหรับทุกการบันทึกสภาพนี้เป็นจริง หากคุณมีหนึ่งหรือมากกว่าหนึ่งบันทึกที่parent_id > idคุณไม่สามารถใช้วิธีนี้
trincot

2
สำหรับผู้ที่ต้องการใช้WITH RECURSIVEวิธีการฉันพบบทความต่อไปนี้เป็นประโยชน์จริง ๆ กับสถานการณ์ต่าง ๆ เช่นความลึกของการเรียกซ้ำความแตกต่างและการตรวจจับและปิดรอบ
Horse

2
ฉันลองใช้วิธีแก้ไขปัญหาหลักใน MySQL5.7 บนคอมพิวเตอร์ของฉันบนโต๊ะของตัวเอง แต่นั่นไม่ได้ผลเนื่องจากข้อ @pv: = concat (@pv, ',', id) ประเมินว่าผิด ฉันแก้ไขโดยเปลี่ยนเป็นความยาว (@pv: = concat (@pv, ',', id))> 0 ดังนั้นจึงเป็นจริงเสมอ
KC วงศ์

82

จากบล็อกการจัดการข้อมูลลำดับชั้นใน MySQL

โครงสร้างตาราง

+-------------+----------------------+--------+
| category_id | name                 | parent |
+-------------+----------------------+--------+
|           1 | ELECTRONICS          |   NULL |
|           2 | TELEVISIONS          |      1 |
|           3 | TUBE                 |      2 |
|           4 | LCD                  |      2 |
|           5 | PLASMA               |      2 |
|           6 | PORTABLE ELECTRONICS |      1 |
|           7 | MP3 PLAYERS          |      6 |
|           8 | FLASH                |      7 |
|           9 | CD PLAYERS           |      6 |
|          10 | 2 WAY RADIOS         |      6 |
+-------------+----------------------+--------+

ค้นหา:

SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
WHERE t1.name = 'ELECTRONICS';

เอาท์พุต

+-------------+----------------------+--------------+-------+
| lev1        | lev2                 | lev3         | lev4  |
+-------------+----------------------+--------------+-------+
| ELECTRONICS | TELEVISIONS          | TUBE         | NULL  |
| ELECTRONICS | TELEVISIONS          | LCD          | NULL  |
| ELECTRONICS | TELEVISIONS          | PLASMA       | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS  | FLASH |
| ELECTRONICS | PORTABLE ELECTRONICS | CD PLAYERS   | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | 2 WAY RADIOS | NULL  |
+-------------+----------------------+--------------+-------+

ผู้ใช้ส่วนใหญ่ในคราวเดียวหรืออีกคนหนึ่งจัดการกับข้อมูลแบบลำดับชั้นในฐานข้อมูล SQL และไม่ต้องสงสัยเลยว่าการจัดการข้อมูลแบบลำดับชั้นนั้นไม่ได้มีไว้สำหรับฐานข้อมูลเชิงสัมพันธ์ ตารางของฐานข้อมูลเชิงสัมพันธ์ไม่ได้เป็นแบบลำดับขั้น (เช่น XML) แต่เป็นเพียงรายการแบบเรียบ ข้อมูลลำดับชั้นมีความสัมพันธ์พาเรนต์ - ชายด์ที่ไม่ได้แสดงตามธรรมชาติในตารางฐานข้อมูลเชิงสัมพันธ์ อ่านเพิ่มเติม

อ้างถึงบล็อกสำหรับรายละเอียดเพิ่มเติม

แก้ไข:

select @pv:=category_id as category_id, name, parent from category
join
(select @pv:=19)tmp
where parent=@pv

เอาท์พุท:

category_id name    parent
19  category1   0
20  category2   19
21  category3   20
22  category4   21

การอ้างอิง: วิธีการทำแบบสอบถามเลือกซ้ำใน Mysql?


23
ไม่เป็นไรตราบใดที่มีลำดับขั้นไม่เกิน 4 ระดับ หากมีระดับ N คุณต้องรู้ว่าการสร้างแบบสอบถามอย่างถูกต้อง
Jonathan Leffler

2
@Damodaran ขอขอบคุณสำหรับการตอบกลับของคุณ ... สิ่งที่ฉันต้องการคือเงื่อนไขที่ไม่ทราบจำนวน childs ... และในบล็อกที่ใช้แนวคิดการเข้าร่วมด้านในซึ่งจำเป็นต้องทราบลำดับชั้นซึ่งไม่เป็นที่รู้จัก ในกรณีของฉัน ... ขอให้ฉันรู้มุมมองของคุณในสิ่งเดียวกัน ... ดังนั้นในคำง่ายๆฉันต้องการแบบสอบถามเพื่อจัดการระดับ 'n' hirerachy ที่ไม่รู้จัก 'n' .....
Tarun Parswani

1
@ user3036105: มันเป็นไปไม่ได้ที่จะทำสิ่งนี้ใน MySQL ด้วยเคียวรี SQL เดี่ยว MySQL นั้นยังไม่ก้าวหน้าพอสำหรับสิ่งนั้น หากคุณต้องการสิ่งนี้จริงๆให้ลองอัปเกรดเป็น DBMS ซึ่งรองรับการสืบค้นแบบเรียกซ้ำ
a_horse_with_no_name

5
> ผู้ใช้ส่วนใหญ่ในคราวเดียวหรืออีกคนหนึ่งจัดการกับข้อมูลแบบลำดับชั้นในฐานข้อมูล SQL และไม่ต้องสงสัยเลยว่าการจัดการข้อมูลแบบลำดับชั้นนั้นไม่ได้มีไว้สำหรับฐานข้อมูลเชิงสัมพันธ์ บางทีคุณอาจหมายถึงฐานข้อมูล MySQL ฐานข้อมูล Oracle จัดการข้อมูลลำดับชั้นและแบบสอบถามได้ค่อนข้างดี
Peter Nosko

1
"... การจัดการข้อมูลแบบลำดับชั้นไม่ใช่สิ่งที่ฐานข้อมูลเชิงสัมพันธ์มีไว้สำหรับ ... " ในขณะนี้อาจไม่ได้เป็นความตั้งใจดั้งเดิมของฐานข้อมูลเชิงสัมพันธ์ในข้อมูลเชิงลำดับชั้นของโลกแห่งความเป็นจริงเป็นเรื่องธรรมดาและ MySQL ควรสะท้อนให้เห็น วิธีที่ผู้คนต้องการใช้ข้อมูลของพวกเขาในสถานการณ์จริง
เดฟ L

9

ลองเหล่านี้:

นิยามของตาราง:

DROP TABLE IF EXISTS category;
CREATE TABLE category (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(20),
    parent_id INT,
    CONSTRAINT fk_category_parent FOREIGN KEY (parent_id)
    REFERENCES category (id)
) engine=innodb;

แถวทดลอง:

INSERT INTO category VALUES
(19, 'category1', NULL),
(20, 'category2', 19),
(21, 'category3', 20),
(22, 'category4', 21),
(23, 'categoryA', 19),
(24, 'categoryB', 23),
(25, 'categoryC', 23),
(26, 'categoryD', 24);

กระบวนงานที่เก็บไว้แบบเรียกซ้ำ:

DROP PROCEDURE IF EXISTS getpath;
DELIMITER $$
CREATE PROCEDURE getpath(IN cat_id INT, OUT path TEXT)
BEGIN
    DECLARE catname VARCHAR(20);
    DECLARE temppath TEXT;
    DECLARE tempparent INT;
    SET max_sp_recursion_depth = 255;
    SELECT name, parent_id FROM category WHERE id=cat_id INTO catname, tempparent;
    IF tempparent IS NULL
    THEN
        SET path = catname;
    ELSE
        CALL getpath(tempparent, temppath);
        SET path = CONCAT(temppath, '/', catname);
    END IF;
END$$
DELIMITER ;

ฟังก์ชั่น Wrapper สำหรับขั้นตอนการจัดเก็บ:

DROP FUNCTION IF EXISTS getpath;
DELIMITER $$
CREATE FUNCTION getpath(cat_id INT) RETURNS TEXT DETERMINISTIC
BEGIN
    DECLARE res TEXT;
    CALL getpath(cat_id, res);
    RETURN res;
END$$
DELIMITER ;

เลือกตัวอย่าง:

SELECT id, name, getpath(id) AS path FROM category;

เอาท์พุท:

+----+-----------+-----------------------------------------+
| id | name      | path                                    |
+----+-----------+-----------------------------------------+
| 19 | category1 | category1                               |
| 20 | category2 | category1/category2                     |
| 21 | category3 | category1/category2/category3           |
| 22 | category4 | category1/category2/category3/category4 |
| 23 | categoryA | category1/categoryA                     |
| 24 | categoryB | category1/categoryA/categoryB           |
| 25 | categoryC | category1/categoryA/categoryC           |
| 26 | categoryD | category1/categoryA/categoryB/categoryD |
+----+-----------+-----------------------------------------+

การกรองแถวด้วยเส้นทางที่แน่นอน:

SELECT id, name, getpath(id) AS path FROM category HAVING path LIKE 'category1/category2%';

เอาท์พุท:

+----+-----------+-----------------------------------------+
| id | name      | path                                    |
+----+-----------+-----------------------------------------+
| 20 | category2 | category1/category2                     |
| 21 | category3 | category1/category2/category3           |
| 22 | category4 | category1/category2/category3/category4 |
+----+-----------+-----------------------------------------+

1
สิ่งนี้จะไม่ทำงานสำหรับเด็กมากกว่าหนึ่งคน เช่น(20, 'category2', 19), (21, 'category3', 20), (22, 'category4', 20),
ทำความสะอาดรหัส

3
ฉันค่อนข้างมั่นใจว่ามันใช้ได้กับเด็กมากกว่าหนึ่งคน ฉันยังทดสอบอีกครั้ง
Fandi Susanto

@Fandi Susanto ขอขอบคุณ alote ช่วยฉันด้วย
Dogan Ozer

9

วิธีที่ดีที่สุดที่ฉันคิดไว้คือ

  1. ใช้ lineage เพื่อเก็บ \ sort \ trace tree นั่นมากเกินพอและทำงานได้เร็วขึ้นนับพันเท่าในการอ่านมากกว่าวิธีอื่น ๆ นอกจากนี้ยังอนุญาตให้อยู่ในรูปแบบนั้นแม้ว่า DB จะเปลี่ยนแปลง (เช่นใด ๆ db จะอนุญาตให้ใช้รูปแบบนั้น)
  2. ใช้ฟังก์ชันที่กำหนดสายเลือดสำหรับรหัสเฉพาะ
  3. ใช้ตามที่คุณต้องการ (ในการเลือกหรือในการดำเนินงาน CUD หรือแม้กระทั่งโดยงาน)

เชื้อสายวิธี descr สามารถพบได้ในทุกที่เช่น ที่นี่หรือที่นี่ ฟังก์ชั่น - นั่นคือสิ่งที่ทำให้ฉันหลงไหล

ในท้ายที่สุด - ได้โซลูชันที่ง่ายขึ้นค่อนข้างเร็วและง่ายขึ้น

ฟังก์ชั่นของร่างกาย

-- --------------------------------------------------------------------------------
-- Routine DDL
-- Note: comments before and after the routine body will not be stored by the server
-- --------------------------------------------------------------------------------
DELIMITER $$

CREATE DEFINER=`root`@`localhost` FUNCTION `get_lineage`(the_id INT) RETURNS text CHARSET utf8
    READS SQL DATA
BEGIN

 DECLARE v_rec INT DEFAULT 0;

 DECLARE done INT DEFAULT FALSE;
 DECLARE v_res text DEFAULT '';
 DECLARE v_papa int;
 DECLARE v_papa_papa int DEFAULT -1;
 DECLARE csr CURSOR FOR 
  select _id,parent_id -- @n:=@n+1 as rownum,T1.* 
  from 
    (SELECT @r AS _id,
        (SELECT @r := table_parent_id FROM table WHERE table_id = _id) AS parent_id,
        @l := @l + 1 AS lvl
    FROM
        (SELECT @r := the_id, @l := 0,@n:=0) vars,
        table m
    WHERE @r <> 0
    ) T1
    where T1.parent_id is not null
 ORDER BY T1.lvl DESC;
 DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
    open csr;
    read_loop: LOOP
    fetch csr into v_papa,v_papa_papa;
        SET v_rec = v_rec+1;
        IF done THEN
            LEAVE read_loop;
        END IF;
        -- add first
        IF v_rec = 1 THEN
            SET v_res = v_papa_papa;
        END IF;
        SET v_res = CONCAT(v_res,'-',v_papa);
    END LOOP;
    close csr;
    return v_res;
END

แล้วคุณก็แค่

select get_lineage(the_id)

หวังว่าจะช่วยใครซักคน :)


9

ทำสิ่งเดียวกันสำหรับคำถามอื่นที่นี่

Mysql เลือกแบบเรียกซ้ำรับเด็กทุกคนที่มีหลายระดับ

แบบสอบถามจะเป็น:

SELECT GROUP_CONCAT(lv SEPARATOR ',') FROM (
  SELECT @pv:=(
    SELECT GROUP_CONCAT(id SEPARATOR ',')
    FROM table WHERE parent_id IN (@pv)
  ) AS lv FROM table 
  JOIN
  (SELECT @pv:=1)tmp
  WHERE parent_id IN (@pv)
) a;

เราจะทำสิ่งนี้ได้อย่างไร SELECT idFolder, (SELECT GROUP_CONCAT(lv SEPARATOR ',') FROM ( SELECT @pv:=(SELECT GROUP_CONCAT(idFolder SEPARATOR ',') FROM Folder WHERE idFolderParent IN (@pv)) AS lv FROM Folder JOIN (SELECT @pv:= F1.idFolder )tmp WHERE idFolderParent IN (@pv)) a) from folder F1 where id > 10; ฉันไม่สามารถอ้างถึง F1.idFolder สำหรับ @pv
Rahul

ฉันสร้างตารางขึ้นใหม่จากคำถามดั้งเดิมของ OP ด้วยข้อมูลตามที่แสดงในความคิดเห็นของพวกเขาจากนั้นเรียกใช้แบบสอบถามของคุณที่นี่และได้NULLผลลัพธ์เดียว คุณรู้ไหมว่าทำไมถึงเป็นเช่นนั้น? มีข้อกำหนดเบื้องต้นในแง่ของเอ็นจิ้นฐานข้อมูลหรือมีการเปลี่ยนแปลงอะไรบ้างตั้งแต่คุณทำคำตอบนี้ทำให้แบบสอบถามนี้ล้าสมัยหรือไม่?
Digital Ninja

7

หากคุณต้องการความเร็วในการอ่านอย่างรวดเร็วตัวเลือกที่ดีที่สุดคือการใช้ตารางปิด ตารางการปิดประกอบด้วยแถวสำหรับแต่ละคู่บรรพบุรุษ / ลูกหลาน ในตัวอย่างของคุณตารางปิดจะมีหน้าตา

ancestor | descendant | depth
0        | 0          | 0
0        | 19         | 1
0        | 20         | 2
0        | 21         | 3
0        | 22         | 4
19       | 19         | 0
19       | 20         | 1
19       | 21         | 3
19       | 22         | 4
20       | 20         | 0
20       | 21         | 1
20       | 22         | 2
21       | 21         | 0
21       | 22         | 1
22       | 22         | 0

เมื่อคุณมีตารางนี้แบบสอบถามแบบลำดับชั้นจะกลายเป็นเรื่องง่ายและรวดเร็ว ในการรับทายาทของหมวดหมู่ 20 ทั้งหมด:

SELECT cat.* FROM categories_closure AS cl
INNER JOIN categories AS cat ON cat.id = cl.descendant
WHERE cl.ancestor = 20 AND cl.depth > 0

แน่นอนว่ามีข้อเสียใหญ่เมื่อใดก็ตามที่คุณใช้ข้อมูลที่ผิดปกติเช่นนี้ คุณต้องบำรุงรักษาตารางการปิดข้างโต๊ะหมวดหมู่ของคุณ วิธีที่ดีที่สุดคือการใช้ทริกเกอร์ แต่มันค่อนข้างซับซ้อนในการติดตามการแทรก / การอัพเดท / การลบอย่างถูกต้องสำหรับตารางการปิด เช่นเดียวกับสิ่งใดก็ตามคุณต้องพิจารณาความต้องการของคุณและตัดสินใจว่าแนวทางใดดีที่สุดสำหรับคุณ

แก้ไข : ดูคำถามตัวเลือกสำหรับการจัดเก็บข้อมูลแบบลำดับชั้นในฐานข้อมูลเชิงสัมพันธ์คืออะไร? สำหรับตัวเลือกเพิ่มเติม มีวิธีแก้ไขปัญหาที่เหมาะสมที่สุดสำหรับสถานการณ์ที่แตกต่างกัน


4

แบบสอบถามง่ายๆเพื่อแสดงรายการการเรียกซ้ำครั้งแรกของเด็ก:

select @pv:=id as id, name, parent_id
from products
join (select @pv:=19)tmp
where parent_id=@pv

ผลลัพธ์:

id  name        parent_id
20  category2   19
21  category3   20
22  category4   21
26  category24  22

... โดยเข้าร่วมซ้าย:

select
    @pv:=p1.id as id
  , p2.name as parent_name
  , p1.name name
  , p1.parent_id
from products p1
join (select @pv:=19)tmp
left join products p2 on p2.id=p1.parent_id -- optional join to get parent name
where p1.parent_id=@pv

วิธีการแก้ปัญหาของ @tincot เพื่อแสดงรายการเด็กทั้งหมด:

select  id,
        name,
        parent_id 
from    (select * from products
         order by parent_id, id) products_sorted,
        (select @pv := '19') initialisation
where   find_in_set(parent_id, @pv) > 0
and     @pv := concat(@pv, ',', id)

ทดสอบทางออนไลน์ด้วยซอฟท์แวร์ Sqlและดูผลลัพธ์ทั้งหมด

http://sqlfiddle.com/#!9/a318e3/4/0


3

คุณสามารถทำเช่นนี้ในฐานข้อมูลอื่น ๆ ได้อย่างง่ายดายด้วยแบบสอบถามแบบเรียกซ้ำ (YMMV กับประสิทธิภาพ)

วิธีอื่นในการทำคือการเก็บข้อมูลสองบิตพิเศษค่าซ้ายและขวา ค่าซ้ายและขวามาจากการสำรวจเส้นทางล่วงหน้าของโครงสร้างต้นไม้ที่คุณเป็นตัวแทน

สิ่งนี้เป็นที่รู้จักกันในชื่อ Modified Preorder Tree Traversal และให้คุณเรียกใช้แบบสอบถามง่ายๆเพื่อรับค่าพาเรนต์ทั้งหมด นอกจากนี้ยังใช้ชื่อ "ชุดซ้อน"


ฉันต้องการเพิ่มความคิดเห็นที่คล้ายกับของคุณ แต่เนื่องจากคุณทำมันฉันจะเพิ่มลิงก์ไปยังตัวอย่างที่ดีของ "ชุดซ้อน": mikehillyer.com/articles/managing-hierarchical-data-in-mysql
Miroslaw Opoka

2

เพียงแค่ใช้BlueM / tree php class สำหรับทำ tree ของตารางความสัมพันธ์ตัวเองใน mysql

Tree และ Tree \ Node เป็นคลาส PHP สำหรับจัดการข้อมูลที่มีโครงสร้างแบบลำดับชั้นโดยใช้การอ้างอิง ID หลัก ตัวอย่างทั่วไปคือตารางในฐานข้อมูลเชิงสัมพันธ์ที่เขตข้อมูล "ผู้ปกครอง" ของแต่ละระเบียนอ้างอิงคีย์หลักของระเบียนอื่น แน่นอน Tree ไม่เพียง แต่ใช้ข้อมูลที่มาจากฐานข้อมูล แต่มีอะไร: คุณให้ข้อมูลและ Tree ใช้มันไม่ว่าข้อมูลจะมาจากที่ใดและประมวลผลอย่างไร อ่านเพิ่มเติม

นี่คือตัวอย่างของการใช้ BlueM / tree:

<?php 
require '/path/to/vendor/autoload.php'; $db = new PDO(...); // Set up your database connection 
$stm = $db->query('SELECT id, parent, title FROM tablename ORDER BY title'); 
$records = $stm->fetchAll(PDO::FETCH_ASSOC); 
$tree = new BlueM\Tree($records); 
...

2

ป้อนคำอธิบายรูปภาพที่นี่

มันเป็นตารางหมวดหมู่

SELECT  id,
        NAME,
        parent_category 
FROM    (SELECT * FROM category
         ORDER BY parent_category, id) products_sorted,
        (SELECT @pv := '2') initialisation
WHERE   FIND_IN_SET(parent_category, @pv) > 0
AND     @pv := CONCAT(@pv, ',', id)

เอาท์พุท :: ป้อนคำอธิบายรูปภาพที่นี่


1
คุณอธิบายได้ไหม แต่ฉันรับประกันได้ว่านี่จะใช้งานได้ ขอบคุณ.
wobsoriano

1
ได้โปรดอธิบายแบบสอบถามและอะไรคือความหมายของ @pv ?? การวนซ้ำทำงานในแบบสอบถามนี้อย่างไร
Amanjot Kaur

2
ดูเหมือนจะไม่ทำงานในทุกระดับหากมีเด็กที่มีรหัสต่ำกว่าพ่อแม่ของพวกเขา :(
Jonas

1
@ Jonas ใช้เวลา 20 นาทีในการระบุปัญหาที่เกิดขึ้นจริงลองใช้ชุดค่าผสมอื่น ใช่คุณพูดถูก. มันจะไม่ทำงานกับ ID ต่ำกว่า ID หลัก คุณมีทางออกหรือไม่?
muaaz

@muaaz ในที่สุดฉันก็แก้ไขได้โดยใช้ฟิลด์ "เส้นทาง" ที่มีเส้นทางสำหรับแถวนั้นเช่นแถวที่มี ID 577 มีเส้นทาง "/ 1/2/45/577 /" หากคุณกำลังมองหาเด็ก ๆ ของ ID 2 คุณสามารถเลือกแถวทั้งหมดด้วยพา ธ LIKE "/ 1/2 /%" ข้อเสียเพียงอย่างเดียวคือคุณต้องอัปเดตพา ธ ในวิธีการอัปเดตของคุณ แต่สำหรับ MySQL 5.6 (ใช้งานร่วมกันได้) มันเป็นทางออกเดียวที่เหมาะกับฉัน
Jonas

1

มันยากหน่อยนะลองตรวจสอบดูว่ามันใช้ได้กับคุณไหม

select a.id,if(a.parent = 0,@varw:=concat(a.id,','),@varw:=concat(a.id,',',@varw)) as list from (select * from recursivejoin order by if(parent=0,id,parent) asc) a left join recursivejoin b on (a.id = b.parent),(select @varw:='') as c  having list like '%19,%';

ลิงค์ของซอร์ด SQL SQL http://www.sqlfiddle.com/#!2/e3cdf/2

แทนที่ด้วยฟิลด์และชื่อตารางของคุณอย่างเหมาะสม


มันจะไม่ทำงานในกรณีนี้sqlfiddle.com/#!2/19360/2พร้อมกับเคล็ดลับนี้อย่างน้อยคุณควรสั่งซื้อตามลำดับชั้นก่อน
Jaugar Chang

1

บางสิ่งที่ไม่ได้กล่าวถึงที่นี่แม้ว่าจะคล้ายกับทางเลือกที่สองของคำตอบที่ยอมรับ แต่แตกต่างกันและต้นทุนต่ำสำหรับแบบสอบถามลำดับชั้นขนาดใหญ่และรายการ (แทรกการปรับปรุงการลบ) ที่ง่ายจะเป็นการเพิ่มคอลัมน์พา ธ ถาวรสำหรับแต่ละรายการ

บางคนชอบ:

id | name        | path
19 | category1   | /19
20 | category2   | /19/20
21 | category3   | /19/20/21
22 | category4   | /19/20/21/22

ตัวอย่าง:

-- get children of category3:
SELECT * FROM my_table WHERE path LIKE '/19/20/21%'
-- Reparent an item:
UPDATE my_table SET path = REPLACE(path, '/19/20', '/15/16') WHERE path LIKE '/19/20/%'

ปรับความยาวเส้นทางให้เหมาะสมและORDER BY pathใช้การเข้ารหัส base36 แทนรหัสเส้นทางที่เป็นตัวเลขจริง

 // base10 => base36
 '1' => '1',
 '10' => 'A',
 '100' => '2S',
 '1000' => 'RS',
 '10000' => '7PS',
 '100000' => '255S',
 '1000000' => 'LFLS',
 '1000000000' => 'GJDGXS',
 '1000000000000' => 'CRE66I9S'

https://en.wikipedia.org/wiki/Base36

การปราบปรามตัวคั่นเครื่องหมายทับ '/' โดยใช้ความยาวคงที่และแพ็ดไปยังรหัสที่เข้ารหัส

คำอธิบายการเพิ่มประสิทธิภาพโดยละเอียดที่นี่: https://bojanz.wordpress.com/2014/04/25/storing-hierarchical-data-materialized-path/

ทำ

การสร้างฟังก์ชั่นหรือขั้นตอนในการแยกเส้นทางสำหรับบรรพบุรุษที่เก็บของหนึ่งรายการ


ขอบคุณ! น่าสนใจกับbase36

0

มันใช้งานได้สำหรับฉันหวังว่ามันจะใช้ได้กับคุณด้วย มันจะให้รูทชุดระเบียนแก่เด็กสำหรับเมนูเฉพาะใด ๆ เปลี่ยนชื่อฟิลด์ตามความต้องการของคุณ

SET @id:= '22';

SELECT Menu_Name, (@id:=Sub_Menu_ID ) as Sub_Menu_ID, Menu_ID 
FROM 
    ( SELECT Menu_ID, Menu_Name, Sub_Menu_ID 
      FROM menu 
      ORDER BY Sub_Menu_ID DESC
    ) AS aux_table 
    WHERE Menu_ID = @id
     ORDER BY Sub_Menu_ID;

ดูเหมือนจะไม่ทำงานในทุกระดับหากมีเด็กที่มีรหัสมากกว่าพ่อแม่ของพวกเขา
muaaz

-1

ฉันพบว่า:

1) สร้างฟังก์ชั่นที่จะตรวจสอบว่ารายการอยู่ที่ใดก็ได้ในลำดับชั้นพาเรนต์ของอีกรายการหนึ่ง บางอย่างเช่นนี้ (ฉันจะไม่เขียนฟังก์ชั่นทำด้วยในขณะที่ทำ):

is_related(id, parent_id);

ในตัวอย่างของคุณ

is_related(21, 19) == 1;
is_related(20, 19) == 1;
is_related(21, 18) == 0;

2) ใช้ตัวเลือกย่อยบางอย่างเช่นนี้:

select ...
from table t
join table pt on pt.id in (select i.id from table i where is_related(t.id,i.id));

-1

ฉันได้ทำแบบสอบถามให้คุณ สิ่งนี้จะทำให้คุณมีหมวดหมู่แบบเรียกซ้ำด้วยแบบสอบถามเดียว

SELECT id,NAME,'' AS subName,'' AS subsubName,'' AS subsubsubName FROM Table1 WHERE prent is NULL
UNION 
SELECT b.id,a.name,b.name AS subName,'' AS subsubName,'' AS subsubsubName FROM Table1 AS a LEFT JOIN Table1 AS b ON b.prent=a.id WHERE a.prent is NULL AND b.name IS NOT NULL 
UNION 
SELECT c.id,a.name,b.name AS subName,c.name AS subsubName,'' AS subsubsubName FROM Table1 AS a LEFT JOIN Table1 AS b ON b.prent=a.id LEFT JOIN Table1 AS c ON c.prent=b.id WHERE a.prent is NULL AND c.name IS NOT NULL 
UNION 
SELECT d.id,a.name,b.name AS subName,c.name AS subsubName,d.name AS subsubsubName FROM Table1 AS a LEFT JOIN Table1 AS b ON b.prent=a.id LEFT JOIN Table1 AS c ON c.prent=b.id LEFT JOIN Table1 AS d ON d.prent=c.id WHERE a.prent is NULL AND d.name IS NOT NULL 
ORDER BY NAME,subName,subsubName,subsubsubName

นี่คือไวโอลิน


โปรดลบ / แก้ไขคำตอบของคุณเพื่อรับชื่อเสียงที่ดีกลับมา
fWd82
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.