【问题标题】:Fill in missing monthly data with an SQLAlchemy query使用 SQLAlchemy 查询填充缺失的月度数据
【发布时间】:2021-06-11 22:09:28
【问题描述】:

我有一个数据库表,其中列出了玩游戏的时间。我现在正在查询此表以收集数据以显示在图表中,以便很好地了解过去一年每月玩的游戏数量。

以下工作完美无缺,直到 covid-19 锁定我们,我们开始看到几个月零游戏。 :-/

data_db = list(                                                             
      games.group_by("year")                                                  
      .group_by("month")                                                      
      .order_by(desc("year"))                                                 
      .order_by(desc("month"))                                                
      .limit(12)                                                              
      .values(                                                                
          func.extract("year", Game.created).label("year"),                   
          func.extract("month", Game.created).label("month"),                 
          func.count().label("games"),                                        
      )                                                                          
)

在哪里

games = Game.query.filter(                                                  
      Game.started.isnot(None), Game.no_players.isnot(-1), Game.room == room  
  )

和游戏一个 SQLAlchemy 模型。

这会产生 [(2020, 2, 20), (2020, 3, 65), (2020, 5, 3), ...] 形式的数据,在此示例中没有 2020 年 4 月的数据。然后,生成的条形图将显示所有非零月份的数据,当然不能真实显示现实情况。

我开始研究通过加入动态生成的日历表来扩展 SQLAlchemy 查询以包括零个月的方法,但并没有真正走得太远。顺便说一下,底层数据库是 SQLite。

我现在已经“解决”了Python中的情况如下:

  data_raw = []                                                               
  for date in rrule.rrule(                                                    
          rrule.MONTHLY,                                                      
          dtstart=datetime.now() - relativedelta.relativedelta(months=11),    
          until=datetime.now(),                                               
  ):                                                                          
      for dbdata in data_db:                                                  
          if dbdata[0:2] == (date.year, date.month):                          
              data_raw.append(dbdata)                                         
              break                                                           
      else:                                                                   
          data_raw.append((date.year, date.month, 0))

这当然有效,但让我嘴里有点酸味。

由于数据集非常小,这更像是一个镀金问题,而不是一个真正的性能问题,但我仍然想看看是否有一个仅限 SQLAlchemy 的解决方案。

(我想图形库(在这种情况下为 Chart.js)也许也可以填补这些空白,但我没有考虑。)

【问题讨论】:

    标签: python sqlite sqlalchemy


    【解决方案1】:

    您可以创建一个包含所有年/月对(包括缺失的)的临时表,然后将其与您的聚合查询 LEFT JOIN(作为 .subquery()):

    import datetime
    
    from sqlalchemy import (
        create_engine,
        Table,
        MetaData,
        Column,
        Integer,
        DateTime,
        desc,
        func,
        and_,
    )
    from sqlalchemy.orm import declarative_base, Session
    
    engine = create_engine(
        "sqlite:///:memory:",
        future=True,
        echo=True,
    )
    
    Base = declarative_base()
    
    
    class Game(Base):
        __tablename__ = "game"
        id = Column(Integer, primary_key=True)
        created = Column(DateTime)
        room = Column(Integer)
        no_players = Column(Integer)
        started = Column(DateTime)
    
    
    Base.metadata.create_all(engine)
    
    # create test data to query
    with Session(engine, future=True) as session:
        session.add_all(
            [
                Game(
                    created=datetime.datetime.now(),
                    room=1,
                    no_players=2,
                    started=datetime.datetime.now(),
                ),
                Game(
                    created=datetime.datetime(2021, 3, 4, 3, 2, 1),
                    room=1,
                    no_players=2,
                    started=datetime.datetime(2021, 3, 4, 5, 6, 7),
                ),
                Game(
                    created=datetime.datetime(2021, 1, 1, 1, 1, 1, 1),
                    room=1,
                    no_players=3,
                    started=datetime.datetime(2021, 1, 1, 1, 1, 2, 3),
                ),
            ]
        ),
        session.commit()
    
    # define temporary table structure and data to insert
    tmp_tbl_months = Table(
        "tmp_tbl_months",
        MetaData(),
        Column("year", Integer, primary_key=True, autoincrement=False),
        Column("month", Integer, primary_key=True, autoincrement=False),
        prefixes=["TEMPORARY"],
    )
    
    current_date = datetime.date.today()
    loop_year = current_date.year
    loop_month = current_date.month
    tmp_data = []
    num_months_to_report = 4
    for i in range(num_months_to_report):
        tmp_data.append({"year": loop_year, "month": loop_month})
        if loop_month == 1:
            loop_month = 12
            loop_year -= 1
        else:
            loop_month -= 1
    print(tmp_data)
    # [
    #  {'year': 2021, 'month': 3},
    #  {'year': 2021, 'month': 2},
    #  {'year': 2021, 'month': 1},
    #  {'year': 2020, 'month': 12}
    # ]
    
    with engine.begin() as conn:
        tmp_tbl_months.create(conn)
        conn.execute(tmp_tbl_months.insert(), tmp_data)
    
        room = 1  # for testing
        games = session.query(Game).filter(
            Game.started.isnot(None), Game.no_players.isnot(-1), Game.room == room
        )
        aggregation = (
            games.group_by("year")
            .group_by("month")
            .order_by(desc("year"))
            .order_by(desc("month"))
            .limit(12)
            .with_entities(
                func.extract("year", Game.created).label("year"),
                func.extract("month", Game.created).label("month"),
                func.count().label("games"),
            )
            .subquery()
        )
        # temp_tbl_months LEFT JOIN aggregation (subquery)
        data_db = list(
            session.query()
            .select_from(tmp_tbl_months)
            .outerjoin(
                aggregation,
                and_(
                    aggregation.c.year == tmp_tbl_months.c.year,
                    aggregation.c.month == tmp_tbl_months.c.month,
                ),
            )
            .with_entities(
                tmp_tbl_months.c.year,
                tmp_tbl_months.c.month,
                func.coalesce(aggregation.c.games, 0),
            )
        )
        print(data_db)
        # [(2021, 3, 2), (2021, 2, 0), (2021, 1, 1), (2020, 12, 0)]
    

    【讨论】:

    • 哇,非常感谢戈德!最后的coalesce 很好。虽然我可以看到这绝对有效,但我对它需要的额外代码量感到惊讶。我想我有点希望在查询或其他东西中动态生成这个临时表。我还查看了sqlite.org/lang_datefunc.html 的修饰符,并且正在考虑一种访问这些修饰符的 SQLAlchemy 方式。无论如何,非常感谢您为熟悉我提出的案例而付出的大量努力!
    猜你喜欢
    • 1970-01-01
    • 2020-02-28
    • 2011-08-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多