2009-03-25 12 views
7

Tengo una tabla con una clave externa y un valor booleano (y un montón de otras columnas que no son relevantes aquí), tales como:¿Cómo puedo realizar un AND en un número desconocido de booleanos en postgresql?

CREATE TABLE myTable 
(
    someKey integer, 
    someBool boolean 
); 

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

Cada someKey podría tener 0 o más entradas. Para cualquier clave dada, necesito saber si a) todas las entradas son verdaderas, o b) cualquiera de las entradas es falsa (básicamente una Y).

se me ha ocurrido con la siguiente función:

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; 

... que da los resultados correctos:

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

Me pregunto si hay una manera mejor de hacer esto. No parece tan malo en este escenario simple, pero una vez que incluye todo el código de soporte, se vuelve más largo de lo que me gustaría. Eché un vistazo a lanzar la columna someBool a una matriz y utilizando la construcción ALL, pero no pude hacerlo funcionar ... ¿alguna idea?

+0

es 'somebool' definido 'NOT NULL'? –

Respuesta

7

No hay necesidad de redefinir las funciones de PostgreSQL ya proporciona: bool_and() hará el trabajo:

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

(En este momento, no se puede probar ahora)

+0

Debe soltar la cláusula "group by someKey". Utilizar un cursor y escanear manualmente le permite detener el escaneo tan pronto como se encuentre "falso", pero esa es una mejora menor que es poco probable que valga la pena en comparación con la inconveniencia de tener una función que envuelva una consulta. No creo que los agregados puedan "detenerse rápido" así. – araqnid

0

Quizás cuente los elementos 'todos' con somekey = somevalue y úselos en una comparación booleana con el recuento de todas las ocurrencias 'Verdaderas' para somekey?

Algunos no probado pseudo-SQL para mostrar lo que quiero decir ...

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

Es un comienzo, pero esto no tiene que ver con el caso en que el ID no existe (se devuelve verdadero, ya que 0 = 0) – rjohnston

+0

Bueno, algo así debería funcionar, pero me gusta más la otra solución; -) http://paste.pocoo.org/show/109644/ – ChristopheD

3

similar a la anterior, pero en una consulta, esto va a hacer el truco, sin embargo, no es limpia ni código fácilmente comprensible:

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 

esto hará que todas las respuestas a la vez, si sólo quiere una clave sólo tiene que añadir una cláusula WHERE

+0

Gracias ... eliminar alguna clave de la lista de selección da el resultado correcto – rjohnston

2

simplemente he instalado PostgreSQL por primera vez esta semana, por lo que Tendrá que limpiar la sintaxis, pero la idea general aquí debería funcionar:

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 

La idea es que sólo tiene que mirar en una fila para ver si existen y si al menos una fila que existe, entonces solo necesita buscar hasta que encuentre un valor falso para determinar que el valor final es falso (o llega al final y es cierto). Suponiendo que tengas un índice en some_key, el rendimiento debería ser bueno, creo.

+0

Exactamente: si encuentra algún "falso", el "y el juego" está activo. – Roboprog

+0

Cambié de opinión - Me gusta más esta solución. Versión limpiada en 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; 

En caso de que no necesita el valor NULL (cuando no hay ninguna fila), sólo tiene que utilizar la consulta a continuación:

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

(muy importante lateral-p unint: creo que su función debe declararse STABLE en lugar de VOLATIL, ya que solo utiliza datos de la base de datos para determinar su resultado.)

Como alguien mencionó, puede detener el escaneo tan pronto como encuentre un valor "falso" .Si eso es un caso común, se puede utilizar un cursor para provocar realmente una "rápida culminación":

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 
$$; 

Este enfoque significa también que sólo está haciendo una sola exploración en myTable. Eso sí, sospecho que necesitas montones y montones de filas para que la diferencia sea apreciable.

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

Esto seleccionará el primer valor booleano ordenado para cada someKey.

Si hay un solo FALSE o un NULL, se devolverá primero, lo que significa que el AND falló.

Si el primer valor booleano es TRUE, entonces todos los demás booleanos también son TRUE para esta clave.

A diferencia del agregado, este usará el índice en (someKey, someBool).

para devolver un OR, simplemente invertir el orden:

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

También puede utilizar every, que es sólo un alias para bool_and:

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

Usando cada hace que su consulta sea más legible. Un ejemplo, mostrar todas las personas que acaban de comer la manzana todos los días:

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

every es semánticamente lo mismo que bool_and, pero ciertamente es claro que every es más legible que bool_and:

select personId 
from personDailyDiet 
group by personId 
having bool_and(fruit = 'apple'); 
Cuestiones relacionadas