在SQLAlchemy中是否有LISTAGG WITHIN GROUP等价物?

时间:2018-02-15 02:37:20

标签: python oracle sqlalchemy oracle12c listagg

这是一个简单的Oracle表:

+-----------+---------+
|   food    | person  |
+-----------+---------+
| pizza     | Adam    |
| pizza     | Bob     |
| pizza     | Charles |
| ice cream | Donald  |
| hamburger | Emma    |
| hamburger | Frank   |
+-----------+---------+

以下是我想做的聚合SELECT的结果:

+-----------+------------------+
|   food    |      people      |
+-----------+------------------+
| hamburger | Emma,Frank       |
| ice cream | Donald           |
| pizza     | Adam,Bob,Charles |
+-----------+------------------+

使用Oracle 11g +,这很容易使用LISTAGG:

SELECT food, LISTAGG (person, ',') WITHIN GROUP (ORDER BY person) AS people
FROM mytable
GROUP BY food;

但我还没能找到在SQLAlchemy中做到这一点的方法。 old question from Stack Overflow显示有人试图实现自定义类来完成工作,但这真的是最好的选择吗?

MySQL具有group_concat功能,因此this questioner解决了func.group_concat(...)的问题。遗憾的是,该功能在Oracle中不可用。

3 个答案:

答案 0 :(得分:3)

version 1.1开始,您可以使用FunctionElement.within_group(*order_by)

In [7]: func.listagg(column('person'), ',').within_group(column('person'))
Out[7]: <sqlalchemy.sql.elements.WithinGroup object at 0x7f2870c83080>

In [8]: print(_.compile(dialect=oracle.dialect()))
listagg(person, :listagg_1) WITHIN GROUP (ORDER BY person)

答案 1 :(得分:0)

Ilja的回答对我有用。在这里它使用SQLAlchemy 1.2.2完全充实(我无法在1.1.10中使用它,但升级工作正在处理)

from sqlalchemy import Column, String, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from lib import project_config
from sqlalchemy import func

db_url = 'oracle://someuser:somepassword@some_connect_string'    

Base = declarative_base()
engine = create_engine(db_url, echo=True)
Session = sessionmaker(bind=engine)
session = Session()

class MyTable(Base):
    __tablename__ = 'my_table'
    food   = Column(String(30), primary_key=True)
    person = Column(String(30), primary_key=True)

Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)

session.add(MyTable(food='pizza', person='Adam'))
session.add(MyTable(food='pizza', person='Bob')) 
session.add(MyTable(food='pizza', person='Charles'))
session.add(MyTable(food='ice cream', person='Donald'))
session.add(MyTable(food='hamburger', person='Emma'))  
session.add(MyTable(food='hamburger', person='Frank'))
session.commit()

entries = session.query(
      MyTable.food,
      func.listagg(MyTable.person, ',').within_group(MyTable.person).label('people')
    ).group_by(MyTable.food).all()

[print('{}: {}'.format(entry.food, entry.people)) for entry in entries]

打印出来:

hamburger: Emma,Frank
ice cream: Donald
pizza: Adam,Bob,Charles

太棒了!唯一剩下的谜团是为什么分隔符(,)前面有一个NULL:

>>> print(entries)
[('hamburger', 'Emma\x00,Frank'), ('ice cream', 'Donald'), ('pizza', 'Adam\x00,Bob\x00,Charles')]

事实上,如果我将func.listagg()中的分隔符更改为<->而不是,之类的其他内容,那么形成分隔符字符串的每个字符都将以null为先:

>>> [print('{}: {}'.format(entry.food, entry.people)) for entry in entries]
hamburger: Emma<->Frank
ice cream: Donald
pizza: Adam<->Bob<->Charles 

>>> print(entries)
[('hamburger', 'Emma\x00<\x00-\x00>Frank'), ('ice cream', 'Donald'), ('pizza', 'Adam\x00<\x00-\x00>Bob\x00<\x00-\x00>Charles')]

不确定那里发生了什么。但如果需要,它很容易从列中删除空值。至少LISTAGG的难点已经完成。

答案 2 :(得分:0)

within_group可以接受多个参数。 func.listagg进行分组,后跟分隔符,inside_group列出进行分组的顺序列表。

query = ( select([func.listagg(A.list_value, ', ')
              .within_group(A.list_value, A.other_column)])
              .where(A.id == B.id)
              .label('list_values_of_a') )

这将转换为:

将A的list_value分组,以逗号和空格分隔,
 并按A.list_value和A.other_column排序
 当A.id等于B.id时。

希望有帮助。