[Windows Phone 7.5] Inserciones masivas en SQL Server CE vs SQL Server vs MongoDb

Hola a todos!

Desde hace unas semanas, mis compañeros Pablo Doval y Unai Zorrilla están inmersos en una lucha por ver quien la tiene mas grande por ver quien consigue insertar 500.000 registros en menos tiempo. Pablo está usando SQL Server y Unai MongoDb podéis ver sus batallas aquí:

En sus últimos artículos ya podemos ver como estos auténticos genios han conseguido llegar a números impresionantes (1,6 segundos Unai, 1,8 segundos Pablo).  Estos post, con el trasfondo de la batalla entre dos compis, son un compendio de optimizaciones y buenas maneras a la hora de trabajar tanto con SQL Server como MongoDb y deberían ser un “Must read” para todo el que desee saber algo más sobre estas plataformas.

Yo que soy muy atrevido y me gusta vivir al límite he decidido unirme al campo de batalla de forma unilateral y armado con mi SQL Server CE incluido en Windows Phone 7.5. Si, lo reconozco, esto es como ir a una guerra entre dos superpotencias armado con una pistola de fogeo… pero quien dijo miedo!!

DISCLAIMER: Antes de empezar: soy muy consciente de que es imposible llegar a los números de los que Unai y Pablo hacen gala, además de por sus conocimientos en las respectivas plataformas, por el ambiente en si mismo. Nunca un dispositivo con un solo core, de bajo consumo y con muy poca memoria y SQL Server CE se podrá comparar a esos dos monstruos que son MongoDb y SQL Server, dos de los mejores sistemas de almacenamiento de datos que existen en la actualidad. Pero si creo que por el camino podemos aprender muchas cosas sobre como optimizar la plataforma de bases de datos en Windows Phone y descubrir detalles, cuanto menos, interesantes.

Missile One

Para empezar, vamos a obtener una referencia desde la que podamos empezar a mejorar, quien sabe, incluso sin ningún tipo de mejora llegamos a los tiempos de Unai y Pablo y podemos echarnos a dormir.

Si no habéis visto como se trabaja con SQL Server CE en Windows Phone, podéis echar un vistazo a este artículo de mi blog.

Para empezar, creamos un proyecto standard de Silverlight para Windows Phone y le  añadimos una referencia al ensamblado System.Data.Linq. A Continuación vamos a crear nuestra entidad:

[Table(Name="Test")]
public class TestTable
{
    [Column(IsPrimaryKey=true)]
    public Guid Id { get; set; }

    [Column()]
    public string Payload { get; set; }
}

Como podéis ver, es imposible hacerla más simple. Un campo GUID y un campo string donde insertaremos 20 caracteres por cada registro. Ahora vamos a crear nuestro contexto de datos:

public class DbContext : DataContext
{
    public DbContext(string connectionString) : base(connectionString) { }

    public Table<TestTable> Test
    {
        get
        {
            return this.GetTable<TestTable>();
        }
    }
}

Igual que nuestra tabla, lo más sencillo que pueda realizar el trabajo, sin trampa ni cartón ni esotéricas optimizaciones. Para ejecutar todo vamos a crear la UX de nuestra aplicación, simplemente necesitamos un botón para iniciar el proceso de inserción y un bloque de texto donde mostrar cuanto hemos tardado (la cifra vendrá expresada en milisegundos):

<StackPanel>
    <Button Content="Start" Click="Button_Click"></Button>
    <TextBlock FontSize="{StaticResource PhoneFontSizeLarge}"
                Foreground="{StaticResource PhoneAccentBrush}"
                Text="Total Time:"
                Name="TotalTime"></TextBlock>
</StackPanel>

Si, la respuesta es SI: Hoy me siento minimalista. Una UX realmente sencilla:

image

Y Por último el código que se encargue de crear la base de datos y ejecutar las inserciones al presionar el botón “Start”:

// Constructor
public MainPage()
{
    InitializeComponent();
    
    using (DbContext context = new DbContext("Data Source='isostore:/Db.sdf'"))
    {
        if (context.DatabaseExists())
            context.DeleteDatabase();
        if (!context.DatabaseExists())
            context.CreateDatabase();
    }
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    
    string load = "XXXXXXXXXXXXXXXXXXXX";

    Stopwatch watch = Stopwatch.StartNew();
    using (DbContext context = new DbContext("Data Source='isostore:/Db.sdf'"))
    {
        for (int i = 0; i < 500000; i++)
        {
            TestTable insert = new TestTable();
            insert.Id = Guid.NewGuid();
            insert.Payload = load;
            context.Test.InsertOnSubmit(insert);
        }

        context.SubmitChanges();
    }
    watch.Stop();

    TotalTime.Text = watch.Elapsed.TotalMinutes.ToString();
}

Como podemos ver no tiene mayor secreto. En el constructor de nuestra página comprobamos si la base de datos existe para borrarla y a continuación crearla (de esta forma tenemos un entorno limpio solo con iniciar la aplicación). En el botón, establecemos la carga a insertar, 20 caracteres, conectamos con la base de datos e insertamos mediante un bucle 500000 registros haciendo un SubmitChanges posteriormente que enviará las inserciones a la base de datos. Usamos la clase StopWatch de System.Diagnostics para medir el tiempo que tardamos en realizar el trabajo y mostrar el resultado al usuario.

¿Cual es el resultado? Tensión… redoble de tambores…:

image

FAIL!!! Después de unos agónicos instantes Obtenemos unas excepción de SQL Server CE indicándonos que hemos excedido el tamaño máximo por defecto de la base de datos… no solo no hemos ganado la batalla, es que nos hemos resbalado en la bañera y hemos muerto antes de salir de casa… vamos a arreglar esto, usando las opciones de la cadena de conexión de SQL Server CE. Aunque es una opción poco documentada y algo oscura, SQL Server CE soporta parámetros adicionales en su cadena de conexión en Windows Phone 7.5 y uno de estos parámetros es Max Database Size. Si no conocemos el tamaño posible de la base de datos, podemos establecerlo desde 16MB hasta 512MB. Para este primer ejemplo y pensando en que nos sirva de referencia, lo estableceremos a 128MB:

// Constructor
public MainPage()
{
    InitializeComponent();

    using (DbContext context = new DbContext("Data Source='isostore:/Db.sdf'; Max Database Size = 128;"))
    {
        if (context.DatabaseExists())
            context.DeleteDatabase();
        if (!context.DatabaseExists())
            context.CreateDatabase();
    }
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    
    string load = "XXXXXXXXXXXXXXXXXXXX";

    Stopwatch watch = Stopwatch.StartNew();
    using (DbContext context = new DbContext("Data Source='isostore:/Db.sdf'; Max Database Size = 128;"))
    {
        for (int i = 0; i < 500000; i++)
        {
            TestTable insert = new TestTable();
            insert.Id = Guid.NewGuid();
            insert.Payload = load;
            context.Test.InsertOnSubmit(insert);
        }

        context.SubmitChanges();
    }
    watch.Stop();

    TotalTime.Text = watch.Elapsed.TotalMinutes.ToString();
}

Volvemos a ejecutar… redoble de tambores…

image

Esta vez SÍ! 4,6 minutos (4 minutos y 36 segundos aprox.) en insertar 500000 registros mediante el emulador… bueno, estamos algo lejos de la marca lograda por Pablo y Unai. Aun así, ¿Hasta que punto es el emulador un reflejo fiel del comportamiento del teléfono? Para descubrirlo vamos a realizar esta prueba en los dos Windows Phone 7.5 que tengo en casa: HTC Mazaa y Nokia Lumia 800. A ver que resultados obtenemos.

Para ejecutarse en dispositivos reales, vamos a indicar en el constructor de nuestra clase App que la aplicación no se desactive aunque el usuario no esté interactuando con ella (que no se apague la pantalla del dispositivo). Para esto usaremos la propiedad UserIdleDetectionMode de la clase PhoneApplicationService:

public App()
{
    // Global handler for uncaught exceptions. 
    UnhandledException += Application_UnhandledException;

    // Standard Silverlight initialization
    InitializeComponent();

    // Phone-specific initialization
    InitializePhoneApplication();

    PhoneApplicationService.Current.UserIdleDetectionMode = IdleDetectionMode.Disabled;
}

Y con esto ya estamos listos para ejecutar la aplicación en los dispositivos, la desplegamos y la iniciamos y tras unos tensos instantes, obtenemos los siguientes resultados (gráfica profesional y chula incluida!!):

image

Mientras que el emulador realizó la operación completa en 4 minutos y 36 segundos, en los dispositivos físicos el tiempo se dispara: El HTC Mazaa con procesador de 1Ghz completo la inserción en 37 minutos y el Nokia Lumia 800 con el procesador más potente de 1.4Ghz lo hizo en 29 minutos y 20 segundos. Bueno… andamos algo lejos de los resultados de Unai y Pablo, pero nadie dijo que fuese a ser fácil jeje. Hora de empezar a optimizar!!

Missile Two

Cogiendo la aplicación donde la hemos dejado, necesitamos identificar donde estamos invirtiendo el tiempo para optimizar las operaciones más costosas. En primer lugar, ¿Cual será el resultado al insertar solamente 20.000 registros? vamos a verlo:

image

El emulador tarda unos 0,08 minutos en insertar los 20000 registros, unos 5 segundos. ¿Y los dispositivos?

image

 

En este caso el Nokia Lumia ha tardado 0,42 minutos, unos 25 segundos, mientras que el HTC Mazaa ha tardado 0,32 minutos, unos 19 segundos. Siguen siendo mucho más lentos que el emulador y además en este caso el HTC ha sido varios segundos más rápido que el Nokia… subamos la apuesta… en un mundo ideal, si duplicamos los registros, deberíamos duplicar el tiempo… vamos a ver que pasa con 40.000 registros:

image

En este caso, mientras el emulador ha duplicado su tiempo, el HTC ha multiplicado su tiempo por 2,5 por lo que ya empezamos a ser más lentos insertando 40.000 que haciendo dos inserciones de 20.000. Por su parte el Nokia ha multiplicado su tiempo de ejecución por 1,4 por lo que en su caso si que nos es más rentable insertar 40.000 que 2 veces 20.000. Viendo estos resultados lo primero que se me ocurre es que podemos estar teniendo un problema de buffer al escribir en la base de datos… vamos a usar otro parámetro de la cadena de conexión “Max Buffer Size” para forzar el buffer a 1024Kb y repetimos la prueba con 40.000 registros:

image

Bien! hemos conseguido mejorar el rendimiento en todos los dispositivos y en el emulador. El más beneficiado es el HTC, que ha bajado 7 segundos, mientras que el Nokia y el emulador solo han mejorado en 1 segundo. El tamaño máximo de buffer que podemos usar en SQL Server CE para Windows Phone 7.5 es de 4096Kb, vamos a hacer una última prueba parcial con el buffer más grande y 40.000 registros, ¿Que pasará? Pues algo sorprendente, los tiempos no solo no mejoran, empeoran!! ¡Por Helm! ¿Porque pasa esto? Pues porque estamos reservando más memoria de la necesaria, y el tiempo de esa operación está acabando con nosotros… Vamos a lanzar ya nuestro segundo misil: Vamos a lanzar 5 inserciones de 100.000 registros, con el buffer establecido a 3072Kb:

En un mundo perfecto, si 40.000 registros con un buffer de 1024Kb tardan unos 40 segundos, 100.000 registros con un buffer de 3072 deberían tardar algo menos de 120 segundos, y 500.000 deberían realizarse en unos 550 segundos, algo menos de 10 minutos, con lo que habríamos rebajado el tiempo en mucho, pero… ¿Vivimos en un mundo ideal?

El código de nuestra aplicación sería este:

private void Button_Click(object sender, RoutedEventArgs e)
{

    string load = "XXXXXXXXXXXXXXXXXXXX";

    Stopwatch watch = Stopwatch.StartNew();
    using (DbContext context = new DbContext("Data Source='isostore:/Db.sdf'; Max Database Size = 128; Max Buffer Size = 3072;"))
    {
        for (int j = 0; j < 5; j++)
        {
            for (int i = 0; i < 100000; i++)
            {
                TestTable insert = new TestTable();
                insert.Id = Guid.NewGuid();
                insert.Payload = load;
                context.Test.InsertOnSubmit(insert);
            }

            context.SubmitChanges();
        }
    }

    watch.Stop();

    TotalTime.Text = watch.Elapsed.TotalMinutes.ToString();
}

Y el resultado es:

image

Positivo! Curiosamente, ambos dispositivos, tanto el Nokia como el HTC han clavado los tiempos en 28 minutos y 36 segundos… en el caso del HTC es una gran mejoría con respecto a nuestra primera intentona, pasando de los 37 minutos a 28 y 36 segundos, 8 minutos y 24 segundos más rápido. En el caso del Nokia la mejoría ha sido de solo 44 segundos. Curiosamente, el emulador ha empeorado en esta prueba, pasando de un tiempo de 4 minutos y 36 segundos a 5 minutos y 18 segundos. Sospecho que estos resultados pueden deberse a las diferencias en el hardware de los dispositivos, el Nokia tiene una memoria flash muy rápida, lo cual le permitía aguantar el tipo antes de aumentar el tamaño de buffer, con lo que al aumentarlo, la mejoría ha sido muy pequeña. En el HTC la memoria es algo más lenta, por lo que al gestionar mejor el tamaño de buffer, estamos optimizando mucho el rendimiento y observamos una gran mejoría.

En el caso del emulador, sospecho que las iteraciones se están realizando tan rápidamente que se está saturando la escritura de cambios en el almacenamiento y por eso tenemos un empobrecimiento del rendimiento.

Vamos a realizar una última prueba, nuestro último cartucho en este camino… vamos a realizar la inserción de los 500.000 elementos como en el primer intento, pero con el buffer establecido al máximo, 4096Kb, a ver si arrancamos algo más de rendimiento…:

private void Button_Click(object sender, RoutedEventArgs e)
{

    string load = "XXXXXXXXXXXXXXXXXXXX";

    Stopwatch watch = Stopwatch.StartNew();
    using (DbContext context = new DbContext("Data Source='isostore:/Db.sdf'; Max Database Size = 128; Max Buffer Size = 4096;"))
    {

        for (int i = 0; i < 500000; i++)
        {
            TestTable insert = new TestTable();
            insert.Id = Guid.NewGuid();
            insert.Payload = load;
            context.Test.InsertOnSubmit(insert);
        }

        context.SubmitChanges();
    }

    watch.Stop();

    TotalTime.Text = watch.Elapsed.TotalMinutes.ToString();
}

Y los resultados finales:

image

Para terminar hemos conseguido volver a mejorar el tiempo en nuestro emulador, bajando hasta los 3 minutos y 48 segundos. En cuanto a nuestros dispositivos físicos, HTC Mazaa está al límite de lo que podemos mejorar por esta vía y baja 3 segundos con respecto a la prueba anterior. El Nokia Lumia 800 saca pecho y hace gala de su hardware más nuevo y potente para bajar hasta los 26 minutos y 21 segundos, algo más de 2 minutos más rápido que el HTC Mazaa y casi 3 minutos más rápido que en su primer intento.

Lecciones aprendidas

Lo más interesante de este artículo, no ha sido intentar batir a MongoDb o SQL Server, algo a priori imposible, lo más interesante es ver las lecciones que nos enseña entre líneas:

  • El emulador de Windows Phone nos permite probar la funcionalidad de nuestra aplicación y nos ofrece una visión general del rendimiento de la misma, pero NUNCA, NUNCA, NUNCA va a ofrecernos el rendimiento que nos ofrece un dispositivo físico. Puede que en el emulador todo vaya genial, pero es importantísimo realizar pruebas sobre dispositivos. Por eso las pruebas de certificación del Marketplace se realizan sobre dispositivos, no sobre emulador. Si no tienes un dispositivo físico, en los foros de Windows Phone en msdn varios colaboradores nos ofrecemos a poner los nuestros a tu servicio, si vives cerca de nosotros y tomar un café, ver la app y probarla, mira aquí, si no puedes acercarte a donde nos encontramos, también puedes enviarnos el xap y seguro que podemos sacar algunos minutos para probarlo y responderte con feedback. Por otro lado, escribiendo un mail a sopwp7@microsoft.com (en español) puedes solicitar que te presten un teléfono de desarrollo durante una semana o dos.
  • Por defecto, SQL Server CE usa un tamaño de base de datos de 16MB, si piensas que tu aplicación puede requerir mucho más tamaño puedes usar la propiedad de la cadena de conexión Max Database Size para indicar el tamaño deseado, entre 16 y 512MB, esto te ahorrará más de un problema si tu base de datos crece demasiado.
  • Si necesitamos insertar muchos registros al mismo tiempo, debemos tener en cuenta el tamaño de cada registro y el buffer a usar, mediante el parámetro Max Buffer Size, para optimizar las escrituras. El valor máximo de este parámetro es 4096Kb. En ciertos casos, un valor demasiado grande, puede tener un efecto nocivo sobre nuestro rendimiento.
  • No todos los dispositivos son iguales, pero son muy parecidos, al optimizar nuestro código, hemos visto como dispositivos tan diferentes como el HTC Mazaa, un prototipo de ingeniería que no está a la venta, y el Nokia Lumia 800, uno de los más nuevos y mejores teléfonos con Windows Phone 7.5, se han igualado mucho, con pequeñas diferencias en el uso tan desproporcionado de 500.000 registros. Debemos tener esto en cuenta e intentar obtener un punto común de optimización.

Conclusión

Es hora de retirarnos del campo de batalla, curar nuestras heridas y… volver a la carga!

Después de los misiles 1 & 2, todavía tengo algo en mi arsenal que puede cambiar el curso de la batalla… lo veremos en breve!! Por ahora, os dejo aquí el código del Missile One y Missile Two para que juguéis con el y si os atrevéis a probarlo en otros terminales nos contéis los resultados.

Un saludo y Happy Coding!

3 comentarios sobre “[Windows Phone 7.5] Inserciones masivas en SQL Server CE vs SQL Server vs MongoDb”

  1. Tremendo tio!! Me ha encantado el análisis; te ha sucedido como en mi caso… a pesar de saber que te metes en una batalla ‘perdida’ de antemano, has descubierto un monton de cosas interesantes por el camino. O al menos nos las has hecho descubrir a los demas!

    Lo dicho, eres un crack! Espero el resto de la artillería 🙂

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *