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

Her*_*ert 25 python mysql performance orm sqlalchemy

我注意到SQLAlchemy缓慢获取(和ORMing)一些数据,使用裸骨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> 
Run Code Online (Sandbox Code Playgroud)

作为粗略测试,查询所有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
Run Code Online (Sandbox Code Playgroud)

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

herbert@dev0 ~ $ python BareORM.py 
query execution time:  0:00:02.198986
total time:  0:00:03.403084
Run Code Online (Sandbox Code Playgroud)

哪个是输出:

#!/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
Run Code Online (Sandbox Code Playgroud)

但是,使用SQLAlchemy减少样板代码,执行相同的工作大约需要25秒:

herbert@dev0 ~ $ python AlchemyORM.py 
total time:  0:00:24.649279
Run Code Online (Sandbox Code Playgroud)

使用此代码:

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
Run Code Online (Sandbox Code Playgroud)

为什么SQLAlchemy的操作比裸SQL解决方案慢10倍,假设SQLAlchemy应该做大致相同的事情?我可以以某种方式加快速度吗?

这是一个更复杂的查询的最小工作示例,它使用预先加载来连接多个表.我正在考虑在单个表上进行简单查询,然后使用字典创建id->对象映射并整理一对N关系.但在此之前,我想确保SQLAlchemy无法更好地执行,因为从软件设计的角度来看,编写自己的ORM是一个坏主意.Imho,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
Run Code Online (Sandbox Code Playgroud)

编辑:作为对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();
   }
}
Run Code Online (Sandbox Code Playgroud)
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; }
}
Run Code Online (Sandbox Code Playgroud)

配置(分别是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>
Run Code Online (Sandbox Code Playgroud)
<?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>
Run Code Online (Sandbox Code Playgroud)

最后pom文件在maven中运行它:

<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>
Run Code Online (Sandbox Code Playgroud)

zzz*_*eek 56

以下是MySQL脚本的SQLAlchemy版本,在4秒内执行,而MySQLdb则为3:

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
Run Code Online (Sandbox Code Playgroud)

运行:

total time:  0:00:04.706010
Run Code Online (Sandbox Code Playgroud)

这是一个使用ORM完全加载对象行的脚本; 通过避免使用yield per一次创建包含所有1M对象的固定列表,使用SQLAlchemy master 在13秒内运行(使用rel 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))
Run Code Online (Sandbox Code Playgroud)

然后我们可以分割这两种方法之间的差异,并使用ORM加载单个列:

for obj in sess.query(Foo.id, Foo.a, Foo.b, Foo.c).yield_per(100).limit(1000000):
    pass
Run Code Online (Sandbox Code Playgroud)

以上再次在4秒内运行.

SQLAlchemy Core的比较是与原始MySQLdb游标的比较.如果您使用ORM但查询单个列,则在最新版本中大约需要4秒.

在ORM级别,速度问题是因为在Python中创建对象很慢,并且SQLAlchemy ORM在获取这些对象时会对这些对象应用大量簿记,这对于它实现其使用合同是必要的,包括单元工作,身份地图,渴望装载,收藏等

要显着加快查询速度,请获取单个列而不是完整对象.请参阅http://docs.sqlalchemy.org/en/latest/faq/performance.html#result-fetching-slowness-orm中的技术, 其中描述了这一点.

为了与PeeWee进行比较,PW是一个更简单的系统,具有更少的功能,包括它不会对身份映射做任何事情.即使使用PeeWee,就像ORM一样简单,它仍然需要15秒,这证明cPython与直接C中的原始MySQLdb提取相比真的很慢.

为了与Java进行比较,Java VM 比cPython更快.Hibernate 非常复杂,但由于JIT,Java VM非常快,甚至所有复杂性最终都会更快地运行.如果要将Python与Java进行比较,请使用Pypy.

  • 对于"两个提取,然后合并结果",SQLAlchemy提供了这个:请参阅http://docs.sqlalchemy.org/en/latest/orm/loading.html.SQLAlchemy是我熟悉的Python世界中唯一提供这些模式的ORM,这主要是因为我们是唯一一个持久存储集合而不是每次都可以加载的ORM. (4认同)
  • 你的评论是不合理的,因为你期望Python的表现与直接的C代码一样快.将它与Matlab,Java等进行比较是完全不公平的比较,但这又与SQLAlchemy没什么关系.SQLAlchemy非常非常快.只是用户倾向于不知道正在传递多少功能,并且将ORM结果集与原始数据库游标的结果集混淆.它们完全不同,SQLAlchemy提供了许多控制"原始"与"自动"混合的选项. (3认同)
  • 如果您*正在寻找使用Python的ORM,但在很大程度上使用C,请参阅[storm orm](https://storm.canonical.com/).它没有提供SQLAlchemy的任何急切加载功能,但从实现的角度来看,它的速度和你一样快 - 它与PeeWee一样极简,但它的整个持久性引擎提供了一个基于C的后端.对此进行基准测试,这是你要做的最好的.或者只是使用Pypy. (2认同)
  • 所以你的说法是,需要应用一个指令“yield_per(100)”,它使查询速度加倍,是不合理的。我期待您的 Python ORM 实现以卓越的方式解决这些问题!祝你好运。 (2认同)

Mat*_*chs 2

SQLAlchemy 很复杂。它必须处理将类型转换为底层数据库本身不支持的 Python、具有继承的表、JOIN、缓存对象、维护一致性、翻译行、部分结果等等。看看sqlalchemy/orm/loading.py:instance_processor——这太疯狂了。

解决方案是拼凑并编译 Python 代码来处理特定查询的结果,就像 Jinja2 对模板所做的那样。到目前为止,还没有人完成这项工作,可能是因为常见的情况是几行(这种优化是悲观的),而需要处理批量数据的人会像您一样手动执行此操作。

  • 我喜欢你的反馈,但是当 SQLAlchemy 声称“成熟、高性能架构”http://www.sqlalchemy.org/features.html 时,它并不是很令人满意,恕我直言,如果它比裸 sql+ 慢 10 倍,那么它就不是高性能。简单哦。我想不创建模式和不合成 python 代码的设计决策是公平的,但由于固有的性能下降,我认为将 SQLAlchemy 称为“高性能”是不公平的。尽管如此,如果有人反对我,我会很高兴!;) (4认同)