【问题标题】:SQLAlchemy unique check on postgresql insert scalabilitySQLAlchemy 对 postgresql 插入可扩展性的唯一检查
【发布时间】:2017-09-25 13:27:35
【问题描述】:

有人可以帮我理解我做错了什么吗?

以下所有内容均可按要求工作,但我遇到了可扩展性问题 -

On the first run i fetched ~70,000 rows into a blank table in ~2-3 s

On the 2nd run i fetched ~80,000 rows into the same table in ~5 min

On the 3rd run i fetched ~50,000 rows into the same table in ~30 min    

On the 4th run i fetched ~120,000 rows into the same table in ~1 hr    

On the 5th run i fetched ~100,000 rows into the same table in ~2 hr

每次我运行代码时,我都会看到客户端和数据库之间稳定的 ~600KB/s 流量,而此活动完成

如您所见,所有这些列的哈希检查似乎根本无法很好地扩展

我的代码试图完成什么?

我需要将每日库存数据添加到 postgres 数据库中。数据每天仅在源头更新一次,API 响应如下 -

{'instrument_token': '210011653'
'exchange_token': '820358'
'tradingsymbol': 'COLG17MAY1020.00PE'
'name': ''
'last_price': 0.0
'expiry': '2017-05-25'
'strike': 1020.0
'tick_size': 0.05
'lot_size': 700
'instrument_type': 'PE'
'segment': 'BFO-OPT'
'exchange': 'BFO'} 

响应中的项目和行数每天都在变化 在给定的一天,我看到我可以在单个响应中获取 50,000 - 120,000 行(即大约 20-30 MB 的 csv 数据)。发送请求会获取给定日期的相同数据。

所以核心问题是 - 我想避免将同一行两次添加到数据库中,以防同一天多次获取数据。

到目前为止我做了什么 -

我是一个 db 新手,我的想法是自动增加一个 id 并添加一个 data_date 列,所以我的架构看起来像这样 -

CREATE TABLE IF NOT EXISTS instruments (
    id                  bigserial,
    data_date           date NOT NULL,
    instrument_token    integer NOT NULL,
    exchange_token      integer NOT NULL,
    tradingsymbol       varchar(40) NOT NULL,
    name                varchar(40) NOT NULL,
    last_price          numeric(15,2) NOT NULL,
    expiry              date,
    strike              numeric(15,2),
    tick_size           numeric,
    lot_size            integer,
    instrument_type     varchar(10),
    segment             varchar(20),
    exchange            varchar(10),
    PRIMARY KEY(id)
    );

我已经建立了一个这样的类 -

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, mapper, relationship
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, Numeric, String, MetaData, Table, ForeignKey, DateTime, union
from sqlalchemy.engine.url import URL

engine = create_engine('postgresql://blah')
Base = declarative_base(engine)

def _unique(session, cls, hashfunc, queryfunc, constructor, arg, kw):
    cache = getattr(session, '_unique_cache', None)
    if cache is None:
        session._unique_cache = cache = {}

    key = (cls, hashfunc(*arg, **kw))
    if key in cache:
        return cache[key]
    else:
        with session.no_autoflush:
            q = session.query(cls)
            q = queryfunc(q, *arg, **kw)
            obj = q.first()
            if not obj:
                obj = constructor(*arg, **kw)
                session.add(obj)
        cache[key] = obj
        return obj

class UniqueMixin(object):
    @classmethod
    def unique_hash(cls, *arg, **kw):
        raise NotImplementedError()

    @classmethod
    def unique_filter(cls, query, *arg, **kw):
        raise NotImplementedError()

    @classmethod
    def as_unique(cls, session, *arg, **kw):
        return _unique(
                    session,
                    cls,
                    cls.unique_hash,
                    cls.unique_filter,
                    cls,
                    arg, kw
               )

class Instrument(UniqueMixin, Base):

        __tablename__ = 'instruments'
        __table_args__ = {'autoload':True}
        __table__ = Table('instruments', Base.metadata,
            Column('id', Integer, primary_key=True),
            Column('data_date', String),
            Column('instrument_token', Integer),
            Column('exchange_token', Integer),
            Column('tradingsymbol', String),
            Column('name', String),
            Column('last_price', Numeric),
            Column('expiry', Integer),
            Column('strike', Numeric),
            Column('tick_size', Numeric),
            Column('lot_size', Integer),
            Column('instrument_type', String),
            Column('segment', String),
            Column('exchange', String))


        @classmethod
        def unique_hash(cls, data_date, instrument_token, exchange_token, tradingsymbol, name, last_price, expiry, strike, tick_size, lot_size, instrument_type, segment, exchange):
            return data_date, instrument_token, exchange_token, tradingsymbol, name, last_price, expiry, strike, tick_size, lot_size, instrument_type, segment, exchange
        @classmethod
        def unique_filter(cls, query, data_date, instrument_token, exchange_token, tradingsymbol, name, last_price, expiry, strike, tick_size, lot_size, instrument_type, segment, exchange):
            return query.filter(Instrument.data_date == data_date, Instrument.instrument_token == instrument_token, Instrument.exchange_token == exchange_token, Instrument.tradingsymbol == tradingsymbol, Instrument.name == name, Instrument.last_price == last_price, Instrument.expiry == expiry, Instrument.strike == strike, Instrument.tick_size == tick_size, Instrument.lot_size == lot_size, Instrument.instrument_type == instrument_type, Instrument.segment == segment, Instrument.exchange == exchange)



        def __init__(self, data_date, instrument_token, exchange_token, tradingsymbol, name, last_price, expiry, strike, tick_size, lot_size, instrument_type, segment, exchange):

            self.data_date = data_date
            self.instrument_token = instrument_token
            self.exchange_token = exchange_token
            self.tradingsymbol = tradingsymbol
            self.name = name
            self.last_price = last_price
            self.expiry = expiry
            self.strike = strike
            self.tick_size = tick_size
            self.lot_size = lot_size
            self.instrument_type = instrument_type
            self.segment = segment
            self.exchange = exchange

        def __repr__(self):
            return "<Instruments - '%s': '%s' - '%s' - '%s' - '%s' - '%s' - '%s' - '%s' - '%s' - '%s' - '%s' - '%s' - '%s' - '%s'>" % (
                                                                                                                                        self.id, 
                                                                                                                                        self.data_date, 
                                                                                                                                        self.instrument_token, 
                                                                                                                                        self.exchange_token, 
                                                                                                                                        self.tradingsymbol, 
                                                                                                                                        self.name, 
                                                                                                                                        self.last_price, 
                                                                                                                                        self.expiry, 
                                                                                                                                        self.strike, 
                                                                                                                                        self.tick_size, 
                                                                                                                                        self.lot_size, 
                                                                                                                                        self.instrument_type, 
                                                                                                                                        self.segment, 
                                                                                                                                        self.exchange
                                                                                                                                        )

插入数据的代码如下-

    for instrument in response:
        #print(instrument)
        if instrument['expiry'] == '' :
            instrument['expiry'] = null()
        market_instrument = Instrument.as_unique(self.session, 
                                                    data_date = datetime.date.today().isoformat(), 
                                                    instrument_token =  instrument['instrument_token'], 
                                                    exchange_token =    instrument['exchange_token'], 
                                                    tradingsymbol = instrument['tradingsymbol'], 
                                                    name =  instrument['name'], 
                                                    last_price =    instrument['last_price'], 
                                                    expiry =    instrument['expiry'], 
                                                    strike =    instrument['strike'],
                                                    tick_size = instrument['tick_size'],
                                                    lot_size =  instrument['lot_size'], 
                                                    instrument_type =   instrument['instrument_type'], 
                                                    segment =   instrument['segment'], 
                                                    exchange = instrument['exchange'], 
                                                    )
        self.session.add(market_instrument)
    self.session.commit()

我正在考虑的选项

你认为什么最好?

选项 1 不再使用 as_unique(

再创建一个 data_update_date 表 (data_date(primary), status(boolean)),该表在每日成功插入结束时更新

检查 data_update_date 获取今天的日期,如果存在则跳过整个块的添加

但是,这个选项并不能帮助我了解我的 as_unique 函数中是否还有其他需要纠正的错误

选项 2 使用 powa 和配置文件设置新数据库

查找并修复瓶颈

我正在使用官方的 postgres docker 映像,我遇到了用 hypopg 和其他必需的扩展扩展 debian 基础的死胡同

看起来centos会简单得多,所以我正在创建一个新的dockerfile来做到这一点

但是,由于我是 postgresql 和 sqlalchemy 的新手,我还需要您对我的代码是否存在一些明显问题提出意见

选项 3 只散列几列

我可以只散列前 3 个,不包括 id

但是我不知道该怎么做

只是减少hash classdef参数导致参数个数比类中定义的少,所以插入失败

选项 4

我没有与 postgresql 或 sqlalchemy 结婚

我应该改用非 ORM 方法吗?

或者,我应该使用数据库以外的东西来存储这种数据

我在 AWS 上的 m2.large 实例上运行它,它应该具有正确的性能,所以也许我使用了错误的方法来存储数据 如果在插入时出现这种情况,那么在进行技术分析时,多个线程将根本无法使用...

我应该改用 hadoop 之类的东西吗?

此外,此选项的一个明显缺点是另一个学习曲线可扩展为 hadoop...

【问题讨论】:

  • 尝试分析并查看瓶颈在哪里。只是一个猜测:您的缓存变得如此之大,以至于它开始写入磁盘并从中读取。
  • plumSemPy 谢谢,是的,我正在整合 powa。我为此更新了问题中的选项 2
  • 您需要让数据库处理唯一性(通过UNIQUE 约束或索引)。
  • univerio unique 无法按预期工作,因为它将应用于单个列
  • sqlalchemy 至少允许唯一的元组

标签: python database postgresql orm sqlalchemy


【解决方案1】:

我跑了一些db profiling on the bulk insert operation

缓存命中率为100%

我没有看到任何磁盘 io

抱歉,我现在不能发布超过 2 个链接,所以我无法向您展示点击率和磁盘点击率的图表,所以您只需要相信我的话 :)

as_unique 方法基本上是使用一种效率极低的方法工作的,该方法会通过大量的查询来访问数据库。如果有的话,我想这只是作为这个服务器构建+配置的一个很好的基准,这让我对它在缓存友好型工作负载方面的性能非常满意

正如来自各种响应的提示所指出的,瓶颈在于架构以及插入在代码中的实现方式

我解决了这样的问题 -

1.添加多列唯一索引

CREATE UNIQUE INDEX market_daily_uq_idx ON instruments (
data_date, 
instrument_token, 
exchange_token, 
tradingsymbol, 
instrument_type, 
segment, 
exchange
);

2。使用 .on_conflict_do_nothing()

                statement = insert(Instrument).values( 
                                    data_date = datetime.date.today().isoformat(), 
                                    instrument_token =  instrument['instrument_token'], 
                                    exchange_token =    instrument['exchange_token'], 
                                    tradingsymbol = instrument['tradingsymbol'], 
                                    name =  instrument['name'], 
                                    last_price =    instrument['last_price'], 
                                    expiry =    instrument['expiry'], 
                                    strike =    instrument['strike'],
                                    tick_size = instrument['tick_size'],
                                    lot_size =  instrument['lot_size'], 
                                    instrument_type =   instrument['instrument_type'], 
                                    segment =   instrument['segment'], 
                                    exchange = instrument['exchange'], 
                                    ).on_conflict_do_nothing()


            self.session.execute(statement)
        self.session.commit()

这很好用&things are much faster now,从而解决了核心问题

非常感谢大家的帮助、提示和建议!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-01-03
    • 1970-01-01
    • 1970-01-01
    • 2011-11-01
    • 1970-01-01
    • 2011-03-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多