2009-03-25 8 views
7

J'ai une table avec une clé étrangère et une valeur booléenne (et un tas d'autres colonnes qui ne sont pas pertinentes), en tant que tel:Comment puis-je effectuer un ET sur un nombre inconnu de booléens dans postgresql?

CREATE TABLE myTable 
(
    someKey integer, 
    someBool boolean 
); 

insert into myTable values (1, 't'),(1, 't'),(2, 'f'),(2, 't'); 

Chaque someKey pourrait avoir 0 ou plusieurs entrées. Pour une quelconque clé donnée, j'ai besoin de savoir si a) toutes les entrées sont vraies, ou b) si l'une des entrées est fausse (essentiellement un AND).

Je suis venu avec la fonction suivante:

CREATE FUNCTION do_and(int4) RETURNS boolean AS 
$func$ 
declare 
    rec record; 
    retVal boolean = 't'; -- necessary, or true is returned as null (it's weird) 
begin 
    if not exists (select someKey from myTable where someKey = $1) then 
     return null; -- and because we had to initialise retVal, if no rows are  found true would be returned 
    end if; 

    for rec in select someBool from myTable where someKey = $1 loop 
     retVal := rec.someBool AND retVal; 
    end loop; 

    return retVal; 
end; 
$func$ LANGUAGE 'plpgsql' VOLATILE; 

... qui donne des résultats corrects:

select do_and(1) => t 
select do_and(2) => f 
select do_and(3) => null 

Je me demande s'il y a une plus belle façon de le faire. Cela ne semble pas trop mal dans ce scénario simple, mais une fois que vous incluez tout le code de support, il devient plus long que je ne le souhaite. J'ai jeté un oeil à la colonne someBool à un tableau et en utilisant la construction ALL, mais je ne pouvais pas le faire fonctionner ... des idées?

+0

est 'somebool' défini 'NOT NULL'? –

Répondre

7

Pas besoin de redéfinir les fonctions PostgreSQL fournit déjà: bool_and() va faire le travail:

select bool_and(someBool) 
    from myTable 
    where someKey = $1 
    group by someKey; 

(Désolé, ne peut pas le tester maintenant)

+0

Vous devez supprimer la clause "group by someKey". L'utilisation manuelle d'un curseur et la numérisation vous permettent d'arrêter l'analyse dès que "false" est rencontré, mais c'est une amélioration si minime qu'il est peu probable que cela vaille la peine comparé à l'inconvénient d'avoir une fonction enveloppant une requête. Je ne pense pas que les agrégats puissent "s'arrêter vite" comme ça. – araqnid

0

Peut-être compter 'tous' éléments avec somekey = somevalue et l'utiliser dans une comparaison booléenne avec le nombre de toutes les occurrences «vrai» pour somekey?

Certains pseudo-sql non-testé pour montrer ce que je veux dire ...

select foo1.count_key_items = foo2.count_key_true_items 
from 
    (select count(someBool) as count_all_items from myTable where someKey = '1') as foo1, 
    (select count(someBool) as count_key_true_items from myTable where someKey = '1' and someBool) as foo2 
+0

C'est un début, mais cela ne concerne pas le cas où l'identifiant n'existe pas (vrai est retourné, puisque 0 = 0) – rjohnston

+0

Eh bien, quelque chose comme ça devrait faire l'affaire, mais j'aime mieux l'autre solution; -) http://paste.pocoo.org/show/109644/ – ChristopheD

3

similaires à la précédente, mais dans une requête, cela fera l'affaire, mais il est pas propre, ni Code facilement compréhensible:

SELECT someKey, 
    CASE WHEN sum(CASE WHEN someBool THEN 1 ELSE 0 END) = count(*) 
        THEN true 
        ELSE false END as boolResult 
FROM table 
GROUP BY someKey 

cela obtenir toutes les réponses à la fois, si vous voulez qu'une seule clé il suffit d'ajouter une clause WHERE

+0

Merci ... supprimer someKey de la liste de sélection donne le bon résultat – rjohnston

2

Je viens d'installer PostgreSQL pour la première fois cette semaine, vous Aurez besoin de nettoyer la syntaxe, mais l'idée générale ici devrait fonctionner:

return_value = NULL 

IF EXISTS 
(
    SELECT 
      * 
    FROM 
      My_Table 
    WHERE 
      some_key = $1 
) 
BEGIN 
    IF EXISTS 
    (
      SELECT 
       * 
      FROM 
       My_Table 
      WHERE 
       some_key = $1 AND 
       some_bool = 'f' 
    ) 
      SELECT return_value = 'f' 
    ELSE 
      SELECT return_value = 't' 
END 

L'idée est que vous avez seulement besoin de regarder une ligne pour voir s'il en existe et si au moins une ligne vous EXISTE alors seulement besoin de regarder jusqu'à ce que vous trouviez une valeur fausse pour déterminer que la valeur finale est fausse (ou vous arrivez à la fin et c'est vrai). En supposant que vous avez un index sur some_key, la performance devrait être bonne, je pense.

+0

Exactement: si vous trouvez des "falses", le "and game" est en hausse. – Roboprog

+0

J'ai changé d'avis - j'aime mieux cette solution. Nettoyé la version à http://paste.pocoo.org/show/109656/ – rjohnston

0
CREATE FUNCTION do_and(int4) 
    RETURNS boolean AS 
$BODY$ 
    SELECT 
    MAX(bar)::bool 
    FROM (
    SELECT 
     someKey, 
     MIN(someBool::int) AS bar 
    FROM 
     myTable 
    WHERE 
     someKey=$1 
    GROUP BY 
     someKey 

    UNION 

    SELECT 
     $1, 
     NULL 
) AS foo; 
$BODY$ 
    LANGUAGE 'sql' STABLE; 

Si vous n'avez pas besoin de la valeur NULL (quand il n'y a pas de lignes), il suffit d'utiliser la requête suivante:

SELECT 
    someKey, 
    MIN(someBool::int)::bool AS bar 
FROM 
    myTable 
WHERE 
    someKey=$1 
GROUP BY 
    someKey 
2

(très mineur côté p oint: Je pense que votre fonction doit être déclarée STABLE plutôt que VOLATILE, car elle utilise simplement les données de la base de données pour déterminer son résultat.)

Comme mentionné précédemment, vous pouvez arrêter le scan dès que vous rencontrez une "fausse" valeur .Si c'est un cas courant, vous pouvez utiliser un curseur pour provoquer en fait une « finishing »:

CREATE FUNCTION do_and(key int) RETURNS boolean 
    STABLE LANGUAGE 'plpgsql' AS $$ 
DECLARE 
    v_selector CURSOR(cv_key int) FOR 
    SELECT someBool FROM myTable WHERE someKey = cv_key; 
    v_result boolean; 
    v_next boolean; 
BEGIN 
    OPEN v_selector(key); 
    LOOP 
    FETCH v_selector INTO v_next; 
    IF not FOUND THEN 
     EXIT; 
    END IF; 
    IF v_next = false THEN 
     v_result := false; 
     EXIT; 
    END IF; 
    v_result := true; 
    END LOOP; 
    CLOSE v_selector; 
    RETURN v_result; 
END 
$$; 

Cette approche signifie également que vous ne faites que sur un seul balayage myTable. Rappelez-vous, je suppose que vous avez besoin de charges et de rangées pour que la différence soit appréciable.

0
SELECT DISTINCT ON (someKey) someKey, someBool 
FROM myTable m 
ORDER BY 
     someKey, someBool NULLS FIRST 

Ceci sélectionnera la première valeur booléenne ordonnée pour chaque someKey.

S'il y a un seul FALSE ou un NULL, il sera retourné en premier, ce qui signifie que le AND a échoué. Si le premier booléen est TRUE, tous les autres booléens sont également TRUE pour cette clé. Contrairement à l'agrégat, l'index sera utilisé sur (someKey, someBool).

Pour retourner un OR, juste inverser la commande:

SELECT DISTINCT ON (someKey) someKey, someBool 
FROM myTable m 
ORDER BY 
     someKey, someBool DESC NULLS FIRST 
1

Vous pouvez également utiliser every, qui est juste un alias à bool_and:

select every(someBool) 
from myTable 
where someKey = $1 
group by someKey; 

En utilisant tous les rend votre requête plus lisible. Un exemple, montrer toutes les personnes qui mangent juste pomme tous les jours:

select personId 
from personDailyDiet 
group by personId 
having every(fruit = 'apple'); 

every est sémantiquement la même chose que bool_and, mais il est certainement clair que every est plus lisible que bool_and:

select personId 
from personDailyDiet 
group by personId 
having bool_and(fruit = 'apple'); 
Questions connexes