【问题标题】:FetchMode Join vs SubSelectFetchMode Join vs SubSelect
【发布时间】:2016-01-04 05:54:43
【问题描述】:

我有两个表 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.JOIN 还是FetchMode.SUBSELECT?我们应该在哪种情况下选择哪一个?

【问题讨论】:

    标签: hibernate jpa join sql-subselect


    【解决方案1】:

    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子查询由外部查询检索到的 ID 过滤)。

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

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

    几篇文章展示了不同的获取策略和 SQL 日志(非常重要):

    总结:

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

    这些表是使用 ascii-tables 构建的。

    【讨论】:

    • 这是严重误导。子选择不会将您的整个数据库提取到内存中。链接的文章是关于子选择忽略来自父级的分页命令的怪癖,但它仍然是子选择。
    • 回想起来,我发现我提出的观点有些迂腐。子选择获取在使用 maxResults 时确实存在一个大问题,这使得两者基本上不兼容。并且发生这种情况的情况完全出乎意料,并且可能会在生产中被忽视。
    • 这个怎么样? “此外,FetchMode.JOIN 充当 FetchType.EAGER 策略。即使我们将关联标记为 FetchType.LAZY,FetchMode.JOIN 也会急切地加载关联。” docs.jboss.org/hibernate/stable/orm/userguide/html_single/…
    【解决方案2】:

    我会说这取决于...

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

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

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

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

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

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

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

    【讨论】:

      【解决方案3】:

      普朗基说

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

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

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

      这是真的,但只有当隐藏了多个部门实体(这意味着多个员工集合未初始化)时,我已经使用 3.6.10.Final 和 4.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_)
      

      然后在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 的员工)。

      如果会话中只有一个 Department 实体处于水合状态,且其员工集合未初始化,则查询就像 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>=?,避免了所有Employees的负载。 这就是 javadoc 的含义

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

      我所说的关于 FetchMode.JOIN 以及与 FetchMode.SUBSELECT 的区别仍然正确(也适用于 FetchMode.SELECT)。

      【讨论】:

      • 感谢您抽出宝贵时间回复。当我说它严重误导时,我想我夸大了。当我说“链接的文章是关于子选择忽略来自父级的分页命令的怪癖”时,我的意思是它描述了使用通常用于分页结果的 limit sql 构造时出现的问题。
      • 我的意思是,它不是那种会加载整个数据库的问题(通过加载所有关联——Hibernate 配置不当时可能会出现的问题)。相反,它只会在最坏的情况下加载表,即使那样,只有当您的初始查询没有 where 子句时。所以我想说,如果您限制结果并且您没有任何 WHERE 条件,则使用子选择查询可能会意外加载整个表。
      【解决方案4】:

      我的一位客户(金融服务)遇到了类似的问题,他想“在一次查询中获取数据”。好吧,我解释说最好有多个查询,原因如下:

      对于 FetchMode.JOIN,部门将从数据库转移到每个员工一次的应用程序,因为联接操作会导致每个员工的部门相乘。如果您有 10 个部门,每个部门有 100 名员工,那么这 10 个部门中的每一个部门都将在一个查询(简单的 SQL)中转移 100 次。因此,在这种情况下,每个部门的传输频率比必要的高 99 倍,从而导致该部门的数据传输开销。

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

      我更喜欢 FetchMode.SUBSELECT。

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

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

      【讨论】:

        猜你喜欢
        • 2012-06-22
        • 1970-01-01
        • 1970-01-01
        • 2019-06-06
        • 1970-01-01
        • 2023-04-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多