首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PDO 查詢結果行數獲取避坑指南:rowCount() 陷阱與三種最佳實踐方案

     編輯:PHP 相關知識

核心結論先行

PDOStatement::rowCount() 不保證返回 SELECT 語句的準確行數。官方文檔明確指出:對於 SELECT,行數依賴於具體數據庫驅動實現。MySQL 默認緩衝模式下可用,但 PostgreSQL、SQLite、ODBC 等驅動或非緩衝模式下均會返回 0 或不準確值。

三種可靠獲取行數方案

方案一:COUNT(*) 獨立查詢(推薦生產環境)

<?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);

// 優勢:內存佔用極低、利用索引覆蓋掃描極快、支持億級大表

方案二:fetchAll() 後 count()(僅適用小結果集)

<?php
$stmt = $pdo->query("SELECT * FROM `config` WHERE `group` = 'app'");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$count = count($rows);

// 適用場景:配置表、字典表等確定 < 1000 行的小表
// 缺點:大結果集會撐爆 PHP 內存(memory_limit)

方案三:SQL_CALC_FOUND_ROWS + FOUND_ROWS()(MySQL 專用,8.0+ 棄用)

<?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)緩衝準確方案一

最佳實踐封裝:通用分頁查詢 Trait

<?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);
    }
}

性能優化提示

  1. 覆蓋索引:COUNT(*) 配合覆蓋索引(如 INDEX(status, created_at))可避免回表,毫秒級完成億級表計數。
  2. 緩存總數:非實時要求場景(如後台列表),將 $total 存入 Redis 緩存 1-5 分鐘,減少 COUNT 查詢壓力。
  3. 避免 SELECT *:分頁查詢僅選取必要欄位,減少網絡傳輸與內存佔用。
  4. 游標分頁替代 OFFSET:深分頁(page > 1000)改用 WHERE id > :last_id ORDER BY id LIMIT 20 避免 OFFSET 掃描性能劣化。

總結

忘掉 rowCount() 用於 SELECT,建立「先 COUNT 再分頁查數據」的肌肉記憶。這不僅是兼容性要求,更是通用的高性能分頁架構基石。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved