sql >> Database teknologi >  >> RDS >> PostgreSQL

SQLAlchemy:grupper efter dag over flere tabeller

SQL arbejder med og returnerer tabeldata (eller relationer, hvis du foretrækker at tænke på det på den måde, men ikke alle SQL-tabeller er relationer). Hvad dette indebærer er, at en indlejret tabel som afbildet i spørgsmålet ikke er så almindelig en funktion. Der er måder at producere noget af den slags i Postgresql, for eksempel ved at bruge arrays af JSON eller kompositter, men det er fuldt ud muligt blot at hente tabeldata og udføre nesting i applikationen. Python har itertools.groupby() , hvilket passer ganske godt, givet sorterede data.

Fejlen column "incoming.id" must appear in the GROUP BY clause... siger, at ikke-aggregater i udvalgslisten, havende klausuler osv. skal vises i GROUP BY klausul eller bruges i en aggregering, så de ikke muligvis har ubestemte værdier . Med andre ord skal værdien kun vælges fra en række i gruppen, fordi GROUP BY kondenserer de grupperede rækker til en enkelt række , og det ville være enhvers gætte, hvilken række de blev valgt fra. Implementeringen kan tillade dette, ligesom SQLite gør og MySQL plejede at gøre, men SQL-standarden forbyder det. Undtagelsen fra reglen er, når der er en funktionel afhængighed ; GROUP BY klausulen bestemmer ikke-aggregaterne. Tænk på en joinforbindelse mellem tabellerne A og B grupperet efter A 's primære nøgle. Uanset hvilken række i en gruppe, vil systemet vælge værdierne for A 's kolonner fra, ville de være de samme, da grupperingen blev udført baseret på den primære nøgle.

For at adressere den generelle tilsigtede 3-punkts tilgang ville en måde være at vælge en forening af indgående og udgående, sorteret efter deres tidsstempler. Da der ikke er noget arvehierarki opsætning – da der måske ikke engang er en, jeg er ikke bekendt med regnskab – – en tilbagevenden til at bruge Core og almindelige resultattupler gør tingene lettere i dette tilfælde:

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)

Derefter for at danne den indlejrede struktur itertools.groupby() bruges:

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]

Slutresultatet er en liste over 2-tupler af dato og en liste over ordbøger over poster i stigende rækkefølge. Ikke helt ORM-løsningen, men får arbejdet gjort. Et eksempel:

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

Som nævnt kan Postgresql producere stort set det samme resultat, som ved at bruge et array af JSON:

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

Hvis faktisk Incoming og Outgoing kan opfattes som børn af en fælles base, for eksempel Entry , kan brug af fagforeninger være noget automatiseret med konkret tabelarv :

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
    }

Bruger desværre AbstractConcreteBase kræver et manuelt opkald til configure_mappers() når alle nødvendige klasser er blevet defineret; i dette tilfælde er den tidligste mulighed efter at have defineret User , fordi Account afhænger af det gennem relationer:

from sqlalchemy.orm import configure_mappers
configure_mappers()

Derefter for at hente alle Incoming og Outgoing i en enkelt polymorf ORM-forespørgsel skal du bruge Entry :

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

og fortsæt med at bruge itertools.groupby() som ovenfor på den resulterende liste over Incoming og Outgoing .



  1. Django på Google App Engine med Cloud SQL i udviklermiljø

  2. Adgang nægtet for brugeren på MySQL-databasen

  3. Hvordan ved man, om en MySQL UPDATE-forespørgsel mislykkes, fordi de oplysninger, der leveres, matcher data, der allerede er i databasen?

  4. dbms_output.put udskriver ikke data