jueves, 27 de mayo de 2010

Casos de Exito SQL Server

Muchas veces he escuchado comentarios sobre que SQL Server no es tan bueno, no soporta mucha informacion y cosas por el estilo. Yo no coincido con ellos, SQL Server es un excelente producto que solo debemos saber explotar.

Les dejo la siguiente liga, yo pues entre mas conosco el producto mas me enamoro de el.

http://www.microsoft.com/sqlserver/2008/en/us/case-studies.aspx

domingo, 23 de mayo de 2010

Identificando planes de ejecuciòn ineficientes


El tema de optimizar nuestra Base de datos es bastante amplio, esta entrada será solo una de una serie que pretendo subir tocando diversos puntos para identificar y solucionar problemas en nuestra Base de Datos.
Recuerdo haber tenido contacto con un administrador de Base de datos al que se le cuestionaba el rendimiento tan pobre de la Base de datos, su respuesta era mostrarnos una grafica del Monitor de performance en donde se veían picos en el servidor y cuellos de botella en el procesador. ¿Pero eso es suficiente? La respuesta es no, los cuellos de botella en nuestro procesador se pueden deber a diversas causas, es decir eso solo es lo que se ve por fuera, pero tenemos que investigar que es lo que exactamente hizo que el procesador estuviera tan demandado.
Para hacerlo una opción es ejecutar una traza y analizarla pero también debemos siempre buscar apoyarnos en las DMVs que SQL desde la versión 2005 nos proporciona, estas son vistas y funciones que nos proporcionan detalle del comportamiento de nuestra Base de datos.
En esta ocasión las usaremos para obtener un script que nos permita identificar las consultas más costosas en términos de procesador, el resultado se debe analizar a fondo, pues un escenario posible es que tengamos una consulta sumamente costosa pero que se ejecuta solo esporádicamente y quizá otra menos costosa pero que se ejecuta cientos o miles de veces, entonces debemos nosotros saber a dónde apuntar nuestra atención.
Select Top 10
total_worker_time/execution_count as avg_cpu_cost,plan_handle,
execution_count,(Select substring(text,statement_start_offset/2+1,
(Case when Statement_end_offset=-1
Then len(convert(nvarchar(max),text))*2
Else Statement_end_offset
End - statement_start_offset)/2)
From sys.dm_exec_sql_text(sql_handle)) as query_text
From sys.dm_exec_query_stats
Order by [avg_cpu_cost] desc

Lo que nos regresa esta consulta es el porcentaje promedio de CPU que es utilizado cada vez que se ejecuta una consulta , el número de veces que es invocada esa consulta , el Id del plan de ejecución y por último la consulta en sí, nuestro trabajo es una vez identificadas poder aislar el porqué de ese costo.
Nota: La ejecuciòn de esta consulta unicamente nos traera los planes que SQL mantiene en el cache, es decir pudieran existir otros que no nos arroje la consulta . Para resolver este problema la recomendaciòn de Microsoft es que ejecutemos periodicamente esta consulta para obtener informaciòn mas completa.

Por ahora con esto termino este post, esto solo es una pequeña parte de las cosas que tenemos que considerar cuando buscamos optimizar nuestra Base de datos, saludos y ojala les sea de utilidad.

Schemas

SQL Server desde la version 2005 nos ofrece como una de las nuevas características el uso de schemas, ¿para qué sirven, que son, realmente es importante calificar los nombres de los objetos con el esquema al que pertenecen?

Los esquemas son objetos que sirven como contenedores de otros objetos, para la gente que es de tecnología de .NET la analogía serian los NameSpaces. Los esquemas nos permiten agrupar objetos en ellos.

La sintaxis para crear un esquema es

Create Schema [NombreEsquema] AUTHORIZATION [NombreUsuario]

Tambien lo podemos crear mediante el asistente para lo cual abrimos el Managment Studio/Object Explorer, nos colocamos en el nodo de nuestra Base de Datos, despues en el nodo [Security] y dentro de el en el nodo [Schemas] podemos crearlo haciendo click con el botòn derecho en la opcion Nuevo esquema.


Vamos a crear nuestro esquema y continuaremos profundizando sobre su uso

Create Schema Audit;

Como se menciono antes los esquemas nos permiten contener objetos de Base de datos.
Crearemos un par de tablas que estaran dentro del esquema Audit.

Create Table Audit.Tbl_Eventos
(
ID Int Identity(1,1) Primary key
,Evento varchar(100)
);
GO
Create Table Audit.Tbl_Usuarios
(
ID Int Identity(1,1) Primary key
,Usuario varchar(100)
);

Veamos como funciona esto mendiante un select a nuestras tablas antes creadas
Lo que muchas veces hacemos al invocar el nombre de un objeto es apuntar directamente a el con algo como:

Select * from Tbl_Eventos;

Lo cual si lo ejecutan veran que les ha generado un error ¿Por qué? , la razon es porque desde SQL 2005 los nombres de objetos se califican mediante cuatro partes [DBServerName].[DBName].[Schema].[Table], en la version de SQL 2000 los nombres se calificaban de una manera un poco distinta y era mediante [DBServer].[DBName].[ObjectOwner].[Table]

Analizando lo anterior,lo que debemos entender es que SQL antes calificaba los nombres de objetos directamente asociandolos al propietario.Desde SQL 2005 los objetos no le pertenecen directamente a un usuario, a quien estan vinculados es a un esquema y el esquema tiene un dueño, esto nos habre posibilidades de las cosas que podemos hacer con los esquemas.
Nuestra instrucción Select debe quedar de la forma

Select * from Audit.Tbl_Eventos;

¿Por qué es bueno indicar el nombre del esquema? SQL al resolver una petición y no tener especificado el esquema al que pertenece el objeto invocado tiene que determinar si existe en el esquema que tiene por default el usuario firmado, parece poco, sin embargo todo es importante y al calificar nuestra tabla con el esquema nos aseguramos de indicarle a SQL en que esquema esta nuestra tabla.
Los esquemas además nos sirven muy bien para asegurar los accesos a nuestros objetos, imaginemos que tenemos cien tablas dentro del esquema Audit y que tenemos que dar acceso de solo lectura a todas ellas a un usuario nuevo, siempre que pensemos en resolver algo lo debemos hacer en términos de “con el menor esfuerzo”.
La instrucción que nos permite asignarle algún permiso
GRANT [Permiso] ON SCHEMA::[SchemaName] TO [NombreUsuario]

Ejemplo:
CREATE LOGIN MyLogin WITH PASSWORD='pa$w00rd'
GO
CREATE USER MyLogin FROM LOGIN MyLogin


GRANT SELECT ON SCHEMA::[Audit] TO [MyLogin]
¿Pero que sucede si un dia necesitamos cambiar de un esquema la ubicación de una tabla? Eso es relativamente sencillo.
Con la instrucciòn Alter Schema lo podemos hacer
Ejemplo
Create schema Audit2
--transferir objetos
Alter schema Audit2 Transfer Audit.Tbl_Eventos

Como podemos ver es realmente simple cambiar de esquema una table.
Por último me gustaría mencionar algo que se llaman Sinónimos en SQL y que nos pueden ahorrar un poco de trabajo al nombrar objetos.
Los sinónimos no son más que objetos que nos sirven para asignarle un alias a otro objeto, si este está en un esquema podemos ahorrarnos entonces tener que escribir el esquema y el nombre completo del objeto.
Ejemplo:
Create Synonym AD For Audit2.Tbl_Eventos
Select * from Ad

Con esto terminamos el post sobre esquemas, solo quiero volver a mencionar la importancia de pensar en términos del menor esfuerzo, ¿Qué significa, que implica? Lo que implica es que sepamos diversas soluciones a un problema, que sepamos o que busquemos cual es la mejor y eso con el pasar del tiempo nos dara oportunidad de tener más tiempo que debemos ocupar en aprender, aprender y seguir aprendiendo. Es redondo, cuando nos preocupamos y ocupamos por lo importante antes que lo urgente nos hace mejores profesionistas. Recuerdo con afecto a un jefe hace años , bastante severo en algunas cosas, pero con mucha experiencia y la verdad con mucha sabiduría.

Un día llamandome la antencion me dijo :Charly imaginate que vas en carretera, conduces tu auto, te urge llegar a tu destino es urgentísimo llegar, pero te das cuenta que la gasolina no te alcanzara, ¿Qué debes hacer? La reflexion es que lo urgente es llegar , pero lo importante es que tengas como hacerlo, se tendria uno que dar un tiempo para recargar gasolina.

Así mismo en nuestro trabajo tenemos lo urgente y lo importante, lo importante es que nuestro conocimiento sea más amplio en buscar la mejor forma de resolver un problema lo cual nos llevara a ser mejores profesionistas y a encajar mejor en nuestro empleo, a poder ayudar de una mejor forma a la empresa para la que trabajamos y eso se los aseguro se traducirá en bienestar para nosotros.
Esto no quiere decir que debemos ser obsesivos con la perfección en más de una ocasión yo he tenido que decidir que así como esta mi código debe quedarse aunque no sea la mejor forma, porque si no nunca terminaríamos un proyecto.

Les dejo mis mejores saludos, y espero les sea de utilidad este post.

sábado, 22 de mayo de 2010

Guia de Instalaciòn de SQL Server

¿Como instalar SQL Server?, en esta ocasiòn la intención es compartirle un documento de Word que contiene los pasos y una breve explicación sobre cada uno de ellos. Me resulta a veces un poco complicado el tema de pegar imágenes en el post y como este tiene muchas decidí mejor subir el archivo y dejarles la liga.

http://docs.google.com/Doc?docid=0AdMayil2lBnHZGRzNHN3c3ZfMTk4YjY0OXpnaw&hl=en

o

Download ...
http://docs.google.com/leaf?id=0B9Mayil2lBnHYTRmODZiMjAtZGVlZi00ZmRmLWE1MzktMDFjYzhjNGM0NTYz&hl=en

Saludos y espero les sea de utilidad

jueves, 20 de mayo de 2010

Save Transaction

Hablemos un poco de transacciones, lo ideal es que nuestras peticiones a la Base de Datos sean ACID, que quiere decir Atómicas, Consistente, Aisladas y durables, siempre que una serie de instrucciones a la Base de datos cumple con estas características se denomina transacción.
ACID : (Atómicas)Lo que quiere decir es que nuestras peticiones sean ejecutadas como un solo bloque en el que se ejecuta todo o nada ,(Consistente) en el que se garantice que la integridad de nuestra base de datos no es alterada ,(Aisladas) que nuestras peticiones estén aisladas u ofrezcan un tipo de aislamiento que nos garantice que otras peticiones sobre los mismos datos no afecten las nuestras y que una vez que se ejecutaron los datos persistan y no se pierdan (durable).
El tema de las transacciones realmente es muy amplio, en este post únicamente pretendo abordar un escenario específico que son los puntos de retorno en una transacción.
En nuestra vida profesional nos puede tocar enfrentarnos a escenarios en los cuales tengamos algún proceso de SQL que requiera que podamos hacer un Rollback parcial. En otras palabras es posible con esto establecer puntos de retorno para nosotros en donde podamos deshacer solo parte de nuestra transacciòn.
Para poder crear puntos de retorno lo hacemos mediante la instrucciòn Save Transation [NOMBRE TRANSACTION] , en nuestro codigo podemos tener tantos como querramos y cuando necesitemos regresar a un punto en especifico lo que se ejecuta es RollBack Transaction [NOMBRE TRANSACTION].

Para dejar atrás la teoría necesitaremos crear una tabla que utilizaremos en nuestro ejemplo.
--Creamos la tabla que usaremos
If Not exists(Select * from sys.Objects where name='Tbl_Clientes'
and Type='U')
Create Table dbo.Tbl_Clientes
(
ClienteId Int Identity(1,1) Primary Key
,Nombre varchar(30) Not null
,[Fecha Registro] DateTime Not null Default Getdate()
)

GO

--
Begin Transaction
Insert into dbo.Tbl_Clientes(Nombre)
Values('Julio Rodriguez');
Insert into dbo.Tbl_Clientes(Nombre)
Values('Oscar Rodriguez');
Insert into dbo.Tbl_Clientes(Nombre)
Values('Lourdes Rodriguez');


Save Transaction PrimerBloque
Select ClienteId,Nombre,[Fecha REgistro] from dbo.Tbl_Clientes

Insert into dbo.Tbl_Clientes(Nombre)
Values('Omar Ramirez');
Insert into dbo.Tbl_Clientes(Nombre)
Values('Mario Ramirez');
Insert into dbo.Tbl_Clientes(Nombre)
Values('Fernando Ramirez');

Save Transaction SegundoBloque
Select ClienteId,Nombre,[Fecha REgistro] from dbo.Tbl_Clientes


Insert into dbo.Tbl_Clientes(Nombre)
Values('Dante Romero');
Insert into dbo.Tbl_Clientes(Nombre)
Values('Ignacio Romero');

Save Transaction TercerBloque
Select ClienteId,Nombre,[Fecha REgistro] from dbo.Tbl_Clientes


Rollback Transaction SegundoBloque
Select ClienteId,Nombre,[Fecha REgistro] from dbo.Tbl_Clientes


Commit Transaction;
GO
El punto a ejemplificar es que las posibilidades de lo que podemos hacer con esto ,pues estan en nuestras manos, nosotros podemos condicionar que bajo algun tipo de error o logica efectuemos un commit de nuestra transaccion y podamos hacer a un lado algo que se ejecuto pero que al final no nos interesa.
Espero les sea de utilidad

domingo, 16 de mayo de 2010

Intersect

Alguna vez platicaba con algunos compañeros expresándoles que SQL Server no solo son sentencias Insert y Select, es necesario que nosotros como desarrolladores conozcamos las diferentes ofertas que nos da SQL para resolver diferentes problemas.
En esta ocasión quiero platicarles de una nueva forma de unir tablas que esta disponible desde SQL Server 2005 y esto es con la instrucción Intersect.
Para poder ejemplificar y buscar dejar claro este tema crearemos un par de tablas y ejecutaremos una unión con INNER y la otra con Intersect para mostrar las diferencias.

Create Table dbo.Tbl_Clientes
(
IdCliente Int Identity(1,1) Primary key
,Nombre varchar(200)
,observaciones varchar(400)
)
Go

Insert into dbo.Tbl_Clientes
Values ('Cliente 1','NA');
Insert into dbo.Tbl_Clientes
Values ('Cliente 2','NA');
Insert into dbo.Tbl_Clientes
Values ('Cliente 3','NA');
Insert into dbo.Tbl_Clientes
Values ('Cliente 4','NA');

Go

Create Table dbo.Tbl_Facturas
(
IdFactura Int Identity(1,1) primary key
,idCliente Int Not null
,Fecha Datetime Not null Default Getdate()
,Iva float Not null
,Importe float Not null
,Total float Not null
,Observaciones varchar(max)
)
GO
Alter Table dbo.Tbl_Facturas add Constraint Fk_Cliente
Foreign key (idCliente) References dbo.Tbl_Clientes(IdCliente)
Select IdCliente,Nombre from dbo.Tbl_Clientes;
Insert dbo.Tbl_Facturas
Values (1,getdate(),16,100,116,'Factura A');

Insert dbo.Tbl_Facturas
Values (1,getdate(),16,100,116,'Factura A.1');

Insert dbo.Tbl_Facturas
Values (1,getdate(),16,100,116,'Factura A.2');

Insert dbo.Tbl_Facturas
Values (2,getdate(),16,100,116,'Factura B');

Insert dbo.Tbl_Facturas
Values (3,getdate(),16,100,116,'Factura A');

Insert dbo.Tbl_Facturas
Values (4,getdate(),16,100,116,'Factura A');

Select IdFactura ,idCliente ,Fecha ,Iva ,Importe ,Total ,Observaciones From dbo.Tbl_Facturas
Ahora veremos justamente las diferencias entre usar INNER e Intersect

Al crear un Select con una union interna veremos que nos regresa seis filas
Select Tbl_Clientes.IdCLiente from dbo.Tbl_Clientes
Inner join dbo.Tbl_Facturas on Tbl_Clientes.IDCliente=Tbl_Facturas.IDCliente

Si agregamos la condiciòn Distinct recuperamos cuatro filas
Select Distinct Tbl_Clientes.IdCLiente from dbo.Tbl_Clientes
Inner join dbo.Tbl_Facturas on Tbl_Clientes.IDCliente=Tbl_Facturas.IDCliente

Si hacemos uso de Intersect veremos que hace lo mismo que el anterior pero de una forma mas facil
Select IdCliente from dbo.Tbl_Clientes
Intersect
Select IdCliente from dbo.Tbl_Facturas


Es decir, Intersect devuelve los valores distintos retornados por las consultas del lado izquierdo y derecho de la consulta
La condición que tenemos para poder utilizar Intersect es que las consultas que estamos ejecutando tengan las mismas columnas y tipo de datos
Saludos y hasta la proxima

Download AdventureWorks

Practicar, practicar y volver a practicar, es la única forma de mejorar nuestro perfil, Microsoft nos proporciona ejemplos en su documentación y mayormente esta referida a las bases de datos de AdventureWorks. SQL Server 2005 y 2008 no instalan por default las bases de datos de ejemplo, si te interesa descargarlas puedes hacerlo desde el sitio:

http://sqlserversamples.codeplex.com/

En donde además encontraras ejemplos.

sábado, 15 de mayo de 2010

El Rol de un arquitecto

Hola a todos nuevamente, es un placer para mi poder contribuir en algo a las nuevas generaciones y porque no también a los que ya estamos encaminados, ciertamente todos los días se puede aprender algo.

Para comenzar este post quisiera poner una pregunta en la mesa ¿nos gusta desarrollar, te apasiona? ¿Quisieras cambiar de actividad y el desarrollo dejarlo en un par de años? . Creo que es válido que tengamos aspiraciones distintas, que nuestros gustos varíen. Si eres uno a quienes les apasiona la tecnología y el desarrollo seguramente esperas crecer tu perfil técnico, obtener alguna certificación y en algún momento erguirte como arquitecto de software.
¿Pero es suficiente saber mucho? En mi opinión la respuesta es no, un arquitecto no solo debe ser un experto en tecnología, no es suficiente eso. Un arquitecto debe ser un excelente comunicador, de hecho si leemos sobre la certificación de Microsoft veremos el peso que ellos mismos le dan a las habilidades blandas de los candidatos.
Un arquitecto efectivamente debe tener un excelente nivel no solo en aplicaciones, creo yo que también en Bases de datos pues uno es el complemento del otro, pero además debe ser capaz de poder compartir su experiencia y guiar a su equipo de trabajo y no hablo de dar un ejemplo sino de ser el ejemplo, de respeto a sus compañeros ,de buscar crear un ambiente de camaradas, en el que la gente se reconoce imperfecta y que sabe pedir ayuda y recibirla; pues siempre es mejor en un proyecto poder ahorrarnos tres días de búsqueda e investigación si en el equipo tenemos a un experto.
Un arquitecto también debe poder hablar en el lenguaje de los usuarios entenderlos y no marearlos porque para ellos sus requerimientos, su funcionalidad debe hablarse en el lenguaje del negocio.
Un arquitecto debe buscar ser un mentor para su equipo, debe apostar por el bienestar del grupo, de su gente. Porque sabe que su papel depende del papel del equipo y que es el pieza clave en la tarea de guiarlos al éxito
Un arquitecto no tiene miedo de compartir su conocimiento porque todos los días hará algo por saber más que el día anterior y sabe también que cuando enseña aprende dos veces, que cuando se vuelve en mentor de un equipo de una persona perdurara en alguien mas y eso es una satisfacción en sí misma.
Un arquitecto debe lograr ser asertivo, tolerante y que su acción sea el detonante para que la gente que trabaje con él o a su lado se esfuerce por ser un mejor profesional.
Un arquitecto no sustituye la labor de un Project Manager, la función de administrar el proyecto sigue siendo del PM

No quisiera redundar en esto, mejor los refiero a las publicaciones de Microsoft en donde la revista Architecture Journal en su número 15 nos habla del The Role of an Architect .
Los invito a subscribirse a http://msdn.microsoft.com/en-us/architecture/bb410935.aspx

En resumen yo diría que el expertice técnico no basta la actitud es importante, es lo más importante, con una buena actitud nos abrimos puertas, nos ganamos un amigo, vencemos la frustración porque les aseguro que más de una vez es la actitud la que nos sacara de un problema aun por encima de los conocimientos técnicos y es redondo porque es una buena actitud la que nos hace humildes, nos permite aprender de los demás sin miedo y es lo que nos mueve a buscar ser mejores.

No pretendo que este post tenga la definiciòn de el Rol de un arquitecto, mi intenciòn solo es enumerar algunas cualidades, espero les sea de utilidad.

lunes, 10 de mayo de 2010

Capacitaciòn gratuita Microsoft

Hola nuevamente ,siguiendo mis convicciones sobre "el conocimiento que no se comparte no se esta utilizando de la manera correcta" les quiero compartir un recurso que recien encontre en esta busqueda de sitios y foros que puedan nutrir.
En el podremos encontrar cursos y carreras, pero para que nos les cuente mejor visitenlo.
Creo que nuestra mentalidad en general debe ser de compartir, de ver crecer a los demas y alegrarnos por ellos, de no conformarnos y de aspirar siempre a un futuro mejor, cuando logramos en nosotros cambiar nuestro comportamiento podemos darle la vuelta al problema de cambiar a la gente, recuerden : No puedes cambiar a la gente pero puedes cambiar tu, si cambias tu ,quiza puedas cambiar a tu hermano a tus hijos , a tus compañeros y entonces podremos cambiar a la gente.

Auditar Base de datos usando Trigger DDL

Recientemente un compañero me pregunto si existía forma de identificar en que fecha se agrego una columna a una tabla en la base de datos, en ese momento respondí que no aunque mi respuesta no fue completa.

Quiero en este blog compartir como debió terminar esa conversación .
SQL Server nos permite auditar lo que sucede en nuestra Base de datos con diferentes mecanismos como C2 Audit ,SQL Server Audit o incluso una traza y desde la versión de SQL 2005 se agrego el uso de trigers DDL(Data Definition Language)
Como siempre hago hincapié en que el objetivo de tener diferentes formas de atacar un problema es para usar lo más adecuado a nuestras necesidades y circustancias, por ejemplo si tenemos problemas de desempeño activar un mecanismo como C2 Audit nos podría generar aun más problemas, aun mas quizá solo nos interesa auditar cambios a la estructura de nuestra BD y no un nivel granular.


Para nuestro caso utilizaremos desencadenadores DDL para auditar cambios en la estructura de nuestra Base de datos, específicamente capturaremos todos los eventos del tipo DDL_DATABASE_LEVEL_EVENTS,a continucación se muestra un arbol de eventos para tener presentes que tipo de acciones estaremos auditando.








Los eventos se recuperan con la función EVENTDATA la cual retorna un XML con el ID del proceso, la fecha, el evento y la sentencia ejecutada


El script para crear y probar nuestro disparador DDL es :


CREATE DATABASE AuditDDL –Creamos l

use AuditDDL

Create schema Audit


CREATE TABLE Audit.DDL_DATABASE_LEVEL_EVENTS_LOG
(
ID uniqueIdentifier primary key Default NewId()
, Fecha datetime Not null
, DB_Session_User nvarchar(100) Not null default Session_User
, DB_System_User nvarchar(100) Not null default System_User
, DB_User_Name nvarchar(100) Not null default User_Name()
, DB_Nombre varchar(100) Not null default Db_Name()
, Host nvarchar(50) Not null default Host_Name()
, Evento nvarchar(1000) Not null
, Sentencia nvarchar(Max) Not null
)


GO


CREATE TRIGGER LogEventos
ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS
AS
BEGIN
Set NoCount ON
DECLARE @data XML
,@Definicion varchar(1000)
SET @data = EVENTDATA();

INSERT Audit.DDL_DATABASE_LEVEL_EVENTS_LOG
( ID
, Fecha
, DB_Session_User
, DB_System_User
, DB_User_Name
, DB_Nombre
, Host
, Evento
, Sentencia )
VALUES
(NewId(),
GETDATE(),
CONVERT(nvarchar(100), Session_User),
CONVERT(nvarchar(100), System_User),
CONVERT(nvarchar(100), User_Name()),
CONVERT(nvarchar(100), Db_Name()),
CONVERT(nvarchar(100), Host_Name()),
@data.value('(/EVENT_INSTANCE/EventType)[1]', 'nvarchar(100)'),
@data.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'nvarchar(2000)')
) ;
END;
GO


Se que en la Web existen ejemplos similares e indudablemente en las referencias de Microsoft siempre encontraremos cada dato que necesitamos, mi idea es solo compartir algún ejemplo y en su caso mi interpretación ,creo que un ejemplo adicional nunca está de mas.

sábado, 8 de mayo de 2010

Reconocimiento Microsoft

Hola, les quiero compartir que hace unos días me llego un paquete por parte de Microsoft con un libro de certificación, una playera, una pluma, una carpeta, un tarjetero muy bonito de madera y un termo.

Antes de eso me habían escrito por mail para hacerme saber que me iban a enviar un reconocimiento por mi asistencia/participación en los eventos del 2009 este gesto considero es sumamente gratificante porque la realidad es que es un regalo solo por aprender. No dejen de buscar y asistir a eventos de Microsoft, en especial a los webcast que son sumamente enriquecedores.