Hibernate比SQL查询慢1000倍。

12
我有这样的设置。
@Table(name ="A")
EntityA {
    Long ID;
    List<EntityB> children;
}

@Table(name ="B")
EntityB {
    Long ID;
    EntityA parent;
    EntityC grandchild;
}

@Table(name ="C")
EntityC {
    Long ID;
}

这是 SQL 查询语句(我省略了不相关的细节):

select top 300 from A where ... and ID in (select parent from B where ... and grandchild in (select ID from C where ...)) order by ...

直接在数据库中或通过Hibernate(3.5)SQL运行的SQL查询比使用Criteria或HQL表达式快1000倍。

从HQL和Criteria生成的SQL与我发布的SQL完全相同

[编辑]:更正- SQL并不相同。我没有尝试在管理工具上使用Hibernate风格的参数设置,因为我直到后来才意识到这一点-请参见我的答案。

如果将子查询分开成单独的查询,则速度会再次加快。

我尝试了:

  • 删除所有子项、父项等的映射,只使用Long Id引用-结果相同,因此它不是获取、延迟、急切相关的。
  • 使用连接而不是子查询,并以所有获取和加载的组合获得相同的缓慢行为。
  • 在ID上设置投影而不是检索实体,因此没有对象转换-仍然很慢

我查看了Hibernate代码,它正在做一些惊人的事情。它遍历了所有300个结果,最终命中了数据库。

private List doQuery(
        final SessionImplementor session,
        final QueryParameters queryParameters,
        final boolean returnProxies) throws SQLException, HibernateException {

    final RowSelection selection = queryParameters.getRowSelection();
    final int maxRows = hasMaxRows( selection ) ?
            selection.getMaxRows().intValue() :
            Integer.MAX_VALUE;

    final int entitySpan = getEntityPersisters().length;

    final ArrayList hydratedObjects = entitySpan == 0 ? null : new ArrayList( entitySpan * 10 );
    final PreparedStatement st = prepareQueryStatement( queryParameters, false, session );
    final ResultSet rs = getResultSet( st, queryParameters.hasAutoDiscoverScalarTypes(), queryParameters.isCallable(), selection, session );

// would be great to move all this below here into another method that could also be used
// from the new scrolling stuff.
//
// Would need to change the way the max-row stuff is handled (i.e. behind an interface) so
// that I could do the control breaking at the means to know when to stop

    final EntityKey optionalObjectKey = getOptionalObjectKey( queryParameters, session );
    final LockMode[] lockModesArray = getLockModes( queryParameters.getLockOptions() );
    final boolean createSubselects = isSubselectLoadingEnabled();
    final List subselectResultKeys = createSubselects ? new ArrayList() : null;
    final List results = new ArrayList();

    try {

        handleEmptyCollections( queryParameters.getCollectionKeys(), rs, session );

        EntityKey[] keys = new EntityKey[entitySpan]; //we can reuse it for each row

        if ( log.isTraceEnabled() ) log.trace( "processing result set" );

        int count;
        for ( count = 0; count < maxRows && rs.next(); count++ ) {

            if ( log.isTraceEnabled() ) log.debug("result set row: " + count);

            Object result = getRowFromResultSet( 
                    rs,
                    session,
                    queryParameters,
                    lockModesArray,
                    optionalObjectKey,
                    hydratedObjects,
                    keys,
                    returnProxies 
            );
            results.add( result );

            if ( createSubselects ) {
                subselectResultKeys.add(keys);
                keys = new EntityKey[entitySpan]; //can't reuse in this case
            }

        }

        if ( log.isTraceEnabled() ) {
            log.trace( "done processing result set (" + count + " rows)" );
        }

    }
    finally {
        session.getBatcher().closeQueryStatement( st, rs );
    }

    initializeEntitiesAndCollections( hydratedObjects, rs, session, queryParameters.isReadOnly( session ) );

    if ( createSubselects ) createSubselects( subselectResultKeys, queryParameters, session );

    return results; //getResultList(results);

}

在这段代码中,
final ResultSet rs = getResultSet( st, queryParameters.hasAutoDiscoverScalarTypes(), queryParameters.isCallable(), selection, session );

它以完整的 SQL 命令向数据库发出请求,但没有任何结果被收集。

然后它继续执行这个循环。

for ( count = 0; count < maxRows && rs.next(); count++ ) {

对于预期的300个结果中的每一个,它最终都会命中数据库以获取实际结果。

这似乎很疯狂,因为在1次查询之后应该已经拥有所有结果。Hibernate日志没有显示在此期间发出任何其他SQL语句。

有人有任何见解吗?我唯一的选择是通过Hibernate转到本地SQL查询。


1
这取决于获取大小,同时比较SQL执行时间和Hibernate的结果就像比较苹果和椰子一样。Hibernate做了更多的事情,例如将结果转换为对象,这需要根据您的映射和结果数量花费不同的时间。 - M. Deinum
2
几个月前,我也遇到了Hibernate运行缓慢的问题。最终放弃使用它来处理超过10条记录的数据库。不要浪费时间在网上寻找解决方案,我曾花费数天时间进行搜索,但没有任何收获。最好的解决方案是,对于任何需要进行大量数据库操作的情况,自己编写查询并将数据转换为对象。我亲身经历,在一个数据密集型应用程序中,通过这种方式可以将运行时间减少约500%。 - gcalex5
2
这就是你为使用混淆关系模型所付出的代价。 - user330315
1
Hibernate应该如何在不循环遍历结果集的情况下收集返回的数据?当然,它会这样做。除非您想执行一个查询而不关心它返回什么,否则您手写的JDBC代码也必须这样做。 - JB Nizet
1
感谢JB Nizet的澄清-我已经查看了基础知识。话虽如此,这是我听过的最愚蠢的事情。数据库在单独的调用中返回行数据-这就像浏览器客户端为HTML页面的每个属性和属性单独发出请求。我无法想象这对性能有多友好。更深入地了解后,它看起来像jdbc具有获取大小,这是它希望在单个旅行中拥有多少行-似乎默认值为10。https://docs.oracle.com/cd/E11882_01/java.112/e16548/resltset.htm#JJDBC28621 - che javara
显示剩余12条评论
2个回答

6
我们在系统中也发现了类似的行为。我们发现,使用硬编码参数编写查询而不是使用 setParameter() 可以解决这个问题。
我们正在使用 MS SQL Server ,经过进一步调查,我们发现我们问题的根本原因是sql server driver的默认配置会将查询参数作为Unicode传输。这导致我们的索引被忽略,因为它们基于查询列上的ascii值。
解决方案是在jdbc url中设置此属性:sendStringParametersAsUnicode=false 更多细节可以在这里找到。 https://dev59.com/I3NA5IYBdhLWcg3wcNbF#32867579

6

我终于找到了问题的根源。问题是由Hibernate将参数与涉及子查询的实际SQL查询分开设置导致的。因此,无论是本地SQL还是其他方式,如果这样做,性能都会很慢。例如,以下查询将表现较差:

String sql = some sql that has named parameter = :value
SQLQuery sqlQuery = session.createSQLQuery(sql);
sqlQuery.setParameter ("value", someValue);
List<Object[]> list = (List<Object[]>)sqlQuery.list();

这将会很快

String sql = some native sql where parameter = 'actualValue'
SQLQuery sqlQuery = session.createSQLQuery(sql);
List<Object[]> list = (List<Object[]>)sqlQuery.list();

似乎由于让Hibernate处理参数,它最终会在结果集获取中卡住。这可能是因为底层数据库查询花费了更长的时间进行参数化。我最终编写了Hibernate Criteria和Restrictions代码的等效版本,直接设置参数如上所示。


14
这个解决方案可能会让您面临潜在的 SQL 注入攻击风险。 - jtahlborn
谢谢,我已经注意到这个问题。 - che javara
你说你使用的是Hibernate 3.5,你是否尝试过更高版本的Hibernate,比如4.3?我自己没有遇到这些性能问题 - 虽然我必须承认我不直接使用Hibernate API,而是使用JPA。 - Gimby
我还没有尝试过更高版本的Hibernate,但我确认减速不是在Hibernate代码中,而是在数据库端由Hibernate代码触发时提供命名参数。这取决于您正在执行的查询以及在注意到效果之前数据库中的总行数。 - che javara
你正在使用Sql Server吗?可能你的问题与jTDS驱动程序和varchar参数有关。有时候,你认为Hibernate正在执行where parameterr = 'actualValue',但实际上它执行的是where parameterr = N'actualValue' - Dherik

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