为什么通过ORM加载SQLAlchemy对象比通过MySQLdb游标加载行慢5-8倍?
我注意到,使用SQLAlchemy获取一些数据(以及进行对象关系映射)时速度很慢,而用简单的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
如果我在Python中使用MySQLdb来做这个,整个过程大约需要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倍,假设SQLAlchemy应该做的事情差不多?我能否加快速度?
这是一个更复杂查询的简单示例,它通过急切加载连接了几个表。我考虑过只对单个表进行简单查询,然后使用字典来创建id到对象的映射,并整理一对多的关系。但在这样做之前,我想确认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
编辑:作为对Matthias的回应,我尝试在Java中用Hibernate做同样的事情,结果大约是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>
最后是运行所有内容的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>
3 个回答
这段话不是在回答我的问题,但可能对大家在处理大数据集时的速度问题有帮助。我发现,选择一百万条记录通常可以在大约3秒内完成,但如果涉及到连接(JOINS),这个过程可能会变慢。在这种情况下,有大约15万条Foo记录,而每条Foo记录与100万条Bar记录有一对多的关系,那么使用连接来选择这些记录可能会很慢,因为每条Foo记录大约会被返回6.5次。我发现,分别选择这两个表,然后在Python中用字典将它们连接起来,速度大约是SQLAlchemy的三倍(大约25秒),也是用普通Python代码连接的两倍(大约17秒)。在我的使用案例中,这段代码用了8秒。选择没有关系的100万条记录,比如上面的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)])
这帮助我加快了查询速度,不过我很乐意听听专家们对这种方法可能的改进意见。
SQLAlchemy 是个复杂的东西。它需要处理很多事情,比如把数据库里不支持的类型转换成 Python 能用的类型,处理有继承关系的表,进行表之间的连接(JOIN),缓存对象,保持数据的一致性,翻译行数据,处理部分结果等等。你可以看看 sqlalchemy/orm/loading.py:instance_processor
,那里的内容真是让人头疼。
一个解决办法是把 Python 代码拼凑起来,编译成可以处理特定查询结果的代码,就像 Jinja2 处理模板那样。不过到目前为止,还没有人做过这样的工作,可能是因为大多数情况下只需要处理几行数据(这种优化反而会适得其反),而需要处理大量数据的人通常是手动完成的,就像你之前做的那样。
这是你用SQLAlchemy写的MySQL脚本,执行时间是四秒,而用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(对象关系映射)来完全加载对象行;通过每次只加载一部分对象,而不是一次性加载所有100万个对象,这样运行时间是13秒(使用SQLAlchemy的最新版本,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在获取这些对象时需要进行大量的管理工作,这是为了满足它的使用要求,包括工作单元、身份映射、急切加载、集合等等。
为了大幅提高查询速度,建议只获取单独的列,而不是完整的对象。可以参考这个链接,里面描述了相关技巧。
与PeeWee的比较,PeeWee是一个简单得多的系统,功能也少很多,包括它不处理身份映射。即使是PeeWee,这个尽可能简单的ORM,执行时间也要15秒,这说明cPython真的很慢,相比之下,原始的MySQLdb获取数据是用C语言写的,速度快得多。
再说说Java,Java虚拟机比cPython快得多。Hibernate虽然复杂得离谱,但由于JIT(即时编译技术),Java虚拟机运行得非常快,即使有那么多复杂的东西,最终运行速度也很快。如果你想把Python和Java进行比较,可以使用Pypy。