SQLAlchemy 中是否有等效的 LISTAGG WITHIN GROUP?

Is there a LISTAGG WITHIN GROUP equivalent in SQLAlchemy?

这是一个简单的 Oracle table:

+-----------+---------+
|   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 显示有人试图实施自定义 class 来完成这项工作,但这真的是最好的选择吗?

MySQL有一个group_concat的特征,因此this questionerfunc.group_concat(...)解决了他的问题。遗憾的是,该功能在 Oracle 中不可用。

version 1.1 you can use 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)

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-preceded:

>>> [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 的困难部分已经完成了。

within_group 可以接受多个参数。 func.listagg 获取要分组的内容,后跟分隔符,within_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 分组,用逗号和 space,
分隔 并按 A.list_value 排序,然后按 A.other_column
排序 当 A.id 等于 B.id.

希望对您有所帮助。