1
votes

Compter les lignes qui ne correspondent pas entre deux tables au sein d'un groupe

J'ai deux tables, chacune avec un user_id et des colonnes group_name .

Par exemple

SELECT * FROM
    (SELECT 
        times_show_up,
        COUNT(user_id) AS num_users,
        group_name1,
        group_name2
    FROM
    (
        SELECT 
            user_id, 
            COUNT(*) AS times_show_up,
            group_name1, 
            group_name2
        FROM
            table1
        INNER JOIN
            (SELECT DISTINCT user_id, group_name2 FROM table2) t2
        USING(user_id)
        GROUP BY user_id, group_name1, group_name2
    ) t1
    GROUP BY times_show_up, group_name1, group_name2) t9
    UNION ALL
    (SELECT
       0 AS times_show_up,
       SUM(CASE WHEN t1.user_id IS NULL 
           THEN 1 ELSE 0 END) AS num_users,
       'groupA' AS group_name1,
       group_name2
     FROM
       table2
     LEFT JOIN
       (SELECT user_id FROM table1 WHERE group_name1 = 'groupA') t1
     USING(user_id)
     GROUP BY group_name2)
     UNION ALL
    (SELECT
       0 AS times_show_up,
       SUM(CASE WHEN t1.user_id IS NULL 
           THEN 1 ELSE 0 END) AS num_users,
       'groupB' AS group_name1,
       group_name2
     FROM
       table2
     LEFT JOIN
       (SELECT user_id FROM table1 WHERE group_name1 = 'groupB') t1
     USING(user_id)
     GROUP BY group_name2)
     --- ORDER BY group_name1, group_name2, times_show_up

J'essaie de créer une répartition des décomptes entre le nombre de fois qu'un utilisateur de la table2 apparaît dans la table1, mais faites cela au sein d'un groupe.

Pour l'exemple ci-dessus, j'obtiendrais

    SELECT 
        COUNT(user_id) AS num_users,
        times_show_up,
        group_name1,
        group_name2
    FROM
    (
        SELECT 
            user_id, 
            COUNT(*) AS times_show_up,
            group_name1, 
            group_name2
        FROM
            table1
        RIGHT JOIN
            (SELECT DISTINCT user_id, group_name2 FROM table2)
        USING(user_id)
        GROUP BY user_id, group_name1, group_name2
    )
    GROUP BY times_show_up, group_name1, group_name2

Pour expliquer certaines lignes comme exemple supplémentaire:

Pour la ligne 1, l'utilisateur avec user_id = 3 dans le groupeL apparaît 0 fois dans le groupeA. Pour la ligne 2, l'utilisateur avec user_id = 2 dans groupL apparaît une fois dans groupA. Et pour la ligne 3, l'utilisateur avec user_id = 1 dans groupL apparaît deux fois dans groupA.

Bien que dans cet exemple, une personne apparaisse jusqu'à un maximum de 2 fois, dans les données réelles, ce nombre est arbitrairement grand nombre que je ne connais pas à l'avance.

De même pour les autres groupes si j'ai rempli tout cela correctement.

J'ai trouvé une requête qui peut faites tout cela sauf en comptant les 0, qui ressemble à ceci:

| times_show_up | number_of_users | group_name1 | group_name2 |
---------------------------------------------------------------
|      0        |       1         |    groupA   |    groupL    |
|      1        |       1         |    groupA   |    groupL    |
|      2        |       1         |    groupA   |    groupL    |
|      0        |       2         |    groupB   |    groupL    |
|      1        |       1         |    groupB   |    groupL    |
|      2        |       0         |    groupB   |    groupL    |
|      0        |       2         |    groupA   |    groupN    |
|      1        |       0         |    groupA   |    groupN    |
|      2        |       1         |    groupA   |    groupN    |
|      0        |       2         |    groupB   |    groupN    |
|      1        |       1         |    groupB   |    groupN    |
|      2        |       0         |    groupB   |    groupN    |
----------------------------------------------------------------

Malheureusement, cela ne renverra pas les nombres de 0 dans la colonne times_show_up , et je n'ai pas trouvé de solution qui puisse accomplir cela sans beaucoup de sous-requêtes. Une méthode possible consiste simplement à exécuter des sous-requêtes pour récupérer tous les 0 pour toutes les combinaisons de tous les groupes, puis simplement UNION ces lignes au reste de la table. Mais je veux éviter les approches qui incluent des sous-requêtes pour chaque combinaison group1, group2 possible car le nombre de groupes est très grand.

Certaines limitations incluent que la partition par sur cet ensemble de données a tendance à manquer de mémoire, je veux donc l'éviter. Exigence mise à jour: De plus, l'utilisation de CROSS JOIN au niveau de l'utilisateur individuel (donc la jonction croisée de la table1 directement à la table2 sans regrouper les lignes au préalable) ne fonctionne pas car chaque table comporte des dizaines de millions de lignes.

Enfin, les lignes avec des 0 dans la colonne number_of_users ne doivent pas apparaître (c'est bien si elles le font car elles peuvent être supprimées avec un simple WHERE code >, mais ne sont pas nécessaires, si cela aide avec la requête)


MISE À JOUR:

J'ai pu proposer une requête qui peut générer les zéros tout en ne nécessitant qu'une seule requête pour chaque nom_groupe1, plutôt qu'une seule requête pour chaque combinaison nom_groupe1, nom_groupe2. Je l'ajoute à la question au cas où cela aiderait à trouver des réponses avec encore moins de requêtes, car il est toujours vrai que le nombre de groupes dans le tableau 1 pourrait être supérieur à 20+, ce qui signifie plus de 20 requêtes ajoutées via UNION ALL .

table1:

| user_id | group_name1|
------------------------
|    1    |   'groupA' |
|    1    |   'groupB' |
|    2    |   'groupA' |
|    1    |   'groupA' |
------------------------


table2:

| user_id | group_name2|
------------------------
|    1    |   'groupL' |
|    2    |   'groupL' |
|    3    |   'groupL' |
|    4    |   'groupN' |
|    1    |   'groupN' |
|    3    |   'groupN' |
------------------------


4 commentaires

Pourquoi l'utilisateur 1 fait-il deux fois partie du groupA ?


@CHP. . . Marquez la question avec la base de données que vous utilisez.


@The Empaler - C'est prévu. Un peu difficile à expliquer sans entrer dans la signification des groupes, mais en général, le groupe A ne regroupe pas des utilisateurs, mais des ensembles d'utilisateurs. Et ici, nous voulons compter le nombre d'ensembles dont un utilisateur du groupe A fait partie.


@GordonLinoff - Merci d'avoir fait cela dans BigQuery et ajouté une balise selon votre suggestion, bien que je devrais pouvoir convertir en BQ si j'obtiens la logique générale, qui, espérons-le, ne repose pas sur des fonctionnalités spécialisées.


3 Réponses :


0
votes

Voici la stratégie.

  1. Utilisez la jointure croisée pour générer les lignes.
  2. Pour cela, récupérez les groupes en utilisant count (distinct) .
  3. Générez le times_show_up à l'aide d'une table dérivée.
  4. Agréger table1 et table2
  5. Rejoignez tout cela ensemble.

Voici la requête:

select g1.group_name1, g2.group_name2, tsu.times_show_up,
       coalesce(t12.cnt, 0) as num_users
from (select distinct group_name1 from table1) g1 cross join
     (select distinct group_name2 from table2) t2 cross join
     (select 0 as times_show_up union all
      select 1 union all
      select 2
     ) tsu left join
     (select t1.group_name1, t2.group_name2, count(*) as cnt
      from table1 t1 join
           table2 t2
           on t2.user_id = t1.user_id
      group by t1.group_name1, t2.group_name2
     ) t12
     on t12.group_name1 = g1.group_name1 and
        t12.group_name2 = g2.group_name2 and
        t12.cnt = tsu.times_show_up;

Vous voudrez peut-être count (distinct user_id) au lieu de count (* ) dans la sous-requête, si vos données ont vraiment des doublons.


1 commentaires

Je ne pense pas que cela fonctionnera car il semble coder en dur le 0, 1, 2, alors que cela pourrait être n'importe quoi dans les données réelles. Une personne peut apparaître 1000 fois ou un autre numéro.



1
votes

Ci-dessous est pour BigQuery Standard SQL et a fini par être relativement simple

#standardSQL
SELECT times_show_up, 
  COUNT(DISTINCT user_id) number_of_users, 
  group_name1, group_name2
FROM (
  SELECT SUM(IF(a.user_id = b.user_id, cnt, 0)) times_show_up, 
    b.user_id, 
    group_name1, group_name2
  FROM (SELECT user_id, group_name1, COUNT(1) cnt FROM table1 GROUP BY user_id, group_name1) a
  CROSS JOIN (SELECT DISTINCT user_id, group_name2 FROM table2) b
  GROUP BY user_id, group_name1, group_name2
)
GROUP BY times_show_up, group_name1, group_name2

S'il faut appliquer aux exemples de données de votre question - le résultat est

Row times_show_up   number_of_users group_name1 group_name2  
1   0               1               groupA      groupL   
2   1               1               groupA      groupL   
3   2               1               groupA      groupL   
4   0               2               groupB      groupL   
5   1               1               groupB      groupL   
6   0               2               groupA      groupN   
7   2               1               groupA      groupN   
8   0               2               groupB      groupN   
9   1               1               groupB      groupN     

... les lignes avec 0 dans la colonne number_of_users ne doivent pas apparaître

Remarque: je suis cette règle car il semble que vous prévoyez de les éliminer de toute façon au cas où le résultat en aurait

Mise à jour pour ... chaque table contient des dizaines de millions de lignes.

Essayez ci-dessous la version "optimisée"

#standardSQL
SELECT times_show_up, 
  COUNT(DISTINCT user_id) number_of_users, 
  group_name1, group_name2
FROM (
  SELECT COUNTIF(a.user_id = b.user_id) times_show_up, 
    b.user_id, 
    group_name1, group_name2
  FROM table1 a
  CROSS JOIN table2 b
  GROUP BY user_id, group_name1, group_name2
)
GROUP BY times_show_up, group_name1, group_name2
-- ORDER BY group_name2, group_name1, times_show_up   

Je n'ai pas de données pertinentes pour tester si cela aidera ou non pour vos données spécifiques p>


10 commentaires

C'est intéressant. J'avais peur d'utiliser la jointure croisée sur les données utilisateur brutes car il y a des dizaines de millions par table. Je lui ai quand même essayé et jusqu'à présent, la requête ne s'est pas plantée, mais elle fonctionne depuis plus de 15 minutes maintenant et je soupçonne sournoisement qu'elle épuisera les ressources de calcul avant de se terminer. Si cela fonctionne et qu'il n'y a pas de moyen simple de créer une requête plus rapide (par exemple, si toutes les autres versions nécessitent une sous-requête pour chaque combinaison group1, group2), j'accepterai cette réponse car c'est une solution soignée. Mais les requêtes plus bâclées s'exécutent au moins en ~ 1 minute.


voyons si cela fera une fin sans erreur: o) Je vais revoir cette requête dans la matinée pour voir si cela peut être optimisé. mais honnêtement je pense qu'il devrait réussir tel quel même pour 10M +


Je n'ai toujours pas planté mais après 83 minutes je l'ai arrêté: D C'était sur un ensemble de données plus petit, donc je ne peux qu'imaginer ce qui se passera sur un ensemble de données plus grand. J'ajouterai le 'pas de jointures croisées au niveau de l'utilisateur individuel' aux exigences de la question :)


Le délai d'expiration de la requête par défaut est de 6 heures. mais il est peut-être trop long d'attendre votre cas o (je viens de voir votre mise à jour - oui, je pense que c'est la bonne direction pour l'optimisation - pré-agrégez d'abord les deux tables.


Bien, même si cela se termine, 83 minutes pour une requête plus petite ne vaut pas la peine car je peux l'obtenir en ~ 1-3 minutes en utilisant simplement des sous-requêtes pour chacun des zéros group1, group2. Par exemple, ma requête initiale qui calcule correctement tout sauf que les zéros se termine en ~ 1 minute


J'ai exécuté votre requête initiale sur des exemples de données et je n'ai pas obtenu le résultat attendu. mais je peux me tromper car je n'ai pas passé beaucoup de temps. de toute façon - je vérifierai celui-ci à nouveau dans la matinée. il est minuit en ce moment à Los Angeles et j'ai encore du travail à faire: o)


Voici un violon: sqlfiddle.com/#!9/78bf1f/4 Ignorer le lignes avec null dans group_name1, je peux facilement les faire disparaître en remplaçant Right join par Inner join (ou une instruction where). Si vous le faites, vous verrez que cette requête génère toutes les lignes où num_users> 0 (comme vous le souhaitez), sauf celles où times_show_up = 0 (le problème que j'essaie de résoudre)


@CHP - ajout de la version "optimisée": o)


Cela prend encore très longtemps, jusqu'à présent pendant 30 minutes et cela ne s'est pas terminé, et je soupçonne que cela ne fonctionnera pas bien avec des ensembles de données plus volumineux non plus (ne se nourrissant que de 3 groupes pour le test, au lieu de 20) :) La requête mise à jour que j'ai postée par contraste se termine en 20 secondes pour 3 groupes. Peut-être étais-je trop naïf pour penser que cela pouvait être fait rapidement sans questions supplémentaires. Il semble juste étrange que seuls les 0 posent des problèmes, j'ai l'impression qu'il doit y avoir une logique qui me manque pour les inclure avec une seule requête qui ne croise pas au niveau user_id.


J'ai fini par l'annuler car cela a pris plus d'une heure, mais j'accepterai la réponse car elle répond à la question d'origine, et rien de plus efficace n'est venu. Je publierai ma requête en tant que réponse distincte pour les personnes ayant des données de taille similaire.



0
votes

La réponse de @Mikhail Berlyant répond aux exigences initiales de ma question. Malheureusement, parce qu'il repose sur une jointure croisée au niveau user_id et qu'il existe des dizaines de millions d'ID utilisateur, cela prend du temps pour mon cas d'utilisation spécifique. Je donne donc la réponse suivante, qui est plus rapide, mais nécessite une requête supplémentaire pour chaque groupe du tableau 1 (mais pas pour chaque combinaison de groupe1 et groupe2), ce qui rend la requête moins concise qui peuvent potentiellement dépasser les limites de la taille de la requête BigQuery si le nombre de groupes est très très important.

Cette approche est préférable si vous pouvez générer par programme les requêtes pour chaque groupe et avoir moins de groupes avec des millions d'utilisateurs, alors que la réponse de @Mikhail Berlyant devrait bien fonctionner dans les situations où il y a beaucoup plus de groupes avec un petit nombre d'utilisateurs chacun, et dans les cas où la génération de requête n'est pas effectuée par programme et que vous devez écrire chacune pour chaque groupe.

SELECT * FROM
    (SELECT 
        times_show_up,
        COUNT(user_id) AS num_users,
        group_name1,
        group_name2
    FROM
    (
        SELECT 
            user_id, 
            COUNT(*) AS times_show_up,
            group_name1, 
            group_name2
        FROM
            table1
        INNER JOIN
            (SELECT DISTINCT user_id, group_name2 FROM table2) t2
        USING(user_id)
        GROUP BY user_id, group_name1, group_name2
    ) t1
    GROUP BY times_show_up, group_name1, group_name2) t9
    # Each subsequent query being UNIONed corresponds to a group in table 1
    UNION ALL
    (SELECT
       0 AS times_show_up,
       SUM(CASE WHEN t1.user_id IS NULL 
           THEN 1 ELSE 0 END) AS num_users,
       'groupA' AS group_name1,
       group_name2
     FROM
       table2
     LEFT JOIN
       (SELECT user_id FROM table1 WHERE group_name1 = 'groupA') t1
     USING(user_id)
     GROUP BY group_name2)
     UNION ALL
    (SELECT
       0 AS times_show_up,
       SUM(CASE WHEN t1.user_id IS NULL 
           THEN 1 ELSE 0 END) AS num_users,
       'groupB' AS group_name1,
       group_name2
     FROM
       table2
     LEFT JOIN
       (SELECT user_id FROM table1 WHERE group_name1 = 'groupB') t1
     USING(user_id)
     GROUP BY group_name2)
     --- ORDER BY group_name1, group_name2, times_show_up```


0 commentaires