miércoles, 5 de diciembre de 2012

Evitar errores en búsquedas sobre campos de texto con mayúsculas y minúsculas en PostgreSQL

Si los campos de texto se almacenan con mezclas de mayúsculas y minúsculas, las búsquedas se ven afectadas y podrían no entregar los resultados que esperamos. Para ilustrarlo supongamos que tenemos una tabla llamada 'persona' con un campo 'nombre', en el que almacenaremos la información introducida por un usuario mediante un formulario y que no procesaremos dicho campo, por lo que el usuario podrá ingresar el texto usando mayúsculas y minúsculas. Luego de varios ingresos tendremos los siguientes datos:

SELECT id, nombre FROM prueba_persona;
 id | nombre
----+---------------------
  1 | CARLOS PEREZ
  2 | Carmen Pereira
  3 | Ricardo Gonzalez
  4 | gonzalo Perez
  5 | PERICLES RICAURTE
  6 | perico portocarrero
(6 filas)

Con estos datos, si quisiéramos obtener los nombres que contengan la cadena 'car' esperaríamos obtener las filas 1, 2, 3 y 6, pero si efectuamos la consulta obtendremos lo siguiente:

SELECT id, nombre FROM persona WHERE nombre LIKE '%car%';
 id | nombre
----+---------------------
  3 | Ricardo Gonzalez
  6 | perico portocarrero
(2 filas)

Porque el motor de Bases de Datos distingue entre mayúsculas y minúsculas como caracteres diferentes. Una solución rápida consiste en modificar la consulta así:

SELECT id, nombre FROM persona WHERE lower(nombre) LIKE '%car%';
 id | nombre
----+---------------------
  1 | CARLOS PEREZ
  2 | Carmen Pereira
  3 | Ricardo Gonzalez
  6 | perico portocarrero
(4 filas)

En esta consulta estamos primero pasando el atributo a minúsculas y sobre el texto modificado efectuamos la consulta. De este modo podremos obtener los datos que realmente buscamos. Hay que aclarar que no se modifican los valores del atributo dentro de la tabla sino en el búfer de búsqueda.

Sin embargo, si hacemos un uso recurrente de este tipo de consultas, es mejor usar un método más eficiente. Si no importa la mezcla de minúsculas y mayúsculas podemos modificar el valor del campo en un trigger antes de hacer el INSERT y almacenar el texto en minúsculas:

CREATE OR REPLACE FUNCTION modificar_nombre_persona() RETURNS TRIGGER AS $$
BEGIN

  NEW.nombre := lower(NEW.nombre);
  RETURN NEW;

END
$$
LANGUAGE 'plpgsql';

CREATE TRIGGER persona_antes_insertar BEFORE INSERT ON persona FOR EACH ROW EXECUTE PROCEDURE modificar_nombre_persona();

De este modo al ingresar los datos éstos quedarán almacenados así:

SELECT id, nombre_busquedas FROM prueba_persona;
 id | nombre_busquedas
----+---------------------
  1 | carlos perez
  2 | carmen pereira
  3 | ricardo gonzalez
  4 | gonzalo perez
  5 | pericles ricaurte
  6 | perico portocarrero
(6 filas)

Y al hacer la misma búsqueda obtendremos los siguiente resultados:

SELECT id, nombre FROM persona WHERE nombre LIKE '%car%';
 id | nombre_busquedas
----+---------------------
  1 | carlos perez
  2 | carmen pereira
  3 | ricardo gonzalez
  6 | perico portocarrero
(4 filas)

Pero si es necesario conservar la forma escrita por el usuario, podemos crear un campo adicional, en el que almacenaremos el valor en minúsculas y no alteraremos el campo original.

ALTER TABLE persona ADD nombre_busquedas varchar(255);

CREATE OR REPLACE FUNCTION crear_nombre_busquedas_persona() RETURNS TRIGGER AS $$
BEGIN

  NEW.nombre_busquedas := lower(NEW.nombre);
  RETURN NEW;

END
$$
LANGUAGE 'plpgsql';

CREATE TRIGGER persona_antes_insertar BEFORE INSERT ON persona FOR EACH ROW EXECUTE PROCEDURE crear_nombre_busquedas_persona();

En este caso los datos quedarían almacenados así:

SELECT * FROM persona;
 id | nombre              | nombre_busquedas
----+---------------------+---------------------
  1 | CARLOS PEREZ        | carlos perez
  2 | Carmen Pereira      | carmen pereira
  3 | Ricardo Gonzalez    | ricardo gonzalez
  4 | gonzalo Perez       | gonzalo perez
  5 | PERICLES RICAURTE   | pericles ricaurte
  6 | perico portocarrero | perico portocarrero
(6 filas)

Y la consulta se haría así:

SELECT id, nombre FROM persona WHERE nombre_busquedas LIKE '%car%';
 id | nombre
----+---------------------
  1 | CARLOS PEREZ
  2 | Carmen Pereira
  3 | Ricardo Gonzalez
  6 | perico portocarrero
(4 filas)

Mediante cualquiera de estas dos formas evitaremos el uso de funciones adicionales en la consulta y obtendremos un poco más de eficiencia.

Eso es todo!

PDTA: También habría que ejecutar los triggers antes de actualizar.