在没有LISTAGG的Oracle SQL中按字符串列表排序

时间:2012-07-18 12:29:48

标签: sql oracle oracle11gr2

我正在使用两个实体:ItemAttribute,它们类似于以下内容:

Item
----
itemId

Attribute
---------
attributeId
name

Item具有Attributes,如关联表中所指定的那样:

ItemAttribute
--------------
itemId
attributeId

当此数据到达客户端时,它将显示每行Item一行,每行将按名称列出Attribute。例如:

Item  Attributes
----  ----------
1     A, B, C
2     A, C
3     A, B

用户可以选择对Attributes列进行排序,因此我们需要按如下方式对数据进行排序:

Item  Attributes
----  ----------
3     A, B
1     A, B, C
2     A, C

目前,我们每ItemAttribute行获得一行数据。基本上是:

  SELECT Item.itemId,
         Attribute.name
    FROM Item
    JOIN ItemAttribute
      ON ItemAttribute.itemId = Item.itemId
    JOIN Attribute
      ON Attribute.attributeId = ItemAttribute.attributeId
ORDER BY Item.itemId;

产生如下结果:

itemId  name
------  ----
1       A
1       B
1       C
2       A
2       C
3       A
3       B

实际的ORDER BY子句基于用户输入。它通常是一个列,因此排序很简单,处理结果集的app端循环将Attribute名称组合成逗号分隔列表,以便在客户端上显示。但是当用户要求对该列表进行排序时,让Oracle对结果进行排序以便 - 使用上面的示例 - 我们得到:

itemId  name
------  ----
3       A
3       B
1       A
1       B
1       C
2       A
2       C

Oracle的LISTAGG函数可用于在排序之前生成属性列表;但是Attribute.name可以是一个非常长的字符串,并且组合列表可能超过4000个字符,这会导致查询失败。

是否有一种干净,有效的方法使用Oracle SQL(11gR2)以这种方式对数据进行排序?

3 个答案:

答案 0 :(得分:4)

这里确实有两个问题:

1)如何聚合超过4000个字符的数据

汇总如此多的数据并将其显示在一列中是否明智?

无论如何,您需要某种大型结构才能显示超过4000个字符,例如CLOB。您可以按照Tom Kyte's thread之一中描述的一般准则编写自己的聚合方法(显然您需要修改它以使最终输出为CLOB)。

我将使用嵌套表和自定义函数演示一个更简单的方法(适用于10g):

SQL> CREATE TYPE tab_varchar2 AS TABLE OF VARCHAR2(4000);
  2  /

Type created.

SQL> CREATE OR REPLACE FUNCTION concat_array(p tab_varchar2) RETURN CLOB IS
  2     l_result CLOB;
  3  BEGIN
  4     FOR cc IN (SELECT column_value FROM TABLE(p) ORDER BY column_value) LOOP
  5        l_result := l_result ||' '|| cc.column_value;
  6     END LOOP;
  7     return l_result;
  8  END;
  9  /

Function created.

SQL> SELECT item,
  2         concat_array(CAST (collect(attribute) AS tab_varchar2)) attributes
  3    FROM data
  4   GROUP BY item;

ITEM ATTRIBUTES
1    a b c
2    a c
3    a b

2)如何对大数据进行排序

不幸的是,您无法按Oracle中任意大的列进行排序:相对于排序键的类型和长度存在已知的限制。

  • 尝试使用clob排序将导致ORA-00932:不一致的数据类型:expected - 得到CLOB。
  • 尝试使用大于数据库块大小的键进行排序(例如,如果您决定将大数据拆分为多个VARCHAR2)将产生ORA-06502:PL / SQL:数字或值错误:字符串缓冲太小

我建议你按属性列的前4000个字节排序:

SQL> SELECT * FROM (
  2     SELECT item,
  3            concat_array(CAST (collect(attribute) AS tab_varchar2)) attributes
  4       FROM data
  5      GROUP BY item
  6  ) order by dbms_lob.substr(attributes, 4000, 1);

ITEM ATTRIBUTES
3    a b
1    a b c
2    a c

答案 1 :(得分:1)

正如Vincent所说,排序键是有限的(没有CLOB,最大块大小)。

我可以提供一个稍微不同的解决方案,它在10g及更新版本中开箱即用,无需使用XMLAgg自定义函数和类型:

with ItemAttribute  as (
 select 'name'||level name
        ,mod(level,3) itemid
   from dual
  connect by level < 2000
)
,ItemAttributeGrouped as (
 select xmlagg(xmlparse(content name||' ' wellformed) order by name).getclobval() attributes
       ,itemid
   from ItemAttribute
  group by itemid
 )
select itemid
      ,attributes
      ,dbms_lob.substr(attributes,4000,1) sortkey
  from ItemAttributeGrouped
order by dbms_lob.substr(attributes,4000,1)
;  

答案 2 :(得分:0)

清洁是主观的,需要检查效率(但它仍然只能击中表格,所以可能不会更糟),但如果你对任何项目的属性数量有一个有限的上限 - 或者在线索中您需要考虑多少订购 - 然后您可以使用多个lead来执行此操作:

SELECT itemId, name
  FROM (
    SELECT itemId, name, min(dr) over (partition by itemId) as dr
      FROM (
        SELECT itemId, name,
            dense_rank() over (order by name, name1, name2, name3, name4) as dr
          FROM (
            SELECT Item.itemId,
                     Attribute.name,
                     LEAD(Attribute.name, 1)
                         OVER (PARTITION BY Item.itemId
                             ORDER BY Attribute.name) AS name1,
                     LEAD(Attribute.name, 2)
                         OVER (PARTITION BY Item.itemId
                             ORDER BY Attribute.name) AS name2,
                     LEAD(Attribute.name, 3)
                         OVER (PARTITION BY Item.itemId
                             ORDER BY Attribute.name) AS name3,
                     LEAD(Attribute.name, 4)
                         OVER (PARTITION BY Item.itemId
                             ORDER BY Attribute.name) AS name4
                FROM Item
                JOIN ItemAttribute
                  ON ItemAttribute.itemId = Item.itemId
                JOIN Attribute
                  ON Attribute.attributeId = ItemAttribute.attributeId
               )
          )
      )
ORDER BY dr, name;

因此,内部查询获取了您关注的两个值,并使用了四个lead调用(仅作为示例,因此可以根据前五个属性名称的最大值进行排序,但可以当然可以通过添加更多!)来扩展每个项目的其他内容。根据您的数据,这给出了:

    ITEMID NAME       NAME1      NAME2      NAME3      NAME4
---------- ---------- ---------- ---------- ---------- ----------
         1 A          B          C
         1 B          C
         1 C
         2 A          C
         2 C
         3 A          B
         3 B

下一个查询对这五个有序属性名称进行dense_rank,为每个itemIDname分配一个等级,给出:

    ITEMID NAME               DR
---------- ---------- ----------
         1 A                   1
         1 B                   4
         1 C                   6
         2 A                   3
         2 C                   6
         3 A                   2
         3 B                   5

下一个查询输出使用min的分析版本找到每个dr计算的itemId值的最小值,因此每个itemID=1获取min(dr) = 1 },itemId=2获取3itemId=3获得2。 (您可以通过选择min(dense_rank(...))来结合这两个级别,但这样(甚至)不那么明确。)

最终的外部查询使用每个项目的最小等级来进行实际排序,给出:

    ITEMID NAME
---------- ----------
         1 A
         1 B
         1 C
         3 A
         3 B
         2 A
         2 C