MySQL 8.0+ 支持 WITH RECURSIVE 递归CTE,可动态展开任意深度部门树:先以用户直属部门为锚点,再通过 UNION ALL 逐层关联 parent_id,避免写死JOIN层数,解决权限继承等层级查询问题。

用 WITH RECURSIVE 展开部门树,而不是写死 JOIN 层数
部门权限继承(比如“技术部→研发中心→总部”)天然具有层级结构,硬写 JOIN department d2 ON d1.parent_id = d2.id 两层或三层,既不可维护,也覆盖不了动态深度。PostgreSQL/SQL Server/MySQL 8.0+ 都支持递归 CTE,这才是正解。
- 必须有非递归锚点:从当前用户所属的直接部门开始,
SELECT dept_id FROM user_dept WHERE user_id = ? - 递归部分只能用
UNION ALL(不是UNION),且必须显式关联上一层的dept_id到下一层的parent_id - PostgreSQL 默认递归深度是 100,如果组织架构特别深(如集团多级子公司),得加
SEARCH DEPTH FIRST BY dept_id SET ordercol并在外部WHERE ordercol 控制
视图里不能直接用 CURRENT_USER 做部门过滤,得靠会话变量或参数化入口
CURRENT_USER 返回的是数据库登录账号,不是业务系统里的用户 ID 或部门 ID。你查出来的可能是 app_user@localhost,而部门表里存的是 dept_id = 101 —— 两者根本对不上。
- PostgreSQL 推荐用
current_setting('app.user_dept_ids', true),应用层在查询前执行SET app.user_dept_ids = '101,102,105',再在视图里WHERE dept_id = ANY(string_to_array(current_setting('app.user_dept_ids', true), ',')::int[]) - SQL Server 可用
SESSION_CONTEXT(N'user_dept_ids'),但需提前调用sp_set_session_context设置,且该值不跨连接池复用 - MySQL 8.0+ 没有等效函数,只能靠应用层拼
IN (101,102,105)到视图定义里——这意味着你不能用固定视图,得用预编译语句或动态 SQL
WHERE EXISTS + 递归结果集,比 LEFT JOIN 更安全
有人想用 LEFT JOIN user_dept ud ON ud.dept_id = d.id 再配 WHERE ud.user_id IS NOT NULL,这在部门树展开后极易误匹配:一个用户属于多个部门,JOIN 会爆炸出笛卡尔积,导致重复行或过滤失效。
- 正确姿势是把递归结果封装成临时关系,然后用
EXISTS关联主表:WHERE EXISTS (SELECT 1 FROM dept_tree dt WHERE dt.dept_id = t.dept_id) -
EXISTS不取数据、不关心重复、不触发隐式转换,性能和语义都更稳 - 别漏掉
SELECT 1—— 写SELECT *或SELECT dept_id不仅多余,还可能让优化器误判执行计划
视图必须配合权限收口,否则形同虚设
只建一个带递归逻辑的视图远远不够。用户只要能绕过视图直查底层表,整套权限就崩了。
- 撤掉用户对原始表(如
t_department、t_order)的SELECT权限,只给视图权限 - PostgreSQL 还要确保用户没有
USAGE权限到 schema,否则可能通过\d看到表结构 - SQL Server 上,视图若引用了其他 schema 的表,用户还需被授予那个 schema 的
VIEW DEFINITION权限,否则报错The SELECT permission was denied
递归逻辑本身不难,难的是把会话上下文、权限边界、执行计划三者扣严。漏掉任意一环,就可能让“部门主管看到全公司订单”这种事故上线。

















