在SQLAlchemy中映射大量类似的表

时间:2014-03-28 04:51:53

标签: python sql database sqlalchemy

我有很多(~2000个)时间序列数据的位置。每个时间序列都有数百万行。我想将它们存储在Postgres数据库中。我目前的方法是为每个位置时间序列设置一个表,以及一个存储有关每个位置(坐标,高程等)的信息的元表。我正在使用Python / SQLAlchemy来创建和填充表。我想在元表和每个时间序列表之间建立关系来执行查询,例如“选择在日期A和日期B之间具有数据的所有位置”和“选择日期A的所有数据并导出带坐标的csv”。创建具有相同结构的许多表(仅名称不同)并与元表建立关系的最佳方法是什么?或者我应该使用不同的数据库设计?

目前我正在使用这种方法生成大量类似的映射:

from sqlalchemy import create_engine, MetaData
from sqlalchemy.types import Float, String, DateTime, Integer
from sqlalchemy import Column, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship, backref

Base = declarative_base()


def make_timeseries(name):
    class TimeSeries(Base):

        __tablename__ = name
        table_name = Column(String(50), ForeignKey('locations.table_name'))
        datetime = Column(DateTime, primary_key=True)
        value = Column(Float)

        location = relationship('Location', backref=backref('timeseries',
                                lazy='dynamic'))

        def __init__(self, table_name, datetime, value):
            self.table_name = table_name
            self.datetime = datetime
            self.value = value

        def __repr__(self):
            return "{}: {}".format(self.datetime, self.value)

    return TimeSeries


class Location(Base):

    __tablename__ = 'locations'
    id = Column(Integer, primary_key=True)
    table_name = Column(String(50), unique=True)
    lon = Column(Float)
    lat = Column(Float)

if __name__ == '__main__':
    connection_string = 'postgresql://user:pw@localhost/location_test'
    engine = create_engine(connection_string)
    metadata = MetaData(bind=engine)
    Session = sessionmaker(bind=engine)
    session = Session()

    TS1 = make_timeseries('ts1')
    # TS2 = make_timeseries('ts2')   # this breaks because of the foreign key
    Base.metadata.create_all(engine)
    session.add(TS1("ts1", "2001-01-01", 999))
    session.add(TS1("ts1", "2001-01-02", -555))

    qs = session.query(Location).first()
    print qs.timeseries.all()

这种方法存在一些问题,最值得注意的是,如果我创建多个TimeSeries,则外键不起作用。以前我已经使用了一些工作,但这看起来像是一个大黑客,我觉得必须有一个更好的方法来做到这一点。我应该如何组织和访问我的数据?

3 个答案:

答案 0 :(得分:12)

Alternative-1: Table Partitioning

一旦我读完完全相同的表结构,就会立即想到

Partitioning。我不是DBA,并且没有太多的使用它的生产经验(在PostgreSQL上更是如此),但是 请阅读PostgreSQL - Partitioning文档。表分区旨在解决您所遇到的问题,但超过1K的表/分区听起来很有挑战性;因此,请在论坛/ SO上进行更多关于此主题的可扩展性相关问题的研究。

鉴于您最常用的两个搜索标准datetime组件非常重要,因此必须有可靠的索引策略。如果您决定使用 partitioning root,则明显的分区策略将基于日期范围。与最新数据相比,这可能允许您将不同块中的旧数据分区,特别是假设旧数据(几乎从未)更新,因此物理布局将是密集且高效的;而你可以采用另一种策略来实现更多"最近"数据

Alternative-2: trick SQLAlchemy

这基本上使您的示例代码工作,通过欺骗SA来假设所有TimeSeries children是使用Concrete Table Inheritance的一个实体的from datetime import date, datetime from sqlalchemy import create_engine, Column, String, Integer, DateTime, Float, ForeignKey, func from sqlalchemy.orm import sessionmaker, relationship, configure_mappers, joinedload from sqlalchemy.ext.declarative import declarative_base, declared_attr from sqlalchemy.ext.declarative import AbstractConcreteBase, ConcreteBase engine = create_engine('sqlite:///:memory:', echo=True) Session = sessionmaker(bind=engine) session = Session() Base = declarative_base(engine) # MODEL class Location(Base): __tablename__ = 'locations' id = Column(Integer, primary_key=True) table_name = Column(String(50), unique=True) lon = Column(Float) lat = Column(Float) class TSBase(AbstractConcreteBase, Base): @declared_attr def table_name(cls): return Column(String(50), ForeignKey('locations.table_name')) def make_timeseries(name): class TimeSeries(TSBase): __tablename__ = name __mapper_args__ = { 'polymorphic_identity': name, 'concrete':True} datetime = Column(DateTime, primary_key=True) value = Column(Float) def __init__(self, datetime, value, table_name=name ): self.table_name = table_name self.datetime = datetime self.value = value return TimeSeries def _test_model(): _NUM = 50 # 0. generate classes for all tables TS_list = [make_timeseries('ts{}'.format(1+i)) for i in range(_NUM)] TS1, TS2, TS3 = TS_list[:3] # just to have some named ones Base.metadata.create_all() print('-'*80) # 1. configure mappers configure_mappers() # 2. define relationship Location.timeseries = relationship(TSBase, lazy="dynamic") print('-'*80) # 3. add some test data session.add_all([Location(table_name='ts{}'.format(1+i), lat=5+i, lon=1+i*2) for i in range(_NUM)]) session.commit() print('-'*80) session.add(TS1(datetime(2001,1,1,3), 999)) session.add(TS1(datetime(2001,1,2,2), 1)) session.add(TS2(datetime(2001,1,2,8), 33)) session.add(TS2(datetime(2002,1,2,18,50), -555)) session.add(TS3(datetime(2005,1,3,3,33), 8)) session.commit() # Query-1: get all timeseries of one Location #qs = session.query(Location).first() qs = session.query(Location).filter(Location.table_name == "ts1").first() print(qs) print(qs.timeseries.all()) assert 2 == len(qs.timeseries.all()) print('-'*80) # Query-2: select all location with data between date-A and date-B dateA, dateB = date(2001,1,1), date(2003,12,31) qs = (session.query(Location) .join(TSBase, Location.timeseries) .filter(TSBase.datetime >= dateA) .filter(TSBase.datetime <= dateB) ).all() print(qs) assert 2 == len(qs) print('-'*80) # Query-3: select all data (including coordinates) for date A dateA = date(2001,1,1) qs = (session.query(Location.lat, Location.lon, TSBase.datetime, TSBase.value) .join(TSBase, Location.timeseries) .filter(func.date(TSBase.datetime) == dateA) ).all() print(qs) # @note: qs is list of tuples; easy export to CSV assert 1 == len(qs) print('-'*80) if __name__ == '__main__': _test_model() 。下面的代码是自包含的,并创建50个表,其中包含最少的数据。但是如果你已经拥有了一个数据库,它应该允许你相当快地检查性能,这样你就可以做出决定,如果它甚至是一个很接近的可能性。

Alternative-3: a-la BigData

Alternative-4: TimeSeries databases

如果您确实遇到使用数据库的性能问题,我可能会尝试:

  • 仍然将数据保存在单独的表/数据库/模式中,就像您现在所做的那样
  • 使用&#34; native&#34;批量导入数据数据库引擎提供的解决方案
  • 使用MapReduce-like分析。
    • 在这里,我将继续使用python和sqlalchemy,并实现自己的分布式查询和聚合(或找到现有的东西)。显然,这只有在您不需要直接在数据库上生成这些结果时才有效。

edit-1: {{1}}

我没有使用大规模的经验,但绝对值得考虑。


如果您以后可以分享您的调查结果和整个决策过程,那将会非常棒。

答案 1 :(得分:3)

我会避免上面提到的数据库设计。我对你正在使用的数据知之甚少,但听起来你应该有两张桌子。一个用于位置的表和一个用于location_data的子表。位置表将存储您在上面提到的数据,例如坐标和高程。 location_data表将存储位置表中的location_id以及您要跟踪的时间序列数据。

这将消除每次添加其他位置时更改的数据库结构和代码更改,并允许您正在查看的查询类型。

答案 2 :(得分:2)

两部分:

仅使用两个表

不需要有数十个或数百个相同的表。只需要一个location的表和一个location_data的表,其中每个条目都会fkey到位。还可以在location_id的location_data表上创建索引,以便您进行有效的搜索。

不要使用sqlalchemy来创建此

我喜欢sqlalchemy。我每天都使用它。它非常适合管理数据库和添加一些行,但您不希望将其用于具有数百万行的初始设置。你想要生成一个与postgres'“COPY”语句兼容的文件[http://www.postgresql.org/docs/9.2/static/sql-copy.html] COPY可以让你快速输入大量数据;它是转储/恢复操作期间使用的内容。

sqlalchemy非常适合查询和添加行。如果你有批量操作,你应该使用COPY。