12 ธ.ค. 2567·อ่าน 2 นาที

แบบจำลองโครงสร้างองค์กรใน PostgreSQL: adjacency lists กับ closure

เปรียบเทียบ adjacency list กับ closure table ในการออกแบบโครงสร้างองค์กรบน PostgreSQL พร้อมตัวอย่างชัดเจนสำหรับการกรอง รายงาน และการตรวจสิทธิ์

แบบจำลองโครงสร้างองค์กรใน PostgreSQL: adjacency lists กับ closure

สิ่งที่ผังองค์กรต้องรองรับ

ผังองค์กรคือแผนที่ว่ารายงานใครให้ใคร และทีมต่าง ๆ รวมกันเป็นแผนกอย่างไร เมื่อคุณออกแบบโครงสร้างองค์กรใน PostgreSQL คุณไม่ได้แค่เก็บ manager_id ในแต่ละคนเท่านั้น แต่ต้องรองรับงานจริง: การเรียกดูโครงสร้าง, รายงาน และกฎการเข้าถึง

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

ในทางปฏิบัติ แบบจำลองที่ดีต้องตอบคำถามที่เกิดซ้ำได้ไม่กี่ข้อ:

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

มันซับซ้อนกว่าแค่ต้นไม้ธรรมดาเพราะองค์กรเปลี่ยนบ่อย ทีมย้ายข้ามแผนก ผู้จัดการสลับกลุ่ม และบางมุมมองไม่ได้เป็นเพียง "คนรายงานคน" ตัวอย่าง: คนหนึ่งอยู่ในทีม และทีมเป็นของแผนก สิทธิ์เพิ่มอีกชั้น: รูปร่างขององค์กรกลายเป็นส่วนหนึ่งของโมเดลความปลอดภัย ไม่ใช่แค่ไดอะแกรม

คำศัพท์บางคำช่วยให้การออกแบบชัดเจน:

  • โหนดคือรายการหนึ่งในลำดับชั้น (บุคคล ทีม หรือแผนก)
  • แม่คือโหนดที่อยู่เหนือโดยตรง (ผู้จัดการ หรือแผนกที่เป็นเจ้าของทีม)
  • บรรพบุรุษคือโหนดใด ๆ ที่อยู่เหนือในระยะทางใดก็ได้ (ผู้จัดการของผู้จัดการของคุณ)
  • ทายาทคือโหนดใด ๆ ที่อยู่ด้านล่างในระยะทางใดก็ได้ (ทุกคนที่อยู่ใต้คุณ)

ตัวอย่าง: ถ้าแผนกขาย (Sales) ย้ายไปอยู่ใต้ VP คนใหม่ สองอย่างนี้ต้องยังคงเป็นจริงทันที แดชบอร์ดยังกรอง "ทั้งแผนก Sales" ได้ และสิทธิ์ของ VP คนใหม่ครอบคลุม Sales โดยอัตโนมัติ

การตัดสินใจก่อนเลือกโครงสร้างตาราง

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

จดคำถามที่หน้าจอและการตรวจสอบสิทธิ์จะถาม หากคุณบอกคำถามไม่ได้นั่นแปลว่าจะได้สกีมาที่ "ดูเหมาะ" แต่ยากต่อการคิวรี

การตัดสินใจที่กำหนดทุกอย่าง:

  • คิวรีไหนต้องเร็ว: ผู้จัดการโดยตรง สายบังคับบัญชาจนถึง CEO subtree ทั้งหมดใต้หัวหน้า หรือ "ทุกคนในแผนกนี้"?
  • เป็นต้นไม้แบบเคร่งครัด (ผู้จัดการคนเดียว) หรือองค์กรแบบเมทริกซ์ (หลายผู้จัดการหรือหัวหน้า)?
  • แผนกเป็นโหนดในลำดับชั้นเดียวกับคนหรือเป็น attribute แยก (เช่น department_id ในแต่ละคน)?
  • ใครสามารถอยู่หลายทีมได้หรือไม่ (shared services, squads)?
  • สิทธิ์ไหลอย่างไร: ลงต้นไม้ ขึ้นต้นไม้ หรือทั้งสอง?

การเลือกเหล่านี้กำหนดว่า "ข้อมูลที่ถูกต้อง" เป็นอย่างไร หาก Alex นำทั้งฝ่าย Support และ Onboarding manager_id เดียวหรือกฎ "หนึ่งหัวหน้าต่อทีม" อาจไม่พอ คุณอาจต้องการตารางเชื่อม (leader-to-team) หรือกฎชัดเจนเช่น "ทีมหลักหนึ่งทีม บวกทีม dotted-line"

แผนกเป็นอีกทางเลือกหนึ่ง หากแผนกเป็นโหนด คุณสามารถแสดง "แผนก A รวมทีม B รวมคน C" หากแผนกเป็นแยก คุณจะกรองด้วย department_id = X ซึ่งเริ่มง่ายแต่จะแตกเมื่อทีมข้ามแผนก

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

Adjacency list: สกีมาง่ายสำหรับผู้จัดการและทีม

ถ้าคุณต้องการส่วนประกอบให้น้อยที่สุด adjacency list เป็นจุดเริ่มต้นคลาสสิก แต่ละคนเก็บพอยน์เตอร์ไปยังผู้จัดการโดยตรง และต้นไม้สร้างจากการตามพอยน์เตอร์เหล่านั้น

การตั้งค่าขั้นต่ำเป็นแบบนี้:

create table departments (
  id bigserial primary key,
  name text not null unique
);

create table teams (
  id bigserial primary key,
  department_id bigint not null references departments(id),
  name text not null,
  unique (department_id, name)
);

create table employees (
  id bigserial primary key,
  full_name text not null,
  team_id bigint references teams(id),
  manager_id bigint references employees(id)
);

คุณอาจข้ามตารางแยกและเก็บ department_name และ team_name เป็นคอลัมน์บน employees ได้ ซึ่งเริ่มเร็วกว่า แต่ยากรักษาความสะอาด (สะกดผิด เปลี่ยนชื่อทีม และการรายงานที่ไม่สอดคล้อง) ตารางแยกทำให้การกรองและกฎสิทธิ์อธิบายได้ง่ายขึ้น

ใส่เกราะป้องกันตั้งแต่ต้น ข้อมูลลำดับชั้นที่ผิดพลาดแก้ยากทีหลัง ขั้นต่ำสุดป้องกันการเป็นผู้จัดการตัวเอง (manager_id <> id) และตัดสินใจว่าผู้จัดการสามารถอยู่นอกทีมหรือแผนกเดียวกันได้หรือไม่ และว่าต้องการ soft deletes หรือการเปลี่ยนแปลงเชิงประวัติศาสตร์หรือไม่ (สำหรับการตรวจสอบสายงาน)

กับ adjacency list การเปลี่ยนแปลงส่วนใหญ่เป็นการเขียนธรรมดา: เปลี่ยนผู้จัดการอัปเดต employees.manager_id และย้ายทีมอัปเดต employees.team_id (มักจะพร้อมกับผู้จัดการ) ข้อเสียคือการเขียนเล็กน้อยอาจมีผลกระทบมาก downstream ผลรวมรายงานเปลี่ยน และกฎ "ผู้จัดการเห็นรายงานทั้งหมด" ต้องตามสายใหม่

ความเรียบง่ายนี้คือจุดแข็งใหญ่ของ adjacency list จุดอ่อนปรากฏเมื่อคุณกรองบ่อย ๆ ด้วย "ทุกคนใต้ผู้จัดการนี้" เพราะมักต้องพึ่งการคิวรี recursive เพื่อเดินต้นไม้ทุกครั้ง

Adjacency list: คิวรีทั่วไปสำหรับการกรองและรายงาน

กับ adjacency list คำถามผังองค์กรที่มีประโยชน์หลายอย่างจะกลายเป็นคิวรีแบบ recursive ถ้าคุณออกแบบผังองค์กรใน PostgreSQL แบบนี้ นี่คือรูปแบบที่คุณจะใช้บ่อย

ผู้รายงานโดยตรง (ระดับเดียว)

กรณีที่ง่ายที่สุดคือทีมโดยตรงของผู้จัดการ:

SELECT id, full_name, title
FROM employees
WHERE manager_id = $1
ORDER BY full_name;

นี่เร็วและอ่านง่าย แต่ลงได้แค่ระดับเดียว

สายบังคับบัญชา (ขึ้นไป)

เพื่อแสดงว่าใครคือผู้ที่คนหนึ่งรายงาน (ผู้จัดการ ผู้จัดการของผู้จัดการ เป็นต้น) ใช้ recursive CTE:

WITH RECURSIVE chain AS (
  SELECT id, full_name, manager_id, 0 AS depth
  FROM employees
  WHERE id = $1

  UNION ALL

  SELECT e.id, e.full_name, e.manager_id, c.depth + 1
  FROM employees e
  JOIN chain c ON e.id = c.manager_id
)
SELECT *
FROM chain
ORDER BY depth;

นี้รองรับการอนุมัติ เส้นทางการยกระดับ และ breadcrumb ของผู้จัดการ

subtree ทั้งหมด (ลงไป)

เพื่อให้ได้ทุกคนใต้หัวหน้า (ทุกระดับ) พลิกการ recursion:

WITH RECURSIVE subtree AS (
  SELECT id, full_name, manager_id, department_id, 0 AS depth
  FROM employees
  WHERE id = $1

  UNION ALL

  SELECT e.id, e.full_name, e.manager_id, e.department_id, s.depth + 1
  FROM employees e
  JOIN subtree s ON e.manager_id = s.id
)
SELECT *
FROM subtree
ORDER BY depth, full_name;

รายงานที่พบได้บ่อยคือ "ทุกคนในแผนก X ใต้หัวหน้า Y":

WITH RECURSIVE subtree AS (
  SELECT id, department_id
  FROM employees
  WHERE id = $1
  UNION ALL
  SELECT e.id, e.department_id
  FROM employees e
  JOIN subtree s ON e.manager_id = s.id
)
SELECT e.*
FROM employees e
JOIN subtree s ON s.id = e.id
WHERE e.department_id = $2;

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

Closure table: มันเก็บลำดับชั้นทั้งหมดอย่างไร

เป็นเจ้าของโค้ดที่สร้างขึ้น
เก็บตัวเลือกในการส่งออกซอร์สโค้ดจริงเมื่อคุณต้องการการควบคุมเต็มรูปแบบ
ส่งออกโค้ด

Closure table เก็บความสัมพันธ์บรรพบุรุษ-ทายาททุกคู่ ไม่ใช่แค่ลิงก์ผู้จัดการโดยตรง แทนที่จะเดินต้นไม้ทีละก้าว คุณสามารถถามว่า: "ใครอยู่ใต้หัวหน้านี้?" แล้วได้คำตอบทั้งหมดด้วยการ join เดียว

คุณมักเก็บสองตาราง: หนึ่งสำหรับโหนด (คนหรือทีม) และหนึ่งสำหรับเส้นทางลำดับชั้น

-- nodes
employees (
  id bigserial primary key,
  name text not null,
  manager_id bigint null references employees(id)
)

-- closure
employee_closure (
  ancestor_id bigint not null references employees(id),
  descendant_id bigint not null references employees(id),
  depth int not null,
  primary key (ancestor_id, descendant_id)
)

Closure table เก็บคู่เช่น (Alice, Bob) หมายถึง "Alice เป็นบรรพบุรุษของ Bob" ยังเก็บแถวที่ ancestor_id = descendant_id พร้อม depth = 0 แถวตัวเองนั้นดูแปลกในตอนแรก แต่ทำให้หลายคิวรีสะอาดขึ้น

depth บอกว่าห่างกันเท่าไร: depth = 1 คือผู้จัดการโดยตรง depth = 2 คือผู้จัดการของผู้จัดการ เป็นต้น นี่สำคัญเมื่อผู้รายงานโดยตรงควรจัดการต่างจากผู้รายงานทางอ้อม

ประโยชน์หลักคือการอ่านที่คาดเดาได้และรวดเร็ว:

  • การค้นหา subtree ทั้งหมดเร็ว (ทุกคนใต้ผู้อำนวยการ)
  • สายบังคับบัญชาง่าย (ผู้จัดการทั้งหมดเหนือใครสักคน)
  • คุณแยกความสัมพันธ์โดยตรงกับทางอ้อมได้โดยใช้ depth

ต้นทุนคือการบำรุงรักษาเมื่ออัปเดต หาก Bob เปลี่ยนผู้จัดการจาก Alice เป็น Dana คุณต้องสร้างแถว closure ใหม่สำหรับ Bob และทุกคนใต้ Bob วิธีทั่วไปคือ: ลบเส้นทางบรรพบุรุษเก่าสำหรับ subtree นั้น แล้วแทรกเส้นทางใหม่โดยการรวมบรรพบุรุษของ Dana กับทุกโหนดใน subtree ของ Bob และคำนวณ depth ใหม่

Closure table: คิวรีทั่วไปสำหรับการกรองที่เร็ว

เพิ่มการอนุมัติตามผู้จัดการ
ตั้งค่าการอนุมัติที่เดินตามสายบังคับบัญชา แม้หลังการปรับโครงสร้าง
สร้างเวิร์กโฟลว์

Closure table เก็บทุกคู่บรรพบุรุษ-ทายาทไว้ล่วงหน้า (มักเป็น org_closure(ancestor_id, descendant_id, depth)) นั่นทำให้การกรององค์กรเร็วเพราะคำถามส่วนใหญ่กลายเป็นการ join เดียว

เพื่อแสดงทุกคนใต้ผู้จัดการ ให้ join หนึ่งครั้งแล้วกรองด้วย depth:

-- Descendants (everyone in the subtree)
SELECT e.*
FROM employees e
JOIN org_closure c
  ON c.descendant_id = e.id
WHERE c.ancestor_id = :manager_id
  AND c.depth > 0;

-- Direct reports only
SELECT e.*
FROM employees e
JOIN org_closure c
  ON c.descendant_id = e.id
WHERE c.ancestor_id = :manager_id
  AND c.depth = 1;

สำหรับสายบังคับบัญชา (บรรพบุรุษทั้งหมดของพนักงานคนหนึ่ง) พลิกการ join:

SELECT m.*
FROM employees m
JOIN org_closure c
  ON c.ancestor_id = m.id
WHERE c.descendant_id = :employee_id
  AND c.depth > 0
ORDER BY c.depth;

การกรองกลายเป็นคาดเดาได้ ตัวอย่าง: "ทุกคนใต้หัวหน้า X แต่เฉพาะในแผนก Y":

SELECT e.*
FROM employees e
JOIN org_closure c ON c.descendant_id = e.id
WHERE c.ancestor_id = :leader_id
  AND e.department_id = :department_id;

เพราะลำดับชั้นคำนวณไว้ล่วงหน้า การนับก็ทำได้ตรงไปตรงมา (ไม่ต้อง recursion) ช่วยแดชบอร์ดและตัวนับสิทธิ์ที่จำกัด และทำงานร่วมกับการแบ่งหน้าและการค้นหาได้ดีเพราะคุณสามารถใช้ ORDER BY, LIMIT/OFFSET และฟิลเตอร์ตรงบนชุดทายาท

แต่ละแบบมีผลอย่างไรต่อสิทธิ์และการตรวจสอบการเข้าถึง

กฎองค์กรทั่วไปคือ: ผู้จัดการดู (และบางครั้งแก้ไข) ทุกอย่างภายใต้ตน การเลือกสกีมาจะเปลี่ยนว่าคุณต้องจ่ายค่าใช้จ่ายแบบใดบ่อยแค่ไหนในการหาว่า "ใครอยู่ใต้ใคร"

กับ adjacency list การตรวจสิทธิ์มักต้อง recursion หากผู้ใช้เปิดหน้าที่แสดงพนักงาน 200 คน คุณมักจะสร้างชุดทายาทด้วย recursive CTE แล้วกรองแถวเป้าหมายตามมัน

กับ closure table กฎเดียวกันมักตรวจสอบได้ด้วยการทดสอบการมีอยู่แบบง่าย: "ผู้ใช้ปัจจุบันเป็นบรรพบุรุษของพนักงานนี้หรือไม่?" ถ้าใช่ อนุญาต

-- Closure table permission check (conceptual)
SELECT 1
FROM org_closure c
WHERE c.ancestor_id = :viewer_id
  AND c.descendant_id = :employee_id
LIMIT 1;

ความเรียบง่ายนี้สำคัญเมื่อคุณนำ row-level security (RLS) มาใช้ ที่ซึ่งทุกคิวรีรวมกฎว่า "คืนเฉพาะแถวที่ผู้ดูเห็นได้" กับ adjacency list นโยบายมักฝัง recursion และ tuning ยากกว่า กับ closure table นโยบายมักเป็น EXISTS (...) ที่ตรงไปตรงมา

ขอบเขตพิเศษเป็นที่ที่ตรรกะสิทธิ์มักพังมากที่สุด:

  • การรายงานแบบ dotted-line: คนหนึ่งมีผู้จัดการสองคนจริง ๆ
  • ผู้ช่วยและผู้ได้รับมอบหมาย: การเข้าถึงไม่ขึ้นกับลำดับชั้น ดังนั้นเก็บสิทธิ์เฉพาะ (มักมีวันหมดอายุ)
  • การเข้าถึงชั่วคราว: สิทธิ์ผูกเวลาควรไม่ถูกรวมในโครงสร้างองค์กร
  • โครงการข้ามทีม: ให้สิทธิ์ตามสมาชิกโครงการ แทนที่จะตามสายการจัดการ

ถ้าคุณสร้างสิ่งนี้ใน AppMaster closure table มักจับคู่ได้ดีในโมเดลข้อมูลแบบภาพและทำให้การตรวจสอบสิทธิ์ง่ายขึ้นทั้งเว็บและมือถือ

ข้อแลกเปลี่ยน: ความเร็ว ความซับซ้อน และการบำรุงรักษา

ปรับใช้ที่ทีมของคุณใช้งาน
ปรับใช้งานภายในทีมของคุณบน AppMaster Cloud หรือผู้ให้บริการคลาวด์ที่ต้องการ
ปรับใช้งาน

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

Adjacency lists ทำให้ตารางเล็กและการอัพเดตง่าย ต้นทุนปรากฏในฝั่งอ่าน: subtree มักหมายถึง recursion นั่นอาจรับได้ถ้าองค์กรของคุณเล็ก UI โหลดแค่ไม่กี่ระดับ หรือฟิลเตอร์ตามลำดับชั้นใช้ในไม่กี่ที่

Closure tables พลิกการแลกเปลี่ยน การอ่านเร็วเพราะคุณตอบ "ทายาททั้งหมด" ด้วย join ปกติ การเขียนซับซ้อนขึ้นเพราะการย้ายหรือการปรับโครงสร้างอาจต้องแทรกและลบหลายแถว

ในการทำงานจริง การแลกเปลี่ยนมักเป็นดังนี้:

  • ประสิทธิภาพการอ่าน: adjacency ต้อง recursion; closure ส่วนใหญ่เป็น join และยังเร็วเมื่อองค์กรโตขึ้น
  • ความซับซ้อนการเขียน: adjacency อัปเดต parent_id เดียว; closure อัปเดตหลายแถวเมื่อย้าย
  • ขนาดข้อมูล: adjacency โตตามคน/ทีม; closure โตตามความสัมพันธ์ (ในกรณีแย่ ๆ ประมาณ N^2 สำหรับต้นไม้ลึก)

การทำดัชนีมีความสำคัญทั้งสองแบบ แต่เป้าหมายต่างกัน:

  • Adjacency list: ทำดัชนี pointer พ่อแม่ (manager_id) รวมถึงฟิลเตอร์ที่ใช้บ่อยเช่นธง "active"
  • Closure table: ทำดัชนี (ancestor_id, descendant_id) และ descendant_id เดี่ยวสำหรับการค้นหาที่ใช้บ่อย

กฎง่าย ๆ: ถ้าคุณไม่ค่อยกรองตามลำดับชั้นและการตรวจสิทธิ์เป็นแค่ "ผู้จัดการเห็นผู้รายงานโดยตรง" adjacency list มักเพียงพอ หากคุณมักรันรายงาน "ทุกคนใต้ VP X" กรองตามต้นไม้แผนก หรือบังคับสิทธิ์ตามลำดับชั้นในหลายหน้าจอ closure table มักคุ้มค่ากับการบำรุงรักษาเพิ่มเติม

ขั้นตอน: ย้ายจาก adjacency list ไป closure table

คุณไม่จำเป็นต้องเลือกแบบในวันแรก เส้นทางปลอดภัยคือเก็บ adjacency list (manager_id หรือ parent_id) และเพิ่ม closure table เคียงข้าง จากนั้นย้ายการอ่านทีละจุด เพื่อลดความเสี่ยงขณะตรวจสอบพฤติกรรมใหม่ในการคิวรีและการตรวจสิทธิ์

เริ่มด้วยการสร้าง closure table (มักชื่อ org_closure) มีคอลัมน์อย่าง ancestor_id, descendant_id, และ depth แยกจากตาราง employees หรือ teams เดิมเพื่อให้คุณ backfill และตรวจสอบได้โดยไม่แตะฟีเจอร์ปัจจุบัน

การเปิดใช้งานขั้นปฏิบัติ:

  • สร้าง closure table และดัชนีในขณะที่ยังคงเก็บ adjacency list เป็นแหล่งความจริง
  • backfill แถว closure จากความสัมพันธ์ผู้จัดการปัจจุบัน รวมทั้งแถวตัวเอง (แต่ละโหนดเป็นบรรพบุรุษของตัวเองที่ depth 0)
  • ตรวจสอบด้วย spot checks: เลือกผู้จัดการไม่กี่คนและยืนยันว่าชุดผู้ใต้บังคับบัญชาตรงทั้งสองแบบ
  • ย้ายเส้นทางการอ่านก่อน: รายงาน ตัวกรอง และสิทธิ์ลำดับชั้นอ่านจาก closure table ก่อนที่จะเปลี่ยนการเขียน
  • คอยอัปเดต closure ในทุกการเขียน (re-parent, hire, move team) เมื่อเสถียรแล้ว เลิกใช้คิวรีแบบ recursive

เมื่อยืนยัน ให้เน้นกรณีที่มักทำให้สิทธิ์ผิด: การเปลี่ยนผู้จัดการ ผู้นำระดับบนสุด และผู้ใช้ที่ไม่มีผู้จัดการ

ถ้าคุณสร้างใน AppMaster คุณสามารถให้ endpoint เก่าใช้งานไปก่อนขณะที่เพิ่ม endpoint ใหม่ที่อ่านจาก closure table แล้วสลับทันทีเมื่อผลตรงกัน

ข้อผิดพลาดทั่วไปที่ทำให้การกรองหรือสิทธิ์พัง

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

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

ปัญหาคลาสสิกคือสร้างวงจรโดยไม่ตั้งใจ: A เป็นผู้จัดการของ B และต่อมามีใครสักคนตั้ง B เป็นผู้จัดการของ A (หรือวงจรยาวผ่าน 3-4 คน) คิวรีแบบ recursive อาจวิ่งไม่รู้จบ คืนแถวซ้ำ หรือตรงเวลาออก สิ่งนี้จะเกิดขึ้นได้แม้กับ closure table ก็สามารถปนเปื้อนแถวบรรพบุรุษ/ทายาทได้

ปัญหาทั่วไปอีกอย่างคือ drift ของ closure: คุณเปลี่ยนผู้จัดการของใครสักคน แต่แค่เปลี่ยนความสัมพันธ์โดยตรงแล้วลืม rebuild แถว closure สำหรับ subtree นั้น ต่อมาฟิลเตอร์เช่น "ทุกคนใต้ VP นี้" จะคืนผลผสมระหว่างโครงสร้างเก่าและใหม่ มักยากจะสังเกตเพราะหน้าโปรไฟล์แต่ละคนยังดูถูกต้อง

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

สิทธิ์พังบ่อยที่สุดเมื่อการตรวจสอบมองแค่ผู้จัดการโดยตรง หากคุณอนุญาตเมื่อ viewer is manager of employee คุณจะพลาดสายบังคับบัญชาทั้งหมด ผลคือบล็อกมากเกินไป (ผู้จัดการข้ามชั้นไม่เห็นองค์กรของตน) หรือแชร์มากเกินไป (ใครบางคนได้สิทธิ์เพราะถูกตั้งเป็นผู้จัดการโดยตรงชั่วคราว)

หน้ารายการที่ช้าบ่อยมาจากการรันการกรองแบบ recursive ในทุกคำขอ (ทุก inbox ทุกรายการ ticket ทุกการค้นหาพนักงาน) หากฟิลเตอร์เดียวกันใช้ทุกที่ คุณควรมีเส้นทาง precomputed (closure table) หรือชุด ID พนักงานที่อนุญาตแบบแคช

การป้องกันปฏิบัติที่แนะนำ:

  • บล็อกวงจรด้วยการตรวจสอบก่อนบันทึกการเปลี่ยนผู้จัดการ
  • ตัดสินใจว่า "แผนก" หมายถึงอะไร และแยกมันจากเส้นการรายงาน
  • ถ้าใช้ closure table ให้ rebuild แถวทายาทเมื่อเปลี่ยนผู้จัดการ
  • เขียนกฎสิทธิ์สำหรับสายทั้งหมด ไม่ใช่แค่ผู้จัดการโดยตรง
  • คำนวณขอบเขตองค์กรที่ใช้ในหน้า list ล่วงหน้า แทนที่จะคำนวณ recursion ทุกครั้ง

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

การตรวจสอบด่วนก่อนปล่อย

สร้างโมเดลข้อมูลองค์กรของคุณ
ออกแบบโมเดลข้อมูลพนักงาน ทีม และตาราง closure บน PostgreSQL แบบภาพ
ทดลองใช้ AppMaster

ก่อนเรียกผังองค์กรว่า "เสร็จ" ให้แน่ใจว่าคุณอธิบายการเข้าถึงเป็นภาษาง่าย ๆ ได้ หากใครถามว่า "ใครเห็นพนักงาน X และทำไม?" คุณควรชี้ไปยังกฎเดียวและคิวรี (หรือวิว) เดียวที่พิสูจน์ได้

ประสิทธิภาพเป็นการตรวจสอบถัดมา กับ adjacency list "แสดงทุกคนใต้ผู้จัดการนี้" กลายเป็นคิวรี recursive ที่ความเร็วขึ้นกับความลึกและการทำดัชนี กับ closure table การอ่านมักเร็ว แต่คุณต้องเชื่อเส้นทางการเขียนว่าจะรักษาตารางให้ถูกต้องหลังการเปลี่ยนแปลงทุกครั้ง

รายการตรวจสอบสั้น ๆ ก่อนปล่อย:

  • เลือกพนักงานคนหนึ่งและตามสิทธิ์จากต้นจนจบ: สายไหนให้สิทธิ์ และบทบาทใดปฏิเสธ
  • ทดสอบ benchmark คิวรี subtree ของผู้จัดการโดยใช้ขนาดคาดหวัง (เช่น 5 ระดับลึกและ 50,000 พนักงาน)
  • บล็อกการเขียนที่ผิด: ป้องกันวงจร การเป็นผู้จัดการตัวเอง และโหนดไร้พ่อแม่ด้วยข้อจำกัดและการตรวจสอบภายใน transaction
  • ทดสอบความปลอดภัยการปรับโครงสร้าง: ย้าย ผสาน การเปลี่ยนผู้จัดการ และ rollback เมื่อบางอย่างล้มเหลวกึ่งกลาง
  • เพิ่มการทดสอบสิทธิ์ที่ยืนยันทั้งการอนุญาตและการปฏิเสธสำหรับบทบาทจริง (HR, ผู้จัดการ, หัวหน้าทีม, ฝ่ายสนับสนุน)

สถานการณ์ปฏิบัติที่ต้องตรวจสอบ: เจ้าหน้าที่ซัพพอร์ตดูได้เฉพาะพนักงานในแผนกที่ได้รับมอบหมาย ในขณะที่ผู้จัดการดู subtree ทั้งหมดของตน หากคุณสามารถจำลองผังองค์กรใน PostgreSQL และพิสูจน์กฎทั้งสองด้วยการทดสอบ คุณก็ใกล้จะปล่อยแล้ว

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

สถานการณ์ตัวอย่างและขั้นตอนต่อไป

ลองนึกถึงบริษัทที่มีสามแผนก: Sales, Support และ Engineering แต่ละแผนกมีสองทีม และแต่ละทีมมีหัวหน้า Sales Lead A อนุมัติส่วนลดสำหรับทีมของตน Support Lead B ดูตั๋วทั้งหมดในแผนกของตน และ VP of Engineering ดูทุกอย่างใน Engineering

จากนั้นเกิดการปรับโครงสร้าง: ทีม Support หนึ่งทีมย้ายไปอยู่ใต้ Sales และเพิ่มผู้จัดการคนใหม่ระหว่างผู้อำนวยการฝ่ายขายกับหัวหน้าทีมสองคน ในวันถัดมา มีคำขอการเข้าถึงว่า: "ให้ Jamie (นักวิเคราะห์ฝ่ายขาย) ดูบัญชีลูกค้าทั้งหมดของแผนก Sales แต่ไม่ใช่ Engineering"

ถ้าคุณโมเดลผังองค์กรใน PostgreSQL ด้วย adjacency list สกีมาจะเรียบง่าย แต่งานของแอปจะย้ายไปที่คิวรีและการตรวจสิทธิ์ ฟิลเตอร์เช่น "ทุกคนใต้ Sales" มักต้อง recursion เมื่อคุณเพิ่มการอนุมัติ (เช่น "เฉพาะผู้จัดการในสายเท่านั้นที่อนุมัติได้") ขอบเขตหลังการปรับโครงสร้างจะเริ่มมีผล

กับ closure table การปรับโครงสร้างต้องงานเขียนมากขึ้น (อัพเดตแถวบรรพบุรุษ/ทายาท) แต่ฝั่งอ่านจะตรงไปตรงมา ฟิลเตอร์และสิทธิ์มักเป็น join ง่าย ๆ: "ผู้ใช้เป็นบรรพบุรุษของพนักงานคนนั้นหรือไม่?" หรือ "ทีมนี้อยู่ใน subtree แผนกนี้หรือไม่?"

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

ขั้นตอนต่อไป:

  1. เขียนกฎสิทธิ์เป็นภาษาง่าย ๆ (ใครเห็นอะไร และทำไม)
  2. เลือกโมเดลที่ตรงกับการตรวจสอบที่พบบ่อยที่สุด (อ่านเร็ว vs เขียนง่าย)
  3. สร้างเครื่องมือแอดมินภายในที่ทดสอบการปรับโครงสร้าง คำขอเข้าถึง และการอนุมัติแบบครบวงจร

ถ้าคุณต้องการสร้างแผงแอดมินและพอร์ทัลที่ตระหนักถึงโครงสร้างองค์กรได้เร็ว AppMaster (appmaster.io) อาจเหมาะ: มันให้คุณโมเดลข้อมูลที่ใช้ PostgreSQL ลงมือทำตรรกะการอนุมัติแบบภาพ และส่งมอบเว็บและแอปมือถือจาก backend เดียวกัน

คำถามที่พบบ่อย

เมื่อใดที่ควรใช้ adjacency list กับ closure table สำหรับโครงสร้างองค์กร?

ใช้ adjacency list เมื่อองค์กรของคุณเล็ก การอัพเดตเกิดบ่อย และหน้าส่วนใหญ่ต้องการแค่รายงานโดยตรงหรือระดับไม่กี่ชั้น ใช้ closure table เมื่อคุณต้องการบ่อย ๆ ฟีเจอร์เช่น “ทุกคนใต้หัวหน้าคนนี้” กรองตามแผนก หรือสิทธิ์ที่อิงโครงสร้างในหลายหน้าจอ เพราะการอ่านจะกลายเป็นการ join ธรรมดาและคงที่เมื่อขยายตัว

วิธีที่ง่ายที่สุดในการเก็บข้อมูล “ใครรายงานใคร” ใน PostgreSQL คืออะไร?

เริ่มด้วยคอลัมน์ employees(manager_id) แล้วดึงรายงานโดยตรงด้วยคำสั่งง่าย ๆ WHERE manager_id = ? เพิ่มการค้นหาแบบ recursive ก็ต่อเมื่อฟีเจอร์จำเป็นต้องใช้แหล่งข้อมูลบรรพบุรุษเต็มหรือ subtree ทั้งหมด เช่น การอนุมัติ ตัวกรอง “องค์กรของฉัน” หรือแดชบอร์ดข้ามชั้น

จะป้องกันวงจร (A เป็นผู้จัดการของ B แล้ว B เป็นผู้จัดการของ A) ได้อย่างไร?

บล็อกการตั้งค่าตัวเองเป็นผู้จัดการด้วยเงื่อนไขเช่น manager_id <> id และตรวจสอบการอัพเดตเพื่อไม่ให้ตั้งผู้จัดการเป็นคนที่อยู่ใน subtree ของพนักงานเอง ในทางปฏิบัติ วิธีที่ปลอดภัยที่สุดคือเช็คความเป็นบรรพบุรุษก่อนบันทึกการเปลี่ยนแปลงผู้จัดการ เพราะวงจรเดียวสามารถทำให้การค้นหาแบบ recursive พังและทำให้ตรรกะสิทธิ์เสียหายได้

ควรให้แผนกเป็นโหนดในลำดับชั้นเดียวกับคนหรือไม่?

ค่าเริ่มต้นที่ดีคือถือว่าแผนกเป็นการจัดกลุ่มเชิงบริหาร และเส้นการรายงานเป็นต้นไม้ของผู้จัดการที่แยกต่างหาก นั่นช่วยป้องกันไม่ให้การย้ายแผนกเปลี่ยนผู้ที่ใครรายงานต่อโดยไม่ได้ตั้งใจ และทำให้การกรองเช่น “ทุกคนใน Sales” ชัดเจนแม้เส้นการรายงานจะไม่ตรงกับขอบเขตแผนก

จะออกแบบองค์กรแบบเมทริกซ์ที่มีคนมีผู้จัดการสองคนได้อย่างไร?

โดยทั่วไปเก็บผู้จัดการหลักบนแถวพนักงานและเก็บความสัมพันธ์แบบ dotted-line แยกต่างหาก เช่น ตารางความสัมพันธ์ผู้จัดการรองหรือการแมป “ผู้นำทีม” วิธีนี้ช่วยหลีกเลี่ยงการทำลายการคิวรีพื้นฐานในขณะที่ยังอนุญาตกฎพิเศษเช่นการเข้าถึงตามโครงการหรือการมอบอำนาจการอนุมัติ

ต้องอัพเดตอะไรบ้างใน closure table เมื่อมีการเปลี่ยนผู้จัดการ?

คุณต้องลบเส้นทางบรรพบุรุษเก่า ๆ สำหรับ subtree ของพนักงานที่ย้ายออก แล้วแทรกเส้นทางใหม่โดยการรวมบรรพบุรุษของผู้จัดการใหม่กับทุกโหนดใน subtree ของพนักงานนั้น และคำนวณ depth ใหม่ ให้ทำทั้งหมดใน transaction เดียวเพื่อไม่ให้เกิดการอัพเดตครึ่งกลางหากมีข้อผิดพลาดเกิดขึ้น

ดัชนีไหนที่สำคัญที่สุดสำหรับการคิวรีโครงสร้างองค์กร?

สำหรับ adjacency list ให้ทำดัชนีที่ employees(manager_id) เพราะแทบทุกคิวรีเกี่ยวกับโครงสร้างเริ่มจากตรงนั้น และเพิ่มดัชนีสำหรับฟิลเตอร์ที่ใช้บ่อยเช่น team_id หรือ department_id สำหรับ closure table ดัชนีที่สำคัญคือคีย์หลัก (ancestor_id, descendant_id) และดัชนีแยกที่ descendant_id เพื่อให้การตรวจสอบ “ใครเห็นแถวนี้ได้?” ทำได้เร็ว

จะทำให้ “ผู้จัดการเห็นทุกคนภายใต้ตน” อย่างปลอดภัยได้อย่างไร?

รูปแบบทั่วไปคือ EXISTS บน closure table: อนุญาตเมื่อ viewer เป็นบรรพบุรุษของพนักงานเป้าหมาย นี่ทำงานได้ดีร่วมกับ row-level security เพราะฐานข้อมูลสามารถบังคับกฎได้อย่างสอดคล้อง แทนที่จะให้ API แต่ละจุดจำตรรกะ recursive เดียวกัน

จะจัดการประวัติการปรับโครงสร้างและการตรวจสอบได้อย่างไร?

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

จะย้ายจาก adjacency list ไปเป็น closure table โดยไม่ทำให้แอปพังได้อย่างไร?

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

ง่ายต่อการเริ่มต้น
สร้างบางสิ่งที่ น่าทึ่ง

ทดลองกับ AppMaster ด้วยแผนฟรี
เมื่อคุณพร้อม คุณสามารถเลือกการสมัครที่เหมาะสมได้

เริ่ม