Is there a LISTAGG WITHIN GROUP equivalent in SQLAlchemy?

冷暖自知 提交于 2019-12-07 12:37:06

问题


Here is a simple Oracle table:

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

And here are the results of an aggregated SELECT I'd like to do:

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

With Oracle 11g+ this is easy enough with a LISTAGG:

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

But I haven't been able to find a way to do this within SQLAlchemy. An old question from Stack Overflow shows where someone was trying to implement a custom class to do the job, but is that really the best option there is?

MySQL has a group_concat feature, and thus this questioner solved his problem with func.group_concat(...). Sadly that function is not available within Oracle.


回答1:


Beginning from 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)



回答2:


Ilja's answer did the trick for me. Here it is fully fleshed out, using SQLAlchemy 1.2.2 (I couldn't get it to work in 1.1.10, but upgrading took care of that)

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]

which prints out:

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

which is great! The only remaining mystery is why the separator character (,) is preceded by a NULL:

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

In fact if I change the separator in the func.listagg() to something else like <-> instead of , then every character the forms the separator string is 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')]

Not sure what's going on there. But if need be, it's easy enough to strip out the nulls from the column. At least the hard part with the LISTAGG is done.




回答3:


within_group can take multiple arguments. func.listagg takes what to group, followed by the separator, and within_group takes a list of what to order the group by.

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') )

This would translate to:

Group the list_value of A separated by a comma and a space,
and ordered by A.list_value and then A.other_column
when A.id is equal to B.id.

Hope that helps.



来源:https://stackoverflow.com/questions/48799232/is-there-a-listagg-within-group-equivalent-in-sqlalchemy

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!