Tag Archives: Ruby

Benchmark MySQL vs PostgreSQL vs SQLite vs MSAccess (vs ruby)

Por razones que no vienen al caso, me he encontrado hoy con un increíble WTF? usando MySQL (la lista no es corta, pero este era sorprendente, digno de Access o peor). He decidido hacer una comparativa con uno de sus competidores directos, PostgreSQL. Además probaré a intentar resolver el problema usando código en vez de dejar todo el “trabajo” a la base de datos, en concreto lo haré todo en ruby, no por eficiencia sino por comodidad (que sino me canso).

[Actualización 6 abril 2008 @ 19h] Ya puestos, he añadido también SQLite y Microsoft Access.

Presentemos el problema, tenemos dos tablas, A y B, cada una de ellas tiene una clave primaria compuesta por dos campos. Queremos averiguar las tuplas de la primera tabla que tienen como valor en uno de sus campos, valores que no se encuentran en ninguna tupla de la segunda tabla. Es decir una resta simplemente. Poniéndolo decentemente sería tal que:

  • Tablas:
    • A(id, otro_id)
    • B(id, otro_id)
  • Objetivo: tuplas de A para las cuales no existe ningún elemento en B cuyo valor del campo otro_id sea igual al campo otro_id de A
    • Formalmente: x ɛ A. ∀y ɛ B y.otro_id != x.otro_id
    • SQL: Lo más intuitivo y simple sería
      SELECT * FROM A WHERE A.otro_id NOT IN (SELECT B.otro_id FROM B)

      o usando LEFT JOINs también es simple expresarlo

      SELECT * FROM A LEFT JOIN B USING (otro_id) WHERE B.otro_id IS NULL

Creo que es algo bastante evidente y simple de entender. Aplicado al MundoReal® puede surgir bastantes veces, no es que estemos antes un tipo de consulta retorcida ni nada por el estilo. Habría que resaltar que estamos usando parte de la clave primaria, en ambas tablas involucradas, por lo que en principio la intuición y nuestros conocimientos de bases de datos relaciones nos sugieren que esto va a ir más rápido que el correcaminos.

¿Cómo resolverías esta consulta?, No me hagas pensar, veamos qué nos dice Postgre:

foo=# EXPLAIN SELECT * FROM A WHERE A.otro_id NOT IN (SELECT B.otro_id FROM B);
                          QUERY PLAN                           
 Seq Scan ON a  (cost=189.91..379.84 ROWS=4877 width=24)
   FILTER: (NOT (hashed subplan))
   SubPlan
     ->  Seq Scan ON b  (cost=0.00..165.53 ROWS=9753 width=12)

Parece buen plan, primero hará un hash de los valores que se le indican en B, le da un coste de menos de 2 décimas (son milésimas los valores) y calcula que devolverá 9753 columnas (que son todas las que hay), luego dice que filtrará todos los de A que no se encuentren en ese hash. Es lo que le hemos pedido, correcto. A este segundo paso le da una estimación de menos de 2 décimas también, y cree que saldrán 4877 resutlados (esto son todo estimaciones, postgre ahora mismo no ha ejecutado nada, solo nos cuenta su vida).

MySQL es un poco más tímido y no da tanto detalle, pero también podemos pedir que nos explique qué va a hacer:

mysql> EXPLAIN SELECT * FROM A WHERE A.otro_id NOT IN (SELECT B.otro_id FROM B) \G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: A
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 178
          ref: NULL
         rows: 9754
        Extra: Using where; Using index
*************************** 2. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: B
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 178
          ref: NULL
         rows: 9753
        Extra: Using where; Using index

Básicamente dice que si, que va a usar clave primaria para ambas partes y punto. ¡Qué sabiduría!.

Ok, pues vamos a comparar. Podía simplemente ejecutar la consulta y santas pascuas, pero como me apetece comparar también cual es el coste a realizar trabajo de la base de datos vía código, he hecho un pequeño script en ruby, usando ActiveRecord para manejar las conexiones a la base de datos de forma simple. El código entero lo pongo targzeado al final, ahora pongo aquí la parte que interesa:

Benchmark.bmbm{ |b|
   # uno! El brikindans!, digo consulta en MySQL "normal" con el NOT IN
   b.report("(1) Consulta en MySQL") do
      execute_with MySQL::A, :normal
   end
   # La misma que en (1) pero con PostgreSQL
   b.report("(2) Consulta en PostgreSQL") do
      execute_with PostgreSQL::A, :normal
   end
   # La misma que en (1) pero con SQLite3
   b.report("(3) Consulta en SQLite3") do
      execute_with Sqlite3::A, :normal
   end
   # Consulta con MySQL pero esta vez usando LEFT JOIN
   b.report("(4) Consulta en MySQL (LEFT JOIN)") do
      execute_with MySQL::A, :left_join
   end
   # La misma que en (4) pero con PostgreSQL
   b.report("(5) Consulta en PostgreSQL (LEFT JOIN)") do
      execute_with PostgreSQL::A, :left_join
   end
   # La misma que en (4) pero con SQLite3
   b.report("(6) Consulta en SQLite3 (LEFT JOIN)") do
      execute_with Sqlite3::A, :left_join
   end
   # Hacemos el proceso en código de forma penosa, con un coste Ɵ(n^2)
   b.report("(7) Consulta en código (noob mode)") do
      as = MySQL::A.find :all
      bs = MySQL::B.find(:all).map{|i| i.otro_id }
      as.select{|i| !bs.include?(i.otro_id) }.size
   end
   # Hacemos el proceso en código pero de forma decente, restando conjuntos
   b.report("(8) Consulta en código (MySQL)") do
      (MySQL::A.find(:all).map{|i| i.otro_id } - 
       MySQL::B.find(:all).map{|i| i.otro_id }).size
   end
   # La misma que en (8) pero con PostgreSQL
   b.report("(9) Consulta en código (PostgreSQL)") do
      (PostgreSQL::A.find(:all).map{|i| i.otro_id } - 
       PostgreSQL::B.find(:all).map{|i| i.otro_id }).size
   end
   # La misma que en (8) pero con SQLite3
   b.report("(10) Consulta en código (SQLite3)") do
      (Sqlite3::A.find(:all).map{|i| i.otro_id } - 
       Sqlite3::B.find(:all).map{|i| i.otro_id }).size
   end
}

He aquí los resultados en mi pc, con MySQL 5.0.45, PostgreSQL 8.2.7 y SQLite 3.4.2 (sin tunear ninguna, tal y cual vienen en Ubuntu 7.10) y unos 10k registros en cada tabla.

$ ./mysqlVsPostgresqlVsSQLite3.rb 
Rehearsal --------------------------------------------------------------------------
(1) Consulta en MySQL                    0.020000   0.000000   0.020000 ( 54.567416)
(2) Consulta en PostgreSQL               0.000000   0.000000   0.000000 (  0.100255)
(3) Consulta en SQLite3                  0.180000   0.030000   0.210000 (  0.295534)
(4) Consulta en MySQL (LEFT JOIN)        0.000000   0.000000   0.000000 ( 54.656340)
(5) Consulta en PostgreSQL (LEFT JOIN)   0.000000   0.000000   0.000000 (  0.080631)
(6) Consulta en SQLite3 (LEFT JOIN)     47.980000   0.360000  48.340000 ( 54.249483)
(7) Consulta en código (noob mode)      21.660000   0.200000  21.860000 ( 30.060858)
(8) Consulta en código (MySQL)           0.520000   0.040000   0.560000 (  0.696692)
(9) Consulta en código (PostgreSQL)      0.960000   0.060000   1.020000 (  1.203388)
(10) Consulta en código (SQLite3)        3.410000   0.190000   3.600000 (  4.141494)
---------------------------------------------------------------- total: 75.610000sec
 
                                             user     system      total        real
(1) Consulta en MySQL                    0.160000   0.010000   0.170000 (  0.200753)
(2) Consulta en PostgreSQL               0.000000   0.000000   0.000000 (  0.027388)
(3) Consulta en SQLite3                  0.140000   0.010000   0.150000 (  0.148729)
(4) Consulta en MySQL (LEFT JOIN)        0.010000   0.000000   0.010000 (  0.000826)
(5) Consulta en PostgreSQL (LEFT JOIN)   0.000000   0.000000   0.000000 (  0.025892)
(6) Consulta en SQLite3 (LEFT JOIN)     51.810000   0.370000  52.180000 ( 69.148641)
(7) Consulta en código (noob mode)      21.340000   0.200000  21.540000 ( 24.971090)
(8) Consulta en código (MySQL)           0.420000   0.010000   0.430000 (  0.503471)
(9) Consulta en código (PostgreSQL)      0.970000   0.070000   1.040000 (  2.188352)
(10) Consulta en código (SQLite3)        3.170000   0.120000   3.290000 (  3.581979)

Adicionalmente he hecho también la prueba usando Microsoft Access 2003, gracias a este connector (que lo he usado para cargar todos los datos). No lo he incluido en las pruebas porque solo funciona bajo Windows, así que he ejecutado las consulta a mano. La primera de ellas le cuesta unos 200 seg aproximadamente mientras que el left join lo ejecuta casi al instante, no más de 0.25 segundos.

Mirando entonces todos los resultados, podemos observar como a MySQL le cuesta dos veces más que la ineficiente consulta con código y 700 veces más que a Postgre. Curioso, oye. En la segunda pasada MySQL ha cacheado el resultado (pero si haces otras consultas y volvemos a hacer ésta, tardaría de nuevo su tiempo “normal”) y es bastante más rápido.

En resumen se podría decir que es totalmente inaceptable (al menos para esta simple consulta de dos tablas) usar:

  • MySQL (en cualquier caso)
  • SQLite usando LEFT JOINs (pero funciona perfectamente con subconsulta)
  • MS Access usando subconsulta (pero funciona perfectamente con LEFT JOINs)
  • Hacer el trabajo en código de forma estúpida (es lo que tiene)

Por lo tanto, según mi propia interpretación, diría que nos quedan solo tres opciones (si consideramos únicamente estas 4 opciones como base de datos a usar) :

  • Usar PostgreSQL
  • Usar la base de datos ‘X’ para un X distinto a MySQL y probar nuestras consultas para ver si le gustan o no al SGBD
  • Usar la base de datos ‘X’, para un X cualquiera, emplear consultas triviales y filtrar en código de forma decente. (opción poco viable para entornos reales cuando tengamos millones de registros y no solo 10 mil)

Y hasta aquí todo, que cada uno saque sus propias conclusiones. No voy a decir que MySQL es una puta mierda, o que PostgreSQL suele ser más lento que el caballo del malo, a decir verdad lo que si que diré es que los benchmark siempre son muy limitados y comparan cosas en un ámbito restringido y controlado, por lo que no sirven para nada, así que no sé para qué narices he escrito todo esto, a decir verdad no sé ni para que estás leyéndolo, pero allá tú.

El código, los dump para postgre y mysql listos para ser cargados en sus respectivas bases de datos, la base de datos en access y la base de datos de sqlite3 los dejó en este fichero por si quieres jugar un rato. Se requiere (aparte de las base de datos obviamente) active record y composite primary keys (ambos instalables como gemas, gem install composite_primary_keys, por ejemplo).

Ruby on Rails, ¿se acabó lo que se daba?

Hace 3 años se publicó lo que fue la primera versión de un increíble framework para el desarrollo Web en un lenguaje de script no muy conocido, Ruby. Este framework era Ruby On Rails, y comenzó la revolución.

Desde entonces, en los dos años siguientes el tema de moda en la programación Web era lo cool que era RoR, que molaba mogollón, que era la hostia, que viva la madre que lo parió, y esas cosas. Incluso el año pasado me dio por probarlo a ver si realmente merecía la pena. Después de probar algunas de sus características lo dejé por dos razones, (1) ruby me parece parecía un lenguaje de script apestoso (yo lo veo como una mezcla de ada y perl, los cuales tienen unos puntos de vista totalmente opuestos!, ¡eso no se hace!), (2) ninguna de sus características es algo revolucionario, sí, son curiosas y chocan la primera vez, pero nada que no se puede hacer en otro lenguaje (en una tarde te programas la característica X que necesitas).

Últimamente se ha ido tranquilizando la cosa y apenas se hablaba ya de Rails, o al menos no tanto. Y hace un mes, a raíz de este post, empezaron las críticas sin parar hacia éste. Famosa es ya la historia de Twitter, aplicación escrita en RoR, que cuando alcanzó su éxito y comenzó su uso intensivo, estaba más tiempo caída que online (exagerando las cosas claro, pero era muy lenta y con bastantes caídas) y a partir de ésto se atacaba a rails criticando su falta de eficiencia y escalabilidad.

En mi opinión todo se está exagerando, simplemente la moda de Ruby On Rails ha pasado (2 años creo que es ya más que suficiente), lo cual no quiere decir que vaya a desaparecer, pero posiblemente significa que no crecerá mucho más, y dado que en el ámbito empresarial no es que haya entrado muy fuerte (principalmente debido a una falta de soporte o entidad detrás del framework, a diferencia de, por ejemplo, GWT o apestosas tecnologías Java en general), esto significa que la caída ha comenzado y no hay quien la pare.

Si hace un año hubiese dicho que Rails es una mierda, la gente me hubiese escupido, pegado una paliza, Google no me indexaría, Microsoft me mandaría bindous con cada telepizza y los teletubbies me dedicarían una canción. Ahora, una vez que ha pasado la moda, ya se permite tocar al intocable, y quien quiera, puede decir lo que le plazca. Ves que bien. Por mi parte, cuando tenga que realizar una aplicación Web en el futuro, rails será una de mis últimas primeras opciones, puesto aunque que por alternativas, no será.

Me pone un debugger con patatas, por favor

He leído este post donde un desarrollador que ha asistido a una charla de Jim Weirich (creador de rake) habla sobre los debugger en general y sobre el poco soporte de un debugger en ruby en particular. Parece ser que algunos piensan que no tener soporte para un debugger es una feature:

Asking why Ruby has weak debugger support is like asking why a dolphin doesn’t have gills. Ruby has weak debugger support because Ruby programmers shouldn’t be using a debugger. Ruby supports TDD and BDD better than any other language except possibly Smalltalk. Debugger support is for languages that you can’t run tests against gracefully.

WTF? ¿Un debugger es para lenguajes en los que no se pueden ejecutar tests fácilmente? Una leche. Un debugger es una herramienta para el desarrollo, punto. Si tu lenguaje favorito resulta que no tiene (aunque ruby tiene), no te inventes teorías o intentes convencer al resto de programadores de otros lenguajes (que si tienen debuggers) que tienes razón. En definitiva, no me llores. Lógicamente ha habido muchas respuestas frente a argumentos tan inconsistentes y frágiles, como la estupenda respuesta cuando tus herramientas apestan….

La ausencia de un debugger no es una característica, es una importante y significativa carencia en tus herramientas

También hay que reconocer que usando TDD, o simplemente pruebas, deberíamos anticiparnos y detectar bugs, y un debugger debería de ser innecesario. Pero prefiero tener la opción de poder usar uno cuando quiera, que su carencia. El ansia que es muy mala, cuanto más, mejor, ¿no?.

Actualización Abril 2008: Meses después se podría considerar que hay herramientas más que suficientes como para debuggear aplicaciones ruby con bastante facilidad. No tan bien como lenguajes como C o Java, pero si decéntemente.