FetchMode加入vs SubSelect

时间:2015-10-07 05:59:24

标签: hibernate jpa join sql-subselect

我有两个表Employee和Department以下是这两个表的实体类

Department.java
@Entity
@Table(name = "DEPARTMENT")
public class Department {
    @Id
    @Column(name = "DEPARTMENT_ID")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer departmentId;
    @Column(name = "DEPARTMENT_NAME")
    private String departmentName;
    @Column(name = "LOCATION")
    private String location;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "department", orphanRemoval = true)
    @Fetch(FetchMode.SUBSELECT)
    //@Fetch(FetchMode.JOIN)
    private List<Employee> employees = new ArrayList<>();
}


Employee.java
@Entity
@Table(name = "EMPLOYEE")
public class Employee {
    @Id
    @SequenceGenerator(name = "emp_seq", sequenceName = "seq_employee")
    @GeneratedValue(generator = "emp_seq")
    @Column(name = "EMPLOYEE_ID")
    private Integer employeeId;
    @Column(name = "EMPLOYEE_NAME")
    private String employeeName;

    @ManyToOne
    @JoinColumn(name = "DEPARTMENT_ID")
    private Department department;
}

以下是我em.find(Department.class, 1);

时触发的查询

- 获取模式= fetchmode.join

    SELECT department0_.DEPARTMENT_ID AS DEPARTMENT_ID1_0_0_,
      department0_.DEPARTMENT_NAME    AS DEPARTMENT_NAME2_0_0_,
      department0_.LOCATION           AS LOCATION3_0_0_,
      employees1_.DEPARTMENT_ID       AS DEPARTMENT_ID3_1_1_,
      employees1_.EMPLOYEE_ID         AS EMPLOYEE_ID1_1_1_,
      employees1_.EMPLOYEE_ID         AS EMPLOYEE_ID1_1_2_,
      employees1_.DEPARTMENT_ID       AS DEPARTMENT_ID3_1_2_,
      employees1_.EMPLOYEE_NAME       AS EMPLOYEE_NAME2_1_2_
    FROM DEPARTMENT department0_
    LEFT OUTER JOIN EMPLOYEE employees1_
    ON department0_.DEPARTMENT_ID   =employees1_.DEPARTMENT_ID
    WHERE department0_.DEPARTMENT_ID=?

- 获取模式= fetchmode.subselect

    SELECT department0_.DEPARTMENT_ID AS DEPARTMENT_ID1_0_0_,
      department0_.DEPARTMENT_NAME    AS DEPARTMENT_NAME2_0_0_,
      department0_.LOCATION           AS LOCATION3_0_0_
    FROM DEPARTMENT department0_
    WHERE department0_.DEPARTMENT_ID=?

    SELECT employees0_.DEPARTMENT_ID AS DEPARTMENT_ID3_1_0_,
      employees0_.EMPLOYEE_ID        AS EMPLOYEE_ID1_1_0_,
      employees0_.EMPLOYEE_ID        AS EMPLOYEE_ID1_1_1_,
      employees0_.DEPARTMENT_ID      AS DEPARTMENT_ID3_1_1_,
      employees0_.EMPLOYEE_NAME      AS EMPLOYEE_NAME2_1_1_
    FROM EMPLOYEE employees0_
    WHERE employees0_.DEPARTMENT_ID=?

我只是想知道我们应该选择哪一个FetchMode.JOINFetchMode.SUBSELECT?我们应该选择哪种情况?

4 个答案:

答案 0 :(得分:27)

Marmite引用的SUBQUERY策略与FetchMode.SELECT有关,而与SUBSELECT无关。

您发布的关于 fetchmode.subselect 的控制台输出很奇怪,因为这不是应该工作的方式。

FetchMode.SUBSELECT

  

使用子选择查询加载其他集合

休眠docs

  

如果必须提取一个惰性集合或单值代理,Hibernate将加载所有这些,在子选择中重新运行原始查询。这与批量获取的工作方式相同,但没有零碎的加载。

FetchMode.SUBSELECT应如下所示:

SELECT <employees columns>
FROM EMPLOYEE employees0_
WHERE employees0_.DEPARTMENT_ID IN
(SELECT department0_.DEPARTMENT_ID FROM DEPARTMENT department0_)

您可以看到第二个查询将内存所有属于某个部门的员工(即employee.department_id不为空),如果不是部门则无关紧要您在第一个查询中检索的内容。 因此,如果员工表很大,这可能是一个主要问题,因为它可能是accidentially loading a whole database into memory

但是,FetchMode.SUBSELECT显着减少了查询的数量,因为与FecthMode.SELECT的N + 1个查询相比只需要两个查询。

您可能认为FetchMode.JOIN的查询次数更少,只有1次,那么为什么要使用SUBSELECT呢?嗯,这是真的,但代价是重复数据和更重的响应。

如果必须使用JOIN获取单值代理,则查询可以检索:

+---------------+---------+-----------+
| DEPARTMENT_ID | BOSS_ID | BOSS_NAME |
+---------------+---------+-----------+
|             1 |       1 | GABRIEL   |
|             2 |       1 | GABRIEL   |
|             3 |       2 | ALEJANDRO |
+---------------+---------+-----------+

如果老板的员工数据指向多个部门并且带有成本,则该副本的员工数据会重复。

如果必须使用JOIN获取延迟集合,则查询可能会检索:

+---------------+---------------+-------------+
| DEPARTMENT_ID | DEPARTMENT_ID | EMPLOYEE_ID |
+---------------+---------------+-------------+
|             1 | Sales         | GABRIEL     |
|             1 | Sales         | ALEJANDRO   |
|             2 | RRHH          | DANILO      |
+---------------+---------------+-------------+

如果部门数据包含多个员工(自然案例),则该部门数据会重复。 我们不仅要承担带宽成本,还要重复duplicated Department objects,我们必须使用SET或DISTINCT_ROOT_ENTITY去重复。

然而,在许多情况下,像延迟较低的pos中的重复数据是很好的折衷,例如Markus Winand says

  

SQL连接仍然比嵌套选择方法更有效 - 即使它执行相同的索引查找 - 因为它避免了大量的网络通信。如果由于每次销售的员工属性的重复而传输的数据总量更大,那么会更快。这是因为性能的两个方面:响应时间和吞吐量;在计算机网络中,我们称之为延迟和带宽。带宽对响应时间的影响很小,但延迟会产生巨大影响。这意味着数据库往返次数对响应时间的影响比传输的数据量更重要。

因此,使用SUBSELECT的主要问题是hard to control,可能正在将整个实体图加载到内存中。 使用批量提取,您可以在单独的查询中获取关联实体作为SUBSELECT(因此您不会遭受重复),逐渐地,最重要的是您只查询相关实体(因此您不会受到可能加载大图的影响),因为IN子查询由outter查询检索的ID过滤。

Hibernate: 
    select ...
    from mkyong.stock stock0_

Hibernate: 
    select ...
    from mkyong.stock_daily_record stockdaily0_ 
    where
        stockdaily0_.STOCK_ID in (
            ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
        )

(如果使用非常高的批量大小的批量提取将像SUBSELECT一样但没有加载整个表的问题,这可能是一个有趣的测试)

一些帖子显示了不同的提取策略和SQL日志(非常重要):

要点:

  • 加入:避免N + 1查询的主要问题,但它可能会检索重复的数据。
  • SUBSELECT:也避免使用N + 1并且不会复制数据,但会将相关类型的所有实体加载到内存中。

表格是使用ascii-tables构建的。

答案 1 :(得分:9)

我说这取决于......

假设您在一个部门中有N名员工,其中包含D字节信息,平均员工包含E字节。 (字节是属性长度与一些开销的总和)。

使用 join 策略执行1次查询并传输N *(D + E)数据。

使用子查询策略执行1 + N次查询,但只传输D + N * E数据。

如果N很大, N + 1查询通常是 NO GO ,因此首选JOIN。

但实际上,您必须检查查询次数和数据传输之间的里程数。

请注意,我并未将其他方面视为Hibernate缓存。

如果员工表很大并且已分区,则其他细微方面可能有效 - 对索引访问的分区修剪也会考虑。

答案 2 :(得分:1)

我的客户(金融服务)也有类似的问题,他想“在一个查询中获取数据”。好吧,我解释说最好有多个查询,因为以下内容:

对于FetchMode.JOIN,每个员工将部门从数据库转移到应用程序一次,因为连接操作会导致每个员工的部门倍增。如果你有10个部门,每个部门有100名员工,那么这10个部门中的每一个都将在一个查询中传输100次,简单的SQL。因此,在这种情况下,每个部门的转移次数超过了必要的99倍,从而导致部门的数据传输费用。

对于Fetchmode SUBSELECT,将向数据库触发两个查询。一个用于获取1000名雇员的数据,一个用于获得10个部门。对我来说,这听起来效率更高。您肯定会确保索引到位,以便可以立即检索数据。

我更喜欢FetchMode.SUBSELECT。

如果每个部门只有一名员工,那将是另一种情况,但正如“部门”所暗示的那样,这种情况不太可能发生。

我建议测量访问时间以支持这一理论。对于我的客户,我对不同类型的访问进行了测量,而我的客户的“部门”表有更多的字段(尽管我没有设计它)。所以很快就可以看出FetchMode.SUBSELECT要快得多。

答案 3 :(得分:1)

普兰基说

  

(1)这极具误导性。 (2)子选择不会将整个数据库提取到内存中。链接的文章是关于一个怪癖,其中subselect(3)忽略来自父级的分页命令,(4)但它仍然是一个子选择。

  1. 在您发表评论后,我再次调查了FetchMode.SUBSELECT,我发现我的答案并不完全正确。
  2. 这是一个假设的情况,其中每个实体的水合作用被完全加载到记忆中(在这种情况下是雇员)将结束许多其他实体的保湿。真正的问题是如果该表包含数千行(即使其中每一行都没有从其他表中急切地获取其他实体),则加载整个表被子选择。
  3. 我不知道您对来自父母的分页命令的意思。
  4. 是的,它仍然是一个子选择,但我不知道你想用这个指出什么。
  5.   

    您发布的有关fetchmode.subselect的控制台输出很奇怪,因为这不是应该工作的方式。

    这是事实,但只有在部门实体被隐藏的情况下(这意味着多个员工集合未初始化),我已使用3.6.10.Final4.3.8.Final对其进行了测试 在方案2.2 (FetchMode.SUBSELECT hidrating 2 of 3 Departments)和3.2 (FetchMode.SUBSELECT hidrating all Departments)中,SubselectFetch.toSubselectString返回以下内容(Hibernate类的链接取自4.3.8.Final标记):

    select this_.DEPARTMENT_ID from SUBSELECT_DEPARTMENT this_
    

    此子查询用于构建OneToManyJoinWalker.initStatementString

    结尾的where子句
    employees0_.DEPARTMENT_ID in (select this_.DEPARTMENT_ID from SUBSELECT_DEPARTMENT this_)
    

    然后在以{/ 3>结尾的CollectionJoinWalker.whereString中添加where子句

    select employees0_.DEPARTMENT_ID as DEPARTMENT3_2_1_, employees0_.EMPLOYEE_ID as EMPLOYEE1_1_, employees0_.EMPLOYEE_ID as EMPLOYEE1_3_0_, employees0_.DEPARTMENT_ID as DEPARTMENT3_3_0_, employees0_.EMPLOYEE_NAME as EMPLOYEE2_3_0_ from SUBSELECT_EMPLOYEE employees0_ where employees0_.DEPARTMENT_ID in (select this_.DEPARTMENT_ID from SUBSELECT_DEPARTMENT this_)
    

    确认此查询,在这两种情况下,所有员工都被检索并补充水分。 这显然是方案2.2中的一个问题,因为我们只为第1和第2部门提供保湿服务,但即使他们不属于这些部门(在这种情况下属于部门3的员工),也为所有员工提供保湿。

    如果会话中只有一个部门实体与其员工集合未初始化,那么查询就像eatSleepCode编写的那样。查看scenario 1.2

    select subselectd0_.department_id as departme1_2_0_, subselectd0_.department_name as departme2_2_0_, subselectd0_.location as location3_2_0_ from subselect_department subselectd0_ where subselectd0_.department_id=?
    

    来自FetchStyle

        /**
         * Performs a separate SQL select to load the indicated data.  This can either be eager (the second select is
         * issued immediately) or lazy (the second select is delayed until the data is needed).
         */
        SELECT,
        /**
         * Inherently an eager style of fetching.  The data to be fetched is obtained as part of an SQL join.
         */
        JOIN,
        /**
         * Initializes a number of indicated data items (entities or collections) in a series of grouped sql selects
         * using an in-style sql restriction to define the batch size.  Again, can be either eager or lazy.
         */
        BATCH,
        /**
         * Performs fetching of associated data (currently limited to only collections) based on the sql restriction
         * used to load the owner.  Again, can be either eager or lazy.
         */
        SUBSELECT
    

    到现在为止,我无法解决这个Javadoc的含义:

    <击>   

    基于用于加载所有者的sql限制   

    <强>更新 普莱斯基说:

      

    相反,它只会在最坏的情况下加载表格,即便如此,只有当您的初始查询没有where子句时才会加载。因此,我会说使用子选择查询可能会意外地加载整个表格,如果您限制结果并且您没有任何WHERE标准

    这是真的,这是我在新scenario 4.2

    中测试的非常重要的细节

    为获取员工而生成的查询是

    select employees0_.department_id as departme3_4_1_, employees0_.employee_id as employee1_5_1_, employees0_.employee_id as employee1_5_0_, employees0_.department_id as departme3_5_0_, employees0_.employee_name as employee2_5_0_ from subselect_employee employees0_ where employees0_.department_id in (select this_.department_id from subselect_department this_ where this_.department_name>=?)
    

    where子句中的子查询包含原始限制 this_.department_name&gt; =?,从而避免了所有Employees的加载。 这就是javadoc的含义

      

    基于用于加载所有者的SQL限制

    我所说的关于FetchMode.JOIN的所有内容以及与FetchMode.SUBSELECT的差异仍然存在(并且也适用于FetchMode.SELECT)。