Me alegra comprobar que, al parecer, el pequeño reto que nos traemos Unai y yo está provocando cierto interés; más allá de haber conseguido que el de Monforte y yo nos hayamos tenido que rascar el bolsillo e invitarnos el uno al otro a varias cenas, algo de debate si que se ha generado en twitter, llegando a contar incluso con alguna prueba similar en otra plataforma!(*)
Para quien no haya leído la entrada anterior, o la respuesta de Unai, os recuerdo brevemente las reglas de nuestra improvisada apuesta:
El ganador será aquel de nosotros dos que sea capaz de insertar 500.000 filas (documentos, en su dialecto) compuestas por un GUID y una cadena de 20 caracteres (no unicode). Recordaros que la solución bcp ha sido declarada ilegal, para que Unai siga manteniendo alguna opción de victoria ;)
Lo cierto es que no tenía pensado publicar nada mas avanzada la semana. Sin embargo, me ha surgido un imprevisto viaje a la pérfida Albión este fin de semana para participar en una taskforce de rendimiento (yeah!), y he tenido bastantes ratos libres entre viajes y transbordos. No se me ocurrían muchas maneras mejores de ocupar este tiempo que compartiendo mi siguiente solución al reto, así que ahí vamos!
Descargo de Responsabilidad: Al igual que hice en la entrada anterior, voy a tratar de volver a cubrirme las espaldas frente a conversaciones que deriven en fanatismos. Siempre que escribo acerca de una tecnología ajena al ecosistema de Microsoft acabo con problemas de fanatismos y guerras de religión. Por ello, dejadme que deje muy claro que este post solo pretende contar la historia de un pique interesante con un compañero, así como tratar de aprovechar la ocasión para comentaros un par de técnicas no triviales para realizar cargas rápidas de datos en SQL Server. Vaya por delante que MongoDB me resulta un producto más que interesante y con escenarios para los que es más idóneo que una base de datos relacional.
NOTA: Si alguien esta leyendo mi blog de modo secuencial y nota la ausencia de una entrada llamada ‘Inserciones Masivas en MongoDB vs SQL Server (II)’, esto es porque esta entrada la ha realizado Unai en su blog, y podéis acceder a ella aquí. De hecho, os recomiendo encarecidamente que la leáis incluso en el caso de que no estéis particularmente interesados en MongoDB o bases de datos NoSQL.
(*): El amigo Javier Torrecilla hizo la prueba en Oracle, aunque me niego a dar los resultados! ;) Por otra parte, aun esperamos la versión en WP7 de Yeray!!! En el factor rendimiento no creo que nos gane ni a SQL Server ni a MongoDB, pero lo que es en cool factor nos va a dar una paliza.
¿Por dónde íbamos?
Si recordáis la entrada anterior, mis esfuerzos de inserción ya no eran capaces de aprovechar mejor los procesadores, a pesar de que no se encontraban aún al máximo; mi cuello de botella se había desplazado a otros lugares, y se materializaba en el lado de SQL Server mediante esperas por dos tipos de eventos concretos (WRITELOG y PAGELATCH_EX) como muestra la siguiente captura:

El tener casi un 50% de las esperas globales en WRITELOG evidencia el uso masivo del log de transacciones por las inserciones. Recordemos que, al contrario que con la escritura de los datos, esta operación es síncrona. A priori poco más podemos hacer de este lado, a no ser que recurriéramos a un escenario de BULK INSERT mínimamente logado, pero lamentablemente esta opción viola las reglas del concurso. Doh… estúpidas reglas!
¡Vamos a ver si la otra espera nos da alguna pista! La espera es por el tipo PAGELATCH_EX, que se trata de una vieja amiga mía. De todos modos, en caso de que os encontréis con alguna espera que no conozcáis, las tenéis casi todas listadas en la entrada relacionada a sys.dm_os_wait_stats de los Books Online… o en vuestro buscador favorito! ;) La respuesta de la documentación para esta espera es la siguiente:
PAGELATCH_EX : Occurs when a task is waiting on a latch for a buffer that is not in an I/O request. The latch request is in Exclusive mode.
Lo que sucede es que al hacer tantas inserciones de modo concurrente, la ultima pagina del heap sufre de contención, lo que limita seriamente nuestra capacidad para hacer inserciones.
¿Que podemos hacer para reducir la contención de esperas PAGELATCH_EX? Aquí viene el momento genial, pero absolutamente contraintuitivo, de esta entrada del blog:
<drum roll>
Vamos a particionar la tabla sobre la que vamos a hacer las inserciones.
</drum roll>
¡¿Particionar la tabla para las inserts?! Se te pira…
Eso es lo que pensé la primera vez que escuché esta técnica; todos estamos más que habituados al particionado de tablas pensando en las mejoras administrativas que nos proporcionan y, como no, de rendimiento a la hora de realizar búsquedas debido a la eliminación de particiones, la menor altura de los índices, etc…. ¿Pero, mejoras en inserciones?
Os podéis imaginar como me quede cuando leí por primera vez acerca de la idea de particionar una tabla para optimizar la carga de datos en un artículo de Thomas Kejser; sin embargo, tiene sentido. Ahora os estoy escuchando a algunos pensar…
¿¡pero que hardware tiene tu portátil?! Si vas a hacer tantas particiones como procesadores (cuatro), ¿no deberías seguir las buenas recomendaciones y poner cada archivo en un disco independiente?
¡Buena pregunta! (y tanto… ¡como que me la he hecho yo a mi mismo! ;)) Pero daros cuenta de un detalle: la contención NO está en el disco, sino en las estructuras internas de SQL Server de la tabla. Lo que pretendo con esto no es optimizar el acceso a disco, sino la sobrecarga que tiene SQL Server cuando tiene varios hilos tratando de insertar en la misma tabla. Al particionarla, estas esperas se reducen, y ese es nuestro objetivo (PAGELATCH_EX).
De cara a realizar este particionado, vamos a realizar un pequeño cambio a mi tabla que, evidentemente, consensué con Unai previamente. De hecho, a priori, este cambio me pone en una situación mas comprometida, ya que implica agregar un campo nuevo (de tipo TINYINT y al que llamaré Hash aunque realmente no es tal…), así como un índice clustered sobre el ID y este nuevo campo.
La siguiente pregunta es: ¿cuantas particiones crear sobre la tabla? En este punto no he sido muy profesional, para ser sinceros: solo he hecho una prueba por el momento, con 8 particiones. Escogí este valor un poco por intuición; mi maquina tiene 4 núcleos reales, y consideré que 8 particiones deberían ser un numero apropiado para poder jugar un poco con la ociosidad de las tareas y minimizar el signal_wait_time. De todos modos, si encuentro algo más de tiempo, trataré de hacer al menos un par de escenarios mas con diferente número de particiones.
Como ya comentamos antes, todas estas particiones apuntaran al mismo filegroup y fichero, por lo que en realidad no se trata de paralelizar las escrituras en ficheros o unidades diferentes; una vez mas recuerdo que solo pretendemos reducir la contención en las estructuras internas de SQL Server.
Así pues, nuestro particionado quedaría tal y como muestra el siguiente diagrama:

A continuación os muestro el script de creación de mi nueva tabla, así como la función y esquema de particionado que he empleado, y el nuevo índice clustered:
CREATE PARTITION FUNCTION pf_hash (TINYINT)
AS RANGE LEFT FOR VALUES
(0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07)
CREATE PARTITION SCHEME ps_hash
AS PARTITION pf_hash TO ([ALL])
GO
CREATE TABLE dbo.Test
(
ID uniqueidentifier NOT NULLDEFAULT NEWID(),
Payload varchar(20) NULL,
Hash tinyint NOT NULL
) ON ps_hash(Hash)
CREATE UNIQUE CLUSTERED INDEX CIX_Hash
ON dbo.Test (ID ASC, Hash ASC) ON ps_hash(Hash)
Además del particionado, voy a adelantarme a un potencial problema de contención de bloqueos en la tabla asegurándome que mi índice no permita bloqueos de pagina ni fila, y estableciendo la política de escalado de bloqueos de la tabla a Auto:
ALTER INDEX CIX_Hash ON dbo.Test
SET (ALLOW_PAGE_LOCKS = OFF, ALLOW_ROW_LOCKS = OFF)
ALTER TABLE dbo.Test SET (LOCK_ESCALATION = AUTO)
El código de la prueba es similar a la versión anterior, pero agregando el código hash a cada una de los Tasks, de modo que cada Task enviará sus inserts a una partición diferente:
private static void DoWork(int hash)
{
int countIterations = 0;
while (countIterations < 63)
{
using (SqlConnection conn = new SqlConnection(connStr))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand())
{
cmd.Connection = conn;
int countValues = 0;
var builder = new StringBuilder();
builder.Append("INSERT INTO Test(Id,Payload, Hash) VALUES ");
while (countValues < 1000)
{
builder.Append("(NEWID(), REPLICATE('X',20), ");
builder.Append(hash.ToString());
if (countValues != 999)
builder.Append("),");
else
builder.Append(")");
countValues++;
}
cmd.CommandText = builder.ToString();
cmd.ExecuteNonQuery();
countIterations++;
}
}
}
}
Y todo esto.. ¿para que?
Solo hay una respuesta a esta pregunta: para derrotar, y humillar si cabe, a Unai y su MongoDB :P Vamos a ver si lo hemos conseguido!
Bueno, en este caso no me entretendré demasiado con los resultados; simplemente os diré que después de los cambios consigo la inserción de los 500.000 registros en 1,8 segundos estables.
Esta solución esta ligeramente por detrás de los 1,6 segundos del MongoDB de Unai, pero aun así me siento muy satisfecho porque es muy estable en tiempos y, bueno… si miro atrás al primer día del reto, no creía que pudiera bajar de los 20 segundos!
Aun así, ya estoy trabajando en la siguiente idea… No se si será mas rápida, pero os garantizo que es bastante espectacular! ;)
Conclusiones:
A pesar de que aún falta lo más importante, el cara a cara final sobre el mismo metal, parece que ya podemos ir obteniendo algunas conclusiones buenas:
- Para empezar, y contra todo pronóstico (al menos por mi parte), parece que en esté escenario concreto SQL Server puede plantar cara a MongoDB. Aún tengo un as en la manga, pero incluso en el caso de que finalmente SQL Server salga derrotado en este pequeño juego, ya me resulta increíble estar en el mismo orden de magnitud de tiempos, así que no digamos estar en una situación tan reñida y con posiciones de victoria.
- Aun no llegamos al limite de nuestras CPUs en este escenario; estoy convencido de que algo más podremos hacer… y aquí entra en juego mi as en la manga.
- En este caso hemos tratado de mejorar las esperas que no se corresponden con las entradas en el log de transacciones; ¿podríamos mejorar este otro frente, que supone el otro 50% del tiempo en nuestro escenario original? Esta parte es mas difícil, por no decir imposible si no podemos realizar el proceso con operaciones mínimamente logadas (BULK), así que no creo que podamos ganar nada por ahí sin hacer trampas a Unai.
Igual que hice en la ocasión anterior, os voy a dejar una pista de lo que será mi siguiente intento para mejorar el rendimiento en las inserciones; solo os diré que voy a tratar de reducir las esperas por SOS_SCHEDULER_YIELD :)
Cuando publiquemos los resultados definitivos, publicaremos también el código de ambas implementaciones; me encantaría que alguien mas se animara a realizar las pruebas y extraer conclusiones!
Por último, quizá sea interesante reflexionar si realmente es necesario tanto esfuerzo para ganar, en este caso, simplemente dos segundos.
Keep Rockin’!
Rock Tip:
“Ready to Roar, ready to rock, ready to give it all I’ve got” reza el estribillo de ‘Roar’, uno de os temas más espectaculares del increíble ‘Coup de Grace’, el último disco de la ya venerable banda de Hard Rock melódico sueca Treat.
Y eso es precisamente lo que me dije a mi mismo cuando Unai y su MongoDB empezaron a apretarme las tuercas! Ready to give it all I’ve got! De hecho, este disco me estuvo acompañando durante gran parte de las pruebas que iba realizando; debería poner al final un grupo de los artistas, discos y temas involucrados en todo el reto ;)
Esta semana estoy en San Sebastián, impartiendo una pequeña formación sobre optimización de SQL Server. A priori, la semana se presentaba tranquila y con gran carga turística al tratarse de la primera ocasión que visito esta ciudad, y mi predilección general por estas tierras del norte.
Lamentablemente, todos mis planes se torcieron tan pronto como descubrí que Unai estaba en la misma ciudad que yo. Como no podía ser de otro modo, ente pintxo y pintxo nuestros temas técnicos favoritos empezaron a surgir en nuestras conversaciones. Y, como cualquiera que conozca a nuestro Unai podrá intuir, no pasó demasiado tiempo antes de que MongoDB secuestrara todos nuestros hilos conversacionales y se convirtiera en nuestro particular trending topic, por delante de otras apasionantes cuestiones como la entropía de la demografía en el postmoderno barroco, o el peculiar criterio que tengo a la hora de escoger mis guitarras.
He de confesar que no recuerdo muy bien la cadena de acontecimientos que nos condujo a la historia que os voy a contar; quizá fuera mi falta de conocimiento respecto a este producto tan de moda (MongoDB), o el número de zuritos que nos habíamos tomado… No lo acabo de tener muy claro, pero el hecho es que, de algún modo, me metí de lleno en un juego/apuesta que acabaría con el escaso tiempo libre que me quedaba a lo largo de la semana.
La Apuesta
Las reglas eran muy sencillas: el ganador sería aquel de nosotros dos que fuera capaz de insertar una carga de trabajo acordada previamente en menos tiempo. Él con su MongoDB de marras, y yo con mi viejo, fiable, pero no siempre bien ponderado SQL Server 2008 R2, ¡como no!
Tras duras negociaciones de paz, Unai y yo llegamos a un acuerdo para evitar emplear armas de destrucción masiva; por este motivo finalmente decidimos no evaluar la que es, de lejos, la manera más rápida de insertar datos en SQL Server: las Bulk Inserts.
La carga de trabajo que nos pusimos de objetivo fue 500.000 registros, cada uno conteniendo un GUID y 20 caracteres textuales (no unicode).
Descargo de Responsabilidad: Siempre que escribo acerca de una tecnología ajena al ecosistema de Microsoft acabo con problemas de fanatismos y guerras de religión. Por ello, dejadme que deje muy claro que este post solo pretende contar la historia de un pique interesante con un compañero, así como tratar de aprovechar la ocasión para comentaros un par de técnicas no triviales para realizar cargas rápidas de datos en SQL Server. Vaya por delante que MongoDB me resulta un producto más que interesante y con escenarios para los que es más idóneo que una base de datos relacional.
Construcción del Entorno de Prueba
En entorno de prueba inicial consistió simplemente en una base de datos que contiene la tabla sobre la que vamos a insertar nuestros 500.000 registros. La única preparación inicial que he realizado de cara a mejorar un poco el rendimiento ha sido dimensionar de modo apropiado los ficheros (para evitar autocrecimientos por uso del log de transacciones en las inserciones) y establecer el modelo de recuperación a SIMPLE.
A continuación os dejo el script inicial, aunque como veréis más adelante hemos realizado algún que otro cambio:
CREATE DATABASE DemoInserts ON PRIMARY
(
NAME = 'DemoInserts',
FILENAME = 'c:\Databases\DemoInserts.MDF',
SIZE = 200,
FILEGROWTH = 10%
)
LOG ON
(
NAME = N'DemoInserts_Log',
FILENAME = N'c:\Databases\DemoInserts_Log.LDF',
SIZE = 200,
FILEGROWTH = 10%
)
GO
ALTER DATABASE DemoInserts SET RECOVERY SIMPLE
GO
USE DemoInserts
GO
CREATE TABLE [Test]
(
ID UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
Payload VARCHAR(20) NULL,
)
¡Vamos a Insertar Filas!
La primera solución consistirá en un simple bucle de 500.000 ejecuciones, en el que para cada iteración insertamos una fila. Su única ‘optimización’ fue el empleo de la opción NCOUNT ON para evitar saturar el output buffer con los típicos mensajes de (1 row affected), tal como se puede ver en el siguiente fragmento de código:
SET NOCOUNT ON
DECLARE @cont INT
SET @cont = 0
WHILE (@cont < 500000)
BEGIN
INSERT INTO Test(ID, Payload) VALUES (DEFAULT, REPLICATE('X', 20))
SET @cont = @cont + 1
END
El tiempo de ejecución de la inserción en mi equipo fue de 3 minutos y 8 segundos.
NOTA: Todos los tiempos tomados en este artículo se han calculado realizando el promedio de tres ejecuciones sucesivas.
Evidentemente, desde antes de realizar esta prueba yo era consciente de que este mecanismo no iba a arrojar buenos resultados, pero me serviría para tener una línea base respecto a la que poder evaluar las mejoras posteriores y para hacerme una idea respecto a lo que podría esperar de SQL Server.
Llegados a este punto le comenté a Unai que me daría con un canto en los dientes si era capaz de bajar mi tiempo de inserción por debajo de los 20 segundos. Mi amigo y competidor me corrigió, asegurándome que sería él mismo quien se encargaría de darme con el susodicho canto en toda mi dentadura si conseguía tal hazaña. Creo que en ese momento empecé a tomarme nuestra pequeña competición en serio ;)
Manos a la Obra… ¿Dónde esta mi cuello de botella?
Siempre hay un cuello de botella; ¡siempre! Aunque el rendimiento sea excelente, siempre hay un factor limitante, y solo uno. En este caso, un vistazo rápido al Task Manager me permitió ver que la CPU no eran en este caso el problema:

Esto me dio una buena idea para el siguiente intento: realizar una pequeña aplicación en C# que se encargara de lanzar INSERTS modo masivo en paralelo, usando múltiples hilos, para tratar de aprovechar al máximo las cuatro CPUs de las que dispone mi equipo de pruebas.
A continuación os pongo el código exacto que he empleado para esta prueba, aunque casi me da vergüenza ;) No me juzguéis por él, es solo un ejemplo ‘quick and dirty’!:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.SqlClient;
using System.Diagnostics;
namespace ConsoleApplication5
{
class Program
{
static void Main(string[] args)
{
Stopwatch watch = Stopwatch.StartNew();
var mainTask = new TaskFactory().StartNew(() =>
{
new Task(() => DoWork(125000), TaskCreationOptions.AttachedToParent).Start();
new Task(() => DoWork(125000), TaskCreationOptions.AttachedToParent).Start();
new Task(() => DoWork(125000), TaskCreationOptions.AttachedToParent).Start();
new Task(() => DoWork(125000), TaskCreationOptions.AttachedToParent).Start();
});
mainTask.ContinueWith((t) =>
{
watch.Stop();
Console.WriteLine(watch.Elapsed);
}, TaskContinuationOptions.OnlyOnRanToCompletion);
Console.ReadLine();
}
private static void DoWork(int rows)
{
string connStr = "Data Source=.;Initial Catalog=DemoInserts
;Integrated Security=true;Application Name=TestInserts";
int countIterations = 0;
while (countIterations < rows)
{
using (SqlConnection conn = new SqlConnection(connStr))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand())
{
cmd.Connection = conn;
cmd.CommandText = "INSERT INTO Test(Id,Payload) VALUES (NEWID(), REPLICATE('X',20))";
cmd.ExecuteNonQuery();
countIterations++;
}
}
}
}
}
}
Como podéis ver, el código simplemente instancia 4 tareas (una por cara núcleo disponible en mi máquina). Estas tareas reciben el número de filas a insertar, y proceden a realizar estas inserciones como comandos SQL dentro de un bucle.
La inserción de las dichosas 500.000 filas por este mecanismo se produjo en 1 minuto y 7 segundos; si bien el valor no está nada mal en términos de mejora, sabía que aún me quedaba mucho para tener un tiempo competitivo. Una nueva visita al Task Manager me hizo darme cuenta rápidamente de que, si bien había aumentado mucho mi consumo de CPU, aparentemente aún disponía de mucho margen adicional:

A la vista de esto, decidí probar a aumentar el número de hilos empleados para lanzar las sentencias INSERT, y monitorizar tras cada prueba tanto la duración de la misma como la utilización de los procesadores durante el transcurso de las inserciones. De este modo pude lograr grandes usos del procesador, pero es importante tener presente una cuestión: el incremento de la actividad de CPU no implica necesariamente un mejor rendimiento en las inserciones.
Para demostrar este punto, os adjunto una pequeña gráfica que muestra los tiempos obtenidos en función del número de hilos empleados:

(Atiende que graficazaaa… ¿verdad que una de estas ayuda a darle un look más profesional a cualquier artículo? ;))
Como se puede apreciar, el sweet spot de este proceso de inserción en mi equipo se encuentra en los 8 hilos, con un tiempo de ejecución de 43 segundos. A continuación os muestro la gráfica de uso de CPU de esta ejecución, para que podáis contrastarla con la primera de todas y apreciéis el notable incremento en la utilización que hemos logrado:

¿Por donde seguimos ahora?
Bien, llegados a este punto ya tenía mas o menos claro el punto más idóneo de paralelismo desde el lado de la aplicación C# de inserciones, pero no conocía lo que estaba sucediendo por parte de SQL Server. Por esto, decidí estudiar las esperas del servidor; borre las estadísticas de esperas, lancé el proceso y a su finalización, use el celebérrimo script de Glenn Berry, personalizado por Paul Randal para la agregación de información de esperas por recursos en SQL Server.
El resultado de la prueba, si bien era de esperar, resulta revelador:

Como se puede ver, un 99.22% del tiempo total de la prueba se destino a realizar entradas en el log de transacciones; esto se debe al hecho de que las inserciones, salvo casos muy puntuales en escenarios de BULK INSERT, son operaciones completamente logeadas. Dicho a lo bestia: por cada inserción, a parte del dato en si, hay que escribir la intención de realizar la inserción previamente en el log de transacciones. Esta operación es síncrona, y es la que esta provocando una gran cantidad de esperas.
Para tratar de remediarlo, se me ocurrió tratar de reducir el número de cláusulas INSERT que enviamos al servidor mediante las sentencias de inserción múltiples de SQL Server 2008, que nos permiten insertar en el mismo statement hasta mil filas(*). Ya sabéis, algo así:
INSERT INTO Test(ID, Payload) VALUES
(NEWID(), 'AAA'),
(NEWID(), 'BBB'),
...
(NEWID(), 'XXX')
Modifique un poco la aplicación para realizar estas sentencias, quedando el código del siguiente modo:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.SqlClient;
using System.Diagnostics;
namespace ConsoleApplication5
{
class Program
{
static void Main(string[] args)
{
Stopwatch watch = Stopwatch.StartNew();
var mainTask = new TaskFactory().StartNew(() =>
{
new Task(() => DoWork(), TaskCreationOptions.AttachedToParent).Start();
new Task(() => DoWork(), TaskCreationOptions.AttachedToParent).Start();
new Task(() => DoWork(), TaskCreationOptions.AttachedToParent).Start();
new Task(() => DoWork(), TaskCreationOptions.AttachedToParent).Start();
new Task(() => DoWork(), TaskCreationOptions.AttachedToParent).Start();
new Task(() => DoWork(), TaskCreationOptions.AttachedToParent).Start();
new Task(() => DoWork(), TaskCreationOptions.AttachedToParent).Start();
new Task(() => DoWork(), TaskCreationOptions.AttachedToParent).Start();
});
mainTask.ContinueWith((t) =>
{
watch.Stop();
Console.WriteLine(watch.Elapsed);
}, TaskContinuationOptions.OnlyOnRanToCompletion);
Console.ReadLine();
}
private static void DoWork()
{
string connStr = "Data Source=.;Initial Catalog=DemoInserts;Integrated Security=true;
Application Name=DemoInserts";
int countIterations = 0;
while (countIterations < 63) // 63
{
using (SqlConnection conn = new SqlConnection(connStr))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand())
{
cmd.Connection = conn;
int countValues = 0;
var builder = new StringBuilder();
builder.Append("INSERT INTO Test(Id,Payload) VALUES ");
while (countValues < 1000)
{
builder.Append("(NEWID(), REPLICATE('X',20)) ");
if (countValues != 999)
builder.Append(",");
countValues++;
}
cmd.CommandText = builder.ToString();
cmd.ExecuteNonQuery();
countIterations++;
}
}
}
}
}
}
Con esta nueva versión, usando ocho hilos, baje el tiempo a… (redoble de tambor, por favor) 4 segundos escasos!!!!
Llegados a este punto me sorprendí yo mismo; no creí que pudiera bajar tanto el tiempo de estas inserciones, y por primera vez vi peligrar mi dentadura ;) Como me encanta tener datos y números de cada mejora que voy logrando, volví a lanzar la prueba y consulté las estadísticas de esperas del servidor durante el tiempo de ejecución de la misma:

Como podéis ver, el hecho de agrupar las inserciones en batches ha hecho milagros! ahora mismo el servidor esta dedicando aproximadamente el mismo tiempo a escribir en el log de transacciones que en en resolver bloqueos de latches, una situación más normal, y que ha repercutido de manera espectacular en el rendimiento de estas inserciones.
(*): Por si alguien se lo pregunta, yo tampoco tengo ni puñetera idea de por qué el hard-limit de mil filas.
Y por hoy ha estado bien, ¿no? ;)
Con esta entrada he buscado abriros un poco el apetito y, con un poco de suerte, picar a alguien más para que entre al pequeño juego que tenemos Unai y yo. He explicado mis aproximaciones iniciales, pero me reservo para la siguiente entrada la solución ‘buena, buena’ desde el lado del SQL Server.
Para los mas ansiosos os iré adelantando que se basa en particionar la tabla para evitar contención sobre la misma; los detalles de implementación los pondré en mi siguiente post, pero como mis actualizaciones son como los despertares del lazy_writer (ya sabéis, puede ser dentro de 3 milisegundos, o dentro de 3 años!), os he querido adelantar la técnica por si queréis investigarla por vosotros mismos.
A modo de resumen, voy a finalizar con otra gráfica de estas elegantes y vistosas, que hoy tengo el día ejecutivo ;)

Por cierto, si os preguntáis que tal lo hizo MongoDb… vais a tener que esperar a que Unai lo publique en su blog. No obstante, os puedo adelantar que fue capaz de bajar de estos 4 segundos….
...
..
.
¡¡¡¡Pero yo también!!!! Muahahahaha!!! ;)
Keep Rockin’!
Rock Tip:
Hacía mucho tiempo que no compartía con vosotros uno de mis rock tips… o mejor dicho, hacía mucho que no compartía nada con vosotros! Tengo el blog muy dejado, pero un servidor no da para más :(
En esta ocasión me traigo a uno de mis descubrimientos del año, los geniales Reckless Love. Banda finlandesa liderada por el increíble Olly Herman, quien fuera vocalista de Crashdiet por una breve temporada y una de las personas con mas carisma y buen rollo que he visto nunca sobre un escenario. Herederos de las mejores raíces del hard rock melódico ochentero, pero con un increíble toque dance y discotequero, puedo afirmar que verles en directo es la experiencia mas cercana a ver al David Lee Roth sin recurrir a un De Lorean con condensador de fluzo.
El tema que he escogido es el single de su segundo (y último hasta la fecha) disco: Hot. No tiene gran relación con la temática de la entrada, pero realmente me apetece compartir este tema con el mundo: si a uno solo de vosotros le hace sentir tan solo una decima parte de lo bien que me sienta a mi escuchar este temazo, ya habrá justificado saltarme mi costumbre de poner temas relacionados.