Проверить существование строки и вернуть идентификатор с помощью sqlalchemy медленно - PullRequest
3 голосов
/ 06 февраля 2012

All

Я читаю CSV-файл и добавляю данные в базу данных MySQL с помощью sqlalchemy. Одной из таблиц является таблица адресов, которая должна содержать только уникальные адреса. Существует связь между этими адресами и другой таблицей «операторов», которые имеют поле внешнего ключа идентификатора адреса.

Итак, для каждой строки в моем файле данных я создаю новый оператор obj, а затем получаю идентификатор для связанного адреса. Если адрес уже существует , этот идентификатор возвращается. В противном случае я создаю новый адрес obj и возвращаю этот идентификатор. Это делается с помощью приведенного ниже кода, адаптированного из этого вопроса SO .

def get_or_create(self, model, rec):
    instance = self.session.query(model).filter_by(**dict(filter(lambda (x,y): x in model.__dict__.keys(), rec.iteritems()))).first()
    if instance:
        return instance
    else:
        instance = model(rec)
        return instance

Я использую GUID для своего поля id, и оно является частью первичного ключа для таблицы адресов:

class address(Base):
    __tablename__ = 'address'
    id = id_column()
    name               = Column(String(75), primary_key=True)
    Address_Line_One   = Column(String(50), primary_key=True)
    Address_Line_Two   = Column(String(50), primary_key=True)
    Address_Line_Three = Column(String(50), primary_key=True)
    Address_Line_Four  = Column(String(50), primary_key=True)

id_column() происходит от здесь , хотя оно было преобразовано в CHAR(32) из-за ограничений в других местах. Наконец, здесь есть фрагмент:

currStatement   = statements(rec, id=currGUID)
currStatement.address = self.get_or_create(address, rec)

Это все работает нормально, но очень медленно. Для ~ 65 000 операторов, вставленных в одну транзакцию, я вижу время вставки 1,5 часа в чистой тестовой БД. Наблюдение за вставкой в ​​реальном времени показывает, что она быстро достигает ~ 10000 строк, затем скорость вставки начинает падать.

Что я могу сделать, чтобы ускорить время вставки?

Edit:

После дальнейшего тестирования я обнаружил, что медленное время вставки частично, потому что каждый объект вставляется индивидуально. Итак, у меня есть ~ 65 000 строк, каждая из которых становится несколькими объектами sqlalchemy, вставленными индивидуально. С sqlalchemy 0.7, как я могу массово вставить мои объекты?

1 Ответ

6 голосов
/ 08 февраля 2012

Хорошо!

Таким образом, ответ таков: я вставлял каждую строку в отдельности и выполнял круговое отключение в БД для каждой проверки адреса. Проверка адреса была худшей частью, так как она стала экспоненциально медленнее. Я рассчитал, что вставка исходных данных (1,5 часа), а затем вставка тех же данных снова займет ~ 9 часов!

Таким образом, этот ответ будет касаться того, что я сделал для преобразования в массовые операторы вставки, а также некоторых моментов, на которые следует обратить внимание.

  1. ORM в sqlalchemy «поможет»

ORM великолепен, но имейте в виду, что он плохо сочетается с объемными вставками. Для массовой вставки требуется использовать операторы нижнего уровня execute в сеансе. Они принимают не объекты ORM в качестве входных данных, а список словарей и объект insert. Поэтому, если вы конвертируете CSV-файл, полный строк, в объекты ORM, вам нужно НЕ добавить их в текущий сеанс, а вместо этого преобразовать их в словари для дальнейшего использования.

def asdict(obj):
    return dict((col.name, getattr(obj, col.name))
         for col in class_mapper(obj.__class__).mapped_table.c)

currGUID = uuid.uuid4()
currPrintOrMail = printOrMail(rec, id=currGUID)
currStatement   = statements(rec, id=currGUID)
currAddress = self.get_or_create(address, rec)
currStatement.address = currAddress

self.currPrintOrMail_bulk.append(asdict(currPrintOrMail))
self.currStatement_bulk.append(asdict(currStatement))

Метод asdict происходит от здесь . Это дает вам словари столбцов в созданных объектах ORM. Они никогда не добавляются в сеанс и вскоре после этого выпадают из памяти.

  1. Отношения будут кусать вас

Если вы установили отношения ORM:

class statements(Base):
    __tablename__ = 'statements'
    id = id_column()
    county   = Column(String(50),default='',nullable=False)

    address_id = Column(CHAR(36), ForeignKey('address.id'))
    address = relationship("address", backref=backref("statements", cascade=""))

    printOrMail_id = Column(CHAR(36), ForeignKey('printOrMail.id'))
    pom = relationship("printOrMail", backref=backref("statements", cascade=""))

    property_id = Column(CHAR(36), ForeignKey('property.id'))
    prop = relationship("property", backref=backref("statements", cascade=""))

Убедитесь, что каскад пустой в обратном ссылочном коде ! В противном случае вставка объекта в отношение в сеанс будет каскадным через остальные объекты. Когда вы попытаетесь выполнить массовую вставку ваших значений позже, они будут отклонены как дубликаты ... если вам повезет.

Это важно, потому что частью требований было получение address_id для действительного адреса, если он существовал, и добавление адреса, если его не было. Поскольку отключение запроса было слишком медленным, я изменил get_or_create на:

def get_or_create(self, model, rec):
    """Check if current session has address. If not, query DB for it. If no one has the address, create and flush a new one to the session."""
    instance = self.session.query(model).get((rec['Name'], rec['Address_Line_One'], rec['Address_Line_Two'], rec['Address_Line_Three'], rec['Address_Line_Four']))
    if instance:
        return instance
    else:
        instance = model(rec)
        self.session.add(instance)
        self.session.flush()
        return instance

Использование get заставляет sqlalchemy сначала проверять сеанс, предотвращая поездки по сети. Но это работает, только если к сеансу добавлено новых адресов! Помните отношения? Это каскадно вставлялось в заявления. Кроме того, если у вас нет flush() или autoflush=True, get не может видеть вновь добавленные объекты.

  1. Когда вы создаете сеанс, сохраняйте ваши объекты!

    self.session = sessionmaker (autoflush = False, expire_on_commit = False)

Если вы не включите expire_on_commit=False, тогда вы потеряете свои адреса и снова начнете совершать обходы.

  1. ORM объекты имеют вставку

Теперь у нас есть список словарей для вставляемых объектов ORM. Но нам также нужен объект вставки.

self.session.execute(printOrMail.__table__.insert(), self.currPrintOrMail_bulk)
self.session.execute(statements.__table__.insert(), self.currStatement_bulk)

Похоронен в документах , кажется, что можно использовать classname.__table__ для необходимого табличного объекта, требуемого insert . Поэтому в сеансе, используя класс ORM, чтобы получить таблицу для получения объекта вставки, выполните команду execute со списком словарей. Не забывайте совершать потом!

  1. Не хватает памяти

Это позволит вам успешно смешивать массовую вставку и ORM со связями и запросами уникальных записей в sqlalchemy. Просто следите за нехваткой памяти. Мне приходилось массово вставлять ~30,000 записей за раз, в противном случае py2.7(32bit) зависал бы на 2G.

...