viernes, 29 de mayo de 2009

Truco rápido: Histórico de cambios en una tabla usando un trigger

No me voy a enrollar explicando que es un trigger ni para que sirve, tenéis información de sobra en este link: CREATE TRIGGER (Transact-SQL)

Lo que voy a explicar aquí es como usar un trigger para que nos guarde en una tabla de históricos los cambios (INSERT y UPDATE) que se producen en una tabla. Para ello supongamos que tenemos una tabla con un identificador (campo Id) y un nombre (campo nombre), y queremos almacenar en la tabla Historico (Id,nombre_antiguo,nombre_nuevo), los cambios que se van produciendo.

Dado que se pueden ejecutar updates sobre varios registros a la vez, será necesario que nuestro trigger obtenga todos los cambios a la vez y los procese uno a uno, eso lo haremos con un cursor.

El truco está en cómo distinguiremos que se trata de un INSERT o de un UPDATE, es muy sencillo, en caso de ser un INSERT el motor de base de datos almacena un registro en la tabla temporal "INSERTED", y en el caso de un update, habrá 2 registros: uno en la tabla "DELETED" y otro en "INSERTED", es como si para actualizarlo, primero se borrase y después se volviese a insertar, cosas del SQL Server :P

Vamos con el código:


CREATE TRIGGER [dbo].[prueba_insert_update]
ON [dbo].[Pruebas]
AFTER INSERT, UPDATE
AS
BEGIN

SET NOCOUNT ON;


-- Identificador del registro
DECLARE @Id int


-- Variable para almacenar el nombre anterior
DECLARE @nombre_antiguo varchar(20)


-- Declaramos el cursor "Cursorito" para que contenga únicamente
-- los ID de la tabla temporal "INSERTED"
DECLARE Cursorito CURSOR FORWARD_ONLY
FOR
SELECT Id FROM INSERTED


-- Abrimos el cursor
OPEN Cursorito


-- Movemos el puntero al primer registro del cursor
FETCH NEXT FROM Cursorito into @Id


-- Recorremos el cursor, cuando @@Fetch_Status sea diferente de 0
-- habremos llegado al final
WHILE @@Fetch_Status = 0
BEGIN


SET @nombre_antiguo = ''


-- Buscamos en la tabla temporal "DELETED" si hay un registro con
-- el Id que toca en esta pasada, lo que significaría que
-- se ha producido un UPDATE
SELECT @nombre_antiguo = ISNULL(nombre,'')
FROM DELETED
WHERE Id = @Id


-- Si no hay ningún registro que cumpla la condición la
-- variable @nombre_antiguo contendrá '' y se tratará
-- de una inserción


-- Añadimos un registro en la tabla de históricos
INSERT INTO Historico
(Id,nombre_antiguo,nombre_nuevo)
SELECT @Id, @nombre_antiguo, nombre
FROM INSERTED
WHERE Id = @Id


-- Nos movemos al siguiente registro
FETCH NEXT FROM Cursorito into @Id

END


-- Cerramos el cursor y liberamos el recurso
CLOSE Cursorito
DEALLOCATE Cursorito

END



Happy coding ;)

miércoles, 27 de mayo de 2009

Cambio de aires

Dicen que estamos en crisis y que es mal momento para encontrar trabajo ... igual es que yo tengo suerte porque hace un par de semanas que me he cambiado de empresa :D

He dejado un cliente final (en el que se vivía muy bien) para volver al fascinante mundo de la consultoría, por ahora en un muy buen proyecto (del que os aburriréis de oir hablar) y con un grupo majo de personas para currar :)

A los que he dejado atrás, ante todo mucho ánimo y suerte!!! ;)

viernes, 8 de mayo de 2009

Buscar en campos de texto con T-SQL: mucho más que LIKE

Una de las primeras cosas que se aprende cuando se empieza con T-SQL es a usar la cláusula LIKE para hacer comparaciones en campos de tipo texto (char, varchar o text). Pero esta cláusula está bastante limitada y no ofrece un buen rendimiento, así que ¿qué podemos utilizar en vez de LIKE?

Una muy buena opción es CONTAINS (que para ser sincero, descubrí de casualidad :P), que nos permite afinar bastante más las comparaciones y ofrece mucho mejor rendimiento.

Para poner un ejemplo del uso de CONTAINS voy a usar la base de datos PUBS (que ofrece Microsoft como ejemplo con SQL Server y se puede descargar desde aquí) y vamos a buscar registros en la tabla jobs filtrando por el campo job_desc.

Para que os hagáis una idea (y si no os apetece descargaros las bases de datos de ejemplo) la tabla jobs tiene los siguientes registros:


job_id job_desc min_lvl max_lvl
-------- -------------------------------------------------- ------- -------
1 New Hire - Job not specified 10 10
2 Chief Executive Officer 200 250
3 Business Operations Manager 175 225
4 Chief Financial Officier 175 250
5 Publisher 150 250
6 Managing Editor 140 225
7 Marketing Manager 120 200
8 Public Relations Manager 100 175
9 Acquisitions Manager 75 175
10 Productions Manager 75 165
11 Operations Manager 75 150
12 Editor 25 100
13 Sales Representative 25 100
14 Designer 25 100


Para poder usar CONTAINS primero debemos crear un índice FULLTEXT en la tabla, que se hace con las siguientes instrucciones:


CREATE FULLTEXT CATALOG ft AS DEFAULT;
CREATE FULLTEXT INDEX ON Jobs(job_desc)
KEY INDEX PK__jobs__173876EA
GO


Siendo PK__jobs__173876EA el índice principal (PRIMARY KEY) de la tabla.

Una vez hecho esto podemos empezar con los ejemplos:

Buscar palabra exacta

Obtendremos todos los registros que contengas ‘manager’ en el campo job_desc. Fijaros que no distingue mayúsculas y minúsculas:


select * from jobs where CONTAINS( job_desc, 'manager' )

job_id job_desc min_lvl max_lvl
-------- -------------------------------------------------- ------- -------
3 Business Operations Manager 175 225
7 Marketing Manager 120 200
8 Public Relations Manager 100 175
9 Acquisitions Manager 75 175
10 Productions Manager 75 165
11 Operations Manager 75 150


Además se pueden buscar frases (más de 1 palabra) y en las búsquedas se omiten los separadores.


select * from jobs where CONTAINS (job_desc, '"hire job"');
job_id job_desc min_lvl max_lvl
-------- -------------------------------------------------- ------- -------
1 New Hire - Job not specified 10 10



Buscar por aproximación con varias opciones

Buscamos los registros que contengan alguna palabra que empiece por ‘pub’ o por ‘edit’.


select * from jobs where CONTAINS(job_desc, '"pub*" OR "edit*"');

job_id job_desc min_lvl max_lvl
-------- -------------------------------------------------- ------- -------
5 Publisher 150 250
6 Managing Editor 140 225
8 Public Relations Manager 100 175
12 Editor 25 100


CONTAINS admite los operadores AND, AND NOT, OR.

Buscar palabra aproximada cerca de otra

Obtenemos registros que contengan una palabra que empiece por ‘pub’ y que esa palabra esté cerca de ‘manager’.


select * from jobs where CONTAINS (job_desc, '"pub*" NEAR manager')

job_id job_desc min_lvl max_lvl
-------- -------------------------------------------------- ------- -------
8 Public Relations Manager 100 175



CONTAINS permite además buscar por palabras derivadas y sinónimos, pero para eso es necesario usar un diccionario de sinónimos.

Happy codding ;)

Bola extra
Además de CONTAINS tenemos otras opciones para este tipo de búsquedas como FREETEXT
y CONTAINSTABLE