web-dev-qa-db-fra.com

Utilisation de GROUP BY WITH ROLLUP sur une colonne contenant NULL

J'ai le tableau suivant ( voir sur SQL Fiddle ) (j'ai créé pour décomposer mon problème):

| ID | Word    |
----------------
| 5  | "Hello" |
| 6  |  NULL   |
| 7  | "World" |
| 8  | "World" |

Maintenant, je veux compter le nombre d'occurrences de chaque mot en utilisant GROUP BY Word WITH ROLLUP. Le NULL dans la colonne Word de la ligne générée par le ROLLUP doit être remplacé par "total":

SELECT
  ID,
  ifnull(Word, "total") as Word,
  count(*) as occurrences
FROM test
GROUP BY Word WITH ROLLUP;

Le problème est qu'il remplace également le NULL dans l'enregistrement par le nombre de lignes où les mots sont NULL:

| ID |  Word | occurrences |
|----|-------|-------------|
|  6 | total |           1 | <- Here lies the problem
|  5 | Hello |           1 |
|  7 | world |           2 |
|  7 | total |           4 |

Donc, j'utilise maintenant count(Word) pour distinguer s'il s'agit d'un NULL de mes données ou d'un NULL créé par ROLLUP afin que je puisse le remplacer par "vide" ou "total":

SELECT
  ID,
  if(count(Word) = 0, "empty", ifnull(Word, "total")) as Word,
  count(*) as occurrences
FROM test
group by Word with ROLLUP;

Cela fonctionne pour mon cas d'utilisation mais il a toujours un défaut: si Word est NULL dans toutes les lignes, count(Word) est également 0 dans la dernière ligne affichant le total: ( SQL Fiddle avec des données différentes (seulement enregistrement # 6) )

| ID |  Word | occurrences |  
|----|-------|-------------|  
|  6 | empty |           1 |  
|  6 | empty |           1 |  <- Word should be "total"

La déclaration suivante fournit le résultat que je souhaite:

SELECT
  ID,
  ifnull(Word, "total") as Word,
  count(*) as occurrences
FROM
(
SELECT
  ID,
  ifnull(Word, "empty") as Word
FROM test
) tmp
GROUP BY Word WITH ROLLUP

Mais comme dans la plupart des réponses ici, l'utilisation des sous-requêtes semble découragée, je me demande s'il existe une meilleure solution.

5
Adrian Aulbach

vous avez une "solution", donc en disant

Je me demande s'il y a une meilleure solution.

Je pense que vous voulez en parler. Permettez-moi de me concentrer sur plusieurs aspects:

  1. SELECT ID [...] GROUP BY Word, indépendamment du fait d'avoir ou non un WITH ROLLUP est faux. Vous sélectionnez un champ dont la valeur est indéterminée . En particulier, avec MySQL, vous êtes exposé à renvoyer une valeur aléatoire; ou si vous utilisez un mode strict (recommandé et par défaut dans les dernières versions de MySQL), la requête échoue: 'db_9_b0ff7.test.ID' n'est pas dans GROUP BY

  2. Que ce soit formellement ou non, WITH ROLLUP Remarque je me réfère à l'implémentation de MySQL) est quelque chose qui est agréable à avoir dans certains cas car cela évite une double nouvelle analyse de la table, mais je n'ai pas vu beaucoup de code de production. Rien de mal à cela, mais ils étaient une fonctionnalité MSSQL exclusive jusqu'à ce qu'elle soit implémentée sur SQL1999, et soit elle n'était pas largement disponible à l'époque ou, comme dans le cas de MySQL, c'était un mise en œuvre limitée . Référence: --- (https://www.percona.com/blog/2007/09/17/using-group-by-with-rollup-for-reporting-performance-optimization/

  3. Je ne vois en principe aucun problème avec votre requête finale, si nous exécutons expliquer sur les deux requêtes (sans ID), nous voyons:

    MariaDB [test]> EXPLAIN SELECT   if(count(Word) = 0, "empty", ifnull(Word, "total")) as Word,   count(*) as occurrences FROM test group by Word with ROLLUP\G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: test
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 4
            Extra: Using filesort
    1 row in set, 1 warning (0.00 sec)
    
    Warning (Code 1052): Column 'Word' in group statement is ambiguous
    MariaDB [test]> EXPLAIN SELECT   ifnull(Word, "total") as Word,   count(*) as occurrences FROM ( SELECT   ifnull(Word, "empty") as Word FROM test ) tmp GROUP BY Word WITH ROLLUP\G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: test
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 4
            Extra: Using filesort
    1 row in set, 1 warning (0.01 sec)
    

    Normalement, la haine envers MySQL est due à ses limites sur les premières versions de MySQL. Je ne vais pas dire qu'il est maintenant parfait, il y aura toujours des problèmes, comme le développement de MySQL, comme il l'a toujours été, axé sur un SGBDR rapide, facile à entrer et à sortir, et non un moteur relationnel complet , mais une sous-requête au bon endroit est correcte (après tout, certaines requêtes nécessiteront une sous-requête, peu importe quoi. Votre utilisation est correcte - elle seulement avoir une surcharge de parcourir les lignes résumées à nouveau et je suppose que sur la vraie affaire, celles-ci ne seront pas significatives par rapport à l'ensemble complet.

    Remarque - sur mon instance ici, la sous-requête n'est pas affichée, mais vous pouvez la voir avec des sous-requêtes matérialisées en 5.6 ici: http://sqlfiddle.com/#!9/b0ff7/7 (It exécutera la requête interne puis la partie externe, sans problème). Un index pourrait certainement aider ici (en raison du port de fichiers), mais cela dépend du nombre final d'enregistrements. La sous-requête peut nuire aux performances si un index (qui n'existe pas actuellement) ne peut pas être utilisé car il. Vous devez tester votre version spécifique de mysql et créer un tel index.

  4. Je pense que votre problème est avec le modèle de données - dans le monde SQL (en dehors d'Oracle) NULL est une valeur inconnue/invalide. Semble être en corrélation avec `` vide '' qui n'est pas correct - si un mot est connu pour ne pas exister, il devrait être '' (la chaîne vide, pas NULL). Je ne sais pas si vous pouvez changer cela, mais je n'aime pas votre modèle actuel . Je comprends que vous n’avez peut-être pas quelque chose à dire à ce sujet, je ne vais donc pas me concentrer là-dessus. Vous comptez aussi NUL mots? Plus de raisons d'utiliser '' au lieu de NULL.
  5. Maintenant, la dernière question est, est-il possible de le faire sans sous-requête sur MySQL, et sera-t-il meilleur/moins laid? Je ne vois pas vraiment de moyen - nous pourrions faire une jointure, mais ce sera toujours une sous-requête. Je peux penser à ne alternative , qui est:

    SELECT * FROM (
        SELECT   Word,   
        count(*) as occurrences 
        FROM test 
        GROUP BY Word with rollup) 
    tmp 
    ORDER BY occurrences;
    

    Cela garantit que la dernière ligne contient le résumé (et s'il y a un autre null, c'est la valeur null réelle). L'ordre se produirait même dans le cas de valeurs nulles uniquement. Au contraire, il peut même être plus lent car il nécessite une commande, et encore une fois, vous pouvez avoir besoin d'un index, et cet index peut ou non être utilisé.

4
jynus

Vous pouvez éviter les sous-requêtes et optimiser les caractères saisis à l'aide de cette requête:

SELECT
  ifnull(Word, "empty") as Word,
  count(*) as occurrences
FROM test
GROUP BY ifnull(Word, "empty") WITH ROLLUP
;

Cela remplace essentiellement les valeurs de null par votre marqueur "vide" avant d'effectuer le regroupement:

  • les nombres nuls peuvent être trouvés dans la ligne contenant la valeur "vide" dans la colonne de regroupement
  • le nombre total peut être trouvé dans une ligne contenant null valeur dans la colonne de regroupement

Une approche similaire peut être utilisée pour PostgreSQL, cela ne devrait pas être nécessaire pour SQL Server car il fournit une fonction spéciale GROUPING.

1
chris544