PDOStatement::rowCount() 不保證返回 SELECT 語句的準確行數。官方文檔明確指出:對於 SELECT,行數依賴於具體數據庫驅動實現。MySQL 默認緩衝模式下可用,但 PostgreSQL、SQLite、ODBC 等驅動或非緩衝模式下均會返回 0 或不準確值。
<?php
// 1. 先查總數
$countStmt = $pdo->query("SELECT COUNT(*) FROM `users` WHERE `status` = 1");
$total = (int)$countStmt->fetchColumn();
// 2. 再分頁查數據
$dataStmt = $pdo->prepare("SELECT * FROM `users` WHERE `status` = 1 LIMIT :offset, :limit");
$dataStmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$dataStmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$dataStmt->execute();
$rows = $dataStmt->fetchAll(PDO::FETCH_ASSOC);
// 優勢:內存佔用極低、利用索引覆蓋掃描極快、支持億級大表
<?php
$stmt = $pdo->query("SELECT * FROM `config` WHERE `group` = 'app'");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$count = count($rows);
// 適用場景:配置表、字典表等確定 < 1000 行的小表
// 缺點:大結果集會撐爆 PHP 內存(memory_limit)
<?php
// MySQL 8.0.17 起棄用,不推薦新項目使用
$stmt = $pdo->query("SELECT SQL_CALC_FOUND_ROWS * FROM `logs` LIMIT 20");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$total = $pdo->query("SELECT FOUND_ROWS()")->fetchColumn();
// 現代替代:兩條獨立查詢(方案一)性能更穩定,優化器支持更好
| 驅動 | 默認模式 | rowCount(SELECT) | 建議方案 |
|---|---|---|---|
| mysqlnd (MySQL 原生) | 緩衝 | 準確 | 方案一 |
| libmysqlclient | 非緩衝 | 0 | 方案一 |
| pgsql (PostgreSQL) | 非緩衝 | 0 | 方案一 |
| sqlite | 緩衝 | 準確 | 方案二可接受 |
| oci (Oracle) | 非緩衝 | 0 | 方案一 |
| sqlsrv (SQL Server) | 緩衝 | 準確 | 方案一 |
<?php
trait PaginatedQuery {
/**
* 執行分頁查詢,自動返回 [總數, 數據列表]
* @param PDO $pdo
* @param string $baseSql 不含 LIMIT 的基礎查詢 SQL
* @param array $params 綁定參數
* @param int $page 頁碼(從 1 開始)
* @param int $pageSize 每頁條數
* @return array [int $total, array $rows]
*/
public function paginate(PDO $pdo, string $baseSql, array $params = [], int $page = 1, int $pageSize = 20): array {
// 1. 構建 COUNT SQL(簡單場景直接包裹,複雜 JOIN 建議手寫優化版)
$countSql = "SELECT COUNT(*) FROM ({$baseSql}) AS _count_sub";
$countStmt = $pdo->prepare($countSql);
$countStmt->execute($params);
$total = (int)$countStmt->fetchColumn();
if ($total === 0) {
return [0, []];
}
// 2. 執行分頁查詢
$offset = ($page - 1) * $pageSize;
$dataSql = $baseSql . " LIMIT :offset, :limit";
$dataStmt = $pdo->prepare($dataSql);
foreach ($params as $key => $val) {
$dataStmt->bindValue($key, $val);
}
$dataStmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$dataStmt->bindValue(':limit', $pageSize, PDO::PARAM_INT);
$dataStmt->execute();
return [$total, $dataStmt->fetchAll(PDO::FETCH_ASSOC)];
}
}
// 使用示例
class UserRepository {
use PaginatedQuery;
public function getActiveUsers(int $page = 1): array {
return $this->paginate($this->pdo, "SELECT * FROM `users` WHERE `status` = 1 ORDER BY `created_at` DESC", [], $page);
}
}
COUNT(*) 配合覆蓋索引(如 INDEX(status, created_at))可避免回表,毫秒級完成億級表計數。$total 存入 Redis 緩存 1-5 分鐘,減少 COUNT 查詢壓力。WHERE id > :last_id ORDER BY id LIMIT 20 避免 OFFSET 掃描性能劣化。忘掉 rowCount() 用於 SELECT,建立「先 COUNT 再分頁查數據」的肌肉記憶。這不僅是兼容性要求,更是通用的高性能分頁架構基石。