【问题标题】:PostgreSQL unnest() with element number带有元素编号的 PostgreSQL unnest()
【发布时间】:2012-02-04 08:02:25
【问题描述】:

当我有一列具有分隔值时,我可以使用unnest() 函数:

myTable
id | elements
---+------------
1  |ab,cd,efg,hi
2  |jk,lm,no,pq
3  |rstuv,wxyz

select id, unnest(string_to_array(elements, ',')) AS elem
from myTable

id | elem
---+-----
1  | ab
1  | cd
1  | efg
1  | hi
2  | jk
...

如何包含元素编号?即:

id | elem | nr
---+------+---
1  | ab   | 1
1  | cd   | 2
1  | efg  | 3
1  | hi   | 4
2  | jk   | 1
...

我想要源字符串中每个元素的原始位置。我尝试过使用窗口函数(row_number()rank() 等),但我总是得到1。可能是因为它们在源表的同一行?

我知道这是一个糟糕的桌子设计。这不是我的,我只是想解决它。

【问题讨论】:

    标签: sql arrays postgresql window-functions set-returning-functions


    【解决方案1】:

    Postgres 9.4 或更高版本

    使用WITH ORDINALITY 设置返回函数:

    FROM 子句中的函数以WITH ORDINALITY 为后缀时,a bigint 列附加到从 1 开始的输出和 函数输出的每一行加 1。这是最 在 unnest() 等集合返回函数的情况下很有用。

    结合LATERAL feature in pg 9.3+,根据这个thread on pgsql-hackers,上面的查询现在可以写成:

    SELECT t.id, a.elem, a.nr
    FROM   tbl AS t
    LEFT   JOIN LATERAL unnest(string_to_array(t.elements, ','))
                        WITH ORDINALITY AS a(elem, nr) ON TRUE;

    LEFT JOIN ... ON TRUE 保留左侧表中的所有行,即使右侧的表表达式不返回任何行。如果这不重要,您可以使用这种其他等效的、不那么冗长 的形式和隐含的CROSS JOIN LATERAL

    SELECT t.id, a.elem, a.nr
    FROM   tbl t, unnest(string_to_array(t.elements, ',')) WITH ORDINALITY a(elem, nr);
    

    如果基于实际数组arr 是数组列),则更简单:

    SELECT t.id, a.elem, a.nr
    FROM   tbl t, unnest(t.arr) WITH ORDINALITY a(elem, nr);
    

    甚至,用最少的语法:

    SELECT id, a, ordinality
    FROM   tbl, unnest(arr) WITH ORDINALITY a;
    

    a 自动成为表列的别名。添加的序数列的默认名称是ordinality。但添加显式列别名和表限定列会更好(更安全、更简洁)。

    Postgres 8.4 - 9.3

    使用row_number() OVER (PARTITION BY id ORDER BY elem),您将根据排序顺序获得数字,而不是字符串中原始序数位置的序数。

    你可以直接省略ORDER BY:

    SELECT *, row_number() OVER (PARTITION by id) AS nr
    FROM  (SELECT id, regexp_split_to_table(elements, ',') AS elem FROM tbl) t;
    

    虽然这通常有效并且我从未见过它在简单查询中失败,但 PostgreSQL 在没有ORDER BY 的情况下不会断言任何关于行的顺序。由于实现细节,它恰好可以工作。

    保证空格分隔的字符串中元素的序数

    SELECT id, arr[nr] AS elem, nr
    FROM  (
       SELECT *, generate_subscripts(arr, 1) AS nr
       FROM  (SELECT id, string_to_array(elements, ' ') AS arr FROM tbl) t
       ) sub;
    

    如果基于实际数组,则更简单:

    SELECT id, arr[nr] AS elem, nr
    FROM  (SELECT *, generate_subscripts(arr, 1) AS nr FROM tbl) t;

    dba.SE 上的相关答案:

    Postgres 8.1 - 8.4

    这些功能目前都不可用:RETURNS TABLEgenerate_subscripts()unnest()array_length()。但这有效:

    CREATE FUNCTION f_unnest_ord(anyarray, OUT val anyelement, OUT ordinality integer)
      RETURNS SETOF record
      LANGUAGE sql IMMUTABLE AS
    'SELECT $1[i], i - array_lower($1,1) + 1
     FROM   generate_series(array_lower($1,1), array_upper($1,1)) i';
    

    请特别注意,数组索引可能与元素的序数位置不同。考虑这个带有扩展功能的演示

    CREATE FUNCTION f_unnest_ord_idx(anyarray, OUT val anyelement, OUT ordinality int, OUT idx int)
      RETURNS SETOF record
      LANGUAGE sql IMMUTABLE AS
    'SELECT $1[i], i - array_lower($1,1) + 1, i
     FROM   generate_series(array_lower($1,1), array_upper($1,1)) i';
    
    SELECT id, arr, (rec).*
    FROM  (
       SELECT *, f_unnest_ord_idx(arr) AS rec
       FROM  (VALUES (1, '{a,b,c}'::text[])  --  short for: '[1:3]={a,b,c}'
                   , (2, '[5:7]={a,b,c}')
                   , (3, '[-9:-7]={a,b,c}')
          ) t(id, arr)
       ) sub;
    
     id |       arr       | val | ordinality | idx
    ----+-----------------+-----+------------+-----
      1 | {a,b,c}         | a   |          1 |   1
      1 | {a,b,c}         | b   |          2 |   2
      1 | {a,b,c}         | c   |          3 |   3
      2 | [5:7]={a,b,c}   | a   |          1 |   5
      2 | [5:7]={a,b,c}   | b   |          2 |   6
      2 | [5:7]={a,b,c}   | c   |          3 |   7
      3 | [-9:-7]={a,b,c} | a   |          1 |  -9
      3 | [-9:-7]={a,b,c} | b   |          2 |  -8
      3 | [-9:-7]={a,b,c} | c   |          3 |  -7
    

    比较:

    【讨论】:

    • 这个答案是 SO 中关于 PostgreSQL 的最全面的答案之一。谢谢欧文。
    • 我们能否在新的 pg 版本中将下面的 unnest2 函数调整为真实的表返回(不是假行)?
    • @erwin-brandstetter,请您详细说明为什么/如果WITH ORDINALITYgenerate_subscripts() 更受欢迎?在我看来,generate_subscripts() 更好,因为它显示了数组中的实际元素位置。这很有用,例如,在更新数组时...我应该改用 WITH ORDINALITY 吗?
    • @losthorse:我会这样概括:WITH ORDINALITY 是在 SQL 查询中为 any 集返回函数获取行号的一般解决方案。这是最快、最可靠的方法,它也恰好适用于一维、基于 1 的数组(Postgres 数组的默认值,consider this)。 如果您使用任何其他类型的数组(大多数人不使用),并且您实际上需要保留/使用原始下标,那么generate_subscripts() 是要走的路。但是unnest() 将所有内容都变平...
    • @z0r_ The manual: Table functions appearing in FROM can also be preceded by the key word LATERAL, but for functions the key word is optional; the function's arguments can contain references to columns provided by preceding FROM items in any case.
    【解决方案2】:

    试试:

    select v.*, row_number() over (partition by id order by elem) rn from
    (select
        id,
        unnest(string_to_array(elements, ',')) AS elem
     from myTable) v
    

    【讨论】:

    • 这不要求您的元素按字典顺序排列吗?我知道他们有问题,但这是一个非常具体的解决方案,社区可能在大多数情况下都需要一个通用的解决方案。
    【解决方案3】:

    使用下标生成函数
    http://www.postgresql.org/docs/current/static/functions-srf.html#FUNCTIONS-SRF-SUBSCRIPTS

    例如:

    SELECT 
      id
      , elements[i] AS elem
      , i AS nr
    FROM
      ( SELECT 
          id
          , elements
          , generate_subscripts(elements, 1) AS i
        FROM
          ( SELECT
              id
              , string_to_array(elements, ',') AS elements
            FROM
              myTable
          ) AS foo
      ) bar
    ;
    

    更简单:

    SELECT
      id
      , unnest(elements) AS elem
      , generate_subscripts(elements, 1) AS nr
    FROM
      ( SELECT
          id
          , string_to_array(elements, ',') AS elements
        FROM
          myTable
      ) AS foo
    ;
    

    【讨论】:

      【解决方案4】:

      如果元素的顺序不重要,可以

      select 
        id, elem, row_number() over (partition by id) as nr
      from (
        select
            id,
            unnest(string_to_array(elements, ',')) AS elem
        from myTable
      ) a
      

      【讨论】:

        【解决方案5】:

        我认为这是相关的,使用相关子查询将任意排名/序数值分配给最终集合。使用 PG 数组处理对数据集进行 De-Pivot 数据集(适用于 PG 9.4),这更像是一种实际应用用途。

        WITH _students AS ( /** CTE **/
                          SELECT * FROM
                            (   SELECT 'jane'::TEXT ,'doe'::TEXT , 1::INT 
                                 UNION
                                SELECT 'john'::TEXT ,'doe'::TEXT , 2::INT 
                                 UNION
                                SELECT 'jerry'::TEXT ,'roe'::TEXT , 3::INT 
                                 UNION
                                SELECT 'jodi'::TEXT ,'roe'::TEXT , 4::INT 
                            ) s ( fn, ln, id )
        ) /** end WITH **/   
        SELECT s.id
         , ax.fanm
         , ax.anm
         , ax.val
         , ax.num
        FROM _students s
        ,UNNEST /** MULTI-UNNEST() BLOCK **/
            (
                ( SELECT ARRAY[ fn, ln ]::text[] AS anm 
                          /** CORRELATED SUBQUERY **/
                         FROM _students s2 WHERE s2.id = s.id 
                 )   
           
                ,( SELECT ARRAY[ 'first name', 'last name' ]::text[] AS fanm )  
             
                ,( SELECT ARRAY[ '9','8','7'] AS val) 
           
                ,( SELECT ARRAY[ 1,2,3,4,5   ] AS num) 
                
           ) ax (  anm, fanm, val, num )
        ;   
        

        去中心化结果集:

        +--+----------+-----+----+---+
        |id|fanm      |anm  |val |num|
        +--+----------+-----+----+---+
        |2 |first name|john |9   |1  |
        |2 |last name |doe  |8   |2  |
        |2 |NULL      |NULL |7   |3  |
        |2 |NULL      |NULL |NULL|4  |
        |2 |NULL      |NULL |NULL|5  |
        |1 |first name|jane |9   |1  |
        |1 |last name |doe  |8   |2  |
        |1 |NULL      |NULL |7   |3  |
        |1 |NULL      |NULL |NULL|4  |
        |1 |NULL      |NULL |NULL|5  |
        |4 |first name|jodi |9   |1  |
        |4 |last name |roe  |8   |2  |
        |4 |NULL      |NULL |7   |3  |
        |4 |NULL      |NULL |NULL|4  |
        |4 |NULL      |NULL |NULL|5  |
        |3 |first name|jerry|9   |1  |
        |3 |last name |roe  |8   |2  |
        |3 |NULL      |NULL |7   |3  |
        |3 |NULL      |NULL |NULL|4  |
        |3 |NULL      |NULL |NULL|5  |
        +--+----------+-----+----+---+
        

        【讨论】:

          【解决方案6】:

          unnest2()作为练习

          pg v8.4 之前的旧版本需要用户定义的unnest()。我们可以修改这个旧函数来返回带有索引的元素:

          CREATE FUNCTION unnest2(anyarray)
            RETURNS setof record  AS
          $BODY$
            SELECT $1[i], i
            FROM   generate_series(array_lower($1,1),
                                   array_upper($1,1)) i;
          $BODY$ LANGUAGE sql IMMUTABLE;
          

          【讨论】:

          • 这在 pg v8.4 之前是行不通的,因为还没有 RETURNS TABLE。我在答案中添加了一章讨论解决方案。
          • @ErwinBrandstetter ,您的回答非常具有指导意义,并且您正在润色 4 年前的文本(!)...您是否正在使用您的 SO 文本编写 PostgreSQL 书? :-)
          • 大家好,这是一个 Wiki,您可以编辑(!)...但是好的,我更正为 setof record
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-06-30
          • 1970-01-01
          • 1970-01-01
          • 2010-12-20
          • 1970-01-01
          相关资源
          最近更新 更多