SQL 中更复杂的孤岛和缺口问题

A more complex Island & Gaps problem in SQL

首先,如果已经讨论过这个问题,我想道歉,但经过 4 小时的搜索,我找不到任何可以帮助我解决问题的东西。

情况是这样的:有一些塔,不同的客户可以在塔上的不同高度安装不同的设备(就像电信中的塔)。

这些是事实:

要求是确定该塔中每个客户端的可用性。如果客户的设备只安装在一侧,则视为部分可用;如果根本没有设备,它将被视为可用。如果客户将设备安装在塔的底部,则该高度将被视为不可用。

这些将是我们正在处理的数据:

create table tower_test
(Tower_Number        VARCHAR2(12),
 Tower_Side          VARCHAR2(1),
 Tower_Height        NUMBER,
 Tower_Height_Um     VARCHAR2(1) default 'm',
 Client              VARCHAR2(25),
 Client_Start_Height NUMBER,
 Range_From          NUMBER,
 Range_To            NUMBER);
 
-- No Client

-- Side A
-- Client 1 
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'A', 50, 'Client_1', 12, 17, 18);
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'A', 50, 'Client_1', 12, 22, 23);
-- Client 2
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'A', 50, 'Client_2', 24, 35, 36);
-- Client 3
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'A', 50, 'Client_3', 40, 40, 41);
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'A', 50, 'Client_3', 40, 47, 48);

-- Side B
-- Client 1
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'B', 50, 'Client_1', 12, 13, 14);
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'B', 50, 'Client_1', 12, 19, 20);
-- Client 2
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'B', 50, 'Client_2', 24, 31, 32);
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'B', 50, 'Client_2', 24, 37, 38);
-- Client 3
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'B', 50, 'Client_3', 40, 43, 44);
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'B', 50, 'Client_3', 40, 46, 47);

-- Side C
-- Client 1 
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'C', 50, 'Client_1', 12, 17, 18);
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'C', 50, 'Client_1', 12, 22, 23);
-- Client 2
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'C', 50, 'Client_2', 24, 28, 29);
-- Client 3
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'C', 50, 'Client_3', 40, 40, 41);
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'C', 50, 'Client_3', 40, 47, 48);

-- Side D
-- Client 1
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'D', 50, 'Client_1', 12, 19, 20);
-- Client 2
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'D', 50, 'Client_2', 24, 31, 32);
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'D', 50, 'Client_2', 24, 37, 38);
-- Client 3
INSERT INTO tower_test (Tower_Number, Tower_Side, Tower_Height, Client, Client_Start_Height, Range_From, Range_To) VALUES ('123456_TWR1', 'D', 50, 'Client_3', 40, 46, 47);

COMMIT;

SELECT * FROM tower_test;

所以,从这组数据开始:

我需要这种格式:

有没有办法在 Oracle SQL 中使用 Gap & Island 方法获得它?如果是的话,你能解释一下如何实现吗?

谢谢!

这是一个填补范围空白的解决方案。

与预期的结果不完全一样。
F.e。空隙获得可用的 'A' 代码。
但它会是你开始的东西。

第一个 CTE 将数据展平,并获取塔侧面的计数。
第二个 CTE 增加了差距。

WITH CTE1_FLATTEND AS 
(
    SELECT 
      Tower_Number
    , Client
    , Client_Start_Height
    , Range_From
    , Range_To
    , COUNT(DISTINCT
            CASE
            WHEN Tower_Side IN ('A','C')
            THEN Tower_Side
            END) AS Total_AC
    , COUNT(DISTINCT
            CASE
            WHEN Tower_Side IN ('B','D')
            THEN Tower_Side
            END) AS Total_BD
    , COUNT(DISTINCT Tower_Side) AS Total_ABCD
    , MAX(Tower_Height) AS Tower_Height
    , MAX(Tower_Height_Um) AS Tower_Height_Um
    FROM tower_test
    GROUP BY
      Tower_Number
    , Client
    , Client_Start_Height
    , Range_From
    , Range_To
)
, CTE2_GAPS_ADDED AS
(
    SELECT
      Tower_Number
    , Client
    , Client_Start_Height
    , Client_Start_Height AS Next_Client_Start_Height
    , COALESCE(LAG(Range_To) 
        OVER (PARTITION BY Tower_Number, Client_Start_Height
              ORDER BY Range_From)+1, Client_Start_Height) AS Range_From
    , Range_From-1 AS Range_To
    , 'A' AS Avlblt_Type -- Available Gap
    , Tower_Height
    , Tower_Height_Um
    FROM CTE1_FLATTEND
   
    UNION

    SELECT
      Tower_Number
    , Client
    , Client_Start_Height
    , COALESCE(LEAD(Client_Start_Height) 
        OVER (PARTITION BY Tower_Number
              ORDER BY Range_From), Client_Start_Height) AS Next_Client_Start_Height
    , Range_To+1 AS Range_From
    , COALESCE(LEAD(Range_From) 
        OVER (PARTITION BY Tower_Number
              ORDER BY Range_From)-1, Tower_Height) AS Range_To
    , 'A' AS Avlblt_Type -- Available Gap
    , Tower_Height
    , Tower_Height_Um
    FROM CTE1_FLATTEND
   
    UNION ALL
    
    SELECT
      Tower_Number
    , Client
    , Client_Start_Height
    , Client_Start_Height AS Next_Client_Start_Height
    , Range_From
    , Range_To
    , CASE
      WHEN Total_ABCD = 1 THEN 'P' -- Partial
      WHEN Total_ABCD > 2 THEN 'F' -- Fault
      WHEN Total_AC = 2   THEN 'T' -- Taken A C
      WHEN Total_BD = 2   THEN 'T' -- Taken B D
      ELSE 'E' -- Exception
      END AS Avlblt_Type
    , Tower_Height
    , Tower_Height_Um
    FROM CTE1_FLATTEND
    
    UNION ALL
    
    SELECT
      Tower_Number
    , 'No Client' AS Client
    , 1 AS Client_Start_Height
    , 0 AS Next_Client_Start_Height
    , 0 AS Range_From
    , MIN(Client_Start_Height)-1 AS Range_To
    , 'T' AS Avlblt_Type
    , Tower_Height
    , Tower_Height_Um
    FROM CTE1_FLATTEND
    GROUP BY 
      Tower_Number
    , Tower_Height
    , Tower_Height_Um
) 
SELECT 
  Tower_Number
, Tower_Height
, Tower_Height_Um
, Client
, Avlblt_Type
, Range_From
, Range_To
--, Client_Start_Height
--, Next_Client_Start_Height
FROM CTE2_GAPS_ADDED
WHERE Range_From <= Range_To
  AND NOT (Client_Start_Height < Next_Client_Start_Height
           AND Range_From >= Next_Client_Start_Height)
ORDER BY Tower_Number, Range_From;
TOWER_NUMBER | TOWER_HEIGHT | TOWER_HEIGHT_UM | CLIENT    | AVLBLT_TYPE | RANGE_FROM | RANGE_TO
:----------- | -----------: | :-------------- | :-------- | :---------- | ---------: | -------:
123456_TWR1  |           50 | m               | No Client | T           |          0 |       11
123456_TWR1  |           50 | m               | Client_1  | A           |         12 |       12
123456_TWR1  |           50 | m               | Client_1  | P           |         13 |       14
123456_TWR1  |           50 | m               | Client_1  | A           |         15 |       16
123456_TWR1  |           50 | m               | Client_1  | T           |         17 |       18
123456_TWR1  |           50 | m               | Client_1  | T           |         19 |       20
123456_TWR1  |           50 | m               | Client_1  | A           |         21 |       21
123456_TWR1  |           50 | m               | Client_1  | T           |         22 |       23
123456_TWR1  |           50 | m               | Client_2  | A           |         24 |       27
123456_TWR1  |           50 | m               | Client_2  | P           |         28 |       29
123456_TWR1  |           50 | m               | Client_2  | A           |         30 |       30
123456_TWR1  |           50 | m               | Client_2  | T           |         31 |       32
123456_TWR1  |           50 | m               | Client_2  | A           |         33 |       34
123456_TWR1  |           50 | m               | Client_2  | P           |         35 |       36
123456_TWR1  |           50 | m               | Client_2  | T           |         37 |       38
123456_TWR1  |           50 | m               | Client_2  | A           |         39 |       39
123456_TWR1  |           50 | m               | Client_3  | T           |         40 |       41
123456_TWR1  |           50 | m               | Client_3  | A           |         42 |       42
123456_TWR1  |           50 | m               | Client_3  | P           |         43 |       44
123456_TWR1  |           50 | m               | Client_3  | A           |         45 |       45
123456_TWR1  |           50 | m               | Client_3  | T           |         46 |       47
123456_TWR1  |           50 | m               | Client_3  | T           |         47 |       48
123456_TWR1  |           50 | m               | Client_3  | A           |         49 |       50

演示 db<>fiddle here

第二个 CTE 大量使用第一个 CTE。
为了提高性能,您可以从一开始就用查询填充临时 table。 然后将 CTE 中的温度 table 与联合一起使用。

如果您的数据为第三范式且数据为四 tables:

,这可能会更容易
tower (tower_number, tower_height, tower_height_um)
tower_sides (tower_number, tower_side)
tower_client (tower_number, client, client_start_height, client_end_height)
tower_equipment(tower, tower_side, client, range_from, range_to)

但是,可以将其拆分为 table 中的那些组件并使用:

SELECT tower_number,
       tower_height,
       tower_height_um,
       client,
       CASE availability
       WHEN 0 THEN 'T'
       WHEN 1 THEN 'P'
       END AS availability,
       range_from,
       range_to
FROM   (
SELECT h.tower_number,
       MAX(h.tower_height) AS tower_height,
       MAX(h.tower_height_um) AS tower_height_um,
       h.height,
       MAX(ch.client) AS client,
       COUNT(t.tower_side) AS availability
FROM   (
         -- Generate all the heights and sides
         WITH heights (tower_number, tower_side, height, tower_height, tower_height_um) AS (
           SELECT tower_number,
                  tower_side,
                  0,
                  MAX(tower_height),
                  MAX(tower_height_um) KEEP (DENSE_RANK LAST ORDER BY tower_height)
           FROM   tower_test
           GROUP BY tower_number, tower_side
         UNION ALL
           SELECT tower_number,
                  tower_side,
                  height + 1,
                  tower_height,
                  tower_height_um
           FROM   heights
           WHERE  height < tower_height
         )
         SELECT * FROM heights
       ) h
       LEFT OUTER JOIN (
         -- Include the client heights
         SELECT *
         FROM   (
           SELECT DISTINCT
                  tower_number,
                  client,
                  client_start_height AS start_height,
                  LEAD(client_start_height - 1, 1, tower_height) OVER (
                    PARTITION BY tower_number
                    ORDER BY client_start_height, range_to
                  ) AS end_height
           FROM   tower_test
         )
         WHERE  start_height < end_height
       ) ch
       ON (    h.tower_number = ch.tower_number
           AND h.height BETWEEN ch.start_height AND ch.end_height)
       -- Include the equipment ranges
       LEFT OUTER JOIN tower_test t
       ON (    t.tower_number = h.tower_number
           AND t.tower_side = h.tower_side
           AND h.height BETWEEN t.range_from AND t.range_to)
GROUP BY
      h.tower_number,
      h.height
)
MATCH_RECOGNIZE(
  PARTITION BY tower_number
  ORDER BY height
  MEASURES
    FIRST(tower_height) AS tower_height,
    FIRST(tower_height_um) AS tower_height_um,
    FIRST(client) AS client,
    FIRST(availability) AS availability,
    FIRST(height) AS range_from,
    LAST(height) AS range_to
  ONE ROW PER MATCH
  PATTERN (client_available+)
  DEFINE 
    client_available
      AS (FIRST(client) = client OR FIRST(client) IS NULL AND client IS NULL)
      AND FIRST(availability) = availability
)
WHERE availability < 2

对于您的示例数据,输出:

TOWER_NUMBER TOWER_HEIGHT TOWER_HEIGHT_UM CLIENT AVAILABILITY RANGE_FROM RANGE_TO
123456_TWR1 50 m null T 0 11
123456_TWR1 50 m Client_1 T 12 12
123456_TWR1 50 m Client_1 P 13 14
123456_TWR1 50 m Client_1 T 15 16
123456_TWR1 50 m Client_1 T 21 21
123456_TWR1 50 m Client_2 T 24 27
123456_TWR1 50 m Client_2 P 28 29
123456_TWR1 50 m Client_2 T 30 30
123456_TWR1 50 m Client_2 T 33 34
123456_TWR1 50 m Client_2 P 35 36
123456_TWR1 50 m Client_2 T 39 39
123456_TWR1 50 m Client_3 T 42 42
123456_TWR1 50 m Client_3 P 43 44
123456_TWR1 50 m Client_3 T 45 45
123456_TWR1 50 m Client_3 T 49 50

db<>fiddle here