45 votes

Comment obtenir le nombre total de lignes d'une requête GROUP BY ?

Extrait du manuel de l'AOP :

PDOStatement::rowCount() retourne la valeur de nombre de lignes touchés par la dernière DELETE, INSERT, ou UPDATE déclaration exécutée par l'objet PDOStatement correspondant.

Si la dernière instruction SQL exécutée par l'instruction PDOS associée était un SÉLECTIONNER déclaration, certaines bases de données peuvent retourner le nombre de lignes retournées par cette instruction . Cependant, ce comportement est non garanti pour toutes les bases de données et ne doit pas être utilisé pour des applications portables.

Je ne l'ai découvert que très récemment. Je venais juste de changer ma couche d'abstraction de données pour ne pas utiliser SELECT COUNT(1) ... car il serait beaucoup plus efficace d'interroger les rangées actuelles et de compter le résultat. Et maintenant PDO ne supporte pas ça ! ?

Je n'utilise pas PDO pour MySQL et PgSQL, mais je le fais pour SQLite. Existe-t-il un moyen (sans modifier complètement le dbal) de compter les lignes comme ceci dans PDO ? En MySQL, ce serait quelque chose comme ceci :

$q = $db->query('SELECT a, b, c FROM tbl WHERE oele = 2 GROUP BY boele');
$rows = $q->num_rows;
// and now use $q to get actual data

Avec les pilotes MySQLi et PgSQL, c'est possible. Avec tous les PDO, ça ne l'est pas !?

PS. Ma solution initiale était d'étendre la méthode SQLResult->count (ma propre méthode) pour remplacer SELECT ... FROM par SELECT COUNT(1) FROM et renvoyer simplement ce nombre (très inefficace, mais seulement pour SQLite PDO). Ce n'est cependant pas suffisant, car dans l'exemple de requête ci-dessus, il y a un fichier GROUP BY ce qui modifierait le sens/la fonction de l'expression COUNT(1) .

0 votes

J'ai déjà été victime de cette situation par le passé, mais il en a toujours été ainsi, cela n'a pas changé soudainement. Postgres vous donnera le compte, MySQL ne le fera pas. Vous supposez que l'exécution de la requête et le comptage des résultats seraient plus efficaces, mais que se passe-t-il si vous avez des dizaines de milliers de lignes ?

0 votes

MySQL vous donnera le compte. Le client SQLlite "natif" de PHP le fait aussi. Mais pas PDO. Avec n'importe quel pilote.

0 votes

Si vous avez besoin d'une solution en dehors de PDO (s'il ne supporte pas countRows), vous pouvez essayer quelque chose comme : "select SUM(1) as count from ... "Je sais que cela fonctionne dans mySQL, il faut juste ajouter 1 pour chaque ligne retournée dans la requête (je n'ai pas testé cela dans sqlLite), à la fin la valeur de "count" = count(*).

44voto

RoboTamer Points 1508

Voici la solution pour vous

$sql="SELECT count(*) FROM [tablename] WHERE key == ? ";
$sth = $this->db->prepare($sql);
$sth->execute(array($key));
$rows = $sth->fetch(PDO::FETCH_NUM);
echo $rows[0];

1 votes

Je n'avais pas besoin des données mais je voulais juste voir si la requête retournait des données, donc je vérifiais if($sth->fetch(PDO::FETCH_NUM) > 0) a fonctionné pour moi. Merci !

0 votes

La solution postée dans cette réponse a fonctionné sans problème. Merci beaucoup.

1 votes

Qu'est-ce que array($key) ?

24voto

mjec Points 1392

C'est un peu inefficace en mémoire mais si vous utilisez les données de toute façon, je l'utilise fréquemment :

$rows = $q->fetchAll();
$num_rows = count($rows);

1 votes

Correct, tu ne peux pas faire un fetchAll() plus tard. Mais, bon, vous l'avez déjà fait ? Ne pouvez-vous donc pas utiliser le résultat de cette opération au lieu de le récupérer à nouveau ? Cela peut nécessiter un peu de réécriture en fonction du code que vous utilisez.

0 votes

Si le résultat est énorme, il est beaucoup plus efficace d'extraire les lignes une par une, même si vous les utilisez toutes de toute façon. Si vous les récupérez une par une et renvoyez les données au navigateur, vous n'avez pas besoin de beaucoup de mémoire. Mais si vous les récupérez toutes, vous avez besoin de la mémoire pour le résultat complet.

0 votes

Je n'ai pas assez bien lu la question. Le point est que lorsque COUNT int une requête avec un GROUP BY en elle. Les résultats ne sont pas ceux que vous attendez. Ce n'était pas assez évident ?

4voto

Denis Points 34131

Je n'utilise pas PDO pour MySQL et PgSQL, mais je le fais pour SQLite. Existe-t-il un moyen (sans modifier complètement le dbal) de compter les lignes de cette manière dans PDO ?

En conséquence ce commentaire le problème de SQLite a été introduit par un changement d'API dans la version 3.x.

Cela dit, il serait bon de vérifier comment PDO implémente réellement la fonctionnalité avant de l'utiliser.

Je ne suis pas familier avec son fonctionnement interne mais je me méfierais de l'idée que PDO analyse votre SQL (puisqu'une erreur de syntaxe SQL apparaîtrait dans les journaux de la base de données) et encore moins qu'il essaie de lui donner le moindre sens afin de compter les lignes en utilisant une stratégie optimale.

En supposant que ce n'est pas le cas, les stratégies réalistes pour qu'il renvoie un compte de toutes les lignes applicables dans une instruction de sélection incluent la manipulation de la clause de limite de votre instruction SQL, et l'une ou l'autre :

  1. Exécution d'une select count() en tant que sous-requête (évitant ainsi le problème que vous avez décrit dans votre PS) ;
  2. Ouverture d'un curseur, exécution de fetch all et comptage des lignes ; ou
  3. Avoir ouvert un tel curseur en premier lieu, et compter de la même manière les rangées restantes.

Une bien meilleure façon de compter, cependant, serait d'exécuter la requête entièrement optimisée qui le fera. Le plus souvent, cela signifie qu'il faut réécrire des parties importantes de la requête initiale que vous essayez de paginer, en supprimant les champs et les opérations d'ordre par inutiles, etc.

Enfin, si vos ensembles de données sont suffisamment importants pour compter tout type de décalage, vous pouvez également envisager de renvoyer l'estimation dérivée de la méthode de l statistiques à la place, et/ou en mettant périodiquement le résultat en cache dans Memcache. À un moment donné, avoir des comptes exacts n'est plus utile...

0 votes

Donc il n'y a pas moyen ? Je ne veux pas compter des milliers de lignes. Je veux juste savoir s'il y a 0, 1 ou plusieurs résultats. Je ne veux pas récupérer 2 résultats quand je n'en ai pas besoin. Je veux seulement en récupérer un, mais je veux savoir s'il y en a plus.

0 votes

Il n'y a absolument aucun moyen pour une base de données de savoir si une ligne est présente ou combien de lignes il y a dans votre jeu sans regarder réellement. Les autres meilleures solutions sont de dériver une approximation en utilisant ses statistiques, de récupérer une ligne supplémentaire (par exemple, limiter 11 au lieu de 10) pour savoir s'il y a une page suivante, et d'éviter de recompter à chaque page grâce à la mise en cache.

1 votes

Bien sûr qu'il devrait regarder, mais il y a une grande différence entre regarder et aller chercher. C'est pourquoi les adaptateurs de base de données ont des classes ResultSet : ils ont exécuté la requête, mais n'ont pas encore récupéré les résultats. C'est ce que je veux (et c'est ce que MySQL, pgSQL et SQLite peuvent faire, mais apparemment PDO ne le peut pas).

2voto

Nev Stokes Points 3162

N'oubliez pas qu'un PDOStatement est Traversable . Étant donné une requête :

$query = $dbh->query('
    SELECT
        *
    FROM
        test
');

Il peut être itéré :

$it = new IteratorIterator($query);
echo '<p>', iterator_count($it), ' items</p>';

// Have to run the query again unfortunately
$query->execute();
foreach ($query as $row) {
    echo '<p>', $row['title'], '</p>';
}

Ou vous pouvez faire quelque chose comme ça :

$it = new IteratorIterator($query);
$it->rewind();

if ($it->valid()) {
    do {
        $row = $it->current();
        echo '<p>', $row['title'], '</p>';
        $it->next();
    } while ($it->valid());
} else {
    echo '<p>No results</p>';
}

1 votes

Un itérateur ne sait pas nécessairement combien d'enregistrements il contient, d'après ce que j'ai compris, il comptera toujours chaque enregistrement du côté client (en php).

0 votes

Ce qui signifie qu'il faut aller chercher les résultats, ce que je veux éviter. Le ResultSet lui-même ( $query est un objet) doit savoir combien de résultats il y a.

0 votes

L'itérateur ne récupérera rien tant que vous ne commencerez pas à le parcourir. Dans le deuxième exemple, vous pouvez voir si des lignes ont été récupérées en vérifiant si l'itérateur est valide. Bien sûr, si vous voulez savoir combien de lignes ont été retournées, vous devrez récupérer le résultat. Idéalement le ResultSet (tel que vous le décrivez) devrait savoir combien de rangées il a, mais il ne le sait pas. J'ai bien peur que ce ne soit le cas avec PDO.

2voto

cwallenpoole Points 34940

Si vous êtes prêt à renoncer à un soupçon d'abstraction, vous pouvez utiliser une classe wrapper personnalisée qui transmet simplement tout au PDO. Disons, quelque chose comme ça : (Attention, code non testé)

class SQLitePDOWrapper
{
    private $pdo;

    public function __construct( $dns, $uname = null, $pwd = null, $opts = null )
    {
        $this->pdo = new PDO( $dns, $unam, $pwd, $opts ); 
    }
    public function __call( $nm, $args )
    {
        $ret = call_user_func_array( array( $this->pdo, $nm ), $args );
        if( $ret instanceof PDOStatement )
        {
            return new StatementWrapper( $this, $ret, $args[ 0 ] ); 
               // I'm pretty sure args[ 0 ] will always be your query, 
               // even when binding
        }

        return $ret;
    }

}

class StatementWrapper
{
    private $pdo; private $stat; private $query;

    public function __construct( PDO $pdo, PDOStatement $stat, $query )
    {
        $this->pdo  = $pdo;
        $this->stat = $stat;
        this->query = $query;
    }

    public function rowCount()
    {
        if( strtolower( substr( $this->query, 0, 6 ) ) == 'select' )
        {
            // replace the select columns with a simple 'count(*)
            $res = $this->pdo->query( 
                     'SELECT COUNT(*)' . 
                          substr( $this->query, 
                              strpos( strtolower( $this->query ), 'from' ) ) 
                   )->fetch( PDO::FETCH_NUM );
            return $res[ 0 ];
        }
        return $this->stat->rowCount();
    }

    public function __call( $nm, $args )
    {
        return call_user_func_array( array( $this->stat, $nm ), $args );
    }
}

3 votes

En quoi cela peut-il aider ? Il crée une instruction (erronée) COUNT(*) que j'ai déjà... ? Je ne vois pas l'avantage de la partie wrapper... Mon framework a déjà un wrapper. Un générique Database et des adaptateurs spécifiques comme MySQL , SQLite , PDOSQLite et des ensembles de résultats spécifiques comme MySQLResult , SQLiteResult , PDOResult . Je pourrais modifier entièrement la classe PDOResult sans changer aucune fonctionnalité ou appel de l'application (, mais je ne préfère pas).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X