最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

PostgreSQL優(yōu)雅的進行遞歸查詢的實戰(zhàn)指南

 更新時間:2026年01月26日 09:50:25   作者:數(shù)據(jù)知道  
在實際開發(fā)中,我們經(jīng)常會遇到樹形結(jié)構(gòu)或圖結(jié)構(gòu)的數(shù)據(jù)需求,這些場景的核心問題是:如何高效查詢具有層級/遞歸關(guān)系的數(shù)據(jù)?所以本文將從基礎(chǔ)到實戰(zhàn),手把手教你掌握遞歸查詢的精髓,需要的朋友可以參考下

在實際開發(fā)中,我們經(jīng)常會遇到樹形結(jié)構(gòu)圖結(jié)構(gòu)的數(shù)據(jù)需求,比如:

  • 組織架構(gòu)(部門 → 子部門)
  • 商品分類(一級類目 → 二級類目 → …)
  • 評論回復(fù)(評論 → 回復(fù) → 回復(fù)的回復(fù))
  • 權(quán)限繼承(角色 → 子角色)
  • 路徑查找(最短路徑、依賴關(guān)系)

這些場景的核心問題是:如何高效查詢具有層級/遞歸關(guān)系的數(shù)據(jù)?

PostgreSQL 提供了強大的 WITH RECURSIVE(公共表表達式遞歸) 功能,是處理此類問題的標(biāo)準(zhǔn) SQL 解決方案。本文將從基礎(chǔ)到實戰(zhàn),手把手教你掌握遞歸查詢的精髓。

一、遞歸查詢基礎(chǔ):CTE 與WITH RECURSIVE

1.1 什么是 CTE(Common Table Expression)?

CTE 是一種臨時結(jié)果集,可被主查詢引用,語法如下:

WITH cte_name AS (
    -- 查詢語句
)
SELECT * FROM cte_name;

優(yōu)點:提升 SQL 可讀性、避免重復(fù)子查詢、支持遞歸

1.2 遞歸 CTE 的基本結(jié)構(gòu)

WITH RECURSIVE cte_name AS (
    -- 1. 初始查詢(錨點成員 Anchor Member)
    SELECT ... FROM table WHERE ...

    UNION [ALL]

    -- 2. 遞歸查詢(遞歸成員 Recursive Member)
    SELECT ... FROM table, cte_name WHERE ...
)
SELECT * FROM cte_name;

核心三要素:

部分作用注意事項
初始查詢定義遞歸起點(如根節(jié)點)必須能終止遞歸
UNION [ALL]合并結(jié)果集UNION 去重,UNION ALL 保留重復(fù)(性能更高)
遞歸查詢引用自身 CTE,向下/向上遍歷必須有連接條件,避免無限循環(huán)

1.3 遞歸查詢的建議

場景推薦方案
標(biāo)準(zhǔn)樹形查詢(上下級)WITH RECURSIVE + UNION ALL
防循環(huán)記錄訪問路徑 ARRAY[id] + != ALL(path)
限制深度添加 depth 字段 + WHERE depth < N
高性能讀物化路徑 / 閉包表(寫少讀多)
返回樹形 JSON自底向上聚合 + jsonb_build_object
Python 集成直接執(zhí)行原生 SQL(SQLAlchemy 支持 CTE)

終極建議
“90% 的樹形查詢,一個精心設(shè)計的 WITH RECURSIVE 就夠了。”
只有在性能成為瓶頸時,才考慮物化路徑等復(fù)雜模型。

二、經(jīng)典場景實戰(zhàn):組織架構(gòu)查詢

假設(shè)有一張部門表 departments

CREATE TABLE departments (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    parent_id INTEGER REFERENCES departments(id)
);

-- 插入示例數(shù)據(jù)
INSERT INTO departments (name, parent_id) VALUES
('總公司', NULL),
('技術(shù)部', 1),
('產(chǎn)品部', 1),
('前端組', 2),
('后端組', 2),
('iOS組', 2),
('設(shè)計組', 3);

2.1 查詢“技術(shù)部”及其所有子部門(向下遞歸)

WITH RECURSIVE dept_tree AS (
    -- 錨點:找到“技術(shù)部”
    SELECT id, name, parent_id, 0 AS level
    FROM departments
    WHERE name = '技術(shù)部'

    UNION ALL

    -- 遞歸:找子部門
    SELECT d.id, d.name, d.parent_id, dt.level + 1
    FROM departments d
    INNER JOIN dept_tree dt ON d.parent_id = dt.id
)
SELECT 
    LPAD('', level * 4, ' ') || name AS hierarchy,  -- 縮進顯示層級
    id, parent_id, level
FROM dept_tree
ORDER BY level;

輸出結(jié)果:

hierarchy        | id | parent_id | level
-----------------|----|-----------|------
技術(shù)部           | 2  | 1         | 0
    前端組       | 4  | 2         | 1
    后端組       | 5  | 2         | 1
    iOS組        | 6  | 2         | 1

技巧:LPAD('', level * 4, ' ') 生成縮進,直觀展示樹形結(jié)構(gòu)

2.2 查詢“后端組”的完整上級路徑(向上遞歸)

WITH RECURSIVE dept_path AS (
    -- 錨點:從“后端組”開始
    SELECT id, name, parent_id, 0 AS level
    FROM departments
    WHERE name = '后端組'

    UNION ALL

    -- 遞歸:找父部門
    SELECT d.id, d.name, d.parent_id, dp.level + 1
    FROM departments d
    INNER JOIN dept_path dp ON d.id = dp.parent_id
    WHERE dp.parent_id IS NOT NULL  -- 避免 NULL 連接
)
SELECT 
    REPEAT(' → ', level) || name AS path_from_root
FROM dept_path
ORDER BY level DESC;  -- 從根到當(dāng)前節(jié)點

輸出結(jié)果:

path_from_root
---------------------------
總公司 → 技術(shù)部 → 后端組

三、高級技巧:控制遞歸深度與防環(huán)

3.1 限制遞歸深度(防止無限循環(huán))

WITH RECURSIVE dept_limited AS (
    SELECT id, name, parent_id, 1 AS depth
    FROM departments
    WHERE parent_id IS NULL  -- 從根開始

    UNION ALL

    SELECT d.id, d.name, d.parent_id, dl.depth + 1
    FROM departments d
    INNER JOIN dept_limited dl ON d.parent_id = dl.id
    WHERE dl.depth < 3  -- 最多查3層
)
SELECT * FROM dept_limited;

3.2 檢測并避免循環(huán)引用(圖結(jié)構(gòu)必備)

如果數(shù)據(jù)存在循環(huán)(如 A→B→C→A),遞歸會無限進行。解決方案:記錄訪問路徑

WITH RECURSIVE graph_traversal AS (
    -- 錨點
    SELECT 
        id, 
        name, 
        parent_id,
        ARRAY[id] AS path,      -- 記錄已訪問節(jié)點
        1 AS depth
    FROM departments
    WHERE name = '技術(shù)部'

    UNION ALL

    -- 遞歸
    SELECT 
        d.id,
        d.name,
        d.parent_id,
        gt.path || d.id,        -- 追加當(dāng)前節(jié)點
        gt.depth + 1
    FROM departments d
    INNER JOIN graph_traversal gt ON d.parent_id = gt.id
    WHERE 
        d.id != ALL(gt.path)    -- 關(guān)鍵:當(dāng)前節(jié)點不在已訪問路徑中
        AND gt.depth < 10       -- 安全兜底
)
SELECT * FROM graph_traversal;

d.id != ALL(gt.path) 確保不重復(fù)訪問節(jié)點,徹底解決循環(huán)問題

3.3 反向應(yīng)用:扁平數(shù)據(jù)轉(zhuǎn)樹形 JSON

PostgreSQL 支持將遞歸結(jié)果直接轉(zhuǎn)為 嵌套 JSON,適合 API 返回。如使用 jsonb_build_object 構(gòu)建樹

WITH RECURSIVE tree AS (
    -- 葉子節(jié)點(無子節(jié)點)
    SELECT 
        id,
        name,
        parent_id,
        jsonb_build_object('id', id, 'name', name, 'children', '[]'::jsonb) AS node
    FROM categories c1
    WHERE NOT EXISTS (
        SELECT 1 FROM categories c2 WHERE c2.parent_id = c1.id
    )

    UNION ALL

    -- 非葉子節(jié)點(聚合子節(jié)點)
    SELECT 
        p.id,
        p.name,
        p.parent_id,
        jsonb_build_object(
            'id', p.id,
            'name', p.name,
            'children', jsonb_agg(t.node)
        ) AS node
    FROM categories p
    INNER JOIN tree t ON t.parent_id = p.id
    GROUP BY p.id, p.name, p.parent_id
)
SELECT node 
FROM tree 
WHERE parent_id IS NULL;  -- 返回根節(jié)點

輸出 JSON:

{
  "id": 1,
  "name": "電子產(chǎn)品",
  "children": [
    {
      "id": 2,
      "name": "手機",
      "children": [
        {"id": 3, "name": "iPhone", "children": []},
        {"id": 4, "name": "華為", "children": []}
      ]
    },
    {
      "id": 5,
      "name": "電腦",
      "children": [
        {"id": 6, "name": "筆記本", "children": []}
      ]
    }
  ]
}

此方法利用 自底向上聚合,天然避免循環(huán),但要求數(shù)據(jù)為嚴(yán)格樹形(無環(huán))

四、實戰(zhàn)案例:商品分類樹

4.1 場景:電商商品分類(多級類目)

CREATE TABLE categories (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    parent_id INTEGER REFERENCES categories(id),
    is_leaf BOOLEAN DEFAULT false  -- 是否葉子節(jié)點
);

-- 插入數(shù)據(jù)
INSERT INTO categories (name, parent_id, is_leaf) VALUES
('電子產(chǎn)品', NULL, false),
('手機', 1, false),
('iPhone', 2, true),
('華為', 2, true),
('電腦', 1, false),
('筆記本', 5, true);

4.2 查詢“電子產(chǎn)品”下所有葉子類目(帶完整路徑)

WITH RECURSIVE category_tree AS (
    -- 錨點:根類目
    SELECT 
        id, 
        name, 
        parent_id,
        name::TEXT AS full_path,  -- 路徑字符串
        1 AS level
    FROM categories
    WHERE name = '電子產(chǎn)品'

    UNION ALL

    -- 遞歸:拼接路徑
    SELECT 
        c.id,
        c.name,
        c.parent_id,
        ct.full_path || ' > ' || c.name,  -- 路徑拼接
        ct.level + 1
    FROM categories c
    INNER JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT 
    full_path,
    id,
    level
FROM category_tree
WHERE is_leaf = true;  -- 只查葉子節(jié)點

輸出:

full_path                   | id | level
----------------------------|----|------
電子產(chǎn)品 > 手機 > iPhone   | 3  | 3
電子產(chǎn)品 > 手機 > 華為     | 4  | 3
電子產(chǎn)品 > 電腦 > 筆記本   | 6  | 3

4.3 Python + SQLAlchemy 實戰(zhàn)

在 Python 中使用遞歸查詢:

from sqlalchemy import text
from sqlalchemy.orm import sessionmaker

def get_dept_tree(session, root_name):
    query = text("""
        WITH RECURSIVE dept_tree AS (
            SELECT id, name, parent_id, 0 AS level
            FROM departments
            WHERE name = :root_name
            UNION ALL
            SELECT d.id, d.name, d.parent_id, dt.level + 1
            FROM departments d
            INNER JOIN dept_tree dt ON d.parent_id = dt.id
        )
        SELECT * FROM dept_tree ORDER BY level;
    """)
    
    result = session.execute(query, {"root_name": root_name})
    return result.fetchall()

# 使用
with Session() as session:
    tree = get_dept_tree(session, "技術(shù)部")
    for row in tree:
        print(f"{'  ' * row.level}{row.name}")

五、性能優(yōu)化:索引與執(zhí)行計劃

5.1 必建索引

-- 對 parent_id 建索引(遞歸連接的關(guān)鍵)
CREATE INDEX idx_departments_parent_id ON departments(parent_id);

-- 如果常按 name 查詢根節(jié)點
CREATE INDEX idx_departments_name ON departments(name);

5.2 查看執(zhí)行計劃

EXPLAIN (ANALYZE, BUFFERS)
WITH RECURSIVE ... ;  -- 你的遞歸查詢

關(guān)鍵觀察點:

  • 是否使用了 Index Scan(而非 Seq Scan
  • 遞歸深度是否合理
  • 內(nèi)存使用(Buffers

5.3 大數(shù)據(jù)量優(yōu)化建議

問題解決方案
遞歸太深(>100層)限制 depth < N,業(yè)務(wù)上通常不需要過深層級
數(shù)據(jù)量大(百萬級)分頁查詢(先查ID再關(guān)聯(lián))、物化路徑(見下文)
頻繁查詢使用 物化路徑(Materialized Path)閉包表(Closure Table)

六、替代方案對比:何時不用遞歸?

雖然 WITH RECURSIVE 很強大,但在某些場景下,其他模型更高效

6.1 物化路徑(Materialized Path)

在每條記錄中存儲完整路徑:

ALTER TABLE categories ADD COLUMN path TEXT; -- 如 "/1/2/3/"

-- 查詢“手機”下所有子類目
SELECT * FROM categories 
WHERE path LIKE '/1/2/%';

? 優(yōu)點:查詢極快(走索引)
? 缺點:移動節(jié)點時需更新大量 path

6.2 閉包表(Closure Table)

額外建一張表存儲所有祖先-后代關(guān)系:

CREATE TABLE category_closure (
    ancestor_id INT,
    descendant_id INT,
    depth INT
);

-- 查詢“手機”(id=2)的所有后代
SELECT c.* 
FROM categories c
JOIN category_closure cl ON c.id = cl.descendant_id
WHERE cl.ancestor_id = 2;

? 優(yōu)點:查詢快,支持任意深度
? 缺點:寫操作復(fù)雜,存儲空間大

選擇建議

  • 讀多寫少 + 深度固定 → 物化路徑
  • 頻繁查詢?nèi)窂?/strong> → 閉包表
  • 通用場景 + 中小數(shù)據(jù)量 → WITH RECURSIVE

七、常見陷阱與避坑指南

陷阱 1:忘記WHERE條件導(dǎo)致無限循環(huán)

-- 錯誤:缺少終止條件
SELECT ... FROM table, cte WHERE table.parent_id = cte.id
-- 如果存在循環(huán)引用,永遠停不下來!

? 解決:始終加上 depth < N 或路徑檢測

陷阱 2:使用UNION而非UNION ALL

  • UNION 會去重,但遞歸中通常不需要(父子ID唯一)
  • 性能損失高達 30%+

? 解決:除非明確需要去重,否則用 UNION ALL

陷阱 3:在遞歸部分使用聚合函數(shù)

-- 錯誤:遞歸成員不能包含聚合
SELECT ..., COUNT(*) FROM ... JOIN cte ...

? 解決:先遞歸,再在外層聚合

以上就是PostgreSQL優(yōu)雅的進行遞歸查詢的實戰(zhàn)指南的詳細內(nèi)容,更多關(guān)于PostgreSQL進行遞歸查詢的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • PostgreSQL三種自增列sequence,serial,identity的用法區(qū)別

    PostgreSQL三種自增列sequence,serial,identity的用法區(qū)別

    這篇文章主要介紹了PostgreSQL三種自增列sequence,serial,identity的用法區(qū)別,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-02-02
  • 查詢PostgreSQL中所有表邏輯外鍵的方法

    查詢PostgreSQL中所有表邏輯外鍵的方法

    本文介紹了如何查詢PostgreSQL中所有表的邏輯外鍵,并指導(dǎo)您如何先刪除再重新建立這些外鍵,本文通過示例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友一起看看吧
    2023-08-08
  • PostgreSQL出現(xiàn)死鎖該如何解決

    PostgreSQL出現(xiàn)死鎖該如何解決

    昨天在對一張表執(zhí)行一條update語句的時候,沒有修改成功,直接終止執(zhí)行,就瘋狂點擊執(zhí)行,執(zhí)行了很多次這條語句導(dǎo)致了表被死鎖了,這篇文章主要給大家介紹了關(guān)于PostgreSQL出現(xiàn)死鎖該如何解決的相關(guān)資料,需要的朋友可以參考下
    2022-05-05
  • PostgreSQL簡介及實戰(zhàn)應(yīng)用

    PostgreSQL簡介及實戰(zhàn)應(yīng)用

    PostgreSQL是一種功能強大的開源關(guān)系型數(shù)據(jù)庫管理系統(tǒng),以其穩(wěn)定性、高性能、擴展性和復(fù)雜查詢能力在眾多項目中得到廣泛應(yīng)用,本文將從基礎(chǔ)概念講起,逐步深入到高級特性、性能優(yōu)化和實戰(zhàn)應(yīng)用,幫助讀者全面掌握PostgreSQL,感興趣的朋友跟隨小編一起學(xué)習(xí)吧
    2025-08-08
  • postgresql數(shù)據(jù)庫執(zhí)行計劃圖文詳解

    postgresql數(shù)據(jù)庫執(zhí)行計劃圖文詳解

    了解PostgreSQL執(zhí)行計劃對于程序員來說是一項關(guān)鍵技能,執(zhí)行計劃是我們優(yōu)化查詢,驗證我們的優(yōu)化查詢是否確實按照我們期望的方式運行的重要方式,這篇文章主要給大家介紹了關(guān)于postgresql數(shù)據(jù)庫執(zhí)行計劃的相關(guān)資料,需要的朋友可以參考下
    2024-01-01
  • postgres 連接數(shù)查看與設(shè)置操作

    postgres 連接數(shù)查看與設(shè)置操作

    這篇文章主要介紹了postgres 連接數(shù)查看與設(shè)置操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-01-01
  • PostgreSQL向數(shù)據(jù)庫表中添加行數(shù)據(jù)的操作

    PostgreSQL向數(shù)據(jù)庫表中添加行數(shù)據(jù)的操作

    這篇文章主要介紹了PostgreSQL向數(shù)據(jù)庫表中添加行數(shù)據(jù)的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-02-02
  • PostgreSQL數(shù)據(jù)庫中窗口函數(shù)的語法與使用

    PostgreSQL數(shù)據(jù)庫中窗口函數(shù)的語法與使用

    這PostgreSQL中提供了窗口函數(shù),一個窗口函數(shù)在一系列與當(dāng)前行有某種關(guān)聯(lián)的表行上進行一種計算。下面這篇文章主要給大家介紹了關(guān)于PostgreSQL數(shù)據(jù)庫中窗口函數(shù)的語法與使用的相關(guān)資料,需要的朋友可以參考下
    2019-03-03
  • PostgreSQL 正則表達式替換-使用變量方式

    PostgreSQL 正則表達式替換-使用變量方式

    這篇文章主要介紹了PostgreSQL 正則表達式替換-使用變量方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-01-01
  • 解決PostgreSQL日志信息占用磁盤過大的問題

    解決PostgreSQL日志信息占用磁盤過大的問題

    解決PostgreSQL日志信息占用磁盤過大的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-01-01

最新評論

乐山市| 普兰店市| 铜山县| 天峻县| 遵化市| 商南县| 桑日县| 同仁县| 四川省| 腾冲县| 鄂托克前旗| 门源| 家居| 松潘县| 德兴市| 曲沃县| 平和县| 蓬溪县| 鄂尔多斯市| 女性| 新巴尔虎左旗| 巴中市| 基隆市| 张家口市| 米脂县| 平遥县| 响水县| 莒南县| 水富县| 广河县| 普格县| 嘉义县| 绵阳市| 会同县| 辽阳市| 柯坪县| 绥中县| 卢氏县| 阜南县| 阿拉尔市| 洛宁县|