Phân trang MySQL mà không cần truy vấn kép?


115

Tôi đã tự hỏi liệu có cách nào để nhận được số lượng kết quả từ truy vấn MySQL và đồng thời giới hạn kết quả không.

Cách hoạt động của phân trang (theo tôi hiểu), trước tiên tôi làm điều gì đó như

query = SELECT COUNT(*) FROM `table` WHERE `some_condition`

Sau khi tôi nhận được num_rows (truy vấn), tôi có số lượng kết quả. Nhưng sau đó để thực sự giới hạn kết quả của tôi, tôi phải thực hiện một truy vấn thứ hai như:

query2 = SELECT COUNT(*) FROM `table` WHERE `some_condition` LIMIT 0, 10

Câu hỏi của tôi: Có cách nào để cả hai truy xuất tổng số kết quả sẽ được đưa ra, VÀ giới hạn kết quả trả về trong một truy vấn không? Hoặc bất kỳ cách nào hiệu quả hơn để làm điều này. Cảm ơn!


7
Mặc dù bạn sẽ không có COUNT (*) trong query2
dlofrodloh

Câu trả lời:


66

Không, đó là cách mà nhiều ứng dụng muốn phân trang phải thực hiện. Nó đáng tin cậy và chống đạn, mặc dù nó thực hiện truy vấn hai lần. Nhưng bạn có thể lưu số đếm trong bộ nhớ cache trong vài giây và điều đó sẽ giúp ích rất nhiều.

Cách khác là sử dụng SQL_CALC_FOUND_ROWSmệnh đề và sau đó gọi SELECT FOUND_ROWS(). Ngoài việc bạn phải thực hiện FOUND_ROWS()cuộc gọi sau đó, có một vấn đề với điều này: Có một lỗi trong MySQL mà điều này gây ảnh hưởng đến ORDER BYcác truy vấn khiến nó chậm hơn nhiều trên các bảng lớn so với cách tiếp cận ngây thơ của hai truy vấn.


2
Tuy nhiên, nó không hoàn toàn bằng chứng về điều kiện chủng tộc, trừ khi bạn thực hiện hai truy vấn trong một giao dịch. Tuy nhiên, điều này thường không phải là một vấn đề.
NickZoic

Bằng cách "đáng tin cậy", ý tôi là bản thân SQL sẽ luôn trả về kết quả bạn muốn và bằng cách "chống đạn", tôi muốn nói rằng không có lỗi MySQL nào cản trở những gì bạn có thể sử dụng SQL. Không giống như sử dụng SQL_CALC_FOUND_ROWS với ORDER BY và LIMIT, theo lỗi tôi đã đề cập.
staticsan

5
Đối với các truy vấn phức tạp, việc sử dụng SQL_CALC_FOUND_ROWS để tìm nạp số lượng trong cùng một truy vấn hầu như sẽ luôn chậm hơn so với thực hiện hai truy vấn riêng biệt. Điều này là do nó có nghĩa là tất cả các hàng sẽ cần được truy xuất đầy đủ, bất kể giới hạn là bao nhiêu, sau đó chỉ những hàng được chỉ định trong mệnh đề LIMIT được trả về. Xem thêm phản hồi của tôi có liên kết.
thomasrutter

Tùy thuộc vào lý do bạn cần điều này, bạn cũng có thể muốn nghĩ đến việc không truy xuất tổng kết quả. Việc triển khai các phương pháp phân trang tự động đang trở thành một thực tiễn phổ biến hơn. Các trang web như Facebook, Twitter, Bing và Google đã sử dụng phương pháp này từ lâu.
Thomas B,

68

Tôi hầu như không bao giờ thực hiện hai truy vấn.

Chỉ cần trả lại một hàng nhiều hơn mức cần thiết, chỉ hiển thị 10 hàng trên trang và nếu có nhiều hàng hơn được hiển thị, hãy hiển thị nút "Tiếp theo".

SELECT x, y, z FROM `table` WHERE `some_condition` LIMIT 0, 11
// iterate through and display 10 rows.

// if there were 11 rows, display a "Next" button.

Truy vấn của bạn sẽ trả về theo thứ tự phù hợp nhất trước. Rất có thể, hầu hết mọi người sẽ không quan tâm đến việc truy cập trang 236 trên 412.

Khi bạn thực hiện tìm kiếm trên google và kết quả của bạn không ở trang đầu tiên, bạn có thể chuyển đến trang hai chứ không phải trang chín.


42
Trên thực tế, nếu tôi không tìm thấy nó trên trang đầu tiên của truy vấn Google, tôi thường bỏ qua trang chín.
Phil

3
@Phil Tôi đã nghe điều này trước đây nhưng tại sao lại làm như vậy?
TK123

5
Hơi muộn, nhưng đây là lý do của tôi. Một số tìm kiếm bị chi phối bởi các trang web liên kết được tối ưu hóa cho công cụ tìm kiếm. Vì vậy, một số trang đầu tiên là các trang trại khác nhau tranh giành nó cho vị trí số 1, kết quả hữu ích có thể vẫn được kết hợp với truy vấn, chỉ là không ở trên cùng.
Phil

4
COUNTlà một hàm tổng hợp. Làm cách nào để bạn trả về tổng số tất cả kết quả trong một truy vấn? Truy vấn trên sẽ chỉ trả về 1 hàng, bất kể hàng LIMITđược đặt ở vị trí nào. Nếu bạn thêm GROUP BY, nó sẽ trả lại toàn bộ kết quả nhưng COUNTsẽ là không chính xác
pixelfreak

2
Đây là một trong những cách tiếp cận được Percona đề xuất: percona.com/blog/2008/09/24/…
techdude

26

Một cách tiếp cận khác để tránh truy vấn kép là tìm nạp tất cả các hàng cho trang hiện tại bằng cách sử dụng mệnh đề LIMIT trước, sau đó chỉ thực hiện truy vấn COUNT (*) thứ hai nếu số lượng hàng tối đa được truy xuất.

Trong nhiều ứng dụng, kết quả có thể xảy ra nhất là tất cả các kết quả đều nằm trên một trang và phải thực hiện phân trang là ngoại lệ chứ không phải là tiêu chuẩn. Trong những trường hợp này, truy vấn đầu tiên sẽ không lấy số lượng kết quả tối đa.

Ví dụ: các câu trả lời cho một câu hỏi stackoverflow hiếm khi tràn sang trang thứ hai. Nhận xét về một câu trả lời hiếm khi vượt quá giới hạn 5 hoặc lâu hơn cần thiết để hiển thị tất cả.

Vì vậy, trong các ứng dụng này, bạn có thể chỉ cần thực hiện truy vấn với LIMIT trước và sau đó, miễn là chưa đạt đến giới hạn đó, bạn biết chính xác có bao nhiêu hàng mà không cần thực hiện truy vấn COUNT (*) thứ hai - điều này sẽ bao gồm phần lớn các tình huống.


1
@thomasrutter Tôi đã có cách tiếp cận tương tự, tuy nhiên đã phát hiện ra một lỗ hổng với nó hôm nay. Khi đó, trang kết quả cuối cùng sẽ không có dữ liệu phân trang. tức là, giả sử mỗi trang phải có 25 kết quả, trang cuối cùng có thể sẽ không có nhiều kết quả đó, giả sử nó có 7 ... điều đó có nghĩa là số lượng (*) sẽ không bao giờ được chạy và do đó, không có phân trang nào được hiển thị cho người sử dụng.
duellsy

2
Không - nếu bạn nói rằng có 200 kết quả, bạn truy vấn 25 kết quả tiếp theo và bạn chỉ nhận lại được 7, điều đó cho bạn biết rằng tổng số kết quả là 207 và do đó bạn không cần thực hiện một truy vấn khác với COUNT (*) bởi vì bạn đã biết nó sẽ nói gì. Bạn có tất cả thông tin bạn cần để hiển thị phân trang. Nếu bạn đang gặp vấn đề với việc phân trang không hiển thị cho người dùng thì bạn có lỗi ở đâu đó.
thomasrutter

15

Trong hầu hết các tình huống, việc thực hiện nó trong hai truy vấn riêng biệt sẽ nhanh hơn và tốn ít tài nguyên hơn nhiều so với thực hiện trong một truy vấn, mặc dù điều đó có vẻ phản trực quan.

Nếu bạn sử dụng SQL_CALC_FOUND_ROWS, thì đối với các bảng lớn, nó làm cho truy vấn của bạn chậm hơn nhiều, thậm chí chậm hơn đáng kể so với việc thực thi hai truy vấn, truy vấn đầu tiên có COUNT (*) và truy vấn thứ hai với LIMIT. Lý do cho điều này là SQL_CALC_FOUND_ROWS khiến mệnh đề LIMIT được áp dụng sau khi tìm nạp các hàng thay vì trước đó, vì vậy nó tìm nạp toàn bộ hàng cho tất cả các kết quả có thể có trước khi áp dụng các giới hạn. Điều này không thể được đáp ứng bởi một chỉ mục vì nó thực sự tìm nạp dữ liệu.

Nếu bạn sử dụng hai cách tiếp cận truy vấn, cách tiếp cận đầu tiên chỉ tìm nạp COUNT (*) và không thực sự tìm nạp và dữ liệu thực tế, điều này có thể được đáp ứng nhanh hơn nhiều vì nó thường có thể sử dụng chỉ mục và không phải tìm nạp dữ liệu hàng thực tế cho mọi hàng nó nhìn vào. Sau đó, truy vấn thứ hai chỉ cần xem xét các hàng $ offset + $ giới hạn đầu tiên và sau đó trả về.

Bài đăng này từ blog hiệu suất MySQL giải thích thêm về điều này:

http://www.mysqlperformanceblog.com/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/

Để biết thêm thông tin về cách tối ưu hóa phân trang, hãy xem bài đăng nàybài đăng này .


2

Câu trả lời của tôi có thể muộn, nhưng bạn có thể bỏ qua truy vấn thứ hai (với giới hạn) và chỉ cần lọc thông tin qua tập lệnh back end của bạn. Trong PHP chẳng hạn, bạn có thể làm một số việc như:

if($queryResult > 0) {
   $counter = 0;
   foreach($queryResult AS $result) {
       if($counter >= $startAt AND $counter < $numOfRows) {
            //do what you want here
       }
   $counter++;
   }
}

Nhưng tất nhiên, khi bạn có hàng nghìn bản ghi để xem xét, nó sẽ trở nên kém hiệu quả rất nhanh. Số đếm được tính trước có thể là một ý tưởng hay để xem xét.

Đây là một bài đọc hay về chủ đề này: http://www.percona.com/ppc2009/PPC2009_mysql_pagination.pdf


Liên kết đã chết, tôi đoán đây là liên kết chính xác: percona.com/files/presentations/ppc2009/… . Sẽ không chỉnh sửa vì không chắc có đúng như vậy không.
hectorg87

1
query = SELECT col, col2, (SELECT COUNT(*) FROM `table`) AS total FROM `table` WHERE `some_condition` LIMIT 0, 10

16
Truy vấn này chỉ trả về tổng số bản ghi trong bảng; không phải là số lượng bản ghi phù hợp với điều kiện.
Lawrence Barsanti

1
Tổng số bản ghi là những gì cần thiết để phân trang (@Lawrence).
Tất cả lập

Oh, tốt, chỉ cần thêm các wherekhoản để truy vấn bên trong và bạn sẽ có được quyền "tổng" cùng với kết quả paged (trang được chọn với limitkhoản
Erenor Paz

số lượng phụ truy vấn (*) sẽ đòi hỏi cùng một mệnh đề where hoặc nếu không nó sẽ không trở về con số chính xác của kết quả
AKrush95

1

Đối với bất kỳ ai đang tìm kiếm câu trả lời vào năm 2020. Theo tài liệu MySQL:

"Công cụ sửa đổi truy vấn SQL_CALC_FOUND_ROWS và hàm FOUND_ROWS () đi kèm không được dùng nữa kể từ MySQL 8.0.17 và sẽ bị xóa trong phiên bản MySQL trong tương lai. Để thay thế, hãy xem xét thực hiện truy vấn của bạn với LIMIT, sau đó là truy vấn thứ hai với COUNT (*) và không có LIMIT để xác định xem có hàng bổ sung hay không. "

Tôi đoán điều đó giải quyết ổn thỏa.

https://dev.mysql.com/doc/refman/8.0/en/information-functions.html# Chức năng_found-rows


0

Bạn có thể sử dụng lại hầu hết truy vấn trong một truy vấn con và đặt nó thành số nhận dạng. Ví dụ: một truy vấn phim tìm các phim có thứ tự của chữ cái theo thời gian chạy sẽ giống như thế này trên trang web của tôi.

SELECT Movie.*, (
    SELECT Count(1) FROM Movie
        INNER JOIN MovieGenre 
        ON MovieGenre.MovieId = Movie.Id AND MovieGenre.GenreId = 11
    WHERE Title LIKE '%s%'
) AS Count FROM Movie 
    INNER JOIN MovieGenre 
    ON MovieGenre.MovieId = Movie.Id AND MovieGenre.GenreId = 11
WHERE Title LIKE '%s%' LIMIT 8;

Xin lưu ý rằng tôi không phải là chuyên gia cơ sở dữ liệu và tôi hy vọng ai đó sẽ có thể tối ưu hóa điều đó tốt hơn một chút. Khi chạy nó trực tiếp từ giao diện dòng lệnh SQL, cả hai đều mất ~ 0,02 giây trên máy tính xách tay của tôi.


-14
SELECT * 
FROM table 
WHERE some_condition 
ORDER BY RAND()
LIMIT 0, 10

3
Điều này không trả lời được câu hỏi và đặt hàng bằng rand là một ý tưởng thực sự tồi.
Dan Walmsley
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.