Primero, un poco de contexto… En el jale tengo que hacer un query para un mini-reporte que servirá de guía para “X” area, así que hice un query usando la siguiente instrucción:
*El SGBDR es PostgreSQL

	SELECT pi.anio,pi.proyecto,pp.area,m.meta,c.promesa	,c.avance,c.comentarios,((c.avance / c.cantidad ) * 100) as porcentaje

	FROM piezas pi FULL JOIN participantes_proyecto pp
	ON pi.proins_clave_anio = pp.proins_clave_anio
	AND pi.id = pp.id 

	FULL JOIN metas m
	ON m.id = pp.id
	AND m.area = pp.area
	AND m.anio = pp.anio

	FULL JOIN calendarizacion c
	ON c.anio = m.anio
	AND c.area = m.area

	WHERE pi.anio = 2010
	AND pp.area = 'Informatica';

En concepto, el query está hecho correctamente solamente que olvidé contemplar algo: ¿Qué sucedería sí cualquiera de los campos c.avance ó c.promesa son 0 ó son vacíos?. La temible, la más culera y sobre todo, la más pendeja de las respuestas: división por cero.
Intenté hacer uso de las funciones NULLIF y de CASE de SQL en el query pero recibía errores de sintáxis, así que dije –Bueno, ¿por qué no invento una función que resuelva mi problema– comencé con el ejemplo básico de

CREATE OR REPLACE FUNCTION suma(INT, INT)
RETURNS INTEGER LANGUAGE SQL
CALLED ON NULL INPUT
AS ’
SELECT CASE
WHEN $1 IS NULL THEN
CASE
WHEN $2 IS NULL THEN NULL
ELSE $2
END
WHEN $2 IS NULL THEN $1
ELSE $1 + $2
END’
IMMUTABLE;

Teniendo como referencia ése código –cabe mencionar que no soy experto haciendo funciones en lenguaje SQL ni en PL/SQL– modifiqué el ejemplo anterior e hice el mío, el código que voy a poner es el que hace PostgreSQL

-- Function: porcentaje_avance(integer, integer)

-- DROP FUNCTION porcentaje_avance(integer, integer);

CREATE OR REPLACE FUNCTION porcentaje_avance(avance integer, promesa integer)
  RETURNS integer AS
$BODY$
	SELECT CASE
		WHEN $1 IS NULL OR $2 IS NULL  THEN
			0
		WHEN $1 IS NULL AND $2 IS NULL THEN
			0
		WHEN $1 = 0 AND $2 = 0 THEN
			0
		WHEN $1 > 0 AND $2 = 0 THEN
			0
		WHEN $1 = 0 AND $2 > 0 THEN
			0
		WHEN $1 > 0 AND $2 > 0 THEN
			(($1 / $2) *100)
		END
$BODY$
  LANGUAGE 'sql' IMMUTABLE;
ALTER FUNCTION porcentaje_avance(integer, integer) OWNER TO sisplan;
GRANT EXECUTE ON FUNCTION porcentaje_avance(integer, integer) TO public;
GRANT EXECUTE ON FUNCTION porcentaje_avance(integer, integer) TO sisplan;
COMMENT ON FUNCTION porcentaje_avance(integer, integer) IS 'Toma dos valores como argumentos y devuelve un entero mayor a cero cuando ambos valores son mayores que 0 y en caso contrario regresa 0';

Luego entonces, una vez creada la función hice el query que tenía al principio

	SELECT pi.anio,pi.proyecto,pp.area,m.meta,c.promesa	,c.avance,c.comentarios	,porcentaje_avance(c.avance,c.cantidad) as porcentaje

	FROM piezas pi FULL JOIN participantes_proyecto pp
	ON pi.proins_clave_anio = pp.proins_clave_anio
	AND pi.id = pp.id 

	FULL JOIN metas m
	ON m.id = pp.id
	AND m.area = pp.area
	AND m.anio = pp.anio

	FULL JOIN calendarizacion c
	ON c.anio = m.anio
	AND c.area = m.area

	WHERE pi.anio = 2010
	AND pp.area = 'Informatica';

De esta manera pude recuperar los registros correctamente pre-procesando los campos en la función que hice para evitar los errores de división por cero.

Espero les funcione y les sea de provecho, no olviden retro-alimentar el post con sus comentarios y sugerencias para complementar el artículo.

Gracias.

SociBook del.icio.us Digg Facebook Google Yahoo Buzz StumbleUpon

  7 Responses to “SQL: Void division by Zero using CASE”

  1. El problema con las divisiones que implican un cero es cuando éste se presenta como divisor (o sea, que le toca estar abajo de la raya, sin albur), de modo que en tu función bien puedes omitir la revisión de $1, puesto que, de resultar cero, la operación devolverá invariablemente esa “cantidad”.

    Con lo anterior, puedes dejar solamente una línea de validación de $2:


    ...
    SELECT CASE
    WHEN $2 IS NULL OR $2 = 0 THEN
    0
    ELSE
    (($1 / $2) *100)
    END
    ...

    Sólo queda una polémica: verificar si $1 es cero para entonces devolver automáticamente eso, cero, y ahorrarse una división resuelta de antemano, o dejar que Postgre haga el cálculo y se ahorre una comparación. Me parece que la diferencia entre uno y otro será de una fracción de milisegundo.

    También desconozco si aquí existe el concepto de “safe-binary”, porque de no haberlo, es probable que se obtenga un error cuando $1 sea NULL, entonces tal vez sí debas mantener:

    WHEN $1 IS NULL THEN
    0

    Como sea, el código ya quedó simplificado.

    Saludos.

  2. Lobo-X:

    En efecto, en lo que mencionas

    SELECT CASE
    WHEN $2 IS NULL OR $2 = 0 THEN
    0
    ELSE
    (($1 / $2) *100)
    END

    Sí podría caber la posibilidad, solo que en la definición de la tabla, el valor por defecto del campo $1 es 0, al igual que para el $2, por esta razón no incluí el case correspondiente.
    Y bueno, tu comentario –excelente– muchas gracias por el feedback :-)

  3. Chaleee no sabia que te estaba ayudando en pgsql u.u NO sabo pgsql

  4. k001:

    Ud. nomás no se me apanique. Apechúguele y a darle, que para eso estamos :-P

  5. *ya vi
    *buen análisis we
    *ese tipo d cosas
    *son chingonas analizarlas
    *comprender la lógica computacional detrás de las escenas

  6. Otro punto, creo, importante es separar el problema, limpiar la expresión. En vez de estar checando en todo caso explícitamente contra 0 o contra NULL, ¿por qué no usas coalesce()? De este modo, por ejemplo, tu segundo SQL podría quedar:

    CREATE OR REPLACE FUNCTION suma(INT, INT)
    RETURNS INTEGER LANGUAGE SQL
    CALLED ON NULL INPUT
    AS ’SELECT coalesce($1, 0) + coalesce($2,0)’
    IMMUTABLE;

    Claro, acá la declaración de función termina siendo innecesaria… Y sí, sé que si tanto $1 como $2 son nulos el resultado debería ser NULL y no 0 — Podrías introducirlo como un único caso especial… O si para tu espacio de problemas da igual.

  7. Gunnar:
    Esa función: coalesce no la conocía. Está interesante, sí me resuelve el problema y me quita la necesidad de hacer la función, muchas gracias por el feedback.

    :-)

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

   
© 2012 Geekeandome Suffusion theme by Sayontan Sinha