Các N + 1 chọn vấn đề thế nào trong ORM (Ánh xạ quan hệ đối tượng) là gì?


1597

"Vấn đề chọn N + 1" thường được nêu là một vấn đề trong các cuộc thảo luận về ánh xạ quan hệ đối tượng (ORM) và tôi hiểu rằng nó có liên quan đến việc phải thực hiện nhiều truy vấn cơ sở dữ liệu cho một thứ có vẻ đơn giản trong đối tượng thế giới.

Có ai có một lời giải thích chi tiết hơn về vấn đề?


2
Đây là một liên kết tuyệt vời với lời giải thích tốt đẹp về việc hiểu vấn đề n + 1 . Nó cũng bao gồm các giải pháp để khắc phục vấn đề này: architects.dzone.com/articles/how-identify-and-resilve-n1
aces.

Có một số bài viết hữu ích nói về vấn đề này và cách khắc phục có thể. Các sự cố ứng dụng phổ biến và cách khắc phục chúng: Vấn đề chọn N + 1 , Viên đạn (Bạc) cho vấn đề N + 1 , Tải
cateyes

Đối với tất cả mọi người đang tìm giải pháp cho vấn đề này, tôi tìm thấy một bài viết mô tả nó. stackoverflow.com/questions/32453989/iêu
damndemon

2
Xem xét các câu trả lời, không nên gọi đây là vấn đề 1 + N? Vì đây có vẻ là một thuật ngữ, đặc biệt, tôi không hỏi OP.
dùng1418717

Câu trả lời:


1017

Giả sử bạn có một bộ sưu tập các Carđối tượng (các hàng cơ sở dữ liệu) và mỗi đối tượng Carcó một bộ sưu tập các Wheelđối tượng (cũng là các hàng). Nói cách khác, CarWheellà mối quan hệ 1-nhiều.

Bây giờ, giả sử bạn cần lặp đi lặp lại qua tất cả các xe và cho mỗi chiếc, hãy in ra một danh sách các bánh xe. Việc triển khai O / R ngây thơ sẽ làm như sau:

SELECT * FROM Cars;

Và sau đó cho mỗi Car:

SELECT * FROM Wheel WHERE CarId = ?

Nói cách khác, bạn có một lựa chọn cho Ô tô và sau đó N chọn thêm, trong đó N là tổng số ô tô.

Ngoài ra, người ta có thể nhận được tất cả các bánh xe và thực hiện tra cứu trong bộ nhớ:

SELECT * FROM Wheel

Điều này giúp giảm số lượng các chuyến đi khứ hồi đến cơ sở dữ liệu từ N + 1 xuống 2. Hầu hết các công cụ ORM cung cấp cho bạn một số cách để ngăn chặn các lựa chọn N + 1.

Tham khảo: Sự bền bỉ của Java với Hibernate , chương 13.


140
Để làm rõ về "Điều này là xấu" - bạn có thể nhận được tất cả các bánh xe với 1 select ( SELECT * from Wheel;), thay vì N + 1. Với N lớn, hiệu suất đạt được có thể rất đáng kể.
tucuxi

212
@tucuxi Tôi ngạc nhiên khi bạn nhận được rất nhiều sự ủng hộ vì đã sai. Một cơ sở dữ liệu rất tốt về các chỉ mục, thực hiện truy vấn cho một CarID cụ thể sẽ trả về rất nhanh. Nhưng nếu bạn đã có tất cả các Bánh xe một lần, bạn sẽ phải tìm kiếm CarID trong ứng dụng của mình, không được lập chỉ mục, điều này chậm hơn. Trừ khi bạn gặp vấn đề về độ trễ lớn khi truy cập cơ sở dữ liệu của bạn, n + 1 thực sự nhanh hơn - và vâng, tôi đã đánh giá nó với rất nhiều mã thế giới thực.
Ariel

74
@ariel Cách 'chính xác' là lấy tất cả các bánh xe, được CarId đặt hàng (1 chọn) và nếu cần nhiều chi tiết hơn CarId, hãy tạo một truy vấn thứ hai cho tất cả các xe (tổng cộng 2 truy vấn). In mọi thứ bây giờ là tối ưu và không yêu cầu chỉ mục hoặc lưu trữ thứ cấp (bạn có thể lặp lại kết quả, không cần phải tải xuống tất cả). Bạn đã điểm chuẩn những điều sai. Nếu bạn vẫn tự tin về điểm chuẩn của mình, bạn có phiền khi đăng bình luận dài hơn (hoặc câu trả lời đầy đủ) giải thích về thí nghiệm và kết quả của bạn không?
tucuxi

92
"Hibernate (Tôi không quen thuộc với các khung ORM khác) cung cấp cho bạn một số cách để xử lý nó." và những cách này là?
Tima

58
@Ariel Hãy thử chạy điểm chuẩn của bạn với cơ sở dữ liệu và máy chủ ứng dụng trên các máy riêng biệt. Theo kinh nghiệm của tôi, các chuyến đi khứ hồi đến cơ sở dữ liệu có chi phí cao hơn so với chính truy vấn. Vì vậy, có, các truy vấn thực sự nhanh chóng, nhưng đó là những chuyến đi vòng quanh tàn phá. Tôi đã chuyển đổi "WHERE Id = const " thành "WHERE Id IN ( const , const , ...)" và nhận được các đơn đặt hàng cường độ tăng từ đó.
Hans

110
SELECT 
table1.*
, table2.*
INNER JOIN table2 ON table2.SomeFkId = table1.SomeId

Điều đó mang lại cho bạn một tập kết quả trong đó các hàng con trong bảng2 gây ra sự trùng lặp bằng cách trả về kết quả của bảng1 cho mỗi hàng con trong bảng2. Các trình ánh xạ O / R nên phân biệt các thể hiện của bảng1 dựa trên một trường khóa duy nhất, sau đó sử dụng tất cả các cột của bảng2 để điền vào các thể hiện con.

SELECT table1.*

SELECT table2.* WHERE SomeFkId = #

N + 1 là nơi truy vấn đầu tiên cư trú đối tượng chính và truy vấn thứ hai cư trú tất cả các đối tượng con cho mỗi đối tượng chính duy nhất được trả về.

Xem xét:

class House
{
    int Id { get; set; }
    string Address { get; set; }
    Person[] Inhabitants { get; set; }
}

class Person
{
    string Name { get; set; }
    int HouseId { get; set; }
}

và các bảng có cấu trúc tương tự. Một truy vấn duy nhất cho địa chỉ "22 Valley St" có thể trả về:

Id Address      Name HouseId
1  22 Valley St Dave 1
1  22 Valley St John 1
1  22 Valley St Mike 1

O / RM nên điền vào một thể hiện của Home với ID = 1, Địa chỉ = "22 Valley St" và sau đó điền vào mảng Người sống với các phiên bản People cho Dave, John và Mike chỉ bằng một truy vấn.

Một truy vấn N + 1 cho cùng một địa chỉ được sử dụng ở trên sẽ dẫn đến:

Id Address
1  22 Valley St

với một truy vấn riêng như

SELECT * FROM Person WHERE HouseId = 1

và dẫn đến một tập dữ liệu riêng biệt như

Name    HouseId
Dave    1
John    1
Mike    1

và kết quả cuối cùng giống như trên với truy vấn duy nhất.

Ưu điểm của một lựa chọn là bạn có được tất cả dữ liệu trước mắt, đây có thể là điều bạn mong muốn cuối cùng. Ưu điểm của N + 1 là độ phức tạp truy vấn giảm và bạn có thể sử dụng tải lười biếng trong đó các tập kết quả con chỉ được tải theo yêu cầu đầu tiên.


4
Ưu điểm khác của n + 1 là nhanh hơn vì cơ sở dữ liệu có thể trả về kết quả trực tiếp từ một chỉ mục. Thực hiện nối và sau đó sắp xếp yêu cầu một bảng tạm thời, chậm hơn. Lý do duy nhất để tránh n + 1 là nếu bạn có nhiều độ trễ nói chuyện với cơ sở dữ liệu của bạn.
Ariel

17
Tham gia và sắp xếp có thể khá nhanh (vì bạn sẽ tham gia vào các trường được lập chỉ mục và có thể được sắp xếp). Làm thế nào lớn là 'n + 1' của bạn? Bạn có nghiêm túc tin rằng vấn đề n + 1 chỉ áp dụng cho các kết nối cơ sở dữ liệu có độ trễ cao không?
tucuxi

9
@ariel - Lời khuyên của bạn rằng N + 1 là "nhanh nhất" là sai, mặc dù điểm chuẩn của bạn có thể đúng. Làm thế nào là có thể? Xem en.wikipedia.org/wiki/Anecdotal_evidence , và cả nhận xét của tôi trong câu trả lời khác cho câu hỏi này.
whitneyland

7
@Ariel - Tôi nghĩ tôi hiểu nó tốt :). Tôi chỉ đang cố gắng chỉ ra rằng kết quả của bạn chỉ áp dụng cho một tập hợp các điều kiện. Tôi có thể dễ dàng xây dựng một ví dụ phản ánh cho thấy điều ngược lại. Điều đó có ý nghĩa?
whitneyland

13
Để nhắc lại, vấn đề CHỌN N + 1 là, cốt lõi của nó: Tôi có 600 bản ghi để truy xuất. Là nhanh hơn để có được tất cả 600 trong số họ trong một truy vấn, hoặc 1 tại một thời điểm trong 600 truy vấn. Trừ khi bạn sử dụng MyISAM và / hoặc bạn có một lược đồ được lập chỉ mục kém / được lập chỉ mục kém (trong trường hợp ORM không phải là vấn đề), một db được điều chỉnh đúng sẽ trả về 600 hàng trong 2 ms, trong khi trả về các hàng riêng lẻ trong khoảng 1 ms mỗi cái. Vì vậy, chúng ta thường thấy N + 1 mất hàng trăm mili giây trong đó việc tham gia chỉ mất một cặp
Chó

64

Nhà cung cấp có mối quan hệ một-nhiều với Sản phẩm. Một Nhà cung cấp có (cung cấp) nhiều Sản phẩm.

***** Table: Supplier *****
+-----+-------------------+
| ID  |       NAME        |
+-----+-------------------+
|  1  |  Supplier Name 1  |
|  2  |  Supplier Name 2  |
|  3  |  Supplier Name 3  |
|  4  |  Supplier Name 4  |
+-----+-------------------+

***** Table: Product *****
+-----+-----------+--------------------+-------+------------+
| ID  |   NAME    |     DESCRIPTION    | PRICE | SUPPLIERID |
+-----+-----------+--------------------+-------+------------+
|1    | Product 1 | Name for Product 1 |  2.0  |     1      |
|2    | Product 2 | Name for Product 2 | 22.0  |     1      |
|3    | Product 3 | Name for Product 3 | 30.0  |     2      |
|4    | Product 4 | Name for Product 4 |  7.0  |     3      |
+-----+-----------+--------------------+-------+------------+

Các nhân tố:

  • Chế độ lười biếng dành cho Nhà cung cấp được đặt thành ăn mặc đúng (mặc định)

  • Chế độ tìm nạp được sử dụng để truy vấn trên Sản phẩm là Chọn

  • Chế độ tìm nạp (mặc định): Thông tin nhà cung cấp được truy cập

  • Bộ nhớ đệm không đóng vai trò lần đầu tiên

  • Nhà cung cấp được truy cập

Chế độ tìm nạp là Chọn Tìm nạp (mặc định)

// It takes Select fetch mode as a default
Query query = session.createQuery( "from Product p");
List list = query.list();
// Supplier is being accessed
displayProductsListWithSupplierName(results);

select ... various field names ... from PRODUCT
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?
select ... various field names ... from SUPPLIER where SUPPLIER.id=?

Kết quả:

  • 1 tuyên bố chọn cho Sản phẩm
  • N chọn câu lệnh cho Nhà cung cấp

Đây là vấn đề chọn N + 1!


3
Có phải là 1 lựa chọn cho Nhà cung cấp sau đó N chọn cho Sản phẩm không?
bencampbell_14

@bencampbell_ Vâng, ban đầu tôi cũng cảm thấy như vậy. Nhưng sau đó với ví dụ của mình, nó là một sản phẩm cho nhiều nhà cung cấp.
Mohd Faizan Khan

38

Tôi không thể nhận xét trực tiếp về các câu trả lời khác, vì tôi không có đủ danh tiếng. Nhưng điều đáng chú ý là vấn đề cơ bản chỉ phát sinh bởi vì, trong lịch sử, rất nhiều dbms đã khá kém khi xử lý các phép nối (MySQL là một ví dụ đặc biệt đáng chú ý). Vì vậy, n + 1 thường, nhanh hơn đáng kể so với tham gia. Và sau đó, có nhiều cách để cải thiện n + 1 nhưng vẫn không cần tham gia, đó là vấn đề ban đầu liên quan.

Tuy nhiên, MySQL bây giờ tốt hơn rất nhiều so với trước đây khi tham gia. Khi tôi mới học MySQL, tôi đã tham gia rất nhiều. Sau đó, tôi phát hiện ra chúng chậm như thế nào và thay vào đó là n + 1 trong mã. Nhưng, gần đây, tôi đã quay trở lại tham gia, bởi vì MySQL bây giờ là một công cụ xử lý chúng tốt hơn rất nhiều so với khi tôi mới bắt đầu sử dụng nó.

Ngày nay, việc tham gia đơn giản vào một tập hợp các bảng được lập chỉ mục chính xác hiếm khi là một vấn đề, về mặt hiệu suất. Và nếu nó mang lại hiệu quả, thì việc sử dụng gợi ý chỉ số thường giải quyết chúng.

Điều này được thảo luận ở đây bởi một trong nhóm phát triển MySQL:

http://jorgenloland.blogspot.co.uk/2013/02/dbt-3-q3-6-x-performance-in-mysql-5610.html

Vì vậy, tóm tắt là: Nếu trước đây bạn đã tránh tham gia vì hiệu suất khủng khiếp của MySQL với họ, thì hãy thử lại trên các phiên bản mới nhất. Có lẽ bạn sẽ ngạc nhiên.


7
Việc gọi các phiên bản đầu tiên của MySQL là DBMS quan hệ khá khó khăn ... Nếu mọi người gặp phải những vấn đề đó đã sử dụng cơ sở dữ liệu thực, họ sẽ không gặp phải những vấn đề như vậy. ;-)
Craig

2
Thật thú vị, nhiều loại vấn đề trong số này đã được giải quyết trong MySQL với việc giới thiệu và tối ưu hóa tiếp theo cho công cụ INNODB, nhưng bạn vẫn sẽ gặp phải những người đang cố gắng quảng bá MYISAM vì họ nghĩ rằng nó nhanh hơn.
Craig

5
FYI, một trong 3 JOINthuật toán phổ biến được sử dụng trong RDBMS 'được gọi là các vòng lặp lồng nhau. Về cơ bản, nó là một lựa chọn N + 1 dưới mui xe. Sự khác biệt duy nhất là DB đã đưa ra một lựa chọn thông minh để sử dụng nó dựa trên các số liệu thống kê và chỉ mục, thay vì mã máy khách buộc nó đi theo đường dẫn đó một cách phân loại.
Brandon

2
@Brandon Vâng! Giống như gợi ý THAM GIA và gợi ý INDEX, buộc một đường dẫn thực thi nhất định trong mọi trường hợp sẽ hiếm khi đánh bại cơ sở dữ liệu. Cơ sở dữ liệu hầu như luôn luôn rất, rất tốt trong việc lựa chọn phương pháp tối ưu để lấy dữ liệu. Có thể trong những ngày đầu tiên bạn cần 'cụm từ' câu hỏi của mình theo cách đặc biệt để dỗ db, nhưng sau nhiều thập kỷ kỹ thuật đẳng cấp thế giới, giờ đây bạn có thể có được hiệu suất tốt nhất bằng cách hỏi cơ sở dữ liệu của mình một câu hỏi quan hệ và để nó sắp xếp làm thế nào để tìm nạp và lắp ráp dữ liệu đó cho bạn.
Chó

3
Không chỉ là cơ sở dữ liệu sử dụng các chỉ mục và số liệu thống kê, tất cả các hoạt động cũng là I / O cục bộ, phần lớn thường hoạt động dựa trên bộ đệm hiệu quả cao thay vì đĩa. Các lập trình viên cơ sở dữ liệu dành rất nhiều sự chú ý để tối ưu hóa các loại điều này.
Craig

27

Chúng tôi đã chuyển khỏi ORM ở Django vì vấn đề này. Về cơ bản, nếu bạn cố gắng và làm

for p in person:
    print p.car.colour

ORM sẽ vui vẻ trả lại tất cả mọi người (thường là các thể hiện của một đối tượng Person), nhưng sau đó nó sẽ cần truy vấn bảng xe cho mỗi Người.

Một cách tiếp cận đơn giản và rất hiệu quả cho vấn đề này là cái mà tôi gọi là " fanfolding ", nhằm tránh ý tưởng vô nghĩa rằng kết quả truy vấn từ cơ sở dữ liệu quan hệ sẽ ánh xạ trở lại các bảng ban đầu mà từ đó truy vấn được tạo.

Bước 1: Chọn rộng

  select * from people_car_colour; # this is a view or sql function

Điều này sẽ trả lại một cái gì đó như

  p.id | p.name | p.telno | car.id | car.type | car.colour
  -----+--------+---------+--------+----------+-----------
  2    | jones  | 2145    | 77     | ford     | red
  2    | jones  | 2145    | 1012   | toyota   | blue
  16   | ashby  | 124     | 99     | bmw      | yellow

Bước 2: Xác định

Hút kết quả vào một trình tạo đối tượng chung với một đối số để phân chia sau mục thứ ba. Điều này có nghĩa là đối tượng "jones" sẽ không được thực hiện nhiều lần.

Bước 3: Kết xuất

for p in people:
    print p.car.colour # no more car queries

Xem trang web này để thực hiện fanfolding cho python.


10
Tôi rất vui vì tôi đã vấp vào bài viết của bạn, bởi vì tôi nghĩ rằng tôi đang phát điên. Khi tôi phát hiện ra vấn đề N + 1, suy nghĩ ngay lập tức của tôi là - tại sao bạn không tạo một chế độ xem chứa tất cả thông tin bạn cần và lấy từ chế độ xem đó? bạn đã xác nhận vị trí của tôi. cảm ơn ngài.
một nhà phát triển

14
Chúng tôi đã chuyển khỏi ORM ở Django vì vấn đề này. Huh? Django select_related, có nghĩa là để giải quyết điều này - trên thực tế, các tài liệu của nó bắt đầu bằng một ví dụ tương tự như p.car.colourví dụ của bạn .
Adrian17

8
Đây là một anwswer cũ, chúng tôi có select_related()prefetch_related()ở Django bây giờ.
Mariusz Jamro

1
Mát mẻ. Nhưng select_related()và bạn bè dường như không thực hiện bất kỳ phép ngoại suy rõ ràng hữu ích nào của việc tham gia như LEFT OUTER JOIN. Vấn đề không phải là vấn đề giao diện, mà là một vấn đề liên quan đến ý tưởng lạ rằng các đối tượng và dữ liệu quan hệ có thể ánh xạ được .... theo quan điểm của tôi.
rorycl

26

Vì đây là một câu hỏi rất phổ biến, tôi đã viết bài viết này , trên đó câu trả lời này dựa trên.

Vấn đề truy vấn N + 1 là gì

Sự cố truy vấn N + 1 xảy ra khi khung truy cập dữ liệu thực thi N câu lệnh SQL bổ sung để tìm nạp cùng một dữ liệu có thể được truy xuất khi thực hiện truy vấn SQL chính.

Giá trị của N càng lớn, càng nhiều truy vấn sẽ được thực hiện, tác động hiệu suất càng lớn. Và, không giống như nhật ký truy vấn chậm có thể giúp bạn tìm các truy vấn chạy chậm, vấn đề N + 1 sẽ không được phát hiện vì mỗi truy vấn bổ sung riêng lẻ chạy đủ nhanh để không kích hoạt nhật ký truy vấn chậm.

Vấn đề là thực thi một số lượng lớn các truy vấn bổ sung, nói chung, mất đủ thời gian để làm chậm thời gian phản hồi.

Hãy xem xét chúng tôi có các bảng cơ sở dữ liệu post và post_comments sau đây tạo thành mối quan hệ một-nhiều bảng :

Các bảng <code> post </ code> và <code> post_comments </ code>

Chúng tôi sẽ tạo ra 4 posthàng sau:

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 1', 1)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 2', 2)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 3', 3)

INSERT INTO post (title, id)
VALUES ('High-Performance Java Persistence - Part 4', 4)

Và, chúng tôi cũng sẽ tạo 4 post_commenthồ sơ con:

INSERT INTO post_comment (post_id, review, id)
VALUES (1, 'Excellent book to understand Java Persistence', 1)

INSERT INTO post_comment (post_id, review, id)
VALUES (2, 'Must-read for Java developers', 2)

INSERT INTO post_comment (post_id, review, id)
VALUES (3, 'Five Stars', 3)

INSERT INTO post_comment (post_id, review, id)
VALUES (4, 'A great reference book', 4)

Vấn đề truy vấn N + 1 với SQL đơn giản

Nếu bạn chọn post_commentssử dụng truy vấn SQL này:

List<Tuple> comments = entityManager.createNativeQuery("""
    SELECT
        pc.id AS id,
        pc.review AS review,
        pc.post_id AS postId
    FROM post_comment pc
    """, Tuple.class)
.getResultList();

Và, sau đó, bạn quyết định tìm nạp liên kết post titlecho mỗi post_comment:

for (Tuple comment : comments) {
    String review = (String) comment.get("review");
    Long postId = ((Number) comment.get("postId")).longValue();

    String postTitle = (String) entityManager.createNativeQuery("""
        SELECT
            p.title
        FROM post p
        WHERE p.id = :postId
        """)
    .setParameter("postId", postId)
    .getSingleResult();

    LOGGER.info(
        "The Post '{}' got this review '{}'",
        postTitle,
        review
    );
}

Bạn sẽ kích hoạt vấn đề truy vấn N + 1 bởi vì, thay vì một truy vấn SQL, bạn đã thực hiện 5 (1 + 4):

SELECT
    pc.id AS id,
    pc.review AS review,
    pc.post_id AS postId
FROM post_comment pc

SELECT p.title FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review
-- 'Excellent book to understand Java Persistence'

SELECT p.title FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review
-- 'Must-read for Java developers'

SELECT p.title FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review
-- 'Five Stars'

SELECT p.title FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review
-- 'A great reference book'

Khắc phục sự cố truy vấn N + 1 rất dễ dàng. Tất cả những gì bạn cần làm là trích xuất tất cả dữ liệu bạn cần trong truy vấn SQL gốc, như sau:

List<Tuple> comments = entityManager.createNativeQuery("""
    SELECT
        pc.id AS id,
        pc.review AS review,
        p.title AS postTitle
    FROM post_comment pc
    JOIN post p ON pc.post_id = p.id
    """, Tuple.class)
.getResultList();

for (Tuple comment : comments) {
    String review = (String) comment.get("review");
    String postTitle = (String) comment.get("postTitle");

    LOGGER.info(
        "The Post '{}' got this review '{}'",
        postTitle,
        review
    );
}

Lần này, chỉ có một truy vấn SQL được thực thi để tìm nạp tất cả dữ liệu mà chúng tôi quan tâm hơn nữa khi sử dụng.

Vấn đề truy vấn N + 1 với JPA và Hibernate

Khi sử dụng JPA và Hibernate, có một số cách bạn có thể kích hoạt sự cố truy vấn N + 1, vì vậy điều rất quan trọng là phải biết cách bạn có thể tránh những tình huống này.

Đối với các ví dụ tiếp theo, hãy xem xét chúng tôi đang ánh xạ postpost_commentscác bảng tới các thực thể sau:

Các thực thể <code> Đăng </ code> và <code> PostVer </ code>

Các ánh xạ JPA trông như thế này:

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    private Long id;

    private String title;

    //Getters and setters omitted for brevity
}

@Entity(name = "PostComment")
@Table(name = "post_comment")
public class PostComment {

    @Id
    private Long id;

    @ManyToOne
    private Post post;

    private String review;

    //Getters and setters omitted for brevity
}

FetchType.EAGER

Sử dụng FetchType.EAGERmột cách ngầm định hoặc rõ ràng cho các hiệp hội JPA của bạn là một ý tưởng tồi bởi vì bạn sẽ tìm nạp thêm dữ liệu mà bạn cần. Hơn nữa, FetchType.EAGERchiến lược cũng dễ xảy ra sự cố truy vấn N + 1.

Thật không may, các hiệp hội @ManyToOne@OneToOnesử dụng FetchType.EAGERtheo mặc định, vì vậy nếu ánh xạ của bạn trông như thế này:

@ManyToOne
private Post post;

Bạn đang sử dụng FetchType.EAGERchiến lược và mỗi khi bạn quên sử dụng JOIN FETCHkhi tải một số PostCommentthực thể bằng truy vấn JPQL hoặc Tiêu chí API:

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

Bạn sẽ kích hoạt vấn đề truy vấn N + 1:

SELECT 
    pc.id AS id1_1_, 
    pc.post_id AS post_id3_1_, 
    pc.review AS review2_1_ 
FROM 
    post_comment pc

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4

Hãy chú ý câu SELECT bổ sung được thực hiện bởi vì các posthiệp hội phải được lấy trước khi trả lại Listcủa PostCommentcác thực thể.

Không giống như gói tìm nạp mặc định mà bạn đang sử dụng khi gọi findphương thức của EnrityManagertruy vấn, JPQL hoặc Tiêu chí API xác định một kế hoạch rõ ràng mà Hibernate không thể thay đổi bằng cách tự động tiêm THAM GIA FETCH. Vì vậy, bạn cần phải làm bằng tay.

Nếu bạn hoàn toàn không cần sự postliên kết, bạn sẽ không gặp may khi sử dụng FetchType.EAGERvì không có cách nào để tránh lấy nó. Đó là lý do tại sao nó tốt hơn để sử dụng FetchType.LAZYtheo mặc định.

Nhưng, nếu bạn muốn sử dụng postliên kết, thì bạn có thể sử dụng JOIN FETCHđể tránh vấn đề truy vấn N + 1:

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    join fetch pc.post p
    """, PostComment.class)
.getResultList();

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

Lần này, Hibernate sẽ thực thi một câu lệnh SQL duy nhất:

SELECT 
    pc.id as id1_1_0_, 
    pc.post_id as post_id3_1_0_, 
    pc.review as review2_1_0_, 
    p.id as id1_0_1_, 
    p.title as title2_0_1_ 
FROM 
    post_comment pc 
INNER JOIN 
    post p ON pc.post_id = p.id

-- The Post 'High-Performance Java Persistence - Part 1' got this review 
-- 'Excellent book to understand Java Persistence'

-- The Post 'High-Performance Java Persistence - Part 2' got this review 
-- 'Must-read for Java developers'

-- The Post 'High-Performance Java Persistence - Part 3' got this review 
-- 'Five Stars'

-- The Post 'High-Performance Java Persistence - Part 4' got this review 
-- 'A great reference book'

Để biết thêm chi tiết về lý do tại sao bạn nên tránh FetchType.EAGERchiến lược tìm nạp, hãy xem bài viết này .

FetchType.LAZY

Ngay cả khi bạn chuyển sang sử dụng FetchType.LAZYrõ ràng cho tất cả các hiệp hội, bạn vẫn có thể gặp phải vấn đề N + 1.

Lần này, posthiệp hội được ánh xạ như thế này:

@ManyToOne(fetch = FetchType.LAZY)
private Post post;

Bây giờ, khi bạn tìm nạp các PostCommentthực thể:

List<PostComment> comments = entityManager
.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

Hibernate sẽ thực thi một câu lệnh SQL duy nhất:

SELECT 
    pc.id AS id1_1_, 
    pc.post_id AS post_id3_1_, 
    pc.review AS review2_1_ 
FROM 
    post_comment pc

Nhưng, nếu sau đó, bạn sẽ tham khảo posthiệp hội tải lười biếng :

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

Bạn sẽ gặp vấn đề truy vấn N + 1:

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 1
-- The Post 'High-Performance Java Persistence - Part 1' got this review 
-- 'Excellent book to understand Java Persistence'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 2
-- The Post 'High-Performance Java Persistence - Part 2' got this review 
-- 'Must-read for Java developers'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 3
-- The Post 'High-Performance Java Persistence - Part 3' got this review 
-- 'Five Stars'

SELECT p.id AS id1_0_0_, p.title AS title2_0_0_ FROM post p WHERE p.id = 4
-- The Post 'High-Performance Java Persistence - Part 4' got this review 
-- 'A great reference book'

postliên kết được tìm nạp một cách lười biếng, nên một câu lệnh SQL thứ cấp sẽ được thực thi khi truy cập vào liên kết lười biếng để xây dựng thông điệp tường trình.

Một lần nữa, sửa chữa bao gồm thêm một JOIN FETCHmệnh đề vào truy vấn JPQL:

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    join fetch pc.post p
    """, PostComment.class)
.getResultList();

for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'", 
        comment.getPost().getTitle(), 
        comment.getReview()
    );
}

Và, giống như trong FetchType.EAGERví dụ, truy vấn JPQL này sẽ tạo ra một câu lệnh SQL.

Ngay cả khi bạn đang sử dụng FetchType.LAZYvà không tham chiếu liên kết con của @OneToOnemối quan hệ JPA hai chiều , bạn vẫn có thể kích hoạt vấn đề truy vấn N + 1.

Để biết thêm chi tiết về cách bạn có thể khắc phục sự cố truy vấn N + 1 do các @OneToOnehiệp hội tạo ra , hãy xem bài viết này .

Cách tự động phát hiện sự cố truy vấn N + 1

Nếu bạn muốn tự động phát hiện vấn đề truy vấn N + 1 trong lớp truy cập dữ liệu của mình, bài viết này giải thích cách bạn có thể làm điều đó bằng cách sử dụng db-utildự án nguồn mở.

Trước tiên, bạn cần thêm phụ thuộc Maven sau:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>db-util</artifactId>
    <version>${db-util.version}</version>
</dependency>

Sau đó, bạn chỉ cần sử dụng SQLStatementCountValidatortiện ích để xác nhận các câu lệnh SQL cơ bản được tạo:

SQLStatementCountValidator.reset();

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    """, PostComment.class)
.getResultList();

SQLStatementCountValidator.assertSelectCount(1);

Trong trường hợp bạn đang sử dụng FetchType.EAGERvà chạy trường hợp thử nghiệm ở trên, bạn sẽ gặp phải trường hợp thử nghiệm sau:

SELECT 
    pc.id as id1_1_, 
    pc.post_id as post_id3_1_, 
    pc.review as review2_1_ 
FROM 
    post_comment pc

SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 1

SELECT p.id as id1_0_0_, p.title as title2_0_0_ FROM post p WHERE p.id = 2


-- SQLStatementCountMismatchException: Expected 1 statement(s) but recorded 3 instead!

Để biết thêm chi tiết về db-utildự án nguồn mở, hãy xem bài viết này .


Nhưng bây giờ bạn có một vấn đề với phân trang. Nếu bạn có 10 chiếc xe, mỗi chiếc xe có 4 bánh và bạn muốn phân trang xe với 5 chiếc mỗi trang. Vì vậy, về cơ bản bạn có SELECT cars, wheels FROM cars JOIN wheels LIMIT 0, 5. Nhưng những gì bạn nhận được là 2 chiếc xe có 5 bánh (xe thứ nhất có tất cả 4 bánh và xe thứ hai chỉ có 1 bánh), vì LIMIT sẽ giới hạn toàn bộ tập kết quả, không chỉ mệnh đề gốc.
CappY

2
Tôi có một bài viết cho điều đó quá.
Vlad Mihalcea

Cảm ơn bạn cho bài viết. Tôi sẽ đọc nó. Bằng cách cuộn nhanh - tôi thấy giải pháp đó là Window Function, nhưng chúng khá mới trong MariaDB - vì vậy vấn đề vẫn tồn tại ở các phiên bản cũ hơn. :)
CappY

@VladMihalcea, tôi đã chỉ ra từ bài viết của bạn hoặc từ bài đăng mỗi khi bạn đề cập đến trường hợp ManyToOne trong khi giải thích vấn đề N + 1. Nhưng thực tế mọi người chủ yếu quan tâm đến trường hợp OneToMany liên quan đến vấn đề N + 1. Bạn có thể vui lòng tham khảo và giải thích trường hợp OneToMany?
JJ Beam

18

Giả sử bạn có CÔNG TY và NHÂN VIÊN. CÔNG TY có nhiều NHÂN VIÊN (tức là NHÂN VIÊN có một trường CÔNG TY_ID).

Trong một số cấu hình O / R, khi bạn có một đối tượng Công ty được ánh xạ và truy cập vào các đối tượng Nhân viên của nó, công cụ O / R sẽ thực hiện một lựa chọn cho mỗi nhân viên, bạn có thể làm mọi thứ trong SQL thẳng, bạn có thể select * from employees where company_id = XX. Do đó N (# nhân viên) cộng 1 (công ty)

Đây là cách các phiên bản ban đầu của EJB Entity Beans hoạt động. Tôi tin rằng những thứ như Hibernate đã loại bỏ điều này, nhưng tôi không chắc lắm. Hầu hết các công cụ thường bao gồm thông tin về chiến lược của họ để lập bản đồ.


18

Đây là một mô tả tốt về vấn đề

Bây giờ bạn đã hiểu vấn đề thường có thể tránh được bằng cách thực hiện tìm nạp tham gia trong truy vấn của bạn. Điều này về cơ bản buộc phải tìm nạp đối tượng được tải lười biếng để dữ liệu được truy xuất trong một truy vấn thay vì truy vấn n + 1. Hi vọng điêu nay co ich.


17

Kiểm tra bài Ayende về chủ đề: Kết hợp vấn đề Chọn N + 1 trong NHibernate .

Về cơ bản, khi sử dụng ORM như NHibernate hoặc EntityFramework, nếu bạn có mối quan hệ một-nhiều (chi tiết chính) và muốn liệt kê tất cả các chi tiết trên mỗi bản ghi chính, bạn phải thực hiện các cuộc gọi truy vấn N + 1 đến cơ sở dữ liệu, "N" là số lượng bản ghi chính: 1 truy vấn để nhận tất cả các bản ghi chính và N truy vấn, một truy vấn trên một bản ghi chính, để có được tất cả các chi tiết cho mỗi bản ghi chính.

Nhiều cuộc gọi truy vấn cơ sở dữ liệu hơn → thời gian trễ hơn → hiệu suất ứng dụng / cơ sở dữ liệu giảm.

Tuy nhiên, ORM có các tùy chọn để tránh vấn đề này, chủ yếu là sử dụng THAM GIA.


3
tham gia không phải là một giải pháp tốt (thường), bởi vì chúng có thể dẫn đến một sản phẩm cartesian, có nghĩa là số hàng kết quả là số kết quả bảng gốc nhân với số lượng kết quả trong mỗi bảng con. đặc biệt xấu trên nhiều cấp độ quân chủ. Chọn 20 "blog" với 100 "bài đăng" trên mỗi và 10 "bình luận" trên mỗi bài đăng sẽ dẫn đến 20000 hàng kết quả. NHibernate có cách giải quyết, như "cỡ lô" (chọn con có mệnh đề trên id gốc) hoặc "subselect".
Erik Hart

14

Phát hành 1 truy vấn sẽ trả về 100 kết quả nhanh hơn nhiều so với phát hành 100 truy vấn, mỗi truy vấn trả về 1 kết quả.


13

Theo tôi, bài báo viết trong Hibernate Cạm bẫy: Tại sao các mối quan hệ nên lười biếng lại hoàn toàn trái ngược với vấn đề N + 1 thực sự.

Nếu bạn cần giải thích chính xác, vui lòng tham khảo Hibernate - Chương 19: Cải thiện hiệu suất - Chiến lược tìm nạp

Chọn tìm nạp (mặc định) cực kỳ dễ bị tổn thương đối với các vấn đề chọn N + 1, vì vậy chúng tôi có thể muốn bật tính năng tìm nạp tham gia


2
tôi đọc trang ngủ đông. Nó không nói vấn đề thực sự chọn N + 1 là gì . Nhưng nó nói bạn có thể sử dụng các phép nối để sửa nó.
Ian Boyd

3
kích thước lô được yêu cầu để tìm nạp, để chọn các đối tượng con cho nhiều cha mẹ trong một câu lệnh chọn. Subselect có thể là một lựa chọn khác. Tham gia có thể trở nên thực sự tồi tệ nếu bạn có nhiều cấp độ phân cấp và một sản phẩm cartesian được tạo ra.
Erik Hart

10

Liên kết được cung cấp có một ví dụ rất đơn giản về vấn đề n + 1. Nếu bạn áp dụng nó cho Hibernate thì về cơ bản nó sẽ nói về điều tương tự. Khi bạn truy vấn một đối tượng, thực thể được tải nhưng mọi liên kết (trừ khi được cấu hình khác) sẽ được tải lười biếng. Do đó, một truy vấn cho các đối tượng gốc và một truy vấn khác để tải các liên kết cho mỗi đối tượng này. 100 đối tượng được trả về có nghĩa là một truy vấn ban đầu và sau đó 100 truy vấn bổ sung để có được liên kết cho mỗi, n + 1.

http://pramatr.com/2009/02/05/sql-n-1-selects-explained/


9

Một triệu phú có N xe. Bạn muốn có được tất cả (4) bánh xe.

Một (1) truy vấn tải tất cả các xe, nhưng đối với mỗi (N) xe, một truy vấn riêng được gửi cho các bánh xe tải.

Chi phí:

Giả sử chỉ số phù hợp với ram.

1 + N truy vấn phân tích cú pháp và lập kế hoạch + tìm kiếm chỉ mục VÀ truy cập tấm 1 + N + (N * 4) để tải trọng tải.

Giả sử các chỉ số không phù hợp với ram.

Chi phí bổ sung trong trường hợp xấu nhất truy cập tấm 1 + N cho chỉ số tải.

Tóm lược

Cổ chai là truy cập tấm (khoảng 70 lần mỗi giây truy cập ngẫu nhiên trên hdd) Một lựa chọn tham gia háo hức cũng sẽ truy cập vào tấm 1 + N + (N * 4) lần cho tải trọng. Vì vậy, nếu các chỉ mục phù hợp với ram - không có vấn đề gì, nó đủ nhanh vì chỉ có các hoạt động ram liên quan.


9

Vấn đề chọn N + 1 là một nỗi đau, và thật hợp lý khi phát hiện những trường hợp như vậy trong các bài kiểm tra đơn vị. Tôi đã phát triển một thư viện nhỏ để xác minh số lượng truy vấn được thực hiện bởi một phương thức thử nghiệm nhất định hoặc chỉ là một khối mã tùy ý - JDBC Sniffer

Chỉ cần thêm quy tắc JUnit đặc biệt vào lớp kiểm tra của bạn và đặt chú thích với số lượng truy vấn dự kiến ​​trên các phương thức kiểm tra của bạn:

@Rule
public final QueryCounter queryCounter = new QueryCounter();

@Expectation(atMost = 3)
@Test
public void testInvokingDatabase() {
    // your JDBC or JPA code
}

5

Vấn đề như những người khác đã tuyên bố một cách thanh lịch hơn là bạn có sản phẩm Cartesian của các cột OneToMany hoặc bạn đang thực hiện N + 1 Chọn. Tương ứng có thể là kết quả khổng lồ hoặc trò chuyện với cơ sở dữ liệu.

Tôi ngạc nhiên khi điều này không được đề cập nhưng đây là cách tôi đã giải quyết vấn đề này ... Tôi tạo một bảng id bán tạm thời . Tôi cũng làm điều này khi bạn có IN ()giới hạn mệnh đề .

Điều này không hoạt động đối với tất cả các trường hợp (có thể không phải là đa số) nhưng nó hoạt động đặc biệt tốt nếu bạn có nhiều đối tượng con sao cho sản phẩm của Cartesian sẽ vượt khỏi tầm kiểm soát (tức là rất nhiều OneToManycột, số lượng kết quả sẽ là một phép nhân của các cột) và nhiều hơn một đợt như công việc.

Trước tiên, bạn chèn id đối tượng cha của bạn dưới dạng bó vào bảng ids. Lô_id này là thứ chúng tôi tạo trong ứng dụng của mình và giữ lại.

INSERT INTO temp_ids 
    (product_id, batch_id)
    (SELECT p.product_id, ? 
    FROM product p ORDER BY p.product_id
    LIMIT ? OFFSET ?);

Bây giờ với mỗi OneToManycột, bạn chỉ cần thực hiện một SELECTbảng trên bảng id trong bảng INNER JOINcon với một WHERE batch_id=(hoặc ngược lại). Bạn chỉ muốn đảm bảo rằng bạn đặt hàng theo cột id vì nó sẽ giúp việc kết hợp các cột kết quả dễ dàng hơn (nếu không bạn sẽ cần HashMap / Bảng cho toàn bộ tập kết quả có thể không tệ).

Sau đó, bạn chỉ cần định kỳ làm sạch bảng ids.

Điều này cũng hoạt động đặc biệt tốt nếu người dùng chọn nói 100 hoặc hơn các mục riêng biệt cho một số loại xử lý hàng loạt. Đặt 100 id khác nhau trong bảng tạm thời.

Bây giờ số lượng truy vấn bạn đang thực hiện bằng số lượng cột OneToMany.


1

Lấy ví dụ về Matt Solnit, hãy tưởng tượng rằng bạn xác định mối liên hệ giữa Xe và Bánh xe là LAZY và bạn cần một số trường Bánh xe. Điều này có nghĩa là sau lần chọn đầu tiên, ngủ đông sẽ thực hiện "Chọn * từ Bánh xe trong đó car_id =: id" CHO MACHI Xe.

Điều này làm cho lựa chọn đầu tiên và nhiều hơn 1 lựa chọn cho mỗi chiếc xe N, đó là lý do tại sao nó được gọi là vấn đề n + 1.

Để tránh điều này, hãy làm cho hiệp hội tìm nạp một cách háo hức, để ngủ đông tải dữ liệu với một liên kết.

Nhưng chú ý, nếu nhiều lần bạn không truy cập vào Bánh xe được liên kết, tốt hơn là giữ LAZY hoặc thay đổi loại tìm nạp với Tiêu chí.


1
Một lần nữa, tham gia không phải là một giải pháp tốt, đặc biệt là khi có thể tải hơn 2 cấp bậc. Thay vào đó, hãy chọn "subselect" hoặc "batch-size"; cái cuối cùng sẽ tải con theo ID cha trong mệnh đề "in", chẳng hạn như "select ... từ các bánh xe có car_id trong (1,3,4,6,7,8,11,13)".
Erik Hart
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.