PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

SQLAlchemy:Gruppieren Sie nach Tag über mehrere Tabellen

SQL arbeitet mit tabellarischen Daten (oder Relationen, wenn Sie es vorziehen, aber nicht alle SQL-Tabellen sind Relationen) und gibt diese zurück. Dies impliziert, dass eine verschachtelte Tabelle, wie sie in der Frage dargestellt ist, kein so häufiges Merkmal ist. Es gibt Möglichkeiten, so etwas in Postgresql zu erzeugen, zum Beispiel mit Arrays von JSON oder Composites, aber es ist durchaus möglich, nur tabellarische Daten abzurufen und die Verschachtelung in der Anwendung durchzuführen. Python hat itertools.groupby() , was bei sortierten Daten ganz gut ins Bild passt.

Die Fehlerspalte column "incoming.id" must appear in the GROUP BY clause... bedeutet, dass Nicht-Aggregate in der Auswahlliste, in der Klausel, etc. in GROUP BY erscheinen müssen -Klausel oder in einem Aggregat verwendet werden, damit sie möglicherweise unbestimmte Werte haben . Mit anderen Worten, der Wert müsste nur aus einer Zeile in der Gruppe ausgewählt werden, da GROUP BY komprimiert die gruppierten Zeilen zu einer einzigen Zeile , und jeder kann erraten, aus welcher Reihe sie ausgewählt wurden. Die Implementierung könnte dies zulassen, wie es SQLite und MySQL früher taten, aber der SQL-Standard verbietet dies. Die Ausnahme von der Regel ist, wenn eine funktionale Abhängigkeit besteht; die GROUP BY Klausel bestimmt die Nicht-Aggregate. Denken Sie an einen Join zwischen den Tabellen A und B gruppiert nach A Primärschlüssel von . Unabhängig von der Zeile in einer Gruppe würde das System die Werte für A auswählen 's Spalten aus, sie wären gleich, da die Gruppierung auf Basis des Primärschlüssels erfolgt ist.

Um den allgemein beabsichtigten 3-Punkte-Ansatz zu adressieren, bestünde eine Möglichkeit darin, eine Vereinigung von eingehenden und ausgehenden Nachrichten auszuwählen, die nach ihren Zeitstempeln geordnet sind. Da es keine Vererbungshierarchie gibt Setup – da es vielleicht nicht einmal einen gibt, bin ich mit Buchhaltung nicht vertraut – macht eine Rückkehr zur Verwendung von Kern- und einfachen Ergebnistupeln die Dinge in diesem Fall einfacher:

incoming = select([literal('incoming').label('type'), Incoming.__table__]).\
    where(Incoming.accountID == accountID)

outgoing = select([literal('outgoing').label('type'), Outgoing.__table__]).\
    where(Outgoing.accountID == accountID)

all_entries = incoming.union(outgoing)
all_entries = all_entries.order_by(all_entries.c.timestamp)
all_entries = db_session.execute(all_entries)

Dann, um die verschachtelte Struktur itertools.groupby() zu bilden wird verwendet:

date_groups = groupby(all_entries, lambda ent: ent.timestamp.date())
date_groups = [(k, [dict(ent) for ent in g]) for k, g in date_groups]

Das Endergebnis ist eine Liste von 2-Tupeln mit Datum und eine Liste von Wörterbüchern mit Einträgen in aufsteigender Reihenfolge. Nicht ganz die ORM-Lösung, aber erledigt die Arbeit. Ein Beispiel:

In [55]: session.add_all([Incoming(accountID=1, amount=1, description='incoming',
    ...:                           timestamp=datetime.utcnow() - timedelta(days=i))
    ...:                  for i in range(3)])
    ...:                  

In [56]: session.add_all([Outgoing(accountID=1, amount=2, description='outgoing',
    ...:                           timestamp=datetime.utcnow() - timedelta(days=i))
    ...:                  for i in range(3)])
    ...:                  

In [57]: session.commit()

In [58]: incoming = select([literal('incoming').label('type'), Incoming.__table__]).\
    ...:     where(Incoming.accountID == 1)
    ...: 
    ...: outgoing = select([literal('outgoing').label('type'), Outgoing.__table__]).\
    ...:     where(Outgoing.accountID == 1)
    ...: 
    ...: all_entries = incoming.union(outgoing)
    ...: all_entries = all_entries.order_by(all_entries.c.timestamp)
    ...: all_entries = db_session.execute(all_entries)

In [59]: date_groups = groupby(all_entries, lambda ent: ent.timestamp.date())
    ...: [(k, [dict(ent) for ent in g]) for k, g in date_groups]
Out[59]: 
[(datetime.date(2019, 9, 1),
  [{'accountID': 1,
    'amount': 1.0,
    'description': 'incoming',
    'id': 5,
    'timestamp': datetime.datetime(2019, 9, 1, 20, 33, 6, 101521),
    'type': 'incoming'},
   {'accountID': 1,
    'amount': 2.0,
    'description': 'outgoing',
    'id': 4,
    'timestamp': datetime.datetime(2019, 9, 1, 20, 33, 29, 420446),
    'type': 'outgoing'}]),
 (datetime.date(2019, 9, 2),
  [{'accountID': 1,
    'amount': 1.0,
    'description': 'incoming',
    'id': 4,
    'timestamp': datetime.datetime(2019, 9, 2, 20, 33, 6, 101495),
    'type': 'incoming'},
   {'accountID': 1,
    'amount': 2.0,
    'description': 'outgoing',
    'id': 3,
    'timestamp': datetime.datetime(2019, 9, 2, 20, 33, 29, 420419),
    'type': 'outgoing'}]),
 (datetime.date(2019, 9, 3),
  [{'accountID': 1,
    'amount': 1.0,
    'description': 'incoming',
    'id': 3,
    'timestamp': datetime.datetime(2019, 9, 3, 20, 33, 6, 101428),
    'type': 'incoming'},
   {'accountID': 1,
    'amount': 2.0,
    'description': 'outgoing',
    'id': 2,
    'timestamp': datetime.datetime(2019, 9, 3, 20, 33, 29, 420352),
    'type': 'outgoing'}])]

Wie bereits erwähnt, kann Postgresql so ziemlich das gleiche Ergebnis liefern wie die Verwendung eines JSON-Arrays:

from sqlalchemy.dialects.postgresql import aggregate_order_by

incoming = select([literal('incoming').label('type'), Incoming.__table__]).\
    where(Incoming.accountID == accountID)

outgoing = select([literal('outgoing').label('type'), Outgoing.__table__]).\
    where(Outgoing.accountID == accountID)

all_entries = incoming.union(outgoing).alias('all_entries')

day = func.date_trunc('day', all_entries.c.timestamp)

stmt = select([day,
               func.array_agg(aggregate_order_by(
                   func.row_to_json(literal_column('all_entries.*')),
                   all_entries.c.timestamp))]).\
    group_by(day).\
    order_by(day)

db_session.execute(stmt).fetchall()

Wenn tatsächlich Incoming und Outgoing können als Kinder einer gemeinsamen Basis betrachtet werden, zum Beispiel Entry , kann die Verwendung von Vereinigungen mit concrete-table-inheritance etwas automatisiert werden :

from sqlalchemy.ext.declarative import AbstractConcreteBase

class Entry(AbstractConcreteBase, Base):
    pass

class Incoming(Entry):
    __tablename__ = 'incoming'
    id          = Column(Integer,   primary_key=True)
    accountID   = Column(Integer,   ForeignKey('account.id'))
    amount      = Column(Float,     nullable=False)
    description = Column(Text,      nullable=False)
    timestamp   = Column(TIMESTAMP, nullable=False)
    account     = relationship("Account", back_populates="incomings")

    __mapper_args__ = {
        'polymorphic_identity': 'incoming',
        'concrete': True
    }

class Outgoing(Entry):
    __tablename__ = 'outgoing'
    id          = Column(Integer,   primary_key=True)
    accountID   = Column(Integer,   ForeignKey('account.id'))
    amount      = Column(Float,     nullable=False)
    description = Column(Text,      nullable=False)
    timestamp   = Column(TIMESTAMP, nullable=False)
    account     = relationship("Account", back_populates="outgoings")

    __mapper_args__ = {
        'polymorphic_identity': 'outgoing',
        'concrete': True
    }

Leider mit AbstractConcreteBase erfordert einen manuellen Aufruf von configure_mappers() wenn alle notwendigen Klassen definiert wurden; in diesem Fall ist die früheste Möglichkeit nach der Definition von User , weil Account hängt davon ab durch Beziehungen:

from sqlalchemy.orm import configure_mappers
configure_mappers()

Dann um alle Incoming abzuholen und Outgoing Verwenden Sie in einer einzelnen polymorphen ORM-Abfrage Entry :

session.query(Entry).\
    filter(Entry.accountID == accountID).\
    order_by(Entry.timestamp).\
    all()

und fahren Sie mit der Verwendung von itertools.groupby() fort wie oben auf der resultierenden Liste von Incoming und Outgoing .