tag:blogger.com,1999:blog-76125278796763390242024-02-14T13:52:10.272-06:00Código y DatosEn este blog podrás encontrar artículos que abordan temas diversos de SQL Server y .NET, espero te resulten de mucha utilidad.Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.comBlogger57125tag:blogger.com,1999:blog-7612527879676339024.post-28019432599274672262024-02-01T12:38:00.002-06:002024-02-01T12:40:13.780-06:00WRITELOG<div id="fb-root"></div>
<script async defer crossorigin="anonymous" src="https://connect.facebook.net/es_LA/sdk.js#xfbml=1&version=v9.0&appId=257852894317495&autoLogAppEvents=1" nonce="zDRTfgmA"></script>
<div class="fb-like" data-href="https://blog.jorgetoriz.com/2024/02/writelog.html" data-width="" data-layout="standard" data-action="like" data-size="small" data-share="true"></div>
<hr />
<p>Ya ha pasado mucho tiempo desde la última entrada que escribí en este blog, lamentablemente hasta me había olvidado de él y lo había dejado de lado para atender otros temas.</p><p>He querido retomarlo con un tema que me resultó muy interesante y que significó en una que otra discusión álgida con otro proveedor de un cliente.</p><p>Sucede que el cliente estaba experimentando una lentitud muy importante en las transacciones de índole de escritura, es decir los queries del tipo INSERT, DELETE, UPDATE, MERGE, etc.</p><p>¿Qué es lo primero que hay que hacer?, darle una revisada a lo que se está ejecutando en este momento, analizar lo que lleva más tiempo ejecutándose y el tipo de bloqueo que está reportando. Esta información la podríamos obtener con el siguiente query:</p><code>
SELECT ER.session_id, DB_NAME(ES.database_id) AS database_name, DATEDIFF(SECOND, ER.start_time, GETDATE()) AS seconds_elapsed, ER.command, ES.host_name, ES.program_name, ES.host_process_id, ER.blocking_session_id, ER.wait_type<br />
FROM sys.dm_exec_requests ER<br />
INNER JOIN sys.dm_exec_sessions ES<br />
ON ER.session_id = ES.session_id<br />
WHERE ES.is_user_process = 1<br />
AND ER.last_wait_type NOT IN ('BROKER_RECEIVE_WAITFOR', 'XE_TIMER_EVENT', 'XE_DISPATCHER_WAIT', 'XE_LIVE_TARGET_TVF', 'FT_IFTS_SCHEDULER_IDLE_WAIT')
</code><p>Me encontré con que había una lista importante de queries en ejecución y cuya cantidad de segundos transcurridos ya estaba por encima de los 10 segundos. Noté que el valor de la columna wait_type en casi todos los casos era "WRITELOG".</p><p>Si verificamos la documentación de este tipo de wait, encontraremos que está relacionado con el tiempo que le está llevando el escribir en el archivo de transacciones de las bases de datos, es decir que se está tardando en escribir en el sistema de almacenamiento.</p><p>Para confirmar la sospecha generé un medidor de rendimiento en el PerfMon y agregué el contador Physical Disk - Avg. Disk sec/Write seleccionando la unidad de almacenamiento donde se encontraban los archivos de registro de transacciones de las bases de datos.</p><p>¿Qué fue lo que encontré?, que la escritura le estaba tomando, en promedio, 170 milisegundos en ejecutarse. ¡Sí!, 170 milisegundos; no perdamos de vista que el tiempo máximo de escritura y de lectura es de 15 milisegundos, es decir que estaban muy por encima del tiempo máximo aceptable.</p><p>Después de correos, revisiones, discusiones, reuniones y llamadas; el proveedor del sistema de almacenamiento encontró (y aceptó) que el sistema de almacenamiento estaba teniendo problemas y que solamente tenía que cambiar las unidades problemáticas.</p><p>Hay veces que le echarán la culpa a SQL porque es lo que ven y lo que percibe tanto el usuario final como el departamento de TI de nuestro cliente, pero es importante tener información sólida a la mano para determinar que el problema no es el motor sino algún recurso que necesita el motor para ejecutarse correctamente.</p><p>¡Saludos y que este 2024 esté lleno de salud y de trabajo!</p>
<hr/>
<div class="fb-like" data-href="https://blog.jorgetoriz.com/2024/02/writelog.html" data-width="" data-layout="standard" data-action="like" data-size="small" data-share="true"></div>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-10368374675380173402022-08-12T17:59:00.001-05:002022-09-27T13:29:38.577-05:00CONVERT vs FORMATHace poco estaba revisando el código T-SQL de un cliente y le comentaba que resulta un poco complicado el deducir qué formato de fecha se estará devolviendo al usuario cuando utiliza la función CONVERT, por ejemplo:<div><br /></div><div><code>CONVERT(VARCHAR(10), GETDATE(), 23)</code><div><br /></div>
<div>De forma inmediata recordé que existe la función FORMAT que hace mucho más legible el código. Si tomamos como base la expresión que anteriormente mencioné, su versión en FORMAT sería:</div><div><br /></div><div><code>FORMAT(GETDATE(), 'yyyy-MM-dd')</code></div><div><br /></div><div>Es evidente que es muchísimo más legible, y de manera inevitable me brincó la pregunta, ¿cuál de las dos opciones es mejor? Para responder a esta pregunta he preparado un ejercicio bastante sencillo, pero que nos ayudará a comprender la enorme diferencia.</div></div><div><br /></div><div>Comencemos por crear nuestra tabla y llenarla con 500 mil registros:</div><div><br /></div>
<code>
CREATE TABLE TestFormat(<br />
Fecha DATETIME2(0)<br />
)<br />
GO<br />
SET NOCOUNT ON<br />
INSERT INTO TestFormat (Fecha)<br />
VALUES (DATEADD(MINUTE, ABS(CHECKSUM(NEWID())) % 15768000, '1992-01-01 00:00:00'))<br />
GO 500000<br />
</code>
<div><br /></div><div>Vamos a comenzar por obtener las fechas en el formato yyyy-MM-dd con CONVERT y tomaremos los tiempos de ejecución. Para tener una mejor perspectiva, ejecutaremos el ejercicio tres veces.</div><div><br /></div>
<code>
SET STATISTICS TIME ON<br />
SELECT CONVERT(VARCHAR(10), Fecha, 23)<br />
FROM TestFormat<br />
</code>
<div><br /></div><div>Y después vamos a obtener el mismo formato pero utilizando la función FORMAT. Al igual que en el ejercicio anterior, vamos a ejecutarlo unas tres veces para tener más datos de comparación.</div><div><br /></div><div>
<code>
SET STATISTICS TIME ON<br />
SELECT FORMAT(Fecha, 'yyyy-MM-dd')<br />
FROM TestFormat<br />
</code>
<div><br /></div></div><div>La diferencia en las métricas es muy importante:</div><div><br /></div><div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEhyKutCbqpdlb_ITYlcEwx0AJUImm1sgOnYB3jMUTNsEmF3ro6XFsHiK8xQb62UIQD2wqhHTTK7n2EFFHna3EW91Eq9TSXIjBqGlTRtyRVVDhjL99Sv5fn8epq33ujeYysDPxWm0P1sS69v9pt_HQAacyp3R4y-DgB9nMIHamFEEOccbfd2ckb7P7QT" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="125" data-original-width="263" height="152" src="https://blogger.googleusercontent.com/img/a/AVvXsEhyKutCbqpdlb_ITYlcEwx0AJUImm1sgOnYB3jMUTNsEmF3ro6XFsHiK8xQb62UIQD2wqhHTTK7n2EFFHna3EW91Eq9TSXIjBqGlTRtyRVVDhjL99Sv5fn8epq33ujeYysDPxWm0P1sS69v9pt_HQAacyp3R4y-DgB9nMIHamFEEOccbfd2ckb7P7QT" width="320" /></a></div><div class="separator" style="clear: both; text-align: center;"><br /></div><div class="separator" style="clear: both; text-align: left;"><br /></div>Ahora vamos a hacer el mismo ejercicio pero obteniendo la fecha y hora en formato yyyy-MM-dd HH:mm:ss. Con CONVERT quedaría de la siguiente manera:</div>
<div><br /></div>
<div>
<code>
SET STATISTICS TIME ON<br />
SELECT CONCAT(CONVERT(VARCHAR(10), Fecha, 23), ' ', CONVERT(VARCHAR(10), Fecha, 24))<br />
FROM TestFormat<br />
</code>
<div><br /></div><div>Y con FORMAT quedaría de la siguiente manera:</div><div><br /></div>
<code>
SET STATISTICS TIME ON<br />
SELECT FORMAT(Fecha, 'yyyy-MM-dd HH:mm:ss')<br />
FROM TestFormat<br />
</code>
<div><br /></div><div>En la imagen siguiente podremos notar que el tiempo de CPU de CONVERT se incrementa y que el tiempo de CPU de FORMAT se mantiene muy parecido:</div><div><br /></div><div><div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/a/AVvXsEh9JheQGNLTpMYHUa83bE34IS4AFMFW_XZr-6BvkTrkHQIYcUrn-F3lMe1Qf-IdimT_E5ATulceHpGA1a1-KAeJrGzhW0-UceGB516prfHtjVz8OBfzs_GxaxrD5hicfErTZLi7vPlGVIVD6hzc3PCHa54L8hzhts8dTUX8lRTPrJDjc_VI5W0MQjpt" style="margin-left: 1em; margin-right: 1em;"><img alt="" data-original-height="128" data-original-width="264" height="155" src="https://blogger.googleusercontent.com/img/a/AVvXsEh9JheQGNLTpMYHUa83bE34IS4AFMFW_XZr-6BvkTrkHQIYcUrn-F3lMe1Qf-IdimT_E5ATulceHpGA1a1-KAeJrGzhW0-UceGB516prfHtjVz8OBfzs_GxaxrD5hicfErTZLi7vPlGVIVD6hzc3PCHa54L8hzhts8dTUX8lRTPrJDjc_VI5W0MQjpt" width="320" /></a></div><br />Pero ni con el incremento de tiempo de CPU en el formato yyyy-MM-dd HH:mm:ss nos acercamos al tiempo de CPU de FORMAT, porque FORMAT se ejecuta casi 10 veces más lento cuando incluimos la hora y casi 20 veces más lento cuando únicamente tenemos la fecha.</div></div><div><br /></div><div>¿Qué factores debemos tomar en cuenta para entender estos datos?</div><div><ol style="text-align: left;"><li>FORMAT está implementada a través de un ensamblado de .NET, es decir que se está ejecutando de forma externa.</li><li>CONVERT se va al doble cuando hay que incluir la hora porque podemos ver que hay dos ejecuciones de la función, una para obtener la fecha y otra para obtener la hora.</li></ol><div>La recomendación que te puedo hacer, es que en el código incluyan un comentario donde se indique el formato de fecha que se estará obteniendo, por ejemplo:</div></div>
<div><br /></div>
<code>
--yyyy-MM-dd HH:mm:ss<br />
SELECT CONCAT(CONVERT(VARCHAR(10), Fecha, 23), ' ', CONVERT(VARCHAR(10), Fecha, 24))<br />
FROM TestFormat<br />
</code><div><br /></div><div>De esta forma será mucho más sencillo el darle mantenimiento al código porque podremos ver de forma inmediata lo que se está obteniendo. La programación considerada no es una buena práctica, es una obligación.</div><div><br /></div><div>Espero te haya resultado útil esta entrada.</div>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-64688616492029486402021-05-28T18:55:00.003-05:002021-05-29T10:22:11.395-05:00PAGEIOLATCH_EX<div id="fb-root"></div>
<script async defer crossorigin="anonymous" src="https://connect.facebook.net/es_LA/sdk.js#xfbml=1&version=v9.0&appId=257852894317495&autoLogAppEvents=1" nonce="zDRTfgmA"></script>
<div class="fb-like" data-href="https://blog.jorgetoriz.com/2021/05/pageiolatchex.html" data-width="" data-layout="standard" data-action="like" data-size="small" data-share="true"></div>
<hr />
<p>He querido hacer esta entrada porque este tipo de wait es muy buscado, poco entendido y puede significar un dolor de cabeza.</p><p>El día de hoy recibimos el reporte de un cliente de que su sistema estaba sumamente lento, que la cantidad de timeouts estaba siendo insoportable y que estaba afectando seriamente la operación.</p><p>Al ingresar y visualizar la lista de sentencias que estaban en ejecución, notamos que la mayoría de ellas estaban con un wait del tipo PAGEIOLATCH_EX o del tipo PAGEIOLATCH_SH.</p><p>Primero, ¿qué diferencia existe entre un PAGEIOLATCH_XX y un PAGELATCH_XX?, es simple, el primer grupo se refiere a eventos de lectura y escritura desde el sistema de almacenamiento y el segundo se refiere a eventos de lectura y escritura sobre el buffer de datos de SQL Server. En nuestro caso notamos que todas las esperas eran del tipo PAGEIOLATCH_XX.</p><p>Utilizando la función <a href="https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-io-virtual-file-stats-transact-sql?view=sql-server-ver15">sys.dm_io_virtual_file_stats</a> obtuvimos información acerca de los tiempos de espera para actividades de IO desde y hacia el sistema de almacenamiento por cada uno de los archivos de la base de datos del ERP. El query utilizado fue el siguiente:</p>
<code>
SELECT F.name, F.filename, FS.io_stall_read_ms / FS.num_of_reads END AS avg_read_wait_ms, FS.io_stall_write_ms / FS.num_of_writes AS avg_write_wait_ms<br/>
FROM sys.sysfiles F<br/>
CROSS APPLY sys.dm_io_virtual_file_stats(DB_ID(), F.fileid) FS</code><br /><br/>
Los resultados que obtuvimos nos indicaron problemas catastróficos sobre la unidad de almacenamiento donde están ubicados los archivos de datos de la base de datos del ERP. Recordemos que el tiempo de espera ideal no debe de pasar de los 15 milisegundos.</p><p>El personal de infraestructura nos ayudó a verificar la situación actual del arreglo de discos y fue posible determinar que uno de los tres discos que formaban el RAID estaba dando muchos problemas, y es por ello que los tiempos de espera de lectura y escritura estaban por los cielos.</p><p>Se quitó el disco del arreglo y después de que pasó algún tiempo en lo que SQL terminaba de "calentar" la memoria, el rendimiento volvió a su estado óptimo.</p><p>Espero te resulte de utilidad esta información, nos vemos la siguiente.</p>
<hr/>
<div class="fb-like" data-href="https://blog.jorgetoriz.com/2021/05/pageiolatchex.html" data-width="" data-layout="standard" data-action="like" data-size="small" data-share="true"></div>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-28434798869357954392021-01-30T23:09:00.006-06:002021-02-02T13:08:17.650-06:00Login failed for user 'X'<div id="fb-root"></div>
<script async defer crossorigin="anonymous" src="https://connect.facebook.net/es_LA/sdk.js#xfbml=1&version=v9.0&appId=257852894317495&autoLogAppEvents=1" nonce="zDRTfgmA"></script>
<div class="fb-like" data-href="https://blog.jorgetoriz.com/2021/01/login-failed-for-user-x.html" data-width="" data-layout="standard" data-action="like" data-size="small" data-share="true"></div>
<hr />
<div>Hace bastante tiempo que no venía por aquí para compartir otra entrada en relación a SQL Server o .NET. Pero en esta ocasión la situación lo amerita,
debido que un cliente me pidió solucionar un problema de conexión que jamás había visto en 18 años y me gustaría compartir lo que he ido haciendo para encontrar la solución.</div><div><br /></div><div>Problema: No te puedes conectar con el SSMS a la instancia local utilizando autenticación de SQL Server, el mensaje que devuelve al momento de conectarse es el famosísimo:</div><div><br /></div><div>Login failed for user 'X' (Microsoft SQL Server, Error: 18456)</div><div><br /></div><div>Si abres la ventana para obtener mayor información del error te muestra:</div><div><br /></div><div>Server Name: .</div><div>Error Number: 18456</div><div>Severity: 14</div><div>State: 1</div><div>Line Number: 65536</div><div><br /></div><div>Y si vas al archivo ErrorLog de la instancia te encuentras algo así:</div><div><br /></div><div><div>Logon Error: 18456, Severity: 14, State: 8.</div><div>Logon Login failed for user 'X'. Reason: Password did not match that for the login provided. [CLIENT: <local machine>]</div></div><div><br /></div><div>Antes de que comiences a pensar en lo que podría ser, te comparto la configuración verificada:</div><div><ol style="text-align: left;"><li>Autenticación de SQL Server habilitada en la instancia y ésta ya fue reiniciada en cuanto fue aplicado el cambio.</li><li>Ya se verificó que la contraseña sea correcta, inclusive estableciendo la contraseña en el login copiando su valor desde un bloc de notas.</li><li>Debido a que la conexión es local, ya se verificó que el protocolo Shared Memory esté habilitado.</li><li>Service Pack 4 instalado sobre la instancia (SQL Server 2012) y aplicado también en el SQL Server Management Studio.</li></ol><div>Pruebas que ya se realizaron y su resultado:</div></div><div><ol style="text-align: left;"><li>Conexión local utilizando sqlcmd y con autenticación de SQL: la conexión fue exitosa.</li><li>Conexión remota con SQL Server Management Studio y autenticación de SQL: la conexión fue exitosa. </li></ol>Con todo lo anterior podemos saber que el problema no es la autenticación debido a que sí se ha podido establecer una conexión tanto local como remota con autenticación de SQL Server. El único lugar donde está fallando la conexión es en el SSMS local, ya se intentó por ".", "localhost" y por dirección IP, y el resultado siempre es el mismo.</div><div><br /></div><div>Para poder averiguar un poco más de lo que podría estar haciendo el SSMS al momento de intentar iniciar la conexión con autenticación de SQL Server, utilizamos el <a href="https://docs.microsoft.com/en-us/sysinternals/downloads/procmon" target="_blank">procmon de sysinternals</a> pero nada de lo que encontramos ayudó a intentar identificar el problema.</div><div><br /></div><div>Releí el mensaje de error que se almacena en el archivo ErrorLog de la instancia y encontré algo extraño, que está reportando que la contraseña <b>no es correcta</b> para el login. Lo que me parece súper extraño por lo siguiente:</div><div><ul style="text-align: left;"><li>Si la contraseña la copio desde un bloc de notas y la pego en la línea de comandos con el sqlcmd, sí se conecta.</li><li>Si la contraseña la copio desde un bloc de notas y la pego en la caja de texto de contraseña del SSMS, ¡no conecta!</li></ul><div>La siguiente prueba que se me ocurrió fue cambiarle la contraseña al login "X" y ponerle una contraseña vacía, ¿y qué crees?, ¡SÍ CONECTÓ DESDE EL SSMS!</div></div><div><br /></div><div>Lo anterior me lleva a pensar que por alguna muy extraña razón (y me parece extraña porque al momento de escribir esto la desconozco) la contraseña escrita en la caja de texto de la pantalla de ingreso del SSMS, está siendo alterada de alguna manera antes de llegar a SQL Server.</div><div><br /></div><div>Para no dejar algo de lado y sin probar, se me ocurrió hacer un programa que pidiera servidor, usuario y contraseña para conectarse, en los framework 2.0, 3.5, 4 y 4.5. Todas las pruebas fueron exitosas.</div><div><br /></div><div>No encontré alguna referencia del lenguaje de programación utilizado para el SSMS 2012, no sé si fue hecho en C++ o en el .NET Framework. Pero para verificar que no solamente fuera local el problema, generé un login en otro servidor de base de datos de pruebas y que está expuesto a la internet, hice la prueba de conexión ¡y no conectó!, el error reportado en el otro servidor es el mismo: Password did not match that for the login provided.</div><div><br /></div><div>Esto comienza a preocuparme un poco más de lo normal, primero por la incertidumbre de no saber lo que está sucediendo, y segundo porque la contraseña que escribes en la caja de texto no es la misma que le está llegando al servidor destino, ¿habrá sido comprometido el servidor?, lo revisaré más a fondo para ver si encuentro algo extraño (sí, más)</div><div><br /></div><div>En cuanto tenga más noticias, regresaré a actualizar esta entrada.</div><div><br /></div><div>Se realizó una revisión de componentes del sistema operativo y también se corrió una revisión completa contra malware, ambas operaciones no reportaron problemas.</div><div><br /></div><div>Lamentablemente, no se encontró otra alternativa que reinstalar el SQL Server Management Studio y el problema dejó de ocurrir. Y pongo "lamentablemente" porque no me deja satisfecho la solución, pero el tiempo ya se tenía encima y no había forma de seguir investigando para encontrar el problema de raíz.</div>
<hr/>
<div class="fb-like" data-href="https://blog.jorgetoriz.com/2021/01/login-failed-for-user-x.html" data-width="" data-layout="standard" data-action="like" data-size="small" data-share="true"></div>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-89957746090360556382018-10-29T18:38:00.001-06:002018-10-29T18:41:53.401-06:00Leyendo RSS desde SQL Server <div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2018/10/leyendo-rss-desde-sql-server.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
<p>En esta entrada quiero abordar una forma en la que podemos obtener información de un RSS e integrarla a una base de datos.</p>
<p>Es común que haya información que resulte relevante o interesante para la operación de una empresa y ésta se encuentre disponible a través de fuentes RSS, el cual es un formato bien definido a través de un esquema XML que podemos encontrar en <a href="http://cyber.law.harvard.edu/rss/rss.html" target="_blank">RSS 2.0 at Harvard Law</a>.</p>
<p>Elegí esa dirección para mostrar el esquema del documento porque siento que la documentación ahí mostrada está ordenada de una forma muy sencilla de entender.</p>
<p>Para implementar una solución que nos permita obtener fuentes RSS necesitamos de un módulo que lea la información de la URL donde está ubicado el XML y posteriormente se lo dé a SQL Server para que tome el XML y lo convierta a una representación entidad-relación que es mucho más manejable.</p>
<p>Este proyecto lo dividiremos en los siguientes pasos:</p>
<ol>
<li>Tomar una fuente RSS como muestra.</li>
<li>Crear un módulo en .NET que pueda ser integrado a SQL Server.</li>
<li>Crear un assembly en SQL Server y crear la función que devuelva la información contenida en la URL que reciba como parámetro.</li>
</ol>
<h2>URL de muestra</h2>
<p>Tomaré como base el <a href="http://www.espn.com/espn/rss/news" rel="nofollow" target="_blank">RSS de deportes</a> del sitio web de ESPN, si abrimos la liga nos mostrará un XML con el contenido de la fuente.</p>
<h2>Módulo de .NET</h2>
<p>Voy a crear un proyecto del tipo Biblioteca de Clases en Visual Studio 2017 y lo llamaré SQLRSSReader, en él crearemos un par de clases:</p>
<ol>
<li>RSSItem para representar cada entrada de la fuente RSS.</li>
<li>Lector para implementar la función que SQL Server llamará.</li>
</ol>
<p>A continuación el código de RSSItem:</p>
<code>
namespace SQLRSSReader<br />
{<br />
public class RSSItem<br />
{<br />
public string Title { get; set; }<br />
public string Description { get; set; }<br />
public string Link { get; set; }<br />
public string PubDate { get; set; }<br />
}<br />
}
</code>
<p>Para exponer una función tabular a SQL Server, es necesario que se den las siguientes condiciones:</p>
<ol>
<li>Tener el atributo SqlFunctionAttribute.</li>
<li>Pública y estática.</li>
<li>Devolver IEnumerable.</li>
<li>Los tipos de parámetros de entrada deben ser de SQL Server.</li>
</ol>
<p>La función quedaría de la siguiente manera:</p>
<code>
[SqlFunction(FillRowMethodName = "LlenarItem")]<br />
public static IEnumerable ObtenerRSS(SqlString url)<br />
{<br />
HttpWebRequest webRequest = HttpWebRequest.Create(url.Value) as HttpWebRequest;<br />
HttpWebResponse webResponse = webRequest.GetResponse() as HttpWebResponse;<br />
List<RSSItem> result = null;<br />
<br />
using (StreamReader sr = new StreamReader(webResponse.GetResponseStream()))<br />
{<br />
XmlDocument xdRSS = new XmlDocument();<br />
xdRSS.LoadXml(sr.ReadToEnd());<br />
result = new List<RSSItem>();<br />
foreach (XmlNode xnItem in xdRSS.SelectNodes("/rss/channel/item"))<br />
{<br />
result.Add(new RSSItem()<br />
{<br />
Description = (xnItem.SelectSingleNode("./description") != null ? xnItem.SelectSingleNode("./description").InnerText : null),<br />
Link = (xnItem.SelectSingleNode("./link") != null ? xnItem.SelectSingleNode("./link").InnerText : null),<br />
PubDate = (xnItem.SelectSingleNode("./pubDate") != null ? xnItem.SelectSingleNode("./pubDate").InnerText : null),<br />
Title = (xnItem.SelectSingleNode("./title") != null ? xnItem.SelectSingleNode("./title").InnerText : null)<br />
});<br />
}<br />
}<br />
<br />
return result;<br />
}
</code>
<p>Lo que hace nuestra función es descargar el RSS, llenar una lista de objetos RSSItem y devolverla como resultado.</p>
<p>Notarás que el atributo SqlFunction tiene un parámetro llamado FillRowMethodName, en él se coloca el nombre de la función que se mandará a llamar para llenar la tabla resultante. Por cada objeto que contenga IEnumerable se llamará a la función a la que apunta FillRowMethodName.</p>
<p>Ahora veamos la función LlenarItem:</p>
<code>
public static void LlenarItem(object rssItemObject, out SqlString Title, out SqlString Description, out SqlString Link, out SqlString PubDate)<br />
{<br />
RSSItem rssItem = rssItemObject as RSSItem;<br />
if (rssItem.Title != null)<br />
Title = new SqlString(rssItem.Title);<br />
else<br />
Title = SqlString.Null;<br />
if (rssItem.Description != null)<br />
Description = new SqlString(rssItem.Description);<br />
else<br />
Description = SqlString.Null;<br />
if (rssItem.Link != null)<br />
Link = new SqlString(rssItem.Link);<br />
else<br />
Link = SqlString.Null;<br />
if (rssItem.PubDate != null)<br />
PubDate = new SqlString(rssItem.PubDate);<br />
else<br />
PubDate = SqlString.Null;<br />
}
</code>
<p>Vamos a analizar los parámetros uno por uno:</p>
<ol>
<li>rssItemObject. Es donde recibiremos la instancia de RSSItem que se quiere operar en ese momento.</li>
<li>Title. Primera columna de la tabla resultante.</li>
<li>Description. Segunda columna de la tabla resultante.</li>
<li>Link. Tercera columna de la tabla resultante.</li>
<li>PubDate. Cuarta columna de la tabla resultante.</li>
</ol>
<p>También debemos notar que cada una de las columnas tiene "out", eso nos obliga a darle un valor (aunque sea null) antes de terminar la ejecución de la función.</p>
<p>Compilemos nuestro proyecto para generar el DLL correspondiente.</p>
<h2>Función en SQL Server</h2>
<p>Para probar nuestra función vamos a crear una base de datos llamada SQLBlog y le habilitaremos la opción TrustWorthy para que se le considere una base de datos de confianza.</p>
<code>
USE master<br />
GO<br />
IF EXISTS (SELECT name FROM sys.databases WHERE name = N'SQLBlog')<br />
BEGIN<br />
ALTER DATABASE SQLBlog<br />
SET SINGLE_USER<br />
WITH ROLLBACK IMMEDIATE;<br />
DROP DATABASE SQLBlog<br />
END<br />
GO<br />
CREATE DATABASE SQLBlog<br />
ON PRIMARY (<br />
NAME = 'SQLBlog_dat',<br />
FILENAME = 'C:\LaCarpeta\SQLBlog.mdf'<br />
)<br />
LOG ON (<br />
NAME = 'SQLBlog_log',<br />
FILENAME = 'C:\LaCarpeta\SQLBlog.ldf'<br />
)<br />
GO<br />
ALTER DATABASE SQLBlog<br />
SET TRUSTWORTHY ON
</code>
<p>Ahora vamos a crear el ensamblado que alojará el DLL que acabamos de crear en .NET:</p>
<code>
USE SQLBlog<br />
GO<br />
CREATE ASSEMBLY SQLRSSReader<br />
FROM 'C:\ElEnsamblado\SQLRSSReader.dll'<br />
WITH PERMISSION_SET = UNSAFE
</code>
<p>Finalmente creamos la función y la probamos:</p>
<code>
CREATE FUNCTION udfGetRSS(<br />
@URL NVARCHAR(256)<br />
)<br />
RETURNS TABLE(<br />
Title NVARCHAR(200),<br />
Description NVARCHAR(4000),<br />
Link NVARCHAR(256),<br />
PubDate NVARCHAR(50)<br />
)<br />
AS EXTERNAL NAME SQLRSSReader.[SQLRSSReader.Lector].ObtenerRSS<br />
GO<br />
SELECT * FROM udfGetRSS(N'http://www.espn.com/espn/rss/news')
</code>
<p>Antes de terminar con esta entrada quiero que notes lo siguiente:</p>
<ol>
<li>El ensamblado debe ser creado con PERMISSION_SET en UNSAFE porque va a realizar lectura de sitios web, es decir que necesita salir del servidor.</li>
<li>Las cadenas expuestas por la función deben ser del tipo UNICODE, por ello notarás que son NVARCHAR.</li>
<li>La notación para apuntar a la función es la siguiente: AssemblyName.[Namespace.Class].Function</li>
</ol>
<p>Prueba con otra dirección, por ejemplo: <a href="https://www.tendencias21.net/xml/syndication.rss" rel="nofollow" target="_blank">https://www.tendencias21.net/xml/syndication.rss</a></p>
<p>Espero te haya resultado de utilidad esta entrada, ¡saludos!</p>
<hr />
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2018/10/leyendo-rss-desde-sql-server.html" data-send="true" data-show-faces="false" data-width="450">
</div>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-24451328677074164382018-10-25T09:00:00.000-05:002018-10-25T09:00:05.553-05:00Vistas, funciones y procedimientos almacenados<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2018/10/funciones-vistas-y-procedimientos-almacenados.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
¿Para qué utilizar vistas, funciones y procedimientos almacenados?, ¿solamente para que se vea bonita la base de datos con muchos objetos de todos los tipos posibles?<br />
<br />
Tenemos dos objetivos principales cuando ocupamos estos objetos en nuestra base de datos:<br />
<br />
<ol>
<li>Encapsular la forma en que se obtienen los datos de la base de datos.</li>
<li>Implementar una capa de seguridad por encima del acceso a los datos.</li>
</ol>
<p>
<b><span style="font-size: large;">Encapsulamiento</span></b>
</p>
<p>
Para atacar este tema lo voy a dividir en dos partes, en "Vistas y funciones" y en "Procedimientos almacenados".
</p>
<p>
<b>Funciones y vistas</b>
</p>
<p>
No sé si te haya tocado trabajar con la base de datos de algún ERP o con la base de datos de un sistema a cuyo programador le resultó divertido ofuscar los nombres de los objetos.
</p>
<p>
La irremediable consecuencia de lo anterior es que la extracción de datos se vuelve un dolor de cabeza debido a que los nombres de los objetos no es descriptivo por sí mismo.
</p>
<p>
Supongamos que tenemos una tabla de este estilo:
</p>
<code>
CREATE TABLE XT56(<br />
A INT IDENTITY(1, 1),<br />
B VARCHAR(80) NOT NULL,<br />
C DECIMAL(8, 2) NOT NULL,<br />
D BIT NOT NULL DEFAULT 1,<br />
CONSTRAINT pkXT56 PRIMARY KEY (Id)<br />
)
</code>
<p>
Resulta muy complejo establecer para qué es la tabla XT56 y el objetivo funcional de la información que está guardando. Si después de un trabajo de revisión del sistema y de la base de datos determinamos que es la tabla de productos, bien podríamos hacer lo siguiente para clarificar la extracción de los productos:
</p>
<code>
CREATE VIEW vProductos<br />
AS<br />
SELECT A AS Id, B AS Nombre, C AS Precio, D AS Activo<br />
FROM XT56
</code>
<p>
Al utilizar esta vista podremos obtener la información de una forma mucho más clara y entendible.
</p>
<p>
¿Qué pasaría si frecuentemente estamos obteniendo la lista de productos activos cuyo precio sea mayor o igual a un valor X? Podemos encapsular el código para clarificar la forma en que devuelve la información y también para reutilizar un algoritmo que ya resolvió un problema en específico:
</p>
<code>
CREATE FUNCTION udfDameProductosPorPrecio(<br />
@Precio DECIMAL(8, 2)<br />
)<br />
RETURNS TABLE<br />
AS<br />
RETURN<br />
SELECT A AS Id, B AS Nombre, C AS Precio<br />
FROM XT56<br />
WHERE C = @Precio<br />
AND D = 1<br />
</code>
<p>
A través del encapsulamiento podemos proyectar la información de la forma en la que lo necesitamos y también crear objetos que nos permitan reutilizar algo que ya fue resuelto previamente.
</p>
<p>
<b>Procedimientos almacenados</b>
</p>
<p>
En un grupo de programación en T-SQL siempre habrá alguien que tenga una forma óptima de codificar y que resuelva los problemas utilizando la menor cantidad de recursos posibles.
</p>
<p>
Una excelente idea es que esta persona se encargue de resolver los problemas operativos (en la base datos) más complejos y encapsule el algoritmo dentro de un procedimiento almacenado, de manera tal que éste pueda ser utilizado por otros sin tener que estarse quebrando la cabeza en cómo resolver un problema.
</p>
<p>
<b>Planes de ejecución</b>
</p>
<p>
Otra gran ventaja de los objetos parametrizados (funciones y procedimientos almacenados) es que permiten que se almacene el plan de ejecución de éstos y así no tenga que recalcularse la siguiente ocasión que se requiera su utilización.
</p>
<p>
El caché de planes de ejecución es una cosa muy valiosa de SQL Server debido a que reduce el trabajo de uno de los procesos más caros, el cálculo del plan de ejecución.
</p>
<p>
<b><span style="font-size: large;">Capa de seguridad</span></b>
</p>
<p>
Habitualmente no es buena idea que se tenga acceso directo de lectura y/o escritura a las tablas de una base de datos. En este momento se me ocurren por lo menos las siguientes X razones:
</p>
<p>
<ol>
<li>El usuario debe poder leer datos de una tabla, pero ella contiene información confidencial a la que no debería tener acceso el usuario. Es decir, solamente necesita acceso a un subconjunto de columnas.</li>
<li>El usuario debe poder insertar datos pero necesitamos estar absolutamente seguros que la información que ingrese sea consistente con la especificación de requerimientos.</li>
<li>Si el usuario tiene permiso de escritura sobre la tabla, entonces podrá eliminar información y hacerlo de forma equivocada.</li>
</ol>
<p>
Seguramente habrá más de uno que inmediatamente pensó "pero si le puedo dar permisos muy granulares a un usuario de manera tal que solamente pueda leer o escribir ciertas columnas". Coincido totalmente en que se podría, pero la administración de la seguridad se haría un dolor de cabeza y el dar esos permisos granulares no nos asegura que la información insertada sea adecuada.
</p>
<p>
Supongamos que tenemos una tabla como la siguiente:
</p>
<code>
CREATE TABLE Productos(<br />
Id INT IDENTITY(1, 1),<br />
Nombre VARCHAR(80) NOT NULL,<br />
Precio DECIMAL(8, 2) NOT NULL,<br />
Activo BIT NOT NULL DEFAULT 1,<br />
CONSTRAINT pkProductos PRIMARY KEY (Id)<br />
)
</code>
<p>
Si un usuario únicamente tuviera permitido actualizar precios, podríamos crearle un procedimiento como el siguiente:
</p>
<code>
CREATE PROC pActualizaPrecioProducto(<br />
@Id INT,<br />
@Precio DECIMAL(8, 2)<br />
)<br />
AS<br />
BEGIN<br />
UPDATE Productos<br />
SET Precio = @Precio<br />
WHERE Id = @Id<br />
END
</code>
<p>
Vamos a mejorar este tema, vamos a crear una tabla de histórico de precios guardando auditoría de quién lo ejecutó:
</p>
<code style="overflow-x: auto;">
CREATE TABLE ProductosHistoricoPrecio(<br />
Id INT IDENTITY(1, 1)<br />
Id_Producto INT NOT NULL,<br />
PrecioAnterior DECIMAL(8, 2),<br />
PrecioNuevo DECIMAL(8, 2),<br />
Usuario SYSNAME,<br />
Fecha DATETIME2(0) NOT NULL DEFAULT GETDATE(),<br />
CONSTRAINT pkProductosHistoricoPrecio PRIMARY KEY (Id),<br />
CONSTRAINT fkProductosHistoricoPrecio_Productos FOREIGN KEY (Id_Producto) REFERENCES Productos (Id)<br />
)
</code>
<p>
Y nuestro procedimiento almacenado quedaría de la siguiente manera:
</p>
<code>
CREATE PROC pActualizaPrecioProducto(<br />
@Id INT,<br />
@Precio DECIMAL(8, 2)<br />
)<br />
AS<br />
BEGIN<br />
BEGIN TRAN<br />
BEGIN TRY<br />
INSERT INTO HistoricoProductosPrecio (Id_Producto, PrecioAnterior, PrecioNuevo, Usuario)<br />
SELECT Id, Precio, @Precio, SUSER_NAME()<br />
FROM Productos<br />
WHERE Id = @Id<br />
<br />
UPDATE Productos<br />
SET Precio = @Precio<br />
WHERE Id = @Id<br />
<br />
COMMIT<br />
END TRY<br />
BEGIN CATCH<br />
ROLLBACK<br />
<br />
DECLARE @Error VARCHAR(4000) = ERROR_MESSAGE()<br />
THROW 50001, @Error, 1<br />
END CATCH<br />
END
</code>
<p>
Si a nuestro usuario le damos permiso de ejecución sobre el procedimiento almacenado:
</p>
<ol>
<li>No será necesario que tenga permiso de escritura sobre HistoricoProductosPrecio ni sobre Productos.</li>
<li>No es necesario que sepa que se está guardando un histórico de precios, tarea que podría olvidar realizar si él actualizara directamente el precio del producto.</li>
</ol>
<p>Espero te haya resultado de utilidad esta entrada, ¡saludos!</p>
<hr />
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2018/10/funciones-vistas-y-procedimientos-almacenados.html" data-send="true" data-show-faces="false" data-width="450">
</div>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-74802301850945585552018-10-24T15:18:00.001-05:002018-10-25T08:53:00.772-05:00optimize for ad hoc workloads<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="https://blog.jorgetoriz.com/2018/10/optimize-for-ad-hoc-workloads.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
<p>Ya tiene mucho tiempo que no me metía a publicar alguna entrada en mi blog. Hay muchas razones para explicarlo, pero intentaré tener ventanas de tiempo que me permitan seguir creando más entradas y compartir un poco de lo que he podido aprender a lo largo de estos años.</p>
<p>Durante los proyectos de consultoría en SQL Server en los que he estado trabajando de un tiempo para acá, me he encontrado algo muy interesante, que la memoria de SQL está sumamente ocupada almacenando planes de ejecución.</p>
<p>Antes de abordar el tema de este blog de forma directa, me gustaría platicar un poco de los planes de ejecución para que se entienda de mejor manera el impacto que tendrá la opción optimize for ad hoc workloads de SQL Server.</p>
<p>Cuando mandas a ejecutar un query, SQL server realiza una revisión sintáctica y semántica del mismo, una vez que ha pasado la validación procede a calcular un plan de ejecución que resulte óptimo para el query en cuestión. Para este cálculo toma en cuenta estadísticas, índices, llaves, cantidad de filas, tipos de dato, etc.</p>
<p>El proceso del cálculo de plan de ejecución es de lo más pesado y costoso para SQL Server, realmente mucha parte de la magia de SQL se encuentra ahí. Debido a este costo SQL Server guarda en memoria el plan de ejecución para que pueda ser reutilizado por un query posterior, es decir que se ahorrará el proceso de cálculo y simplemente se avocará a la ejecución del mismo, optimizando el uso de recursos y mejorando los tiempos de respuesta.</p>
<p>Hasta aquí todo pinta re bien, el problema es cuando nuestra memoria se llena de planes de ejecución de uso único, es decir que el plan de ejecución únicamente ha sido utilizado una vez y nunca volvió a ser reutilizado por otro query.</p>
<p>Para ejemplificar esto vamos a hacer un ejercicio sobre la base de datos AdventureWorks2012 (pueden elegir otra versión de AdventureWorks y no habrá mayor problema)</p>
<p>Es indispensable que <b>no realices este ejercicio en un servidor productivo.</b></p>
<p>Lo primero será vaciar la memoria de planes de ejecución y el buffer de datos para que tengamos el espacio en blanco:</p>
<code>
DBCC FREEPROCCACHE<br />
DBCC DROPCLEANBUFFERS
</code>
<p>
Después nos conectaremos a la base de datos AdventureWorks2012 y ejecutaremos primero este query:
</p>
<code>SELECT ProductID, ListPrice, Color FROM Production.Product WHERE ProductID = 321</code>
<p>
Y después este otro query:
</p>
<code>SELECT ProductID, ListPrice, Color FROM Production.Product WHERE ProductID = 328</code>
<p>
Recordemos que vaciamos la memoria de planes de ejecución y por lo tanto SQL Server tuvo que calcular el mejor plan de ejecución (de los que encontró) Vamos a revisar la memoria de nuestros planes de ejecución para ver qué tiene:
</p>
<code>
SELECT CP.usecounts, CP.size_in_bytes, CP.cacheobjtype, CP.objtype, QP.query_plan<br />
FROM sys.dm_exec_cached_plans CP<br />
CROSS APPLY sys.dm_exec_query_plan(CP.plan_handle) QP<br />
</code>
<p>
Nos deberá dar un resultado como el que muestro en la siguiente imagen:
</p>
<p class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3lok1_ENqQ7dnyXEoTHAcfT9rVgSe1nP-2Xlm2haojJM_hTiONo8leHVz59ZkKqU8IbK7bXnc3YdNOALZtNU9V9U_CCbAzciQLxCntXB1v6k5-9k6yKQBqQc7qD4ANX9rQ5ta5nk3l8g/s1600/Cached_Plans.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="98" data-original-width="578" height="54" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi3lok1_ENqQ7dnyXEoTHAcfT9rVgSe1nP-2Xlm2haojJM_hTiONo8leHVz59ZkKqU8IbK7bXnc3YdNOALZtNU9V9U_CCbAzciQLxCntXB1v6k5-9k6yKQBqQc7qD4ANX9rQ5ta5nk3l8g/s320/Cached_Plans.png" width="320" /></a>
</p>
<p>
Podemos ver que tenemos tres planes de ejecución que han sido ocupados una sola vez. Abre cada uno de los planes de ejecución y notarás que:
</p>
<ul>
<li>Uno corresponde al query que buscó al producto 321.</li>
<li>Uno corresponde al query que buscó al producto 328</li>
<li>Uno corresponde al query que obtuvo la información de los planes de ejecución.</li>
</ul>
<p>
Si ejecutamos ahora el siguiente query:
</p>
<code>SELECT ProductID, ListPrice, Color FROM Production.Product WHERE ProductID = 329</code>
<p>
Y volvemos a obtener el contenido del caché de planes de ejecución, ahora veremos que se incrementó el usecounts del plan de ejecución que obtiene el contenido del caché de planes de ejecución y que tenemos un nuevo plan almacenado resultado de la búsqueda del producto 329.
</p>
<p>
¿Estamos de acuerdo que pudo haberse utilizado el mismo plan de ejecución para buscar los productos 321, 328 y 329?, finalmente la búsqueda se hizo en base al CLUSTERED INDEX. Peor aún, si nos pusiéramos a buscar muchos productos por su identificador acabaríamos con muchos planes de ejecución idénticos en la memoria, desperdiciando recursos que podríamos ocupar en cosas más valiosas.
</p>
<p>
Aquí es donde entra la configuración del optimize for ad hoc workloads. Esta opción de SQL Server busca parametrizar planes de ejecución para que puedan ser reutilizados varias veces y no terminar con una memoria atascada de basura.
</p>
<p>
Para probar esto, vamos a habilitar la opción optimize for ad hoc workloads de la siguiente manera:
</p>
<code>
sp_configure 'show advanced options', 1<br />
GO<br />
RECONFIGURE<br />
GO<br />
sp_configure 'optimize for ad hoc workloads', 1<br />
GO<br />
RECONFIGURE
</code>
<p>
Ahora vamos a limpiar la memoria:
</p>
<code>
DBCC FREEPROCCACHE<br />
DBCC DROPCLEANBUFFERS<br />
</code>
<p>
Acto seguido ejecutaremos las cosas en el mismo orden que lo hicimos antes de habilitar la opción optimize for ad hoc workloads:
</p>
<code>
SELECT ProductID, ListPrice, Color FROM Production.Product WHERE ProductID = 321
</code>
<p>
Después:
</p>
<code>
SELECT ProductID, ListPrice, Color FROM Production.Product WHERE ProductID = 328
</code>
<p>
Después:
</p>
<code>
SELECT CP.usecounts, CP.size_in_bytes, CP.cacheobjtype, CP.objtype, QP.query_plan<br />
FROM sys.dm_exec_cached_plans CP<br />
CROSS APPLY sys.dm_exec_query_plan(CP.plan_handle) QP
</code>
<p>
Después:
</p>
<code>
SELECT ProductID, ListPrice, Color FROM Production.Product WHERE ProductID = 329
</code>
<p>
Y finalmente:
</p>
<code>
SELECT CP.usecounts, CP.size_in_bytes, CP.cacheobjtype, CP.objtype, QP.query_plan<br />
FROM sys.dm_exec_cached_plans CP<br />
CROSS APPLY sys.dm_exec_query_plan(CP.plan_handle) QP
</code>
<p>
Si revisamos y comparamos la memoria utilizada del antes y el después de habilitar la opción optimize for ad hoc workloads notaremos un ahorro ¡del 39.68%!
</p>
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEih7VUN16gyFAeS0whB_7R57alNGE1D5JB227TpVh5sPZNMkCiEMxoc78US2os7YrkYmaV9U-NgH-1eU6l2-1rXIf_pMIibfcrxSboZj3QPIyb7hwDfHbIEmyge8ngcYMi96vFUBDuE9h8/s1600/Cached_Plans+02.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="117" data-original-width="602" height="61" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEih7VUN16gyFAeS0whB_7R57alNGE1D5JB227TpVh5sPZNMkCiEMxoc78US2os7YrkYmaV9U-NgH-1eU6l2-1rXIf_pMIibfcrxSboZj3QPIyb7hwDfHbIEmyge8ngcYMi96vFUBDuE9h8/s320/Cached_Plans+02.png" width="320" /></a>
</div>
<p>
Esto resulta especialmente útil en entornos donde SQL Server recibe solicitudes enviadas desde sistemas que utilizan el .NET Entity Framework.
</p>
<p>
¡Aguas!, no es la cura para todos los males, esto nos obligará a realizar las mejores prácticas y crear una capa de acceso a datos mediante vistas, procedimientos almacenados y funciones para obtener la información de nuestras bases de datos. Aunque, siendo sincero, la opción de la capa de datos la considero obligatoria independientemente del estado de la opción optimize for ad hoc workloads.
</p>
<p>
De hecho es considerada una buena práctica el habilitar esta opción en toda instalación de SQL Server.
</p>
<p>Espero te haya resultado de utilidad esta entrada, ¡saludos!</p>
<hr />
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="https://blog.jorgetoriz.com/2018/10/optimize-for-ad-hoc-workloads.html" data-send="true" data-show-faces="false" data-width="450">
</div>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-31318219469525377912018-02-01T18:59:00.002-06:002018-02-01T18:59:57.575-06:00¿Qué PAC debo elegir?<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2018/02/que-pac-debo-elegir.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
Desde la entrada de la facturación electrónica a México, ha ido creciendo el número de proveedores certificados que ofrecen sus servicios para poder realizar el proceso de timbrado y/o cancelación de tus comprobantes fiscales.<br />
<br />
La elección del proveedor te puede significar una enorme diferencia entre detener la operación de embarques de tu empresa y tener fluidez en dicha tarea, entre tener dolores de cabeza interminables y pagar un poco más pero tener tranquilidad.<br />
<br />
Lo más probable es que te inclines por buscar a uno económico, que por la cantidad de timbres que quieras comprar te puedan dar un precio bajo y así te ahorres una buena lana. Antes de elegir un proveedor, debes preguntarte lo siguiente:<br />
<ol>
<li>¿Puedo soportar todo un fin de semana sin respuesta de soporte?</li>
<li>¿Estoy dispuesto a modificar los precios de mis productos para que el proveedor me timbre mi comprobante?</li>
<li>¿Significaría un problema para la operación que no se pueda facturar?</li>
</ol>
<br />
La facturación no siempre significa un proceso restrictivo en las empresas, algunas pueden esperar horas o días para poder emitir o cancelar su factura y otras dependen de la emisión para poder continuar con su operación.<br />
<br />
Es importante señalar que he colaborado en muchos sistemas diferentes que están relacionados con la facturación electrónica, desde procesamiento de archivos planos, lectura de bases de datos del ERP, obtención de los datos desde Web Services, recepción de datos por Webhooks, captura en sistemas, etc. Me ha tocado convivir (y sufrir) a algunos PAC, de los cuales en esta entrada platicaré de 4 en particular: Edicom, SolucionFactible.com, Expide tu Factura y Profact.<br />
<br />
¿Por dónde comenzamos?, ¿por las buenas noticias o por las malas? Para no terminar con un mal sabor de boca esta entrada, comenzaremos con los peores y terminaremos con el mejor (desde mi punto de vista)<br />
<br />
<b>Expide tu factura</b><br />
El compromiso en este PAC es inexistente y su personal de soporte se ve desbordado por la cantidad de problemas que tienen (o tenían hasta que tuve la lamentable experiencia de utilizar sus servicios), lo cual se traduce en respuestas al vuelo y promesas que escribieron en el hielo.<br />
<br />
El año pasado se había comenzado con la implementación del CFDI v3.3 con meses de anticipación, esto con el fin de evitar las sorpresas que se pudieran recibir durante las validaciones ejecutadas por el PAC.<br />
<br />
En agosto de 2017 se había obtenido el dato de que su servicio web de timbrado quedaría en productivo durante la primera semana de septiembre. En cada comunicación que se tenía con ellos al llegar la fecha comprometida, lo único que se recibía era un "el departamento de sistemas está trabajando duro", al final tiraron la toalla y el compromiso ya fue imposible de obtener (porque no lo iban a cumplir)<br />
<br />
Llegó el Día D, ¡y qué crees!, lo que ya sabíamos que iba a pasar pero nos negábamos a creer, ¡no servía el servicio web!, ya en fechas de productivo obligatorias por el SAT, su servicio seguía inundado de problemas y el personal de soporte estaba que se arrancaba las pestañas de la cantidad de trabajo que les cayó encima.<br />
<br />
¿Los utilizaría?, no, en definitiva perdieron el rumbo desde la versión 3.3 del CFDI. Antes estaban bien, funcionaba en niveles muy aceptables pero se vinieron abajo con la nueva versión.<br />
<br />
<b>Profact</b><br />
Este proveedor se compromete a tener el servicio activo el 99.9% del tiempo, te comentan que utilizarán 8 horas al mes para mantenimiento y éstas no deberán considerarse dentro del 99.9%, es decir que 96 horas al año no estarán disponibles por cuestiones de mantenimiento.<br />
<br />
Por el mantenimiento no le veo mayor problema, todo sistema necesita mantenimiento y lo más probable es que tenga que estar fuera de línea para recibirlo. Tomando en cuenta esto, el 99.9% se deberá calcular sobre las 8664 horas restantes del año.<br />
<br />
Con la caída que tuvieron apenas ya andan en 99.9569%, lo cual los deja con poco margen para lo que resta del año, pero no se les puede reclamar aún porque siguen dentro del parámetro anunciado. Es curioso que en su SLA le hacen pensar al usuario que lograrán un 99.9999%, pero con inteligencia únicamente se comprometen al 99.9%<br />
<br />
Tienen errores en su codificación, errores que no reconocen aunque se les demuestre que sí existen y que son latentes... así que si eres programador y te gira un poco el coco, te causarán muchos disgustos estos personajes (si es que encuentras los errores)<br />
<br />
El personal con el que se tuvo intercambio de información demostró un conocimiento bastante cortito, tan corto que no sabían lo que es el timbrado del Sector Primario, se quedaron con la mente en blanco y no supieron qué contestar.<br />
<br />
¿Los utilizaría?, solamente si la facturación fuera algo casi sin importancia en la operación y vendiera en precios que tengan una posición decimal como máximo.<br />
<br />
<b>SolucionFactible.com</b><br />
Tiene un nivel de servicio muy aceptable, al menos en el tiempo que se ha estado utilizando. Su personal de soporte atiende las solicitudes de información y se esfuerzan en apoyarte en todo lo posible.<br />
<br />
Timbran todos los complementos y también el sector primario, cuentan con librerías para todos los lenguajes o conectividad por Web Service si quieres establecer la comunicación por esa vía.<br />
<br />
¿Por qué no lo coloco como el mejor?, porque el que le sigue solamente me ha tocado verlo caer una vez en los años que llevo en esto.<br />
<br />
¿Lo utilizaría y lo recomendaría?, seguro que sí, la experiencia ha sido muy grata.<br />
<br />
<b>Edicom</b><br />
Como lo he mencionado antes, solamente me ha tocado verlo caer una vez en años. La caída fue por un error en sus bases de datos, error que se corrigió después de unas horas, pero cumplieron con su SLA al final del año.<br />
<br />
Hay poco qué decir de este proveedor, porque casi todo es positivo. La única mosca en el arroz es que el soporte lo prefieren telefónico, y hay algunas personas que preferimos la comunicación vía correo elctrónico.<br />
<br />
¿Lo utilizaría y lo recomendaría?, ¡por supuesto!, ciertamente tendrá un costo más elevado, pero si tu operación está muy ligada a tu proceso de facturación, este es el proveedor por excelencia.<br />
<br />
<b>Comentarios finales</b><br />
Es importante notar que cada quién habla de cómo le fue en la fiesta, estos comentarios son expuestos a partir de la experiencia que he tenido con cada uno de ellos. Es muy probable que tú no tengas la misma percepción de alguno de ellos si es que ya te tocó que te causaran un problema.<br />
<br />
La persona que se dedica al desarrollo sabe que todos los sistemas fallan, lo importante es que:<br />
<br />
<ol>
<li>El impacto sea tan mínimo como sea posible.</li>
<li>El tiempo de respuesta para solucionarlo sea reducido.</li>
<li>Se acepten los errores y se corrijan.</li>
</ol>
<div>
Espero te haya resultado de utilidad esta entrada, ¡saludos!</div>
<hr />
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2018/02/que-pac-debo-elegir.html" data-send="true" data-show-faces="false" data-width="450">
</div>
Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-86834553306490491212017-08-25T13:14:00.002-05:002017-08-25T13:18:11.380-05:00cross database ownership chaining<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2017/08/cross-database-ownership-chaining.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
En esta entrada exploraremos una característica muy interesante de SQL Server que permite brincar de una base de datos a otra con permisos mínimos para el usuario.<br />
<br />
Para que esto sea posible será necesario que ambas bases de datos tengan el mismo propietario.<br />
<br />
Vamos a crear dos bases de datos, cada una con una tabla muy sencilla.<br />
<br />
<code>USE master<br />
GO<br />
CREATE DATABASE DatabaseA<br />
GO<br />
CREATE DATABASE DatabaseB<br />
GO<br />
USE DatabaseA<br />
GO<br />
CREATE TABLE TableA(<br />
Valor VARCHAR(10)<br />
)<br />
GO<br />
USE DatabaseB<br />
GO<br />
CREATE TABLE TableB(<br />
Valor VARCHAR(10)<br />
)</code><br />
<br />
<br />
Ahora vamos a crear un procedimiento almacenado que nos permita insertar un registro en TableA de DatabaseA y que en el mismo procedimiento se inserte un registro en TableB de DatabaseB.<br />
<br />
<code>USE DatabaseA<br />
GO<br />
CREATE PROC pAgregarValor(<br />
@Valor VARCHAR(10)<br />
)<br />
AS<br />
BEGIN<br />
BEGIN TRAN<br />
BEGIN TRY<br />
INSERT INTO TableA (Valor) VALUES (@Valor)<br />
INSERT INTO DatabaseB.dbo.TableB (Valor) VALUES (@Valor)<br />
<br />
COMMIT<br />
END TRY<br />
BEGIN CATCH<br />
ROLLBACK<br />
<br />
DECLARE @Error VARCHAR(4000) = ERROR_MESSAGE()<br />
RAISERROR (@Error, 16, 1)<br />
END CATCH<br />
END</code><br />
<br />
Crearemos un login y le daremos acceso a DatabaseA, también dándole permiso de ejecución sobre el procedimiento almacenado que acabamos de crear.<br />
<br />
<code>USE master<br />
GO<br />
CREATE LOGIN LoginA WITH PASSWORD = 'password'<br />
GO<br />
USE DatabaseA<br />
GO<br />
CREATE USER LoginA FOR LOGIN LoginA<br />
GO<br />
GRANT EXEC ON pAgregarValor TO LoginA</code><br />
<br />
Si nos conectamos con el login que acabamos de crear e intentamos ejecutar el procedimiento almacenado en DatabaseA nos va a marcar el siguiente error:<br />
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj93OLURWjWgZzqZorlCgFPWqcgi3hn1NBZ7HpSWJAoO0TO3_qf5BNQ6cFih4npr7CHkMStEdd0r9WFRBoAb1qgyS0xLBBztC4La_JCTzaW-YXOgpxdEWO0ZpXe77Bm7IOkgq8_AjobTnQ/s1600/Error01.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="218" data-original-width="789" height="88" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj93OLURWjWgZzqZorlCgFPWqcgi3hn1NBZ7HpSWJAoO0TO3_qf5BNQ6cFih4npr7CHkMStEdd0r9WFRBoAb1qgyS0xLBBztC4La_JCTzaW-YXOgpxdEWO0ZpXe77Bm7IOkgq8_AjobTnQ/s320/Error01.png" width="320" /></a></div>
<br />
El login por lo menos debe tener acceso a DatabaseB. Vamos a darle un usuario al login en esa base de datos para que pueda ingresar. Aquí hay que notar que lo único que tendrá permitido es entrar a la base de datos y ejecutar las tareas que tenga permitidas el rol "public".<br />
<br />
<code>USE DatabaseB<br />
GO<br />
CREATE USER LoginA FOR LOGIN LoginA</code><br />
<br />
Si volvemos a ejecutar el procedimiento almacenado en DatabaseA, el mensaje de error cambiará debido a que el usuario ya puede entrar a DatabaseB pero no tiene permiso de inserción sobre TableB.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGAD9jcP4MTywx7txj7e7aplk9uBLd5UBTpdkMbVcU_T9b8vX9WwC6xb-Im_yU5ilKA6lRpCW6kLRNz-0apYn0uJ8DtAl3w7ZTD0FXkxEQ8MAsjMhquFTRRqf8pBUsoHiJA8jlradlZJY/s1600/Error02.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="222" data-original-width="648" height="109" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgGAD9jcP4MTywx7txj7e7aplk9uBLd5UBTpdkMbVcU_T9b8vX9WwC6xb-Im_yU5ilKA6lRpCW6kLRNz-0apYn0uJ8DtAl3w7ZTD0FXkxEQ8MAsjMhquFTRRqf8pBUsoHiJA8jlradlZJY/s320/Error02.png" width="320" /></a></div>
<br />
He aquí donde utilizaremos la opción "cross database ownership chaining" de SQL Server. Al darle permiso de ejecución en un procedimiento almacenado de DatabaseA que realice una modificación en DatabaseB, el usuario tendrá la autorización porque ambas bases de datos tienen el mismo propietario.<br />
<br />
Vamos a habilitar la opción cross database ownership chaining.<br />
<br />
<code>sp_configure 'cross db ownership chaining', 1<br />
GO<br />
RECONFIGURE<br />
GO<br />
sp_configure 'cross db ownership chaining'</code><br />
<br />
Si ejecutamos nuestro procedimiento almacenado de nuevo, veremos que la inserción resultó exitosa.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgcoTZAYAV2RT3bDGG7YFpKsajs_uTgyR30A0CGMZ6fQHfC9iLcuyLRWs8QnlIC6F4lfpCCmJazmyFZLXcUmAyUluCZyF5jSZ6tWcMZya6s2DrOoEeXlRU5ucAYKiTBHXKbFeq3S_GVqAk/s1600/Exitoso.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="189" data-original-width="225" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgcoTZAYAV2RT3bDGG7YFpKsajs_uTgyR30A0CGMZ6fQHfC9iLcuyLRWs8QnlIC6F4lfpCCmJazmyFZLXcUmAyUluCZyF5jSZ6tWcMZya6s2DrOoEeXlRU5ucAYKiTBHXKbFeq3S_GVqAk/s1600/Exitoso.png" /></a></div>
<br />
<br />
Notarás que no es necesario darle permiso de inserción a LoginA sobre TableB de DatabaseB, tuvo permiso porque el propietario de ambas bases de datos es el mismo y cross database ownership chaining está habilitado.
<br /><br/>
Espero esta entrada te haya gustado y te ayude a mejorar en tu trabajo. Te espero en la siguiente, ¡saludos!<br />
<hr />
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2017/08/cross-database-ownership-chaining.html" data-send="true" data-show-faces="false" data-width="450">
</div>
Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com1tag:blogger.com,1999:blog-7612527879676339024.post-38657155136188623392017-07-12T16:51:00.000-05:002017-07-13T08:44:33.508-05:00ORDER BY<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2017/07/ORDER-BY.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
Durante la semana pasada estaba de visita con un cliente y me encontré con un tema que resulta muy interesante, los conjuntos de resultados ordenados.<br />
<br />
Cuando uno ejecuta una sentencia en SQL Server sin utilizar ORDER BY, los resultados son contemplados como conjuntos de datos, éstos no tienen un orden bien definido, son simplemente conjuntos donde sus datos satisfacen las características de los predicados utilizados en las sentencias.<br />
<br />
Si utilizas el ORDER BY, entonces lo que estás solicitando es conocido como CURSOR. Es importante no confundirlos con los objetos que nos permiten lecturas fila a fila.<br />
<br />
Cuando la combinación de las columnas que aparecen en el criterio de ordenamiento no aseguran una combinación única, el orden del resultado no está totalmente asegurado, esto se debe a que varias formas de ordenar el mismo resultado cumplirían con los criterios de ordenamiento.<br />
<br />
Para explicar mejor este punto vamos a realizar un ejercicio bastante sencillo pero que ayudará a entender mejor este tema tan interesante.<br />
<br />
Vamos a conectarnos a una base de datos que tengas de pruebas (yo usaré AdventureWorks2014), crearemos una tabla de prueba y la llenaremos con datos aleatorios con el siguiente query.<br />
<br />
<code>
IF NOT OBJECT_ID('dbo.TestOrderTORAB') IS NULL<br />
DROP TABLE dbo.TestOrderTORAB<br />
GO<br />
CREATE TABLE dbo.TestOrderTORAB(<br />
Id INT IDENTITY(1, 1),<br />
Nombre VARCHAR(32) NOT NULL,<br />
Color VARCHAR(8) NOT NULL,<br />
CONSTRAINT pkTestOrderTORAB PRIMARY KEY (Id)<br />
)<br />
GO<br />
DECLARE @i INT = 1<br />
WHILE @i <= 500<br />
BEGIN<br />
INSERT INTO dbo.TestOrderTORAB (Nombre, Color) VALUES (REPLACE(CAST(NEWID() AS VARCHAR(36)), '-', ''), 'Amarillo')<br />
INSERT INTO dbo.TestOrderTORAB (Nombre, Color) VALUES (REPLACE(CAST(NEWID() AS VARCHAR(36)), '-', ''), 'Blanco')<br />
INSERT INTO dbo.TestOrderTORAB (Nombre, Color) VALUES (REPLACE(CAST(NEWID() AS VARCHAR(36)), '-', ''), 'Rojo')<br />
INSERT INTO dbo.TestOrderTORAB (Nombre, Color) VALUES (REPLACE(CAST(NEWID() AS VARCHAR(36)), '-', ''), 'Azul')<br />
<br />
SET @i += 1<br />
END</code>
<br />
<br />
En una nueva conexión (ventana) ejecuta la siguiente sentencia. Es importante mencionar que tu resultado y mi resultado serán muy diferentes, esto es debido a que la tabla fue poblada con información aleatoria. Aquí lo importante es que veas en tu resultado las filas que forman parte de él.<br />
<br />
<code>
SELECT TOP 10 Id, Nombre, Color FROM dbo.TestOrderTORAB ORDER BY Color<br />
</code><br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjWZVfIErJKDVT3TAcHSaCypRv4AheaR8KB5B1LoLPL6ms6aq5b92tktn1L-FszaDov6LFtf1rxyabShrk_siKKMlVenp7nXtkOtmCqfPcLCpKpCa3xMhqjKjfYtNkBR0-6a3u92EEkAZM/s1600/OrderBy01.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="211" data-original-width="307" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjWZVfIErJKDVT3TAcHSaCypRv4AheaR8KB5B1LoLPL6ms6aq5b92tktn1L-FszaDov6LFtf1rxyabShrk_siKKMlVenp7nXtkOtmCqfPcLCpKpCa3xMhqjKjfYtNkBR0-6a3u92EEkAZM/s1600/OrderBy01.png" /></a></div>
<br />
Supongamos que por tareas de optimización se vio la necesidad de crear un índice sobre la tabla TestOrderTORAB. Ejecuta este query en otra ventana para que no pierdas el resultado de la sentencia que anteriormente ejecutamos.<br />
<br />
<code>
CREATE INDEX ixTestOrderTORAB ON dbo.TestOrderTORAB (Color, Nombre)<br />
</code>
<br />
Si abrimos otra ventana y volvemos a ejecutar la misma sentencia que obtiene los 10 primeros productos ordenados por color, ¡veremos que es diferente el resultado!<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiRI1weJVC6d73J2y_oDLJsl1gyPpcaH9g7R6pEKnmWmZHsbnNFPX8HeAziCrDMhQ-XVN-WtMJQuhmVIKtR4gmLU5VNCoKtqgoGf_6g1UkwUfMowSn7jo6eKzsAK7PQTXFDeIkYfxnSjlk/s1600/OrderBy02.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="211" data-original-width="353" height="191" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiRI1weJVC6d73J2y_oDLJsl1gyPpcaH9g7R6pEKnmWmZHsbnNFPX8HeAziCrDMhQ-XVN-WtMJQuhmVIKtR4gmLU5VNCoKtqgoGf_6g1UkwUfMowSn7jo6eKzsAK7PQTXFDeIkYfxnSjlk/s320/OrderBy02.png" width="320" /></a></div>
<br />
Esto a pesar de que es el mismo query y que los datos de la tabla no han sido modificados. ¿Cuál es la razón?, que el plan de ejecución con el que se despachó el query cambió de uno a otro y que la condición de tomar los primeros 10 productos ordenados por color sigue siendo cumplida con un conjunto de resultados diferente.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEim20gg7pTsZaymy5__w-1snHemVpjGyj6zETqGDPG9fM9qTkh1W8Xc95OwXXdN8wRVsUor_7oIszRLnty8gnt9X8FG5fVzKaSSXeI0ZsgerpMI-ISHJX1pTmHQeJyEpJcV6JX4iT7jp_U/s1600/ExecutionPlan01.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="238" data-original-width="585" height="130" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEim20gg7pTsZaymy5__w-1snHemVpjGyj6zETqGDPG9fM9qTkh1W8Xc95OwXXdN8wRVsUor_7oIszRLnty8gnt9X8FG5fVzKaSSXeI0ZsgerpMI-ISHJX1pTmHQeJyEpJcV6JX4iT7jp_U/s320/ExecutionPlan01.png" width="320" /></a></div>
<br />
<b>Ambos resultados son correctos</b>, y la razón es que ambos cumplen con devolver los primeros 10 elementos que SQL Server encontró ordenándolos por nombre.<br />
<br />
Si deseamos que esto no suceda, es necesario incluir una columna que ayude a que sea única la combinación de valores de las columnas utilizadas en el ORDER BY, a esta columna se le conoce como <b>tiebreaker</b>.<br />
<br />
Vamos a eliminar el índice con la siguiente sentencia:<br />
<br />
<code>
DROP INDEX ixTestOrderTORAB ON dbo.TestOrderTORAB<br />
</code>
<br />
Modificamos el query para incluir la columna id como tiebreaker y ejecutamos para observar los resultados:<br />
<br />
<code>
SELECT TOP 10 Id, Nombre, Color FROM dbo.TestOrderTORAB ORDER BY Color, Id<br />
</code><br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghwJnqZbQCcyEBzV3RVyywQsTj2G6ARY1s3Y__4P4jCqYkotluSxb4X31f84Vs2Gv-LkIxRrP6OaXGBWyVXvj4bNx8IYzLOuXcnbVydzxN9aK8dlI2iCQDXOVciYsqI99JRhKpLVFX9eA/s1600/OrderBy03.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="211" data-original-width="338" height="199" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEghwJnqZbQCcyEBzV3RVyywQsTj2G6ARY1s3Y__4P4jCqYkotluSxb4X31f84Vs2Gv-LkIxRrP6OaXGBWyVXvj4bNx8IYzLOuXcnbVydzxN9aK8dlI2iCQDXOVciYsqI99JRhKpLVFX9eA/s320/OrderBy03.png" width="320" /></a></div>
<br />
Creamos de nueva cuenta el índice con la siguiente sentencia:<br />
<br />
<code>
CREATE INDEX ixTestOrderTORAB ON dbo.TestOrderTORAB (Color, Id, Nombre)<br />
</code>
<br />
Ejecutamos el mismo query (el que tiene id como tiebreaker) en otra ventana y veremos que el resultado es el mismo. No importa que se haya creado un índice, el resultado no fue modificado y la razón es que la combinación de valores de las columnas utilizadas en el ORDER BY es única.<br />
<br />
Si comparamos los planes de ejecución de ambas sentencias (antes del índice vs después del índice) veremos que cambió, lo cual es totalmente deseable cuando uno genera índices, que éstos sean contemplados por el optimizador de consultas para mejorar el rendimiento.<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgThvjIMzBJ1cyOmqy0gse1y77aQWowSG6dnIbR-uxE6ZJU5OsPf8pKuzlxJrYzat3KHPinAKJQ8E2ziA4vc_0x6fpiOSpx-utC7y3BCo_CZsQCINsIUnWYUaxjxIdVY8RemQf_74njGyQ/s1600/ExecutionPlan03.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="247" data-original-width="619" height="127" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgThvjIMzBJ1cyOmqy0gse1y77aQWowSG6dnIbR-uxE6ZJU5OsPf8pKuzlxJrYzat3KHPinAKJQ8E2ziA4vc_0x6fpiOSpx-utC7y3BCo_CZsQCINsIUnWYUaxjxIdVY8RemQf_74njGyQ/s320/ExecutionPlan03.png" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
</div>
<br />
En conclusión, es muy recomendable incluir una columna <b>tiebreaker</b> en caso de que la combinación de valores de las columnas utilizadas en un ORDER BY no sean únicos.
<br />
<br />
Espero esta entrada te haya gustado y te ayude a mejorar en tu trabajo. Te espero en la siguiente, ¡saludos!<br />
<hr />
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2017/07/ORDER-BY.html" data-send="true" data-show-faces="false" data-width="450">
</div>
Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-83159257689515350412016-10-04T09:35:00.002-05:002016-10-04T09:38:16.555-05:00Chat uno a uno del SAT<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/10/eig-abonando-un-ladron-de-varias-cabezas.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr/>
El día 30 de septiembre de 2016 el tres veces heroico cuerpo de desarrollo de la Secretaría de Administración Tributaria (o uno de sus fabulosísimos proveedores) liberó la versión 1.2 del esquema para el complemento de Nómina.<br />
<br />
Inmediatamente tuvimos que ponernos manos a la obra para poder comenzar a trabajar en los ajustes necesarios en los sistemas para poder generar XML que respeten ese esquema en enero de 2017, y... ¡oh sorpresa!, ¡lo que esperaba!, le encontramos un error (no sé cuántos vaya a tener)<br />
<br />
Fíjense cómo está el texto que acompaña al atributo "CuentaBancaria" del nodo "Receptor":<br />
<br />
"<em>Atributo condicional para la expresión de la cuenta bancaria a 11 posiciones o número de teléfono celular a 10 posiciones o número de tarjeta de crédito, débito o servicios a 15 ó 16 posiciones o la CLABE a 18 posiciones o número de monedero electrónico, donde se realiza el depósito de nómina.</em>"<br />
<br />
Siendo que el número más grande que aparece en la documentación es de 18 posiciones, entonces te esperas una expresión regular más o menos así: "\d{10,18}", y sí, nos encontramos con algo así:<br />
<br />
<code>
<xs:simpleType name="t_CuentaBancaria"><br/>
<xs:annotation><br/>
<xs:documentation>Tipo definido para expresar la cuenta bancarizada.</xs:documentation><br/>
</xs:annotation><br/>
<xs:restriction base="xs:int"><br/>
<xs:whiteSpace value="collapse"/><br/>
<xs:pattern value="[0-9]{10,18}"/><br/>
</xs:restriction><br/>
</xs:simpleType><br/>
</code>
<br />
Después de notar que el valor máximo que soportaría es de 2147483647, realizamos una conexión para hacerle saber al heorico equipo de soporte técnico del SAT lo que estaba pasando. Y, como dijera la canción que cantaban Alberto Vázquez y Joan Sebastian, "esto fue lo que sucedió".<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEik_bvxTfIQ_bT190dQ1OOs6bP1kKkuh8EE0XoIEhn72UOqZuEG96M1mIx3eG8YvmMOikU3RKS0jYVDj8YpMNp21A9axTrjDtSaxlbJ3K9ys5G2emesGPKE5JrBVY83ioQLXguyCCXEYWE/s1600/SAT.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="313" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEik_bvxTfIQ_bT190dQ1OOs6bP1kKkuh8EE0XoIEhn72UOqZuEG96M1mIx3eG8YvmMOikU3RKS0jYVDj8YpMNp21A9axTrjDtSaxlbJ3K9ys5G2emesGPKE5JrBVY83ioQLXguyCCXEYWE/s320/SAT.png" width="320" /></a></div>
<br />
<br />
Para el equipo de soporte técnico en el chat uno a uno del SAT el problema es de los bancos, supongo que es culpa de los bancos que hayan hecho la CLABE de 18 dígitos, inclusive también sería culpa de los bancos por hacer que los números de tarjeta sean de 15 o 16 dígitos.<br />
<br />
No es triste que la gente que trabaja ahí no tenga capacidad cerebral para identificar que le está siendo reportado algo que está fuera de su alcance, lo triste es que se monten en su caballo de que la culpa es de todos menos de ellos y que su pequeño conjunto de neuronas les permita tener un sueldo para cobrar por no trabajar.<br />
<br />
Esperemos algún día (antes del 1 de enero de 2017) el brillante equipo de desarrollo del SAT, o de su proveedor, se den cuenta del error y lo corrijan. A menos que lo hayan hecho con la intención de que esté mal y así dar prórroga hasta junio de 2017... lo cual también es probable.
<hr/>
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2016/10/chat-uno-a-uno-del-sat.html" data-send="true" data-show-faces="false" data-width="450">
</div>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-28571107291815483422015-10-17T14:19:00.000-05:002015-10-19T16:33:38.923-05:00EIG - Abonando a un ladrón de varias cabezas<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/10/eig-abonando-un-ladron-de-varias-cabezas.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
Hace algunos meses realicé la adquisición de un servicio de alojamiento con un proveedor llamado <a href="http://www.arvixe.com/" target="_blank">Arvixe</a>, no puedo quejarme de esos primeros meses, el servicio muy bueno, la atención por parte del personal de soporte muy buena, y el rendimiento del servidor bastante bueno.<br />
<br />
Después de un buen tiempo, hubo un cambio radical en sus servicios, y puedo decir que en toda su gama por la cantidad de tickets y quejas que llueven en twitter, a lo que nuestros amigos siempre se limitan a responder algo así:<br />
<ol>
<li>"Gracias por proporcionar el id del ticket, te responderé en él".</li>
<li>"¿Podrías proporcionar el id del ticket?"</li>
</ol>
Obviamente tienen un programa muy sencillo donde reciben sus tweets y apretando un botón, se realiza la respuesta del mismo. ¿El esfuerzo?, elegir entre la respuesta 1 o la respuesta 2, de ahí en fuera, nada.<br />
<br />
Después de esperar por más de 3 horas y media con el chat abierto, finalmente una persona atiende la conversación, después de comentar la situación y mencionar que el 99.99% que GARANTIZAN en su página de inicio no se estaba cumpliendo, esta personita comentaba que como era un problema grave ellos no podían dar ese 99.99%.<br />
<br />
En este momento pensé, vamos 2-0 y la atención es inexistente. Procedí a levantar un ticket, después de esperar 6 horas, levanté otro... y así siguió el ciclo, el cual no interrumpí para verificar en qué momento o qué tipo de ticket es el que responden.<br />
<br />
¡Finalmente respondieron!, después de 48 horas con el servidor totalmente caído y sin respuesta por ningún lado, pude ver en la lista que uno de tantos estaba respondido, ¿qué podría ser?, entre emoción y coraje abrí el ticket, ¡wow!, la respuesta fue increíble: "olvidó colocar los últimos 4 caracteres de la contraseña de su usuario de facturación, para poder atender su ticket, necesitamos esa información".<br />
<br />
Este ir y venir con esta gente lo voy a hacer a un lado, y no por falta de más cosas qué decir, sino que me llamaba la atención que la calidad en el servicio decayera de una forma tan estrepitosa y en tan poco tiempo.<br />
<br />
Después de investigar un poco, supe que un corporativo con las siglas EIG (Endurance International Group) había adquidido a Arvixe, y ahí comencé a darle al clavo.<br />
<br />
Sucede que estos amigos de EIG son dueños de muchos (pero muchos) proveedores de alojamiento, ¿qué tienen en común aparte del dueño?, que sus primeros meses de servicio son "aceptables", posteriormente comienzan los problemas y niegan cualquier posibilidad de devolución de dinero para sus clientes, ¿qué sucede?, el cliente termina yéndose a otra compañía.<br />
<br />
Encontrarán muchas entradas de blog donde te recomiendan pasarte del proveedor X al proveedor Y, que con el proveedor Y están súper contentos y que el servicio es espectacular. Si le buscamos un poco, ¡el proveedor Y pertenece también a EIG!, ¿resultado?, basta con buscar un poco en los foros de soporte y cuentas de redes sociales del proveedor Y, para darte cuenta que es igual de asqueroso que el proveedor X.<br />
<br />
Al final, en todos estos blogs encuentras que te recomiendan ir saltando entre proveedores que pertenecen a esta empresa EIG, al final le caerá dinero a los mismos ladrones y tu servicio seguirá siendo de pésima calidad.<br />
<br />
Quiero finalizar esta entrada de blog con una recomendación sumamente simple, cuando vayas a contratar algún servicio de alojamiento, ¡verifica que no sea una empresa que forme parte del grupo EIG!, te ahorrarás muchos dolores de cabeza y pérdidas económicas.
<hr/>
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/10/eig-abonando-un-ladron-de-varias-cabezas.html" data-send="true" data-show-faces="false" data-width="450">
</div>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-19103919314689411172015-05-05T14:38:00.002-05:002015-05-14T13:31:19.280-05:00Incorrect syntax near 'go'<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/05/incorrect-syntax-near-go.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
El día de hoy me encontré con que un compañero estaba sufriendo bastante para crear un programa en .NET (aplicación de escritorio) que instalara una base de datos, desde su aprovisionamiento hasta la inicialización.<br />
<br />
Al revisar el código que estaba utilizando, me encontré que los comandos en el archivo de recursos incluían la palabra GO.<br />
<br />
Antes de abordar una solución simple, me gustaría explicar para qué sirve la palabra reservada GO en una sentencia SQL.<br />
<br />
Vamos a abrir la página de opciones del SQL Server Management Studio:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBOJyK8EsVUEvKXOfk1SFw4LUfvUoC_YzZdlH68no8s7QAHsk4GF5GhuB4b2UFV6XewqOfV4eUQTuvfN5d1-UnmOMg7UiFNJ9n4vqsAx8ET05qe6A-LGGl88zb7dAFjc3iedDdsewD_Vo/s1600/01.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="186" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiBOJyK8EsVUEvKXOfk1SFw4LUfvUoC_YzZdlH68no8s7QAHsk4GF5GhuB4b2UFV6XewqOfV4eUQTuvfN5d1-UnmOMg7UiFNJ9n4vqsAx8ET05qe6A-LGGl88zb7dAFjc3iedDdsewD_Vo/s320/01.png" width="320" /></a></div>
<br />
Notemos que la opción "Batch separator" tiene el valor GO. De ello podemos deducir que es una opción de configuración para SQL, de manera tal que en un sólo script podamos meter varias sentencias sin que una interfiera con la otra. La palabra reservada GO no hace más que decir que ya terminó la sentencia y que la sentencia posterior es totalmente independiente a la anterior.<br />
<br />
Vamos a probar esto con un ejemplo muy sencillo:<br />
<br />
<code>DECLARE @CurrentTime DATETIME = GETDATE()<br />
PRINT @CurrentTime<br />
GO<br />
PRINT @CurrentTime</code><br />
<br />
El resultado será el siguiente:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNgNildLjBZbgD3FW7CsV-9VTXf3qfo-A6hcp15MsmXWOlCaFOyPpT5Gm8uhGCYmfatODEEtjn7P_lc_KjY3BzJ9RWPP2-iZVe8ALEXEAv4SJ7LG0pyR15u5mBiC4uNhS1-Z6_I4qKdPo/s1600/02.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="62" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhNgNildLjBZbgD3FW7CsV-9VTXf3qfo-A6hcp15MsmXWOlCaFOyPpT5Gm8uhGCYmfatODEEtjn7P_lc_KjY3BzJ9RWPP2-iZVe8ALEXEAv4SJ7LG0pyR15u5mBiC4uNhS1-Z6_I4qKdPo/s320/02.png" width="320" /></a></div>
<br />
La primera sentencia PRINT sí se está ejecutando de forma exitosa, pero la segunda está marcando un error, y esto se debe a que la palabra reservada GO hace un "borrón y cuenta nueva", de manera tal que la variable @CurrentTime ya no existe en la segunda sentencia PRINT.<br />
<br />
Debido a esto, el query que está ocupando mi compañero, está marcando el mensaje de error "Incorrect syntax near 'GO'"<br />
<br />
Lo que menos se quería era tener que modificar el script para poder instalar la base de datos, por lo que encontramos una solución muy sencilla, crear una función que recibiera el script completo (con todo y GO) y lo dividiera en varios scripts tal como hipotéticamente se hace en SQL Server cuando se ejecuta, y el resultado fue el siguiente:<br />
<br />
<code>static void ExecuteCommand(string sqlCommandText, SqlConnection conn)<br />
{<br />
SqlCommand comm = new SqlCommand("", conn);<br />
foreach (string sqlCommand in sqlCommandText.Split(new string[] { "GO\r\n" }, StringSplitOptions.RemoveEmptyEntries))<br />
{<br />
comm.CommandText = sqlCommand;<br />
comm.ExecuteNonQuery();<br />
}<br />
}</code><br />
<br />
Nuestra función aparte recibe una conexión abierta y disponible para ser utilizada.<br />
<br />
Podemos notar que lo único que se está haciendo es dividir el script completo en pedazos ejecutables desde .NET.<br />
<br />
La solución es sumamente simple, no hubo que ajustar nada en el código del script y la instalación de la base de datos ya tiene un resultado exitoso.<br />
<br />
Espero te haya resultado de utilidad, nos vemos la siguiente entrada.
<hr />
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/05/incorrect-syntax-near-go.html" data-send="true" data-show-faces="false" data-width="450">
</div>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-21371290278809191342015-04-30T15:54:00.000-05:002015-04-30T15:54:25.305-05:00Covering indexes<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/04/covering-indexes.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
Los índices de cobertura, son aquellos que nos ayudan a mejorar el rendimiento de ciertas consultas que utilizan un índice y que cuyo resultado incluye un mismo conjunto de columnas.<br />
<br />
Tal como en las anteriores entradas, vamos a aterrizar esto en un ejercicio para que se vea la utilidad de estos elementos.<br />
<br />
Tomaremos la base de datos AdventureWorks como ejemplo y partiremos del siguiente query:<br />
<br /><code>
SELECT ProductID, Name, ProductNumber, MakeFlag<br />
FROM Production.Product<br />
WHERE Color = 'White'</code>
<br /><br />
Si revisamos el plan de ejecución, notaremos que se está llevando a cabo un barrido del CLUSTERED INDEX de la tabla Production.Product:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGPs9VXMACGqvMNib_inzFwG6EazPdfDOCeBJjHCrwcJwAIRDI-ST9ADMxP0UVG7SFpNYIBh80GvQ8rPddvvGg3s6bzYiua8lkLfdtLHWpeugmy2en5t9XZyoggAmfbj_pA7Nyh3HRknM/s1600/02.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGPs9VXMACGqvMNib_inzFwG6EazPdfDOCeBJjHCrwcJwAIRDI-ST9ADMxP0UVG7SFpNYIBh80GvQ8rPddvvGg3s6bzYiua8lkLfdtLHWpeugmy2en5t9XZyoggAmfbj_pA7Nyh3HRknM/s1600/02.png" height="45" width="320" /></a></div>
<br />
Con un costo estimado de 0.0127253, el cual ciertamente es bastante bajo, pero esto se debe a la poca cantidad de elementos que tiene nuestro conjunto.<br />
<br />
¿Qué es lo primero que haríamos para mejorar el rendimiento de esta consulta?, claro, irnos a la parte del WHERE y analizar el predicado. Después de revisarlo podemos proponer un índice sobre la columna Color de la tabla Production.Product.<br />
<br /><code>
CREATE INDEX ixColor<br />
ON Production.Product (Color)</code><br /><br />
Vamos a revisar nuevamente nuestro plan de ejecución:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-Ld_YCCMshLtzUVVGPn4novNuCBHCv7ufgdwMULwmaZfCQXEGMtsi2mV7r0vsTK_65KRdgJt9D4rUSZUbsuhiQ6seDTxqosSSf_XqZRO9wKxc7ag6OVAcQhS7lx14oaCz4_3w1jePn8I/s1600/03.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh-Ld_YCCMshLtzUVVGPn4novNuCBHCv7ufgdwMULwmaZfCQXEGMtsi2mV7r0vsTK_65KRdgJt9D4rUSZUbsuhiQ6seDTxqosSSf_XqZRO9wKxc7ag6OVAcQhS7lx14oaCz4_3w1jePn8I/s1600/03.png" height="84" width="320" /></a></div>
<br />
Notaremos que hay un cambio importante, ahora se está realizando una búsqueda en el índice ixColor, justamente el que acabamos de crear. Notemos también que se está llevando a cabo una tarea Key Lookup que sirve para encontrar las columnas que nos faltan para poder dar el resultado, las cuales son ProductID, Name, ProductNumber y MakeFlag.<br />
<br />
El costo de este query cambió de 0.0127253 a 0.0126079, lo cual significa una variación menor al 1%, en verdad que no es nada impresionante si lo comparamos contra el costo que tendrá para la base de datos el mantener al día este nuevo índice respecto a los cambios que sufra la tabla a la que apunta.<br />
<br />
Pero bueno, tomemos en cuenta que tenemos sólo 504 filas en la tabla de productos, también es por ello que la variación es tan pequeña.<br />
<br />
Ahora, vamos a ahondar un poco más en el Key Lookup:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLOrYyNormd5o8E584Y8VuiTE9rgfC4ae8jELIxdV3nAG8gxYd-fJ7Jnzjn9IVLaiOB_3hrxd2nyBDlH-pxiEajBKna5Nt83_YHr-VpJPAWTSed12GWRqssS5PWgaZToPIkbW9Oen90dk/s1600/04.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLOrYyNormd5o8E584Y8VuiTE9rgfC4ae8jELIxdV3nAG8gxYd-fJ7Jnzjn9IVLaiOB_3hrxd2nyBDlH-pxiEajBKna5Nt83_YHr-VpJPAWTSed12GWRqssS5PWgaZToPIkbW9Oen90dk/s1600/04.png" height="320" width="228" /></a></div>
<br />
Nuestro índice ixColor, sólo guarda los valores del Color y una liga hacia la tabla Production.Product donde se almacena la fila a la que pertenece, de ahí que se tenga que hacer ese Key Lookup para encontrar los datos que le hacen falta al query para devolver el resultado completo. Notemos que en la imagen aparecen en Output List Name, ProductNumber y MakeFlag.<br />
<br />
¿Qué pasa si siempre regresamos las mismas columnas?, ¿qué pasa si este query regresa un subconjunto de columnas que habitualmente se ocupan? Si este es el caso, estamos ante la perfecta opción de utilizar un índice de cobertura (covering index)<br />
<br />
¿Qué es un covering index?, es aquel que almacena también los valores que forman parte del resultado, es decir, que ya no debe de ir a la tabla original por ellos dado que tiene una copia actualizada en él.<br />
<br />
Vamos a crearlo:<br />
<br /><code>
CREATE INDEX ixColorCovering<br />
ON Production.Product (Color)<br />
INCLUDE (Name, ProductNumber, MakeFlag)</code><br /><br />
Y analicemos nuevamente el plan de ejecución para ver los cambios:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_S2XB2b2AY_Vg-E2VD8BzP8G12U1tlrOXbj3tKyVr-KTd702FYi_Bera9GG9bBBwk57xIccG2EyEl3bQKreicmzHMK-98gIxWDgXG7e8viarM8Vp1i7D8RGSte3RLriSFDs8rqrbCREE/s1600/05.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_S2XB2b2AY_Vg-E2VD8BzP8G12U1tlrOXbj3tKyVr-KTd702FYi_Bera9GG9bBBwk57xIccG2EyEl3bQKreicmzHMK-98gIxWDgXG7e8viarM8Vp1i7D8RGSte3RLriSFDs8rqrbCREE/s1600/05.png" height="46" width="320" /></a></div>
<br />
Podemos notar que ahora se está utilizando únicamente nuestro índice ixColorCovering. ¿Qué costo tiene actualmente el query?, el costo es de 0.0032864, lo cual significa una mejora del 74.17%.<br />
<br />
¿Qué tal?, ahora sí impresiona ¿no?, y eso que estamos hablando de una tabla con 504 filas, ahora imagina que estuviéramos trabajando en una tabla con miles o millones de filas, verás dos impactos mayúsculos:<br />
<ol>
<li>Tiempo de respuesta muchísimo menor.</li>
<li>Menor uso de memoria de SQL Server para poder despachar el resultado.</li>
</ol>
Antes de dar por finalizada esta entrada, quisiera remarcar lo siguiente: cuidado con los índices, recuerda que cada vez que generas un nuevo índice, SQL debe actualizar su valor si es que el valor original en la tabla fue alterado, esto significa que una simple escritura se puede traducir en muchas más.<br />
<br />
Espero te haya resultado de utilidad esta entrada, nos vemos la siguiente.
<hr />
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/04/covering-indexes.html" data-send="true" data-show-faces="false" data-width="450">
</div>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com1tag:blogger.com,1999:blog-7612527879676339024.post-25508976106397750802015-04-29T13:55:00.001-05:002015-04-29T13:55:42.098-05:00Twitter automatizado<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/04/twitter-automatizado.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
¿Tienes cuenta en Twitter?, lo más probable es que tu respuesta sea "sí". ¿Qué haces en Twitter?, siendo una red del tipo microblog, lo más probable es que sea para chismear un rato, ya sea para lo bueno o para lo malo, es decir, estar al día o encontrar personas a las cuales molestar mediante comentarios incómodos, insultantes e hirientes.<br />
<br />
Seguro habrás notado que muchos de los mensajes publicados en esta red social, vienen de calendarios programados de publicación, es decir, creo mi lista de mensajes, les pongo fecha y hora ¡y listo!<br />
<br />
Realmente no hay algo de malo en ello si es que se le da un seguimiento a las respuestas o a los comentarios recibidos, de manera tal que la red se enriquezca y también se exploten las características de ésta. Pero, qué pasa cuando...<br />
<ol>
<li>Se programan respuestas automáticas.</li>
<li>Se monitorean cuentas de otras personas o instituciones y se hace retweet sobre cada una de sus publicaciones.</li>
<li>Un sistema recopila los tweets con mayor impacto de las personas que se siguen para después hacer un "resumen personal y ejecutivo" de lo que otros publicaron.</li>
</ol>
Vayamos un poco más allá, ¿qué pasa si se deja que estos sistemitas se pongan a jugar y recopilar información para que entre ellos mismos se sigan y se den un retweet, follow, unfollow, etc.?, ¿no sería lo mismo que no existiera la red?<br />
<br />
Me disculparás por ser tan tradicional, creo más en la relación personal, en ese ir y venir de ideas, comentarios, aseveraciones, discusiones, etc. ¿Para qué colocar sistemas de actividad automatizada en una red social?, es como un "me invitaron a una fiesta, voy a ir, pero dejo a mi robot para que haga como que estoy porque me importa un cacahuate la interacción".
<hr />
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/04/twitter-automatizado.html" data-send="true" data-show-faces="false" data-width="450">
</div>
Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-51175460975596121602015-03-18T11:12:00.001-06:002015-05-14T13:31:41.847-05:00Caracteres inválidos en XML<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/03/caracteres-invalidos-en-xml.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr/>
El otro día un exalumno me preguntaba si había alguna forma amigable de sustituir los caracteres no válidos en un texto para que éstos puedan ser integrados como parte de un documento XML.<br />
<br />
Después de revisar todo el trabajo que había realizado, del cual la mayoría había sido extraído de internet, noté que le estaba dando bastantes vueltas al tema siendo algo tan simple como una sola línea de código.<br />
<br />
Supongamos que tenemos la siguiente descripción:<br />
<br />
<i>El auto <de Juan & Rosy> es llamado "el volador"</i><br />
<br />
Y queremos asignarla al atributo description del siguiente elemento:<br />
<br />
<object description="" /><br />
<br />
La solución es sencilla, utilicemos la función Escape de la clase SecurityElement que encontramos en el espacio de nombres System.Security, el resultado será el siguiente:<br />
<br />
<i>El auto &lt;de Juan &amp; Rosy&gt; es llamado &quot;el volador&quot;</i><br />
<i><br /></i>
Este último texto puede ser asignado sin problemas al atributo.<br />
<br />
Como puedes ver es muy sencillo, no hay que darle tantas vueltas al tema y así te podrás quitar de muchos dolores de cabeza y operaciones sobre un objeto String.
<hr/>
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/03/caracteres-invalidos-en-xml.html" data-send="true" data-show-faces="false" data-width="450">
</div>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-92055100447325650652015-02-27T18:00:00.001-06:002015-02-27T18:03:10.040-06:00Validador de contabilidad digital<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/02/validador-de-contabilidad-digital.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
El día de hoy por la tarde (27 de febrero de 2015) me encontré un <a href="https://twitter.com/SATMX/status/571407852935344128" target="_blank">tweet</a> en la <a href="https://twitter.com/SATMX" target="_blank">cuenta del SAT</a> donde se anunciaba que existe un validador para los archivos XML de contabilidad electrónica, ¿reacción inmediata?, ¡vamos a probar los generadores en los que hemos trabajado!<br />
<br />
¡Oh sorpresa!, entras a la página y al elegir un archivo XML muestra un mensaje de error diciendo:<br />
<br />
"Validación de Nomenclatura: El nombre del Documento es inválido. Tipo identificado: Ninguno."<br />
<br />
No se necesita tener un profundo conocimiento informático para notar que el problema era el nombre del archivo, por lo que inmediatamente busqué alguna liga en la página donde viniera alguna documentación de cómo debe llamarse el archivo para que el validador pueda tomarlo:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj7XTRxRSnrMYLxWcU84SGgiyKCGgK7yBPlG-pxRLyb8MvBQjUeLmmRXlJS2xyPhHw07q7tK77xIr6O1Vc9tpXVL6LY_Mj1-9QwXt2UyTA-v5gFKnHUVwDTcfeRnohl5aGlgZIk1OVB-PM/s1600/Web.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj7XTRxRSnrMYLxWcU84SGgiyKCGgK7yBPlG-pxRLyb8MvBQjUeLmmRXlJS2xyPhHw07q7tK77xIr6O1Vc9tpXVL6LY_Mj1-9QwXt2UyTA-v5gFKnHUVwDTcfeRnohl5aGlgZIk1OVB-PM/s1600/Web.png" height="179" width="320" /></a></div>
<br />
Realmente no hay mucho dónde buscar, lo cual me llevó a deducir que lo publicaron a las prisas (como casi nunca pasa con los sistemas del SAT y las empresas que le desarrollan software)<br />
<br />
¿Qué opción tenemos?, ¡claro!, el <a href="http://www.sat.gob.mx/contacto/orientacion_en_linea/Paginas/chat_uno_a_uno.aspx" target="_blank">chat uno a uno del SAT</a>, ahí he realizado muchas consultas relacionadas con situaciones fiscales y siempre ha funcionado todo muy bien.<br />
<br />
Pero... me surgió la duda... no voy a preguntar de cuestiones fiscales, ¡sino técnicas!, y ahí comenzó la parte triste de la historia, pero bueno, habrá que darles el beneficio y me animé a iniciar un chat con su heroico grupo de técnicos.<br />
<br />
La excelente noticia es que la plática sólo duró 8 minutos, fue bastante rápido... la mala noticia, es que la persona que atendió no tiene ni la mínima idea de cómo se utiliza un sistema <strong>que ellos mismos publicaron</strong>.<br />
<br />
Tristemente, el soporte técnico sigue siendo el talón de Aquiles, las razones no las conozco, pero ya es algo común y creo que les importa 2 kilos de cebollas que sea así.<br />
<br />
¡Pero!, se encontró la solución, después de ponerme a analizar el código de la página, pude deducir los nombres de los archivos y a continuación los presento.<br />
<br />
<strong>Catálogo de cuentas:</strong><br />
<ul>
<li>RFC completo</li>
<li>Los 4 dígitos del año</li>
<li>Número de mes del catálogo de cuentas expresado en dos dígitos.</li>
<li>CT, sí, textualmente las letras CT.</li>
<li>Ejemplo, si queremos validar el XML del catálogo de cuentas del RFC XAXX010101XXX del mes de enero de 2015, el archivo debe llamarse <strong>XAXX010101XXX201501CT.xml</strong></li>
</ul>
<strong>Balanza de comprobación normal:</strong><br />
<ul>
<li>RFC completo</li>
<li>Los 4 dígitos del año</li>
<li>Número de mes de la balanza de comprobación expresado en dos dígitos</li>
<li>BN</li>
<li>Ejemplo, si queremos validar el XML de la balanza de comprobación normal del RFC XAXX010101XXX del mes de febrero de 2015, el archivo debe llamarse <strong>XAXX010101XXX201502BN.xml</strong></li>
</ul>
<strong>Balanza de comprobación complementaria:</strong><br />
<ul>
<li>RFC completo</li>
<li>Los 4 dígitos del año</li>
<li>Número de mes de la balanza de comprobación expresado en dos dígitos</li>
<li>BC</li>
<li>Ejemplo, si queremos validar el XML de la balanza de comprobación complementaria del RFC XAXX010101XXX del mes de enero de 2015, el archivo debe llamarse <strong>XAXX010101XXX201501BC.xml</strong></li>
</ul>
<strong>Pólizas:</strong><br />
<ul>
<li>RFC completo</li>
<li>Los 4 dígitos del año</li>
<li>Número de mes de las pólizas</li>
<li>PL</li>
<li>Ejemplo, si queremos validar la presentación de pólizas del RFC XAXX010101XXX del mes de febrero de 2015, el archivo debe llamarse <strong>XAXX010101XXX201502PL.xml</strong></li>
</ul>
<strong>Auxiliar de folios:</strong><br />
<ul>
<li>RFC completo</li>
<li>Los 4 dígitos del año</li>
<li>Número de mes del auxiliar de folios</li>
<li>XF</li>
<li>Ejemplo, si queremos validar el auxiliar de folios del RFC XAXX010101XXX del mes de enero de 2015, el archivo debe llamarse <strong>XAXX010101XXX201501XF.xml</strong></li>
</ul>
<strong>Auxiliar de cuentas:</strong><br />
<ul>
<li>RFC completo</li>
<li>Los 4 dígitos del año</li>
<li>Número de mes del auxiliar de cuentas</li>
<li>XC</li>
<li>Ejemplo, si queremos validar el auxiliar de cuentas del RFC XAXX010101XXX del mes de febrero de 2015, el archivo debe llamarse <strong>XAXX010101XXX201502XC.xml</strong></li>
</ul>
<hr />
Es importante señalar que la extensión también puede ser .zip, creo que lo manejarán así cuando se presenten las pólizas porque seguramente ese archivo va a ser muy grande.<br />
<br />
Espero te resulte de utilidad y te ahorre mucha (pero mucha) pérdida de tiempo con el soporte técnico del SAT.
<br />
<hr />
<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2015/02/validador-de-contabilidad-digital.html" data-send="true" data-show-faces="false" data-width="450">
</div>
Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com1tag:blogger.com,1999:blog-7612527879676339024.post-19244358657513524712014-07-16T15:01:00.001-05:002014-07-16T15:01:22.276-05:00Redes sociales en el SAT<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2014/07/redes-sociales-en-el-sat.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
En esta entrada quisiera abordar un problema que, en medio de la explosión de las redes sociales para empresas, está sucediendo y cada día es más común.<br />
<br />
Con tal de ahorrar dinero asignando personal no capacitado para atender las redes sociales, las empresas se están equivocando al atender a su cliente potencial o a su cliente actual, esto es a través de la atención equivocada de los mensajes recibidos a través de las redes sociales.<br />
<br />
¿Qué es lo que sucede?, el cliente realiza algún comentario, reclamo, pregunta, aviso o lo que sea a la empresa a través de sus redes sociales, dado que el personal que atiende estas redes no conoce la operación o no le interesa conocerla, las respuestas pueden ir desde un error de ubicación hasta una frase escrita con el estómago en mano.<br />
<br />
En esta ocasión haré alusión a la forma en que se manejan las redes sociales en el SAT, sí, nuestro Servicio de Administración Tributaria.<br />
<br />
Desde el día 16 de julio por la mañana, reporté vía Twitter que el <a href="https://portalcfdi.facturaelectronica.sat.gob.mx/" target="_blank">portal del contribuyente</a> no estaba mostrando factura alguna del año 2014, mencionando el software que actualmente tengo en el equipo con el cual se realizó la conexión (IE 11, Chrome 30, Java 7 update 60) y también mencionando que se utilizaron dos ISP diferentes, Infinitum y Megared.<br />
<br />
Aquí la imagen de cómo comenzó la comunicación:<br />
<span id="goog_2072329543"></span><span id="goog_2072329544"><br /></span>
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSLAe519UBnZKH6wuEz8OWKx1d584I-7NaRVHnOgtcBGe8Pa7tdfzxyjEP2fVZUKDKlKvx-kuLj10x9fDlrkZuj885AAh-qMCtsa_Rkd857YSXKYnOl0iuU9iFUYNXMIdBW9RCvQeUGMA/s1600/tweet1.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgSLAe519UBnZKH6wuEz8OWKx1d584I-7NaRVHnOgtcBGe8Pa7tdfzxyjEP2fVZUKDKlKvx-kuLj10x9fDlrkZuj885AAh-qMCtsa_Rkd857YSXKYnOl0iuU9iFUYNXMIdBW9RCvQeUGMA/s1600/tweet1.png" height="296" width="320" /></a></div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Uno intenta dar toda la información posible del entorno para que el departamento de soporte pueda recibir información que sirva y no un normal "no funciona el sistema", al final somos colegas y sabemos que hay información que les puede resultar de utilidad.</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
A lo cual respondieron con un "intente de nuevo", cosa que se hizo casi de inmediato y el resultado fue el mismo, que no encuentra comprobantes emitidos.</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Me preguntaron por el medio por el cual había hecho la factura... aquí es donde pensé "ups, creo que la persona equivocada es la que está atendiendo esta pequeña plática", esto es porque no importa desde qué PAC realices sus timbrados, éstos siempre deberán aparecer en el portal de contribuyentes; antes aparecían y mágicamente ya no.</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Cuando le dije que la factura se había hecho desde un PAC, la respuesta fue la que temía reafirmaría y comprobaría mi temor de estar siendo atendido por una persona sin capacitación: "consúltalo directamente con tu proveedor de factura electrónica".</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Después de mensajes, impresiones de pantalla e información extra, es más que claro que el problema está en el portal de contribuyentes o bien algo está fallando en la plataforma. Es entendible que las personas que atienden las redes sociales no son personal de sistemas, sino de comunicación con cierta preparación fiscal, pero el problema radica en que lo que leen no lo entienden, supongo que la empresa que administra las redes sociales del SAT tiene la política de batear lo más que pueda.</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Haciendo a un lado la problemática que sigue latente después de más de 21 horas (y no sé cuántas más, si es que se dignan estos personajes a querer entender), es importante notar la importancia que debe tener el contar con gente con criterio atendiendo las redes sociales, es cuestión de pensar un poco para notar que el problema va más allá de un "reinicie y sea feliz", con la información técnica a la mano el personal de redes sociales debe identificar un problema que está fuera de su alcance el resolver.</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Desde hace tiempo he tenido mucha comunicación con un amigo que es consultor en redes sociales, y el tema recurrente es que el ahorro en la atención de redes sociales se traduce en una exposición negativa y posible pérdida de clientes de las empresas. En este caso es el SAT y no hay de otra más que lidiar con ellos, pero esto no es exclusivo del ambiente gubernamental, muchas (y quiero enfatizar en "muchas") empresas no le dan la importancia debida, y cuando el resultado comienza a ser preocupantemente negativo no saben ni por dónde entrarle.</div>
<div class="separator" style="clear: both; text-align: left;">
<br /></div>
<div class="separator" style="clear: both; text-align: left;">
Mi especialidad es SQL Server y el desarrollo de software en .NET, pero esta problemática no necesita de un especialista para notarla, y en definitiva creo que las empresas deberían poner un poco de más atención a estos canales de comunicación y no colocarlos en manos de charlatanes o de personal que no cuente con madurez profesional ni conocimiento del medio y del negocio.</div>
Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-3114728566581586942014-05-02T11:23:00.003-05:002014-05-02T11:27:34.648-05:00Dirección física de carpetas compartidas<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<br />
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2014/05/direccion-fisica-de-carpetas-compartidas.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
Quiero compartir una entrada muy pequeña pero que les puede resultar de mucha utilidad cuando el desorden en el almacenamiento de archivos y creación de carpetas compartidas, se ha convertido en un reto muy importante para una empresa.<br />
<br />
<br />
Hoy me encontraba en una empresa revisando el sistema de gestión de archivos compartidos que se implementaría y me encontré con el detalle de que existía una cantidad muy grande de carpetas compartidas en el equipo que utilizan como repositorio de archivos.<br />
<br />
<br />
En el equipo, no quedaba nada claro cuál era la dirección local de las carpetas compartidas, en realidad toda la información está muy dispersa y desordenada, no existe documentación de la localización de las carpetas ni su razón de existir.<br />
<br />
<br />
El comando que utilicé para encontrar la localización física de las carpetas compartidas fue "net share", ejecutándolo en una consola de línea de comandos fue extremadamente sencillo encontrar la información que necesitaba.<br />
<br />
<br />
Quise compartirlo con ustedes, porque probablemente algún día se encuentren con este problema y será muy simple resolverlo con dicho comando.<br />
<br />
<br />
¡Saludos!Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com1tag:blogger.com,1999:blog-7612527879676339024.post-78924154180298954832013-12-12T15:00:00.000-06:002013-12-12T15:00:01.387-06:00Read uncommitted<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2013/12/read-uncommitted.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
<style type="text/css">
code { white-space: pre; }
</style>
<p>En esta entrada exploraremos cómo funciona el nivel de aislamiento READ UNCOMMITTED.</p>
<p>Antes de comenzar el ejercicio quisiera hacer notar que el nivel de aislamiento por defecto de SQL Server es READ COMMITTED, esto implica que no podemos leer la información
sino hasta que ésta ha sido actualizada de forma definitiva (committed) en la base de datos o bien los cambios han sido deshechos a través de un rollback.</p>
<p>Por ejemplo, supongamos que la transacción 1 ejecuta la siguiente instrucción:<br />
<code>
USE AdventureWorks
GO
BEGIN TRAN
UPDATE Production.Product
SET ListPrice += 1
WHERE ProductID = 1</code></p>
<p>Notamos que la transacción queda "abierta", es decir que no ha sido confirmada hacia la base de datos (committed) o deshecha (rollback), por lo tanto SQL Server mantendrá
el bloqueo de tipo exclusivo sobre el producto cuyo id sea 1.</p>
<p>En otra ventana ejecutaremos la siguiente sentencia:<br />
<code>
USE AdventureWorks
GO
BEGIN TRAN
UPDATE Production.Product
SET ListPrice += 1
WHERE ProductID = 1</code></p>
<p>Veremos que este query queda en espera debido a que el nivel de bloqueo que él requiere (READ COMMITTED) no le permite obtener la información que necesita debido a que
la transacción 1 no ha finalizado. La transacción 1 tiene un bloqueo exclusivo que no es compatible con READ COMMITTED de otra transacción.</p>
<p>Esta información la podemos confirmar con la siguiente sentencia:<br />
<code>
USE AdventureWorks
GO
SELECT DB_NAME(T.resource_database_id) AS database_name, OBJECT_NAME(P.[object_id])
FROM sys.dm_tran_locks T
INNER JOIN sys.partitions P
ON T.resource_associated_entity_id = P.hobt_id
WHERE T.request_mode = 'X'</code></p>
<p>Veremos que se tiene un bloqueo exclusivo sobre algún elemento del objeto Production.Product. También se podría obtener la fila que está siendo bloqueada, pero perderíamos
el enfoque de esta entrada.</p>
<p>Volvamos a la ventana de la transacción 1 y ejecutemos COMMIT, veremos que de forma casi inmediata la transacción 2 será desbloqueada y podrá obtener los datos que requiere.</p>
<p>¿Qué pasa cuando la información que queremos obtener es "inmutable"?, no quisiera utilizar la palabra inmutable en su totalidad debido a que en teoría los únicos datos
inmutables de una base de datos son las llaves primarias, pero podemos suponer que el nombre de un producto es inmutable, sería muy extraño tener que actualizar el nombre de
un producto. En este caso no requerimos que alguna otra transacción genere algún bloqueo sobre la lectura del Id y del Nombre del producto, por lo tanto, necesitamos leer
los datos independientemente de los cambios que se estén realizando sobre ellos.</p>
<p>Antes de hacer cambios en el código, vamos a la ventana de la transacción 2 y ejecutamos COMMIT para no dejar transacciones abiertas (colgadas) en el manejador de bases
de datos.</p>
<p>Volvamos a ejecutar el código de la transacción 1 para generar un bloqueo sobre el producto cuyo Id es 1, y vamos a cambiar un poco el código de la transacción 2:<br />
<code>
<b>SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED</b>
BEGIN TRAN
SELECT ProductID, Name, ListPrice
FROM Production.Product
WHERE ProductID = 1</code></p>
<p>Podemos notar que ahora estamos estableciendo el nivel de aislamiento en READ UNCOMMITTED para poder leer datos que no han sido hechos "oficiales" en la base de datos. Si
ejecutamos la transacción veremos que la información del nombre, producto y precio son inmediatamente devueltos.</p>
<p>Hasta ahora todo parecería que salió tal como lo deseamos, pero... ¿qué pasaría si necesitáramos realizar alguna acción tomando el precio del producto?, he ahí que vendría
un fenómeno conocido como "dirty reads", eso quiere decir que podríamos leer un valor que no ha sido hecho oficial sobre la base de datos y que posiblemente su cambio pudiera
ser deshecho, es decir que operaríamos con un valor que jamás debió ser tomado.</p>
<p>¿Qué debemos hacer?, dependerá de la operación y de los valores que queremos utilizar, deberemos establecer categorías de los datos que requieren un bloqueo exclusivo y
también identificar los datos que pueden ser leídos durante una transacción de actualización.</p>
<p>Espero te haya resultado de utilidad esta entrada, nos vemos la siguiente.</p>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-3903738696615382142013-12-11T15:00:00.000-06:002013-12-11T15:00:05.186-06:00Refrescando los metadatos de una vista<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2013/12/refrescando-los-metadatos-de-una-vista.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
<style type="text/css">
code { white-space: pre; }
</style>
<p>En esta entrada mostraré un fenómeno que se da cuando no establecemos de forma explícita las columnas que queremos devolver en una vista.</p>
<p>Comenzaremos creando una tabla, metiéndole datos y creando una vista que mostrará el contenido de toda la tabla:<br />
<code>
CREATE TABLE Test(
Id INT IDENTITY(1, 1),
Value VARCHAR(10)
)
GO
INSERT INTO Test (Value)
VALUES ('Valor 1'), ('Valor 2'), ('Valor 3')
GO
CREATE VIEW vTest
AS
SELECT * FROM Test
GO
SELECT * FROM vTest</code></p>
<p>Veremos que el resultado de la consulta a la vista nos devolverá el contenido completo de la tabla (tal como lo esperábamos):</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4VhDl-U30lpdfzPShy_v_x5fJPHcNoyEdlQJbmC4WjsDF7lGM5ww7CULId2iyiUjPZY7zsGQY4HbSjHkAh2292vIiLn8HgWr4tmrUm3ZchD9TljE3jb_hxnCTe6AilCkxw6iVYJgy5DU/s1600/01_Result.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg4VhDl-U30lpdfzPShy_v_x5fJPHcNoyEdlQJbmC4WjsDF7lGM5ww7CULId2iyiUjPZY7zsGQY4HbSjHkAh2292vIiLn8HgWr4tmrUm3ZchD9TljE3jb_hxnCTe6AilCkxw6iVYJgy5DU/s1600/01_Result.png" /></a></div>
<p>Ahora vamos a modificar la estructura de nuestra tabla para agregar una columna calculada que muestre la primera letra de la columna Value:<br />
<code>
ALTER TABLE Test
ADD FirstLetter AS SUBSTRING(Value, 1, 1) PERSISTED</code></p>
<p>Ahora vamos a ejecutar un query que muestre todo el contenido de la tabla y también que muestre todo el contenido de la vista:</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjiERRmb04bE6dNGmS66keqMITovtBeXwEFPzMedgMDtu1cO4qcygKrW_3mXLXUUq3UCJvPOSriNssD6HI8Kc6QJYJxyZvDwQsEQKltKb74ijKrRqkYHm_ojLxRuUbSLW8j6Xr7i8qjzF4/s1600/02_Result.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjiERRmb04bE6dNGmS66keqMITovtBeXwEFPzMedgMDtu1cO4qcygKrW_3mXLXUUq3UCJvPOSriNssD6HI8Kc6QJYJxyZvDwQsEQKltKb74ijKrRqkYHm_ojLxRuUbSLW8j6Xr7i8qjzF4/s1600/02_Result.png" /></a></div>
<p>Notemos que la primera sentencia que trabaja sobre la tabla muestra las tres columnas que conforman al objeto, mientras que la segunda sentencia sólo
muestra dos columnas, las columnas que existían en la tabla durante el momento de la creación de la vista. ¿Qué es lo que está pasando?, que los metadatos
de la vista siguen siendo los mismos, es por ello que aunque cambió la definición de la tabla, la vista sigue utilizando las mismas columnas que antes.</p>
<p>La forma de corregirlo es a través del procedimiento almacenado sp_refreshview:<br />
<code>
sp_refreshview 'vTest'
GO
SELECT * FROM vTest</code></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjX-gfqOl6XRX1qOA4w_4baha22E5dSrRcWa5m4cw6WUUXv9jpxVLbvmLnEoEfurY9xVNVnuUJemgxC-kl5MeHUvcJxbykoldLA1f_UJ9YyLJMU_e0bNoKV_2HyatCypPc_GTwESpfGV0U/s1600/03_Result.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjX-gfqOl6XRX1qOA4w_4baha22E5dSrRcWa5m4cw6WUUXv9jpxVLbvmLnEoEfurY9xVNVnuUJemgxC-kl5MeHUvcJxbykoldLA1f_UJ9YyLJMU_e0bNoKV_2HyatCypPc_GTwESpfGV0U/s1600/03_Result.png" /></a></div>
<p>Notemos que ahora ya se están mostrando las tres columnas que forman parte de la tabla.</p>
<p>Aunque te muestro cómo corregir este problema, no es nada recomendable utilizar un query del tipo SELECT * FROM Table porque se estará ignorando cualquier índice
que haya sido creado sobre la tabla para optimizar su lectura.</p>
<p>Espero esta entrada te haya resultado de utilidad, nos vemos la siguiente.</p>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-58466571933966178772013-12-10T15:00:00.000-06:002013-12-10T15:00:04.689-06:00Limpiar historial de respaldos<div id="fb-root">
</div>
<script>
(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2013/12/limpiar-historial-de-respaldos.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
<style type="text/css">
code { white-space: pre; }
</style>
<p>En esta entrada muy corta pero sumamente útil, te mostraré cómo eliminar el historial de copias de respaldo que se acumula en la base de datos msdb a lo largo del tiempo.</p>
<p>Comenzaré explicándote que no importa cómo saques el respaldo de tu base de datos, el historial de haberlo hecho se guarda, haya sido un respaldo de tipo Full, Log,
Differential o de cualquier otro tipo.</p>
<p>Es común que en servidores que tengan implementado un esquema de Log Shipping, el tamaño de la base de datos msdb se dispare, esto se debe a que la cantidad de respaldos
aumenta por la naturaleza propia de este esquema de alta disponibilidad y por lo tanto el historial de respaldos se incrementa.</p>
<p>Antes de limpiar el historial de respaldos es importante sacar un respaldo de la base de datos msdb, esto es para poder tener evidencia clara de las operaciones que se
realizaron y en caso de una auditoría podamos demostrar que se hicieron los respaldos en tiempo y forma. También puede servirnos para demostrar que no se hicieron los
respaldos, a veces la gente encargada de esta pequeña parte de la operación se le va el avión y no revisa que los respaldos se hayan ejecutado de forma correcta.</p>
<p>Si quisiéramos eliminar <strong>todo</strong> el historial de respaldos bastaría con utilizar el siguiente código:<br />
<code>
DECLARE @Today DATETIME = GETDATE()
EXEC msdb.dbo.sp_delete_backuphistory @Today</code></p>
<p>Veamos que el procedimiento almacenado sp_delete_backuphistory recibe como parámetro la fecha más antigua que se guardará en el historial de respaldos, eso implica que todo
registro histórico de respaldo que se haya creado antes de esa fecha ya no estará almacenado en msdb.</p>
<p>Si se sacara un respaldo cada 30 días de la base de datos msdb, bien podríamos estar limpiando el historial de la siguiente manera:<br />
<code>
DECLARE @MonthAgo DATETIME = DATEADD(DAY, -30, GETDATE())
EXEC msdb.dbo.sp_delete_backuphistory @MonthAgo</code></p>
<p>Espero esta entrada te haya resultado útil, nos vemos la siguiente.</p>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-89311277590823246942013-12-09T15:00:00.000-06:002013-12-09T15:00:00.709-06:00IDENTITY vs SCOPE_IDENTITY()<div id="fb-root">
</div>
<script>
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
} (document, 'script', 'facebook-jssdk'));
</script>
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2013/12/identity-vs-scopeidentity.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
<style type="text/css">
code { white-space: pre; }
</style>
<p>En esta entrada exploraremos la diferencia entre estas dos funciones del sistema que mucha gente utiliza
y que es sumamente importante conocer el valor que están devolviendo.</p>
<p>Comenzaremos creando dos tablas, en una agregaremos valores y la otra servirá para llevar un historial
muy sencillo de movimientos:<br />
<code>
CREATE TABLE TestIdentity(
Id INT IDENTITY(1, 1),
Value VARCHAR(10) NOT NULL
)
GO
CREATE TABLE TestIdentityLog(
Id INT IDENTITY(1, 1),
Task VARCHAR(10),
Value VARCHAR(10)
)</code></p>
<p>En la tabla TestIdentity iremos guardando valores de prueba y la tabla TestIdentityLog estará guardando el
historial de movimientos que se han llevado a cabo sobre la tabla TestIdentity. Debemos notar que en las dos tablas
tenemos una columna IDENTITY.</p>
<p>Ahora vamos a crear dos triggers, uno para el INSERT y otro para el DELETE:<br />
<code>
CREATE TRIGGER TestIdentity_Insert
ON TestIdentity
FOR INSERT
AS
BEGIN
INSERT INTO TestIdentityLog (Task, Value)
SELECT 'INSERT', Value
FROM INSERTED
END
GO
CREATE TRIGGER TestIdentity_Delete
ON TestIdentity
FOR DELETE
AS
BEGIN
INSERT INTO TestIdentityLog (Task, Value)
SELECT 'DELETE', Value
FROM DELETED
END</code></p>
<p>Ambos guardan el movimiento en la tabla de historial, sólo que el primero es para INSERT y el segundo para
DELETE</p>
<p>Vamos a crear un procedimiento almacenado para agregar un valor a TestIdentity, de paso mostraremos el resultado
de la ejecución de los dos valores que nos interesan: @@IDENTITY y SCOPE_IDENTITY():<br />
<code>
CREATE PROC pAddValue(
@Value VARCHAR(10)
)
AS
BEGIN
INSERT INTO TestIdentity (Value)
VALUES (@Value)
PRINT '@@IDENTITY: ' + CAST(@@IDENTITY AS VARCHAR)
PRINT 'SCOPE_IDENTITY(): ' + CAST(SCOPE_IDENTITY() AS VARCHAR)
END</code></p>
<p>Ahora vamos a crear un procedimiento almacenado que nos permita eliminar un elemento de la tabla TestIdentity:<br />
<code>
CREATE PROC pDeleteValue(
@Id INT
)
AS
BEGIN
DELETE TestIdentity
WHERE Id = @Id
END</code></p>
<p>Habitualmente se piensa que podemos utilizar cualquiera de los dos sin problema, pero al ejecutar el siguiente código
veremos que ambas nos mostrarán números diferentes en la tercera ejecución:<br />
<code>
SET NOCOUNT ON
GO
EXEC pAddValue 'Valor 1'
GO
EXEC pDeleteValue 1
GO
EXEC pAddValue 'Valor 1'</code></p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhD6UJgnVNzseOGSamhKpl36ugaGU6fCb153QiA_HLDZv0vEdp6k25i6CPoh0WrOf24qhwMGwJU5Qe10mX21VJyHrwqu8Yr-sPYgWBsEtQbOByPETuBXUMSKkUJKZtSw0tv4S45wB0RccE/s1600/01_Result.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhD6UJgnVNzseOGSamhKpl36ugaGU6fCb153QiA_HLDZv0vEdp6k25i6CPoh0WrOf24qhwMGwJU5Qe10mX21VJyHrwqu8Yr-sPYgWBsEtQbOByPETuBXUMSKkUJKZtSw0tv4S45wB0RccE/s1600/01_Result.png" /></a></div>
<p>El primer EXEC mostrará que @@IDENTITY devuelve 1 y SCOPE_IDENTITY() devuelve 1, pero veremos que el tercer EXEC mostrará que @@IDENTITY devuelve 3 y
SCOPE_IDENTITY() devuelve 2.</p>
<p>Esto se debe a que @@IDENTITY devuelve el <strong>último</strong> valor obtenido a través de una columna IDENTITY, este valor pertence a la columna Id
de la tabla TestIdentityLog, los trigger que se están disparando ante el INSERT o DELETE son los que están realizando esa inserción y por lo tanto el <strong>último</strong>
valor asignado es el de la tabla TestIdentityLog.</p>
<p>La función SCOPE_IDENTITY() devuelve el último valor asignado por IDENTITY <strong>en el entorno actual de ejecución</strong>, para nuestro procedimiento pAddValue el único IDENTITY que tiene
a su alcance es el que está en la columna Id de la tabla TestIdentity, es por ello que devuelve el valor 2.</p>
<p>Como podrás notar, es importante conocer la diferencia entre las dos opciones que tenemos, dependerá del objetivo del procedimiento o de la operación cuál te resulte adecuado
a tus necesidades; generalmente utilizarás SCOPE_IDENTITY()</p>
<p>Espero esta entrada te haya resultado útil, nos vemos la siguiente.</p>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-51277592863397005012013-12-06T15:00:00.000-06:002013-12-06T15:00:02.841-06:00Mapa con geolocalización<div id="fb-root">
</div>
<script>
(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2013/12/mapa-con-geolocalizacion.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
<style type="text/css">
code { white-space: pre; }
</style>
<p>En esta entrada crearemos un mapa de Google Maps utilizando la API versión 3.14 y ubicaremos un marcador
en la posición actual del usuario utilizando la geolocalización del dispositivo.</p>
<p>Para comenzar crearemos un sitio web en VS2010 o VS2012 y agregaremos tres carpetas: css, img y js. En la carpeta js
colocaremos <a href="http://code.jquery.com/jquery-1.10.2.min.js">jQuery</a>, creamos un archivo map.js y también lo agregamos a la carpeta js. Creamos un archivo de hoja
de estilos llamado site.css y lo colocamos en nuestra carpeta css.</p>
<p>Para el icono de la posición actual del usuario podemos utilizar uno de los que aparecen en el sitio
<a href="http://mapicons.nicolasmollet.com" target="_blank">http://mapicons.nicolasmollet.com</a>, tienen un diseño atractivo
y se puede personalizar el color de fondo. Una vez elegido el icono lo guardamos en nuestra carpeta img con el nombre current_location.png</p>
<p>Una vez realizados estos pasos nuestro Explorador de Soluciones debería verse de la siguiente forma:</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhi4s01ZmvFABkBLUBmUw9PgErVfJpq32eL0V3fIjiZROImfu3PfGAx5AHlrnmiXTBKUrFGk635luhDAQuTXqjXDh8fP_PZNaLXXs4WIsIMuNDIjJO5OlM0ZEi5TvimMwh0iJYL5ZjCWt0/s1600/01_solution_explorer.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhi4s01ZmvFABkBLUBmUw9PgErVfJpq32eL0V3fIjiZROImfu3PfGAx5AHlrnmiXTBKUrFGk635luhDAQuTXqjXDh8fP_PZNaLXXs4WIsIMuNDIjJO5OlM0ZEi5TvimMwh0iJYL5ZjCWt0/s1600/01_solution_explorer.png" /></a></div>
<p>Vamos a incorporar todos los elementos junto con la referencia a la API de Google Maps en nuestra página HTML con el siguiente código:<br />
<code>
<head>
<title>Ubicación actual</title>
<script src="http://maps.google.com/maps/api/js?v=3.14&sensor=false&language=es-mx" type="text/javascript"></script>
<script src="js/jquery-1.10.2.min.js" type="text/javascript"></script>
<link href="css/site.css" rel="stylesheet" type="text/css" />
<script src="js/map.js" type="text/javascript"></script>
</head>
<body>
<div id="map"></div>
</body>
</html></code></p>
<p>Vamos a abrir nuestra hoja de estilos y vamos a colocar algunos estilos para que el mapa ocupe la pantalla completa del dispositivo:<br />
<code>
html, body { margin: 0px; height: 100%; }
#map { width: 100%; height: 100%; }</code></p>
<p>Ahora vamos a generar el código que nos permitirá la creación del mapa, la localización de la ubicación del usuario y la colocación del respectivo marcador en el mapa.</p>
<p>Abriremos nuestro archivo map.js y comenzaremos con la obtención de la ubicación actual del usuario:<br />
<code>
///<reference path="jquery-1.10.2.min.js" />
var blogger = blogger || {};
blogger.start = function () {
if (window.clientInformation) {
window.clientInformation.geolocation.getCurrentPosition(
function (e) {
var latlng = new google.maps.LatLng(e.coords.latitude, e.coords.longitude);
initialize(latlng);
},
function (e) {
initialize(null);
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
}
else {
window.navigator.geolocation.getCurrentPosition(
function (e) {
var latlng = new google.maps.LatLng(e.coords.latitude, e.coords.longitude);
initialize(latlng);
},
function (e) {
initialize(null);
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
}
function initialize(e) {
}
}
$(document).ready(function () {
blogger.start();
});</code></p>
<p>En la primera línea de código estamos agregando una referencia a la librería jQuery para que el editor tome la definición de las funciones de esa librería y nos facilite
la codificación.</p>
<p>Estamos creando un espacio de nombres llamado blogger para que el manejo de nuestras variables y funciones no vayan a causar conflicto con otras que estén
depositadas en el espacio global. Una vez que haya finalizado la carga de la página estamos mandando a llamar a la función blogger.start que será la encargada de
todo el trabajo.</p>
<p>Estamos validando que exista la propiedad window.clientInformation para poder diferenciar entre el navegador Safari y los demás, veamos que cuando no existe esa propiedad
la obtención de la localización actual del usuario se hace a través del objeto window.navigator</p>
<p>La función getCurrentPosition recibe tres argumentos, el primero marca la función que se mandará a llamar cuando se haya podido obtener la posición del usuario, el segundo
es la función que se ejecutará cuando no se haya podido obtener la localización del usuario y el tercero es un conjunto de opciones que nos permiten establecer si queremos
la mayor exactitud posible, el tiempo en milisegundos que esperaremos para obtener la posición y el tiempo que esta posición estará guardada en caché.</p>
<p>Veamos que cuando la localización haya sido exitosa, se estará construyendo un objeto google.maps.LatLng, este objeto nos permite establecer ubicaciones en la notación
(Latitud, Longitud) en el mapa.</p>
<p>Ahora vamos a codificar nuestra función initialize que creará el mapa y el marcador con la ubicación actual del usuario.</p>
<p>Comenzaremos con la creación del mapa centrándolo en la ubicación del usuario con un zoom de 14:<br />
<code>
function initialize(e) {
var mapOptions = {
center: e,
zoom: 14
};
blogger.map = new google.maps.Map(document.getElementById('map'), mapOptions);
}</code></p>
<p>Veamos que el constructor del objeto google.maps.Map nos pide el nodo donde se alojará el mapa (en nuestro caso un div cuyo id es "map") y también un literal object
que tenga la configuración del mapa. Vamos a abrir nuestro archivo index.htm con el navegador y veamos que nos pedirá la autorización para enviar nuestra localización a
la página web, esto es completamente normal.</p>
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirXMJPem_CjYBnGOfhfF0URqoIvgHpVVpTwBtoISeNVeoGzgGP5gYUkAe8oMaOUkd5Di18DiviNL3Adsr1Tyvq0eGw5DrtK0Yi_Daav80L6eRdlYLJ_GA2C9Ag16gK7_kPzRMMBE1nFYg/s1600/02_location.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="16" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEirXMJPem_CjYBnGOfhfF0URqoIvgHpVVpTwBtoISeNVeoGzgGP5gYUkAe8oMaOUkd5Di18DiviNL3Adsr1Tyvq0eGw5DrtK0Yi_Daav80L6eRdlYLJ_GA2C9Ag16gK7_kPzRMMBE1nFYg/s320/02_location.png" width="320" /></a></div>
<p>Ahora vamos a crear un marcador que utilice la imagen que tenemos en la carpeta img y lo colocaremos en la posición actual del usuario:
<code>
function initialize(e) {
var mapOptions = {
center: e,
zoom: 14
};
blogger.map = new google.maps.Map(document.getElementById('map'), mapOptions);
<b>var markerOptions = {
position: e,
map: blogger.map,
title: 'Usted se encuentra aquí',
icon: 'img/current_location.png'
}
var currentPositionMarker = new google.maps.Marker(markerOptions);</b>
}</code></p>
<p>El constructor del objeto google.maps.Marker nos pide un objeto literal que traiga la configuración del marcador, en este caso nos pide la posición del marcador
(google.maps.LagLnt), el mapa donded estará localizado el marcador (blogger.map), el texto que mostrará como tooltip del marcador y la url del icono que queremos utilizar.
En caso de que no utilicemos el atributo "icon", entonces se colocará el marcador que tiene por defecto Google Maps.</p>
<p>Si visitamos de nueva cuenta nuestro sitio, veremos que colocará el marcador en nuestra posición actual.</p>
<p>Espero te haya resultado de utilidad esta entrada, actualmente muchas aplicaciones están requiriendo del uso de mapas y Google Maps es una opción muy buena dada la
facilidad que tiene su API para ser utilizada.</p>
<p>¡Nos vemos la siguiente!</p>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0tag:blogger.com,1999:blog-7612527879676339024.post-37221326677959190842013-12-05T15:00:00.000-06:002013-12-05T15:00:01.039-06:00Web Workers<div id="fb-root">
</div>
<script>
(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/es_LA/all.js#xfbml=1";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<div class="fb-like" data-href="http://blog.jorgetoriz.com/2013/12/web-workers.html" data-send="true" data-show-faces="false" data-width="450">
</div>
<hr />
<style type="text/css">
code { white-space: pre; }
</style>
<p>En esta entrada veremos la implementación de un Worker de HTML5 muy sencillo pero con implicaciones de conocimiento que vale la pena aprender.</p>
<p>Los Worker se hicieron para ejecución de tareas asíncronas que se ejecutarán en un hilo ajeno al que está ejecutando la página web. Tan ajeno que desde el Worker
no tenemos acceso al DOM de la página, es decir que toda la sincronización se hace a través de mensajes, de objetos que sean serializables. Por excelencia utilizaremos
JSON.</p>
<p>El problema planteado es que necesitamos realizar peticiones cada 10 segundos a un endpoint el cual devolverá un valor estadísticamente único, simulando que debemos estar
checando el servidor para ver si hay datos nuevos para actualizar la interfaz del usuario.</p>
<p>Para esto vamos a preparar nuestro entorno de desarrollo (VS2010 o VS2012):</p>
<ol>
<li>Crear un sitio web</li>
<li>Agregar una carpeta llamada js</li>
<li>Agregar un archivo index.htm</li>
<li>Agregar un Generic Handler llamado GetRandom.ashx</li>
<li>Agregar jQuery a la carpeta js</li>
<li>Agregar dos archivos js a la carpeta: tools.js y randomWorker.js</li>
</ol>
<p>El archivo tools.js será el que esté en comunicación con el randomWorker, éste último es el que realizará las peticiones a GetRandom.ashx para un valor nuevo.</p>
<p>Prepararemos nuestro archivo index.htm con el siguiente código:<br />
<code>
<head>
<title>Valor aleatorio a través de Worker</title>
<script src="js/jquery-1.7.2.min.js" type="text/javascript"></script>
<script src="js/tools.js" type="text/javascript"></script>
</head>
<body>
<p>Este es el valor aleatorio generado desde el servidor: <span id="lblAleatorio"></span></p>
</body></code>
</p>
<p>Vamos a abrir nuestro randomWorker.js para programar la tarea asíncrona que se estará ejecutando cada 10 segundos:<br />
<code>
self.onmessage = function (e) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
postMessage(JSON.parse(xmlhttp.response));
}
}
var url = '../GetRandom.ashx';
xmlhttp.open('GET', url);
xmlhttp.send();
}</code>
</p>
<p>Estamos creando una instancia de XMLHttpRequest para realizar las peticiones a GetRandom.ashx. ¿Por qué no utilizamos $.ajax?, porque jQuery está basado en el DOM y por lo
tanto no puede ser utilizado desde un Worker.</p>
<p>Estamos asignando un manejador al evento onreadystatechange que se dispara cada vez que hay un cambio en el estado de la petición. Los valores para readyState son los
siguientes:<br /><br />
0: Sin inicializar, es decir que el método open no ha sido llamado.<br />
1: Cargando, ya fue abierto pero el método send no ha sido llamado.<br />
2: El método send ya fue llamado.<br />
3: La respuesta está siendo descargada.<br />
4: Todas las operaciones han finalizado.
</p>
<p>El atributo status establece un valor para el tipo de respuesta que se está recibiendo. Los códigos de estado pueden tener los siguientes valores:<br /><br />
1xx: El servidor ya recibió las cabeceras y se puede proceder con el envío del cuerpo de la petición.<br />
2xx: La solicitud fue recibida por el servidor y fue correcta.<br />
3xx: Para terminar la solicitud el cliente debe de tomar acciones adicionales. Generalmente encontraremos que se debe a que el recurso ya fue cambiado de dirección.<br />
4xx: La sintaxis de la petición no es la correcta o bien esta no puede ser procesada. Esto indica un error del lado del cliente.<br />
5xx: Ocurrió un error del lado del servidor.
</p>
<p>Tomando en cuenta lo anterior, el readyState que nos interesa es el 4 y el status es el 200, es por ello que esa validación se está haciendo en el if.</p>
<p>Nuestro GetRandom.ashx estará devolviendo información en formato JSON por lo que procedemos a procesarla y con la función postMessage se la enviamos al cliente del Worker
(tools.js)</p>
<p>Notemos también que no estamos utilizando la función window.setInterval para programar la petición cada 10 segundos, eso es debido a que en el Worker no tenemos acceso al DOM
y por lo tanto no tenemos acceso al objeto window.</p>
<p>Abrimos nuestro archivo tools.js y colocamos el siguiente código:<br />
<code>
var tools = tools || {};
tools.Worker = new Worker('js/randomWorker.js');
tools.Worker.onmessage = function (m) {
$('#lblAleatorio').text(m.data.value);
};
tools.Worker.onerror = function (e) {
alert('Ocurrió un problema durante la ejecución del Worker');
}
$(document).ready(function () {
tools.Worker.postMessage('getRandomValue');
window.setInterval(function () {
tools.Worker.postMessage('getRandomValue');
}, 10000);
});</code>
</p>
<p>Estamos creando un espacio de nombres "tools" para que cualquier variable que utilicemos no entre en conflicto con otra que esté en la página. Después estamos creando una
instancia del objeto Worker que será el encargado de ejecutar la tarea asíncrona establecida en el archivo randomWorker.js.</p>
<p>Estamos asignando un manejador al evento onmessage del objeto Worker que recibirá el resultado de la ejecución asíncrona del Worker. Notemos que el resultado viene en la
propiedad data. El resultado de la ejecución la colocaremos en nuestro elemento span con el id lblAleatorio.</p>
<p>Por si hay algún error, estamos también estableciendo un manejador para el evento onerror. Al finalizar la carga de la página, estamos realizando una petición inicial y
dejamos programada una nueva petición en intervalos de 10 segundos. El método postMessage requiere que le sea pasado un argumento, entonces estamos enviando un texto muy sencillo
y que realmente en este ejercicio no tiene la mayor importancia porque de él no depende alguna ejecución por parte del Worker.</p>
<p>Finalmente vamos a programar nuestro GetRandom.ashx con el siguiente código:<br />
<code>
public class GetRandom : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "application/json";
context.Response.Write("{\"value\": \"" + Guid.NewGuid().ToString() + "\"}");
}
public bool IsReusable
{
get
{
return false;
}
}
} </code>
</p>
<p>Veamos que el ContentType de la respuesta está establecido en el MIME que necesita JSON, también deberemos notar que estamos generando un valor estadísticamente único con
el objeto Guid.</p>
<p>Voy a visitar el archivo index.htm con IE 11. Esto es debido a que quiero hacer notar algo en particular con este navegador:<br /><br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5Vor40qUl2b-7U2MuEmThOyZuXzDjLwyWdv_m_I7tymqrD2aTRDNE-mR-LI3J1FS1iqTHPDbBb2tBGA7yJJeT4WCv2mLks8NjNE8lqnSDLQLr14uVaBSWRRyiY5U7OnwBOJCcV8yZt7c/s1600/IE11_01.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="41" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj5Vor40qUl2b-7U2MuEmThOyZuXzDjLwyWdv_m_I7tymqrD2aTRDNE-mR-LI3J1FS1iqTHPDbBb2tBGA7yJJeT4WCv2mLks8NjNE8lqnSDLQLr14uVaBSWRRyiY5U7OnwBOJCcV8yZt7c/s320/IE11_01.png" width="320" alt="Ejecución con Internet Explorer 11" /></a></div>
</p>
<p>Podemos esperar más tiempo pero veremos que el valor único no es actualizado, esto se debe al caché del navegador. ¿Cómo podemos probarlo?, vamos a abrir las herramientas
de desarrollo presionando F12 y activando la captura de red:<br /><br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj00bvBGg8zo8u0A2MxQZIgnT8K_d2IVe3_pwBACu5P-UmDRGEuV_LjKHuMGkMHVdl33pbVdzgReJnnPR2D9-c4bzf5NQ-zZaZ7Pxzy8gSYpPjQvPMHsweZGBuhjIHLddlH16Z3Lk10DbE/s1600/IE11_02.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="140" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj00bvBGg8zo8u0A2MxQZIgnT8K_d2IVe3_pwBACu5P-UmDRGEuV_LjKHuMGkMHVdl33pbVdzgReJnnPR2D9-c4bzf5NQ-zZaZ7Pxzy8gSYpPjQvPMHsweZGBuhjIHLddlH16Z3Lk10DbE/s320/IE11_02.png" width="320" alt="Captura del tráfico de red con la herramienta para desarrolladores de Internet Explorer 11" /></a></div>
</p>
<p>¿Qué está pasando?, podemos ver que el status es 304, esto significa que la petición está siendo servida desde el caché del navegador y no está yendo hasta el servidor
por el nuevo valor. Para corregir este detalle vamos a ajustar el código de GetRandom.ashx de la siguiente manera:<br />
<code>
context.Response.ContentType = "application/json";
<b>context.Response.AppendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
context.Response.AppendHeader("Pragma", "no-cache");
context.Response.AppendHeader("Expires", "0");</b>
context.Response.Write("{\"value\": \"" + Guid.NewGuid().ToString() + "\"}");</code></p>
<p>Una vez realizado el cambio volvemos a visitar nuestro index.htm y veremos que cada 10 segundos el valor será actualizado.</p>
<p>Este ha sido un ejercicio muy sencillo pero aborda varias cosas que vale la pena notar:</p>
<ol>
<li>No tenemos acceso al DOM desde un Worker.</li>
<li>No podemos utilizar jQuery en un Worker dado que jQuery está basado en el DOM.</li>
<li>La comunicación entre el Worker y la página (en nuestro caso tools.js) debe ser a través de mensajes de texto. Es por ello que se recomienda el uso de JSON.</li>
<li>Deshabilitamos el caché en nuestro Handler para evitar que el navegador no realice la petición al servidor al despachar el resultado desde el caché.</li>
</ol>
<p>Espero les haya resultado de mucha utilidad, nos vemos la siguiente.</p>Jorgehttp://www.blogger.com/profile/01832299082302721537noreply@blogger.com0