ORACLE - 如何使用 LAG 将所有先前行中的字符串显示到当前行中

ORACLE - How to use LAG to display strings from all previous rows into current row

我有如下数据:

group seq activity
A 1 scan
A 2 visit
A 3 pay
B 1 drink
B 2 rest

我希望有 1 个新列“hist”,如下所示:

group seq activity hist
A 1 scan NULL
A 2 visit scan
A 3 pay scan, visit
B 1 drink NULL
B 2 rest drink

我试图用 LAG 函数求解,但 LAG 仅 returns 前一行而不是多行。 非常感谢任何帮助!

使用相关 sub-query:

SELECT t.*,
       (SELECT LISTAGG(activity, ',') WITHIN GROUP (ORDER BY seq)
        FROM   table_name l
        WHERE  t."GROUP" = l."GROUP"
        AND    l.seq < t.seq
       ) AS hist
FROM   table_name t

或分层查询:

SELECT t.*,
       SUBSTR(SYS_CONNECT_BY_PATH(PRIOR activity, ','), 3) AS hist
FROM   table_name t
START WITH seq = 1
CONNECT BY
       PRIOR seq + 1 = seq
AND    PRIOR "GROUP" = "GROUP"

或递归 sub-query 分解子句:

WITH rsqfc ("GROUP", seq, activity, hist) AS (
  SELECT "GROUP", seq, activity, NULL
  FROM   table_name
  WHERE  seq = 1
UNION ALL
  SELECT t."GROUP", t.seq, t.activity, r.hist || ',' || r.activity
  FROM   rsqfc r
         INNER JOIN table_name t
         ON (r."GROUP" = t."GROUP" AND r.seq + 1 = t.seq)
)
SEARCH DEPTH FIRST BY "GROUP" SET order_rn
SELECT "GROUP", seq, activity, SUBSTR(hist, 2) AS hist
FROM   rsqfc

其中,对于示例数据:

CREATE TABLE table_name ("GROUP", seq, activity) AS
SELECT 'A', 1, 'scan'  FROM DUAL UNION ALL
SELECT 'A', 2, 'visit' FROM DUAL UNION ALL
SELECT 'A', 3, 'pay'   FROM DUAL UNION ALL
SELECT 'B', 1, 'drink' FROM DUAL UNION ALL
SELECT 'B', 2, 'rest'  FROM DUAL;

全部输出:

GROUP SEQ ACTIVITY HIST
A 1 scan null
A 2 visit scan
A 3 pay scan,visit
B 1 drink null
B 2 rest drink

db<>fiddle here

为了在 Oracle 中聚合字符串,我们使用 LISAGG 函数。

一般来说,需要一个windowing_clause来指定一个滑动window的解析函数来计算运行总数。

可惜LISTAGG不支持

要模拟此行为,您可以使用 select 语句的 model_clause。下面是一个带有解释的例子。

select
  group_
  , activity
  , seq
  , hist
from t
model
  /*Where to restart calculation*/
  partition by (group_)
  /*Add consecutive numbers to reference "previous" row per group.
    May use "seq" column if its values are consecutive*/
  dimension by (
    row_number() over(
      partition by group_
      order by seq asc
    ) as rn
  )
  measures (
    /*Other columnns to return*/
    activity
    , cast(null as varchar2(1000)) as hist
    , seq
  )
  rules update (
    /*Apply this rule sequentially*/
    hist[any] order by rn asc =
      /*Previous concatenated result*/
      hist[cv()-1]
      /*Plus comma for the third row and tne next rows*/
      || presentv(activity[cv()-2], ',', '') /**/
      /*lus previous row's value*/
      || activity[cv()-1]
  )
GROUP_ | ACTIVITY | SEQ | HIST      
:----- | :------- | --: | :---------
A      | scan     |   1 | null      
A      | visit    |   2 | scan      
A      | pay      |   3 | scan,visit
B      | drink    |   1 | null      
B      | rest     |   2 | drink     

db<>fiddle here

更多变体(没有子查询):

SELECT--+ NO_XML_QUERY_REWRITE
       t.*,
       regexp_substr(
         listagg(activity, ',')
            within group(order by SEQ)
            over(partition by "GROUP")
        ,'^([^,]+,){'||(row_number()over(partition by "GROUP" order by seq)-1)||'}'
        )
        AS hist1
      ,xmlcast(
         xmlquery(
           'string-join($X/A/B[position()<$Y]/text(),",")'
           passing
            xmlelement("A", xmlagg(xmlelement("B", activity)) over(partition by "GROUP")) as x
           ,row_number()over(partition by "GROUP" order by seq) as y
          returning content
          )
         as varchar2(1000)
         ) hist2
FROM   table_name t;

DBFIddle: https://dbfiddle.uk/?rdbms=oracle_21&fiddle=9b477a2089d3beac62579d2b7103377a

带有输出的完整测试用例:

with table_name ("GROUP", seq, activity) AS (
SELECT 'A', 1, 'scan'  FROM DUAL UNION ALL
SELECT 'A', 2, 'visit' FROM DUAL UNION ALL
SELECT 'A', 3, 'pay'   FROM DUAL UNION ALL
SELECT 'B', 1, 'drink' FROM DUAL UNION ALL
SELECT 'B', 2, 'rest'  FROM DUAL
)
SELECT--+ NO_XML_QUERY_REWRITE
       t.*,
       regexp_substr(
         listagg(activity, ',')
            within group(order by SEQ)
            over(partition by "GROUP")
        ,'^([^,]+,){'||(row_number()over(partition by "GROUP" order by seq)-1)||'}'
        )
        AS hist1
      ,xmlcast(
         xmlquery(
           'string-join($X/A/B[position()<$Y]/text(),",")'
           passing
            xmlelement("A", xmlagg(xmlelement("B", activity)) over(partition by "GROUP")) as x
           ,row_number()over(partition by "GROUP" order by seq) as y
          returning content
          )
         as varchar2(1000)
         ) hist2
FROM   table_name t;

GROUP         SEQ ACTIV HIST1                          HIST2
------ ---------- ----- ------------------------------ ------------------------------
A               1 scan
A               2 visit scan,                          scan
A               3 pay   scan,visit,                    scan,visit
B               1 drink
B               2 rest  drink,                         drink