PHP PDO количество строк - как правильно?

641
08 сентября 2017, 22:48

У меня есть такой вопрос. Нужно узнать количество строк, которые вернулись в результате запроса SELECT. Наткнулся на различные варианты решения, но не могу понять какой правильный. На англоязычном StackOverflow почему-то очень многие рекомендуют просто вместо SELECT * FROM... делать SELECT COUNT(*) FROM.... А если у меня запрос на пол-экрана? С вложенными SELECT'ами, JOIN'ами и прочей прелестью? Мне его что, дублировать и добавлять везде COUNT()? Это криво, ИМХО. Не хочу так.

Есть второй вариант. Делать так:

$result = $pdo->query( 'SELECT * FROM `foo_bar`;' );
$count_result = $pdo->query( 'SELECT FOUND_ROWS();' );
$row_count = $count_result->fetchColumn();

И тогда нам вернётся то, что нужно. Но сразу вопрос - какие есть ограничения/баги в таком подходе? Подводные камни, либо неочевидные проблемы.

Есть третий вариант.

$result = $pdo->query( 'SELECT * FROM `foo_bar`;' );
$row_count = $result->rowCount();

В официальной доке по PDO написано, что rowCount() не работает с SELECT-запросами. Вернее, написано, что не гарантируется его корректная работа со всеми БД. Мои тесты показывают, что MySQL и PostgreSQL rowCount() работает как надо.

Вопрос - как, всё-таки, правильно посчитать количество строк, которые выбрал SELECT-запрос?

P.S. count( $result->fetchAll() ) не предлагать. Ибо даже на жалких 100к записей... сами понимаете.

пример запроса:

$sql="SELECT SQL_CALC_FOUND_ROWS 
    metering.id,
    metering.super_number,
    metering.contract,
    metering.name, 
    metering.city,
    metering.address,
    metering.phone,
    metering.additional_phone,
    metering.responsible,
    metering.name_metering,
    metering.number_card_customer,
    metering.date_contract, 
    metering.date_metering, 
    metering.time_metering,
    metering.status_customer, 
    metering.status, 
    metering.notification,
    metering.date_status, 
    metering.date, 
    metering.creator, 
    metering.type, 
    metering.importance, 
    metering.source_attraction, 
    metering.production, 
    metering.status_metering, 
    metering.delivery_date, 
    metering.delivery_time, 
    metering.production_date,
    user_retail.login AS login_responsible, 
    login_name_metering.login AS login_name_metering, 
    (SELECT COUNT(*) FROM prepay_metering WHERE prepay_metering.id_custom = metering.id) AS prepay,
    (SELECT COUNT(*) FROM manufacture_cost WHERE manufacture_cost.name = metering.id) AS prepay_cost,
    (SELECT COUNT(*) FROM `communication_metering` WHERE `communication_metering`.`id_metering` = `metering`.`id`) AS comment      
    FROM `metering` 
    LEFT JOIN user_retail ON user_retail.id = metering.responsible
    LEFT JOIN user_retail AS login_name_metering ON login_name_metering.id = metering.name_metering
    WHERE
    metering.group_id = '".$_SESSION['group_id']."%' AND
    metering.id LIKE '".$_SESSION['id']."%' AND
    metering.super_number LIKE '".$_SESSION['super_number']."%' AND
    metering.contract LIKE '".$_SESSION['contract']."%' AND
    metering.name LIKE '%".$_SESSION['name']."%' AND
    metering.city LIKE '%".$_SESSION['city']."%' AND
    metering.address LIKE '%".$_SESSION['address']."%' AND
    (metering.phone LIKE '%".$_SESSION['phone']."%' OR metering.additional_phone LIKE '%".$_SESSION['phone']."%') AND
    metering.name_metering LIKE '".$_SESSION['name_metering']."%' AND
    metering.number_card_customer LIKE '".$_SESSION['number_card_customer']."%' AND
    metering.type LIKE '".$_SESSION['type']."%' AND
    metering.responsible LIKE '".$_SESSION['name_responsible']."%'
    ";

Это исходный запрос. Дальше два экрана того, что с этим запросом происходит:

$_SESSION['status_customer']=$_POST['status_customer'];
    if (!empty($_SESSION['status_customer'])) $sql.="AND metering.status_customer='".$_SESSION['status_customer']."'";
    $_SESSION['status']=$_POST['status'];
    if (!empty($_SESSION['status'])) $sql.="AND metering.status_customer=2 AND metering.status='".$_SESSION['status']."'";
    $_SESSION['contract_yes']=$_POST['contract_yes'];
    if (!empty($_SESSION['contract_yes'])) $sql.="AND metering.contract!=''";
    $_SESSION['metering_sr']=$_POST['metering'];
    if (!empty($_SESSION['metering_sr'])) $sql.="AND metering.metering='3'";
    //Дата создания
    if (!empty($_SESSION['date1'])) {
        $start_date = substr($_SESSION['date1'],6,4).'-'.substr($_SESSION['date1'],3,2).'-'.substr($_SESSION['date1'],0,2);
        $sql=$sql."AND metering.date >= '".$start_date."'";
    }
    if (!empty($_SESSION['date2'])) {
        $end_date = substr($_SESSION['date2'],6,4).'-'.substr($_SESSION['date2'],3,2).'-'.substr($_SESSION['date2'],0,2);
        $sql=$sql."AND metering.date <= '".$end_date."' ";
    }
    //Дата замера
    if (!empty($_SESSION['date_metering1'])) {
        $start_date = substr($_SESSION['date_metering1'],6,4).'-'.substr($_SESSION['date_metering1'],3,2).'-'.substr($_SESSION['date_metering1'],0,2);
        $sql=$sql."AND metering.date_metering >= '".$start_date."'";
    }
    if (!empty($_SESSION['date_metering2'])) {
        $end_date = substr($_SESSION['date_metering2'],6,4).'-'.substr($_SESSION['date_metering2'],3,2).'-'.substr($_SESSION['date_metering2'],0,2);
        $sql=$sql."AND metering.date_metering <= '".$end_date."' ";
    }
    //Дата контракта
    if (!empty($_SESSION['date_contract1'])) {
        $start_date = substr($_SESSION['date_contract1'],6,4).'-'.substr($_SESSION['date_contract1'],3,2).'-'.substr($_SESSION['date_contract1'],0,2);
        $sql=$sql."AND metering.date_contract >= '".$start_date."'";
    }
    if (!empty($_SESSION['date_contract2'])) {
        $end_date = substr($_SESSION['date_contract2'],6,4).'-'.substr($_SESSION['date_contract2'],3,2).'-'.substr($_SESSION['date_contract2'],0,2);
        $sql=$sql."AND metering.date_contract <= '".$end_date."' ";
    }   
    //Дата доставки
    if (!empty($_SESSION['date_delivery1'])) {
        $start_date = substr($_SESSION['date_delivery1'],6,4).'-'.substr($_SESSION['date_delivery1'],3,2).'-'.substr($_SESSION['date_delivery1'],0,2);
        $sql=$sql."AND metering.delivery_date >= '".$start_date."'";
    }
    if (!empty($_SESSION['date_delivery2'])) {
        $end_date = substr($_SESSION['date_delivery2'],6,4).'-'.substr($_SESSION['date_delivery2'],3,2).'-'.substr($_SESSION['date_delivery2'],0,2);
        $sql=$sql."AND metering.delivery_date <= '".$end_date."' ";
    }       
    //Постраничное отображение
    if (isset($_GET['pag'])) $pag=$_GET['pag'];
    else $pag=0;
    if (empty($_GET['lim'])) $sql .=" LIMIT ".$pag.", 30;";

Код не мой. Стоит задача перевести на PDO. Поэтому не хотелось бы видеть комментариев в стиле "кривые руки" и "кто так пишет", ибо будет не по адресу :) спасибо

Answer 1

Тебе это не нужно

Если коротко, то rowCount() для селекта - это самая бессмысленная функция во всех Database API

Случаев, когда тебе может понадобиться что-то посчитать, ровно два:

  1. У тебя уже есть данные, нужно знать их количество. Нет ничего проще: count(данные).
  2. Тебе не нужны данные, а нужно только количество. Тут только SELECT count(*) из базы без вариантов.

Всё, больше случаев нет, все любимые ламерами mysql_num_rows и rowCount не у дел.

SQL_CALC_FOUND_ROWS не слишком оптимален, и может использоваться только если общее число строк невелико. В противном случае оно будет слишком сильно нагружать базу.

Поэтому, как правильно предложено в комментариях, сделать список полей переменной, и подставлять в зависимости от запроса либо ее, либо count(*). Т.е.

$select = "SELECT ид, супер нумбер, Авраам родил Исаака, Исаак родил Иакова, Иаков родил Иуду и братьев его и так далее до седьмого колена...";
$from = "FROM `metering` ... и далее весь развесистый SQL без лимита";
$count = $pdo->query("SELECT count(*) $from"))->fetchColumn();
$rows = $pdo->query("$select $from LIMIT 0, 20")->fetchAll();
Answer 2

Запрос делаешь таким образом "SELECT COUNT(*) FROM...", а в PDO делаешь после query этого запроса, fetchColumn(); оно вернёт число записей. http://php.net/manual/ru/pdostatement.fetchcolumn.php

Пример: Запрос: $res = $pdo->query("SELECT COUNT(*) FROM table"); Получаем число: $countNum = $res->fetchColumn();

READ ALSO
PHP, помогите с регуляркой

PHP, помогите с регуляркой

Есть категории товаров, которые отдаются в виде

150
Добавление элемента в инфоблок

Добавление элемента в инфоблок

Пытаюсь добавить в инфоблок новый элемент, но на выходе получаю ошибку такого рода "Для добавления элементов инфоблоков используйте вызов...

376
Тестовые письма на почте. Laravel

Тестовые письма на почте. Laravel

Такие сообщение(обратный звонок на сайте) приходят на почту каждый день, по 10-15 шт

299
Защищен ли запрос от SQL инъекций?

Защищен ли запрос от SQL инъекций?

Подскажите функции для защиты) вот сам запрос) Все параметры в метод передаются из строки запроса

239