对于某些图算法,我需要从数据库中获取大量记录到内存(~1M记录)。我想要快速完成这项工作,并希望记录成为对象(即:我想要ORM)。为了粗略地对不同的解决方案进行基准测试,我创建了一个带有1M Foo对象的表的简单问题,就像我在这里做的那样:Why is loading SQLAlchemy objects via the ORM 5-8x slower than rows via a raw MySQLdb cursor?。
可以看到使用裸SQL获取它们非常快;使用简单的for循环将记录转换为对象也很快。两者都在大约2-3秒内执行。但是使用像SQLAlchemy和Hibernate这样的ORM,这需要20-30秒:如果你问我的话会慢得多,这只是一个没有关系和连接的简单例子。
SQLAlchemy为自己提供了一个功能"成熟,高性能的架构," (http://www.sqlalchemy.org/features.html)。同样适用于Hibernate"高性能" (http://hibernate.org/orm/)。在两种方式都是正确的,因为它们允许非常通用的面向对象的数据模型来回映射到MySQL数据库。另一方面,它们非常错误,因为它们比SQL和本机代码慢10倍。我个人认为他们可以做更好的基准来展示这一点,即与原生SQL + java或python相比较的基准。但这不是问题所在。
当然,我不想要SQL +本机代码,因为它很难维护。所以我想知道为什么不存在类似于面向对象的数据库,它处理数据库 - >对象映射本机。有人建议使用OrientDB,因此我尝试了它。 API非常好:当你的getter和setter正确时,该对象是可插入的和可选择的。
但我想要的不仅仅是API甜蜜,所以我尝试了1M示例:
import java.io.Serializable;
public class Foo implements Serializable {
public Foo() {}
public Foo(int a, int b, int c) { this.a=a; this.b=b; this.c=c; }
public int a,b,c;
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; }
}
import com.orientechnologies.orient.object.db.OObjectDatabaseTx;
public class Main {
public static void insert() throws Exception {
OObjectDatabaseTx db = new OObjectDatabaseTx ("plocal:/opt/orientdb-community-1.7.6/databases/test").open("admin", "admin");
db.getEntityManager().registerEntityClass(Foo.class);
int N=1000000;
long time = System.currentTimeMillis();
for(int i=0; i<N; i++) {
Foo foo = new Foo(i, i*i, i+i*i);
db.save(foo);
}
db.close();
System.out.println(System.currentTimeMillis() - time);
}
public static void fetch() {
OObjectDatabaseTx db = new OObjectDatabaseTx ("plocal:/opt/orientdb-community-1.7.6/databases/test").open("admin", "admin");
db.getEntityManager().registerEntityClass(Foo.class);
long time = System.currentTimeMillis();
for (Foo f : db.browseClass(Foo.class).setFetchPlan("*:-1")) {
if(f.getA() == 345234) System.out.println(f.getB());
}
System.out.println("Fetching all Foo records took: " + (System.currentTimeMillis() - time) + " ms");
db.close();
}
public static void main(String[] args) throws Exception {
//insert();
fetch();
}
}
使用 OrientDB 获取1M Foo大约需要 18秒。带有getA()的for循环是强制将对象字段实际加载到内存中,因为我注意到默认情况下它们是懒惰地获取的。我想这也可能是提取Foo的速度很慢的原因,因为它在获取所有内容(包括字段)时对每次迭代进行db-access而不是db-access。
我尝试使用setFetchPlan(&#34; *: - 1&#34;)来修复它,我认为它也可能适用于字段,但这似乎不起作用。
问题:有没有办法快速完成此操作,最好是在2-3秒范围内?为什么这需要18秒,而裸SQL版本需要3秒?
添加:使用像@ frens-jan-rumph这样的ODatabaseDocumentTX,建议只给你一个大约5的加速,但是大约是2.调整下面的代码给了我大约9秒的运行时间。这仍然比原始sql慢3倍,但没有执行到Foo的转换。几乎所有的时间都进入了for循环。
public static void fetch() {
ODatabaseDocumentTx db = new ODatabaseDocumentTx ("plocal:/opt/orientdb-community-1.7.6/databases/pits2").open("admin", "admin");
long time = System.currentTimeMillis();
ORecordIteratorClass<ODocument> it = db.browseClass("Foo");
it.setFetchPlan("*:0");
System.out.println("Fetching all Foo records took: " + (System.currentTimeMillis() - time) + " ms");
time = System.currentTimeMillis();
for (ODocument f : it) {
//if((int)f.field("a") == 345234) System.out.println(f.field("b"));
}
System.out.println("Iterating all Foo records took: " + (System.currentTimeMillis() - time) + " ms");
db.close();
}
答案 0 :(得分:0)
答案在于方便。
在一次采访中,当我向候选人询问他们对LINQ的看法(C#我知道,但与您的问题相关)时,他们非常正确地回答说这是对表现的牺牲,而不是方便。
手写的SQL语句(无论是否调用存储过程)总是比使用自动神奇地将查询结果转换为nice的ORM更快,易于使用的POCO。
那就是说,差异应该不如你所经历的那么好。是的,以自动神奇的方式进行操作会产生开销,但它不应该那么好。我确实有这方面的经验,在C#中我不得不使用特殊的反射类来减少进行这种自动魔法映射所需的时间。
对于大量数据,我预计ORM会出现最初的减速,但之后可以忽略不计。 3秒到18秒巨大。
答案 1 :(得分:0)
如果您对测试进行了剖析,您会发现执行以下四种方法需要大约60-80%的CPU时间:
是的,在此设置中,瓶颈在ORM层中。使用ODatabaseDocumentTx可提供大约5倍的加速。可能只是让你到达你想去的地方。
仍有很多时间(接近50%)用于com.orientechnologies ... OJNADirectMemory.getInt(...)。仅从内存位置读取整数就很昂贵。不明白为什么不在这里使用java nio字节缓冲区。节省了大量穿越Java /本地边界等。
除了这些微观基准和OrientDB的显着行为之外,我认为至少还有两个需要考虑的因素:
P.S。我建议在基准测试之前预热代码
答案 2 :(得分:0)
你在这里做的是最糟糕的情况。当您为数据库编写(或应该已经编写)时,您的测试只是读取一个表并将其直接写入任何流中。
所以你看到的是很多魔法的全部开销。通常,如果你做一些更复杂的事情,比如加入,选择,过滤和订购你的ORM的开销,可以达到5到10%的更合理的份额。
你应该考虑的另一件事 - 我猜东方也在做同样的事情 - ORM解决方案是创建新对象增加内存消耗和Java在内存消耗方面非常糟糕,以及我一直在内存表中使用自定义的原因处理大量数据/对象。
您知道对象在表中的哪一行。
你的对象得到的另一件事也被插入到列表/地图中(至少Hibernate正在这样做)。它会在您更改对象后跟踪对象的脏污程度。重新缩放时,此插入也会花费大量时间,这也是我们使用分页列表或映射的原因。如果区域增长,复制1M引用就会变慢。