2014-09-05 22 views
5

Tôi đã có một bảng trong postgres 9.3.5 trông như thế này:Postgres truy vấn đệ quy với row_to_json

CREATE TABLE customer_area_node 
(
    id bigserial NOT NULL, 
    customer_id integer NOT NULL, 
    parent_id bigint, 
    name text, 
    description text, 

    CONSTRAINT customer_area_node_pkey PRIMARY KEY (id) 
) 

tôi truy vấn với:

WITH RECURSIVE c AS (
     SELECT *, 0 as level, name as path FROM customer_area_node WHERE customer_id = 2 and parent_id is null 
     UNION ALL 
     SELECT customer_area_node.*, 
     c.level + 1 as level, 
     c.path || '/' || customer_area_node.name as path 
    FROM customer_area_node 
    join c ON customer_area_node.parent_id = c.id 
) 
SELECT * FROM c ORDER BY path; 

này dường như làm việc để xây dựng những con đường như building1/floor1/room1, building1/floor1/room2, v.v.

Điều tôi muốn làm là dễ dàng biến nó thành một trong hai json đại diện cho cấu trúc cây mà tôi đã được thông báo rằng tôi có thể làm row_to_json. Là một lựa chọn hợp lý, bất kỳ cách nào khác tôi có thể định dạng dữ liệu thành một cơ chế hiệu quả hơn để tôi có thể dễ dàng biến nó thành một cấu trúc cây thực tế mà không cần phải có một chuỗi string.splits trên /.

Có cách nào dễ dàng hợp lý để thực hiện việc này với row_to_json không?

+0

bạn có thể cung cấp dữ liệu mẫu không? –

Trả lời

7

Bạn không thể làm điều đó với thông thường CTE đệ quy, bởi vì gần như không thể đặt giá trị json sâu trong phân cấp của nó. Nhưng bạn có thể làm điều đó đảo ngược: xây dựng lên cây bắt đầu từ lá của nó, cho đến khi rễ của nó:

-- calculate node levels 
WITH RECURSIVE c AS (
    SELECT *, 0 as lvl 
    FROM customer_area_node 
    -- use parameters here, to select the root first 
    WHERE customer_id = 2 AND parent_id IS NULL 
    UNION ALL 
    SELECT customer_area_node.*, c.lvl + 1 as lvl 
    FROM customer_area_node 
    JOIN c ON customer_area_node.parent_id = c.id 
), 
-- select max level 
maxlvl AS (
    SELECT max(lvl) maxlvl FROM c 
), 
-- accumulate children 
j AS (
    SELECT c.*, json '[]' children -- at max level, there are only leaves 
    FROM c, maxlvl 
    WHERE lvl = maxlvl 
    UNION ALL 
    -- a little hack, because PostgreSQL doesn't like aggregated recursive terms 
    SELECT (c).*, array_to_json(array_agg(j)) children 
    FROM (
     SELECT c, j 
     FROM j 
     JOIN c ON j.parent_id = c.id 
    ) v 
    GROUP BY v.c 
) 
-- select only root 
SELECT row_to_json(j) json_tree 
FROM j 
WHERE lvl = 0; 

Và điều này sẽ làm việc ngay cả với PostgreSQL 9.2+

SQLFiddle

Cập nhật: Một biến thể, cũng nên xử lý các nút lá lừa đảo (được đặt với một mức từ 1 đến cấp cao nhất):

WITH RECURSIVE c AS (
    SELECT *, 0 as lvl 
    FROM customer_area_node 
    WHERE customer_id = 1 AND parent_id IS NULL 
    UNION ALL 
    SELECT customer_area_node.*, c.lvl + 1 
    FROM customer_area_node 
    JOIN c ON customer_area_node.parent_id = c.id 
), 
maxlvl AS (
    SELECT max(lvl) maxlvl FROM c 
), 
j AS (
    SELECT c.*, json '[]' children 
    FROM c, maxlvl 
    WHERE lvl = maxlvl 
    UNION ALL 
    SELECT (c).*, array_to_json(array_agg(j) || array(SELECT r 
                 FROM (SELECT l.*, json '[]' children 
                   FROM c l, maxlvl 
                   WHERE l.parent_id = (c).id 
                   AND l.lvl < maxlvl 
                   AND NOT EXISTS (SELECT 1 
                        FROM c lp 
                        WHERE lp.parent_id = l.id)) r)) children 
    FROM  (SELECT c, j 
       FROM c 
       JOIN j ON j.parent_id = c.id) v 
    GROUP BY v.c 
) 
SELECT row_to_json(j) json_tree 
FROM j 
WHERE lvl = 0; 

Điều này nên cũng hoạt động trên PostgreSQL 9.2+, tuy nhiên, tôi không thể kiểm tra điều đó. (Tôi chỉ có thể thử nghiệm trên 9.5+ ngay bây giờ).

Các giải pháp này có thể xử lý bất kỳ cột nào trong bất kỳ bảng phân cấp nào, nhưng sẽ luôn thêm một thuộc tính JSON int đã nhập lvl vào đầu ra của chúng.

http://rextester.com/YNU7932

+1

Tôi thấy rằng để làm việc này tất cả các lá phải có cùng cấp độ. – Macario

+0

@Macario trong ví dụ SQLFiddle, có các lá ở nhiều cấp độ (f.ex. '1.3.7' so với '1.3.6.9') và tất cả các nút được thu thập. – pozs

+1

@Macario Tôi gặp vấn đề tương tự như được báo cáo bởi @pozs. Tôi đã tạo một ví dụ SQLFiddle cho thấy trường hợp này với '1.4.10', chỉ có 2 lá thay vì 3 lá giống như tất cả các phần còn lại. Vì vậy, với SQL này, bạn phải có tất cả các chi nhánh với độ sâu chính xác tương tự. – ryanfelton

3

Xin lỗi vì sự rất muộn câu trả lời nhưng tôi nghĩ rằng tôi tìm thấy một giải pháp thanh lịch mà có thể trở thành một câu trả lời chấp nhận cho câu hỏi này.

Dựa trên tuyệt vời "ít hack" được tìm thấy bởi @pozs, tôi đã đưa ra một giải pháp mà:

  • giải quyết "lá giả mạo" tình hình với rất ít mã (tận dụng NOT EXISTS ngữ)
  • tránh những thứ tính bình quân cả tốt nghiệp/Thực trạng
WITH RECURSIVE customer_area_tree("id", "customer_id", "parent_id", "name", "description", "children") AS (
    -- tree leaves (no matching children) 
    SELECT c.*, json '[]' 
    FROM customer_area_node c 
    WHERE NOT EXISTS(SELECT * FROM customer_area_node AS hypothetic_child WHERE hypothetic_child.parent_id = c.id) 

    UNION ALL 

    -- pozs's awesome "little hack" 
    SELECT (parent).*, json_agg(child) AS "children" 
    FROM (
    SELECT parent, child 
    FROM customer_area_tree AS child 
    JOIN customer_area_node parent ON parent.id = child.parent_id 
) branch 
    GROUP BY branch.parent 
) 
SELECT json_agg(t) 
FROM customer_area_tree t 
LEFT JOIN customer_area_node AS hypothetic_parent ON(hypothetic_parent.id = t.parent_id) 
WHERE hypothetic_parent.id IS NULL 

cập nhật:

Tested with very simple data, nó hoạt động, nhưng như posz chỉ ra trong một bình luận, with his sample data, một số nút lá rogue bị lãng quên. Nhưng, tôi phát hiện ra rằng with even more complex data, câu trả lời trước đó không hoạt động, bởi vì chỉ có các nút lá giả mạo có tổ tiên chung với nút "mức tối đa" bị bắt (khi "1.2.5.8" không có ở đó, "1.2.4" và "1.2.5" vắng mặt vì chúng không có tổ tiên chung với bất kỳ nút "mức tối đa" nào.

Vì vậy, đây là một đề xuất mới, pha trộn công việc posz với tôi bằng cách chiết NOT EXISTS subrequest và làm cho nó một nội UNION, tận dụng UNION khả năng de-duplication (tận dụng khả năng so sánh jsonb):

<!-- language: sql --> 
WITH RECURSIVE 
c_with_level AS (

    SELECT *, 0 as lvl 
    FROM customer_area_node 
    WHERE parent_id IS NULL 

    UNION ALL 

    SELECT child.*, parent.lvl + 1 
    FROM customer_area_node child 
    JOIN c_with_level parent ON parent.id = child.parent_id 
), 
maxlvl AS (
    SELECT max(lvl) maxlvl FROM c_with_level 
), 
c_tree AS (
    SELECT c_with_level.*, jsonb '[]' children 
    FROM c_with_level, maxlvl 
    WHERE lvl = maxlvl 

    UNION 
    (
     SELECT (branch_parent).*, jsonb_agg(branch_child) 
     FROM (
      SELECT branch_parent, branch_child 
      FROM c_with_level branch_parent 
      JOIN c_tree branch_child ON branch_child.parent_id = branch_parent.id 
     ) branch 
     GROUP BY branch.branch_parent 

     UNION 

     SELECT c.*, jsonb '[]' children 
     FROM c_with_level c 
     WHERE NOT EXISTS (SELECT 1 FROM c_with_level hypothetical_child WHERE hypothetical_child.parent_id = c.id) 
    ) 
) 
SELECT jsonb_pretty(row_to_json(c_tree)::jsonb) 
FROM c_tree 
WHERE lvl = 0; 

Thử nghiệm trên http://rextester.com/SMM38494;)

+0

[Tôi sợ] (http://rextester.com/ZPMUB60906), nếu không có sự xử lý đặc biệt của các cấp, bạn sẽ kết thúc như nhiều "nhánh" riêng biệt trong đầu ra, vì nhiều lá đang tồn tại ở các cấp độ khác nhau. Dữ liệu mẫu của bạn chỉ có 0 hoặc 1 con cho mỗi nút, đó là lý do tại sao nó không hiển thị. – pozs

+0

Cảm ơn bạn đã quan sát! Tôi đã làm một số công việc xung quanh này, và tôi phát hiện ra rằng thủ thuật của bạn để xử lý các nút lá rogue là không hoàn toàn hiệu quả hoặc vì trong ví dụ của bạn, nếu bạn không có nút "1.2.5.8", chi nhánh "1.2 "không bao giờ bị bắt" 1.2.4 "và" 1.2.5 "không có kết quả cuối cùng. Đó là bởi vì bạn chỉ bắt các nút lá rogue khi chúng có một tổ tiên chung với các nút lá "max level". Tôi đã tìm được giải pháp cho điều này, tôi sẽ chỉnh sửa câu trả lời của tôi sau một phút;) –

+0

Cuối cùng! Cái này hoạt động rất tốt cho những cây bị rách rưới. –

0

Phát triển câu trả lời của pozs xa hơn một chút để có được lá thư đệ quy với các subtrees của chúng. Vì vậy, câu trả lời này thực sự trả về cây đầy đủ.

CREATE OR REPLACE FUNCTION pg_temp.getTree(bigint) 
    RETURNS TABLE( 
      id bigint, 
      customer_id integer, 
      parent_id bigint, 
      name text, 
      description text, 
      children json 
     ) 
     AS $$ 

     WITH RECURSIVE relations AS ( 
      SELECT 
       can.id, 
       can.customer_id, 
       can.parent_id, 
       can.name, 
       can.description, 
       0 AS depth 
       FROM customer_area_node can 
       WHERE can.id = $1 
      UNION ALL 
      SELECT 
       can.id, 
       can.customer_id, 
       can.parent_id, 
       can.name, 
       can.description, 
       relations.depth + 1 
       FROM customer_area_node can 
       JOIN relations ON can.parent_id = relations.id AND can.id != can.parent_id 
     ),  

     maxdepth AS ( 
      SELECT max(depth) maxdepth FROM relations 
     ), 

     rootTree as ( 
      SELECT r.* FROM 
       relations r, maxdepth 
       WHERE depth = maxdepth 
      UNION ALL 
      SELECT r.* FROM 
       relations r, rootTree 
       WHERE r.id = rootTree.parent_id AND rootTree.id != rootTree.parent_id 
     ), 

     mainTree AS ( 
      SELECT 
       c.id, 
       c.customer_id, 
       c.parent_id, 
       c.name, 
       c.description, 
       c.depth, 
       json_build_array() children 
       FROM relations c, maxdepth 
       WHERE c.depth = maxdepth 
      UNION ALL 
      SELECT 
       (relations).*, 
       array_to_json( 
        array_agg(mainTree) 
        || 
        array( 
         SELECT t 
          FROM ( 
           SELECT 
            l.*, 
            json_build_array() children 
           FROM relations l, maxdepth 
            WHERE l.parent_id = (relations).id 
            AND l.depth < maxdepth 
            AND l.id NOT IN ( 
             SELECT id FROM rootTree 
            ) 
          ) r 
          JOIN pg_temp.getTree(r.id) t 
          ON r.id = t.id 
         )) 
       children 
    FROM ( 
     SELECT relations, mainTree 
      FROM relations 
     JOIN mainTree 
      ON ( 
       mainTree.parent_id = relations.id 
       AND mainTree.parent_id != mainTree.id 
      ) 
    ) v 
    GROUP BY v.relations 
    ) 

     SELECT 
      id, 
      customer_id, 
      parent_id, 
      name, 
      description, 
      children 
     FROM mainTree WHERE id = $1 
    $$ 
    LANGUAGE SQL; 

    SELECT * 
    FROM 
     customer_area_node can 
     JOIN pg_temp.getTree(can.id) t ON t.id = can.id 
    WHERE can.parent_id IS NULL;