sql >> Databasteknik >  >> RDS >> PostgreSQL

SQLAlchemy:gruppera efter dag över flera tabeller

SQL arbetar med och returnerar tabelldata (eller relationer, om du föredrar att tänka på det så, men alla SQL-tabeller är inte relationer). Vad detta antyder är att en kapslad tabell som avbildas i frågan inte är så vanlig. Det finns sätt att producera något liknande i Postgresql, till exempel med arrayer av JSON eller kompositer, men det är fullt möjligt att bara hämta tabelldata och utföra kapslingen i applikationen. Python har itertools.groupby() , vilket passar ganska bra, givet sorterade data.

Felet column "incoming.id" must appear in the GROUP BY clause... säger att icke-aggregat i urvalslistan, med klausul etc. måste visas i GROUP BY sats eller användas i ett aggregat, så att de inte har möjligen obestämda värden . Med andra ord skulle värdet behöva väljas från bara någon rad i gruppen, eftersom GROUP BY kondenserar de grupperade raderna till en enda rad , och det skulle vara någons gissning vilken rad de valdes från. Implementeringen kanske tillåter detta, som SQLite gör och MySQL brukade göra, men SQL-standarden förbjuder sådant. Undantaget från regeln är när det finns ett funktionellt beroende ; GROUP BY klausulen bestämmer icke-aggregaten. Tänk på en koppling mellan tabellerna A och B grupperad efter A s primärnyckel. Oavsett vilken rad i en grupp skulle systemet välja värdena för A s kolumner från, skulle de vara desamma eftersom grupperingen gjordes baserat på primärnyckeln.

För att ta itu med det allmänna tillvägagångssättet med tre punkter, skulle ett sätt vara att välja en förening av inkommande och utgående, sorterade efter deras tidsstämplar. Eftersom det inte finns någon arvshierarki inställningar – eftersom det kanske inte ens finns en, jag är inte bekant med redovisning – en återgång till att använda Core och vanliga resultattupler gör det enklare i det här fallet:

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)

Sedan för att bilda den kapslade strukturen itertools.groupby() används:

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 är en lista med 2-tuplar av datum och lista över ordböcker med poster i stigande ordning. Inte riktigt ORM-lösningen, men får jobbet gjort. Ett exempel:

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ämnts kan Postgresql producera ungefär samma resultat som när man använder en array av 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()

Om faktiskt Incoming och Outgoing kan ses som barn av en gemensam bas, till exempel Entry , att använda fackföreningar kan automatiseras något med arv av konkreta tabeller :

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
    }

Använder tyvärr AbstractConcreteBase kräver ett manuellt anrop till configure_mappers() när alla nödvändiga klasser har definierats; i detta fall är den tidigaste möjligheten efter att ha definierat User , eftersom Account beror på det genom relationer:

from sqlalchemy.orm import configure_mappers
configure_mappers()

Sedan för att hämta alla Incoming och Outgoing i en enda polymorf ORM-fråga använd Entry :

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

och fortsätt att använda itertools.groupby() som ovan på den resulterande listan över Incoming och Outgoing .



  1. sök efter cross-field dubbletter i postgresql

  2. Infogar enstaka citattecken i JDBC för SQL Query fungerar inte

  3. Hur startar man om id-räkning på en tabell i PostgreSQL efter att ha raderat några tidigare data?

  4. Android Room:Hur man migrerar kolumnbyte?