为什么通过ORM加载SQLAlchemy对象比通过原始MySQLdb游标加载行慢5-8倍?

46

我注意到使用SQLAlchemy获取(和ORM)某些数据速度较慢,但使用纯SQL语句则可以更快地获取。首先,我创建了一个拥有一百万条记录的数据库:


mysql> use foo
mysql> describe Foo;
+-------+---------+------+-----+---------+-------+
| Field | Type    | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| id    | int(11) | NO   | PRI | NULL    |       |
| A     | int(11) | NO   |     | NULL    |       |
| B     | int(11) | NO   |     | NULL    |       |
| C     | int(11) | NO   |     | NULL    |       |
+-------+---------+------+-----+---------+-------+
mysql> SELECT COUNT(*) FROM Foo;
+----------+
| COUNT(*) |
+----------+
|  1000000 |
+----------+
mysql> 

作为一个简单的测试,查询所有Foo需要大约2秒钟:

herbert@dev0 ~ $ date; echo 'use foo; select * from Foo;' | mysql -uroot -pxxx > /dev/null; date
zo apr 20 18:48:49 CEST 2014
zo apr 20 18:48:51 CEST 2014

如果我使用MySQLdb在Python中执行此操作,则需要大约3秒钟的时间,包括Foo对象的构建:

herbert@dev0 ~ $ python BareORM.py 
query execution time:  0:00:02.198986
total time:  0:00:03.403084

以下哪个是输出结果:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import MySQLdb
import sys
import time
import datetime

class Foo:
    def __init__(self, a, b, c):
        self.a=a; self.b=b; self.c=c;

try:
    start = datetime.datetime.now()
    con = MySQLdb.connect('localhost', 'root', 'xxx', 'foo')
    cur = con.cursor();

    cur.execute("""SELECT * FROM Foo LIMIT 1000000""")
    print "query execution time: ", datetime.datetime.now()-start
    foos = [];
    for elem in cur:
        foos.append(Foo(elem[1], elem[2], elem[3]))
    con.commit()

except MySQLdb.Error, e:
    print "Error %d: %s" % (e.args[0], e.args[1])
    sys.exit(1)

finally:
    if con: con.close()
    print "total time: ",  datetime.datetime.now()-start

然而,使用SQLAlchemy来减少样板代码的话,执行相同的任务需要大约25秒钟:

herbert@dev0 ~ $ python AlchemyORM.py 
total time:  0:00:24.649279

使用以下代码:

import sqlalchemy
import datetime
import MySQLdb

from sqlalchemy import Column, Integer, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship, backref

Base = declarative_base()

class Foo(Base):
    __tablename__ = 'Foo'
    id = Column(Integer, primary_key=True)
    A  = Column(Integer(unsigned=False), nullable=False)
    B  = Column(Integer(unsigned=False), nullable=False)
    C  = Column(Integer(unsigned=False), nullable=False)

engine  = create_engine('mysql+mysqldb://root:xxx@localhost/foo')
Session = sessionmaker(bind=engine)
session = Session()
start = datetime.datetime.now()
foos  = session.query(Foo).limit(1000000).all()
print "total time: ", datetime.datetime.now()-start

为什么假设SQLAlchemy应该做同样的事情,它的操作速度比裸的SQL慢大约10倍?我能用什么方法加快速度吗?

以下是更复杂的查询的最简工作示例,使用贪婪加载连接多个表。 我考虑只在单个表上进行简单的查询,然后使用字典创建id ->对象映射,并整理一对N关系。 但在这样做之前,我想确保SQLAlchemy无法表现得更好,因为从软件设计的角度来看,编写自己的ORM是一个糟糕的主意。 在我看来,2倍的减速是可以接受的(也许)。

如果您了解其他(更快的)Python-SQL ORM或类似BigTable的解决方案(已经是ORM),请随时在评论中提及。

编辑:也尝试了Peewee,结果是约15秒。

from peewee import *
import datetime;
database = MySQLDatabase("foo", host="localhost", port=3306, user="root", passwd="xxx")

class Foo(Model):
        id = IntegerField()
        A  = IntegerField()
        B  = IntegerField()
        C  = IntegerField()

        class Meta:
                db_table = 'Foo'
                database = database

start = datetime.datetime.now()
foos = Foo.select()
cnt=0;
for i in foos: cnt=cnt+1
print "total time: ", datetime.datetime.now() - start

编辑:作为对马蒂亚斯的回应,我尝试使用Hibernate在Java中做同样的事情,结果大约是8到10秒,虽然不算快,但比25秒要快得多。代码从一些类开始,以一些配置结束:

package herbert.hibernateorm;

import java.util.List;

import org.hibernate.Session; 
import org.hibernate.Transaction;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class App {
   public static void main(String[] args) throws Exception {
      SessionFactory factory = new Configuration().configure().buildSessionFactory();
      Session session = factory.openSession();
      Transaction tx = session.beginTransaction();
      long start = System.currentTimeMillis();
      List foos = session.createQuery("FROM Foo").list(); 
      System.out.println(foos.size());
      System.out.printf("total time: %d\n", System.currentTimeMillis() - start);
      session.close();
   }
}
package herbert.hibernateorm;

public class Foo {
    private int id, a, b, c;
    public Foo() {}
    public Foo(int A, int B, int C) { this.a=A; this.b=B; this.c=C; }

    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public int getA() { return a; }
    public void setA(int a) { this.a = a; }
    public int getB() { return b; }
    public void setB(int b) { this.b = b; }
    public int getC() { return c; }
    public void setC(int c) { this.c = c; }
}

配置文件分别为 hibernate.cfg.xml 和 hibernate.hbm.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/foo?zeroDateTimeBehavior=convertToNull</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.connection.password">xxx</property>
    <mapping resource="hibernate.hbm.xml"/>
  </session-factory>
</hibernate-configuration>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="herbert.hibernateorm.Foo" table="Foo" catalog="foo">
        <id name="id" type="int">
            <column name="id" />
            <generator class="assigned" />
        </id>
        <property name="a" type="int">
            <column name="A" not-null="true" />
        </property>
        <property name="b" type="int">
            <column name="B" not-null="true" />
        </property>
        <property name="c" type="int">
            <column name="C" not-null="true" />
        </property>
    </class>
</hibernate-mapping>

最后是运行所有内容的Maven pom文件:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>herbert</groupId>
    <artifactId>hibernateORM</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>hibernateORM</name>
    <url>http://maven.apache.org</url>
    <repositories>
        <repository>
            <id>unknown-jars-temp-repo</id>
            <name>A temporary repository created by NetBeans for libraries and jars it could not identify. Please replace the dependencies in this repository with correct ones and delete this repository.</name>
            <url>file:${project.basedir}/lib</url>
        </repository>
    </repositories>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.21</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>4.0.1.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>4.0.1.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.common</groupId>
            <artifactId>hibernate-commons-annotations</artifactId>
            <version>4.0.1.Final</version>
        </dependency>   
        <dependency>
            <groupId>nz.ac.waikato.cms.weka</groupId>
            <artifactId>weka-dev</artifactId>
            <version>3.7.10</version>
        </dependency>
        <dependency>
            <groupId>commons-configuration</groupId>
            <artifactId>commons-configuration</artifactId>
            <version>1.9</version>
        </dependency>
        <dependency>
            <groupId>commons-net</groupId>
            <artifactId>commons-net</artifactId>
            <version>3.1</version>
            <classifier>examples</classifier>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>maven</groupId>
            <artifactId>maven-jetty-plugin</artifactId>
            <version>1.1</version>
            <type>plugin</type>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>
        <dependency>
                <groupId>com.kenai.nbpwr</groupId>
                <artifactId>org-slf4j-jdk14</artifactId>
                <version>1.6.1-201106101300</version>
                <type>nbm</type>
        </dependency>

    </dependencies>
</project>

作为一个建议,我在这里找到了(http://pythonguy.wordpress.com/2011/08/17/sqlalchemy-tips-performance/#comment-1284)尝试分页,**foos = session.query(Foo).yield_per(X).all(),其中X=1000,10 000, 100 000,但不幸的是,这些也产生了大约26秒的执行时间。我还尝试了foos = session.query(Foo).execution_options(stream_results=True).all()**,但注意到我没有使用psycopg2,因为我使用的是MySQL而不是PostgreSQL。这需要大约25秒。假设2014年的常识,MySQL应该在100万条记录结果上表现良好。 - Herbert
@herbert:我使用yield_per为1000时,加载1M个完整对象行大约需要14秒,比peewee更快。 - zzzeek
根据我的经验,如果你想保持表达能力(即避免使用SQLAlchemy作为编写SQL代码的薄层),那么Python + SQLAlchemy在性能方面并不理想。到目前为止,我还没有找到一种Python解决方案,可以在从数据库中获取数千行数据并对其进行处理时提供良好的性能。 - Ezequiel
@Ezequiel 很久以前我写了一个简单的包装器,它在一张表上执行了一个 select 操作,并将其映射到一个 Python 对象的生成器中。然后是一些函数,它们通过创建内存中的 Python 字典将这些生成器快速连接起来,实现了生成器的物化。在 SQL 诞生之初,这可能会消耗太多内存,但对我而言,这导致了查询时间仅有几秒钟,而使用 SQLAlch 则需要 30 秒。我认为,使用 Python 进行联接而不是 SQL 的 JOIN 可以真正帮助提高性能。对于大数据提取,我找不到使用 SQL-JOIN 的好理由,除了这是每个人都在做的事情。 - Herbert
1
仔细阅读后,我感觉你在Python中重构了散列连接(整个过程中“创建内存中的python字典”),以克服MySQL仅支持嵌套循环连接(至少曾经如此)的事实。其他DBMS长期以来一直支持散列连接和其他策略。 - Ilja Everilä
显示剩余3条评论
3个回答

74

这是你的MySQL脚本的SQLAlchemy版本,与MySQLdb相比,它可以在四秒内完成:

from sqlalchemy import Integer, Column, create_engine, MetaData, Table
import datetime

metadata = MetaData()

foo = Table(
    'foo', metadata,
    Column('id', Integer, primary_key=True),
    Column('a', Integer(), nullable=False),
    Column('b', Integer(), nullable=False),
    Column('c', Integer(), nullable=False),
)


class Foo(object):
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

engine = create_engine('mysql+mysqldb://scott:tiger@localhost/test', echo=True)
start = datetime.datetime.now()

with engine.connect() as conn:
    foos = [
        Foo(row['a'], row['b'], row['c'])
        for row in
        conn.execute(foo.select().limit(1000000)).fetchall()
    ]


print "total time: ", datetime.datetime.now() - start

运行时:

total time:  0:00:04.706010

这里有一个使用ORM加载对象行的脚本;通过避免一次性创建包含所有1M个对象的固定列表,而是使用yield per,这个脚本在SQLAlchemy主分支下运行时间为13秒(在版本0.9下为18秒):

import time
from sqlalchemy import Integer, Column, create_engine, Table
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Foo(Base):
    __table__ = Table(
        'foo', Base.metadata,
        Column('id', Integer, primary_key=True),
        Column('a', Integer(), nullable=False),
        Column('b', Integer(), nullable=False),
        Column('c', Integer(), nullable=False),
    )


engine = create_engine('mysql+mysqldb://scott:tiger@localhost/test', echo=True)

sess = Session(engine)

now = time.time()

# avoid using all() so that we don't have the overhead of building
# a large list of full objects in memory
for obj in sess.query(Foo).yield_per(100).limit(1000000):
    pass

print("Total time: %d" % (time.time() - now))

我们可以在这两种方法之间取得平衡,使用ORM仅加载单个列:
for obj in sess.query(Foo.id, Foo.a, Foo.b, Foo.c).yield_per(100).limit(1000000):
    pass

上述代码在4秒内运行。

与 SQLAlchemy Core 的比较更适合与原始的 MySQLdb 游标进行比较。如果您使用 ORM 但是查询单个列,则在最新版本中大约需要四秒钟。

在 ORM 级别上,速度问题是因为在 Python 中创建对象很慢,并且 SQLAlchemy ORM 在获取这些对象时应用了大量的簿记,这是必要的,以便它能够实现其使用契约,包括工作单元、标识映射、急切加载、集合等。

为了大大加快查询速度,请获取单个列而不是完整对象。请参见http://docs.sqlalchemy.org/en/latest/faq/performance.html#result-fetching-slowness-orm中描述此内容的技术。

对于与 PeeWee 的比较,PW 是一个功能更少的系统,包括它不会处理任何关于标识映射的事情。即使对于 PeeWee,这是一个可行的简单 ORM,它仍然需要15秒,这表明与直接使用 C 语言编写的 MySQLdb 获取相比,cPython非常缓慢

与 Java 的比较,Java VM 比 cPython 快得多。Hibernate 非常复杂,但是由于 JIT,Java VM 仍然非常快,并且即使所有这些复杂性也运行得更快。如果您想将 Python 与 Java 进行比较,请使用 Pypy。


1
感谢您的建议和解释!然而,我的一般问题是1M条记录并不多,实际上,考虑到我们大约需要4 (11位) 1M = 5.5兆字节,这是一个非常小的数量。将这么多的“东西”加载到内存中通常非常快,例如在Matlab中读取该大小的图像的imread(),它只需0.058425秒。然而,您的观点是“逻辑”使用时间,但是我的BareORM示例表明,简单的逻辑也应该能够快速执行。 - Herbert
4
你的评论不太合理,因为你期望Python的性能能和纯C代码一样快。将其与Matlab、Java等东西进行比较完全是不公平的比较,但这与SQLAlchemy关系不大。SQLAlchemy非常非常快。只是用户往往不知道它提供了多少功能,并将ORM结果集与原始数据库游标混淆。它们是非常不同的,而SQLAlchemy提供了许多控制“原始”与“自动化”混合的选项。 - zzzeek
5
对于“两次获取,然后根据结果合并”,SQLAlchemy提供了这个功能,请参见http://docs.sqlalchemy.org/en/latest/orm/loading.html。在Python世界中,我熟悉的唯一ORM是SQLAlchemy,它提供了这些类型的模式,这在很大程度上是因为我们是唯一一个将集合持久存储而不是每次访问时进行加载的ORM。 - zzzeek
3
如果你正在寻找一款使用Python但是在很大程度上使用C的ORM,请参考storm orm。它没有SQLAlchemy的任何主动加载功能,但从实现的角度来看,它是最快的 - 它像PeeWee一样简约,但其整个持久性引擎提供了基于C的后端。对其运行基准测试,那就是你所能做到的最好水平。或者只需使用Pypy。 - zzzeek
2
所以你的说法是,需要应用一个单一指令“yield_per(100)”来使查询速度翻倍是不合理的。我期待着你的Python ORM实现能够以更优越的方式解决这些问题!祝你好运。 - zzzeek
显示剩余6条评论

2

这并不是对我的问题的回答,但可能会帮助大众解决大数据集速度问题。我发现选择一百万条记录通常可以在约3秒内完成,然而使用JOINS可能会减慢这个过程。如果有大约150k个Foo与1M个Bars之间的1-多关系,则使用JOIN选择它们可能会很慢,因为每个Foo被返回约6.5次。我发现分别选择两个表格,并使用Python中的字典将它们连接起来,比SQLAlchemy(约25秒)快约3倍,比使用JOIN的“裸”Python代码(约17秒)快约2倍。在我的用例中,代码花费了8秒钟。选择没有关系的1M条记录,如上面的Bar示例,只需3秒。我使用了以下代码:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import MySQLdb
import sys
import time
import datetime
import inspect
from operator import itemgetter, attrgetter

# fetch all objects of class Class, where the fields are determined as the
# arguments of the __init__ constructor (not flexible, but fairly simple ;))
def fetch(Class, cursor, tablename, ids=["id"], where=None):
    arguments = inspect.getargspec(Class.__init__).args; del arguments[0];
    fields = ", ".join(["`" + tablename + "`.`" + column + "`" for column in arguments])
    sql = "SELECT " + fields + " FROM `" + tablename + "`"
    if where != None: sql = sql + " WHERE " + where
    sql=sql+";"
    getId = itemgetter(*[arguments.index(x) for x in ids])
    elements = dict()

    cursor.execute(sql)
    for record in cursor:
        elements[getId(record)] = Class(*record)
    return elements

# attach the objects in dict2 to dict1, given a 1-many relation between both
def merge(dict1, fieldname, dict2, ids):
    idExtractor = attrgetter(*ids)
    for d in dict1: setattr(dict1[d], fieldname, list())
    for d in dict2:
        dd = dict2[d]
        getattr(dict1[idExtractor(dd)], fieldname).append(dd)

# attach dict2 objects to dict1 objects, given a 1-1 relation
def attach(dict1, fieldname, dict2, ids):
    idExtractor = attrgetter(*ids)
    for d in dict1: dd=dict1[d]; setattr(dd, fieldname, dict2[idExtractor(dd)])

这有助于我加快查询速度,但我很乐意听取专家关于此方法可能改进的建议。


2
SQLAlchemy很复杂。它必须处理将不支持本地的Python类型转换为底层数据库的表,继承,JOIN,缓存对象,保持一致性,翻译行,部分结果等问题。查看sqlalchemy/orm/loading.py:instance_processor - 这太疯狂了。
解决方案是组合和编译Python代码以处理特定查询的结果,就像Jinja2为模板所做的那样。到目前为止,还没有人做过这项工作,可能是因为通常情况下只有几行数据(这种优化是不切实际的),需要处理大量数据的人会手动完成,就像您所做的那样。

我的SQL示例还需要将INT(11)转换为Python整数,我假设MySQLdb会这样做。此外,SQLAlchemy需要找出继承和连接的时间不应该与结果数量成线性比例增长(100k需要约2秒,而1M需要约24秒)。就个人而言,我不愿意为继承付出速度代价。正如您所看到的,我还尝试在Java中运行我的示例,它需要大约8-10秒。如果我可以总结您的答案是“否,如果您想要速度,您需要自己创建对象/ SQL代码,SQLAlchemy并不快。” - Herbert
4
我喜欢你的反馈,但当SQLAlchemy声称“成熟、高性能的架构”时,我并不太满意。在我看来,如果它比裸 SQL+简单的面向对象要慢 10 倍,那么它就不算高性能。我猜这是因为设计决策不创建模式和不合成 Python 代码所导致的性能下降。因此,我认为把SQLAlchemy称为“高性能”是不公平的。不过,如果有人与我持相反看法,我会非常高兴!;) - Herbert
对于类型转换,peewee也是如此,相关代码:https://github.com/coleifer/peewee/blob/master/peewee.py#L1523-L1658 - coleifer
问题是,您是否非常需要SQLAlchemy的ORM速度,以至于要(共同)资助为其开发Python代码合成器?到目前为止,显然没有人这样做。 - Matthias Urlichs
1
我的观点是SQLAlchemy并不是“高性能”的,这种速度上的权衡应该在特性页面中更加明显地提到。不这样做会让人们误以为在SQLAlchemy中编写对象模型可能并不是最好的解决方案。我尝试着研究编写一个小框架,可以编译成cpp,但似乎只比使用Python的字典略快一些。我的方法是:连接所有需要的表,为所有字段和主键创建itemgetter,为每个连接创建一个PK->object的字典,使用这些字典来“关联”对象。我还不确定这样做有多快。 - Herbert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接