lunes, 8 de junio de 2015

Inserción múltiple de registros: SQLs INSERT SELECT en Rails

Un problema típico de cualquier aplicación, sea Rails o no (o sea web o no), es el querer hacer una copia de un bloque de datos a otro sitio, con algún pequeño cambio y/o filtrado. Por ejemplo, puedo querer copiar los banners de la sección de depositos a una tabla de banners activos...

Un enfoque "de programador novato", pero que también puede verse en libros de programación de nivel básico, es hacerlo con un bucle:

  Ad.where(section: 'depositos').each do |ad|
    ActiveAd.create(title: ad.title, image_active: ad.image, url_active: ad.url)
  end

Pero si te miras los logs (algo que siempre debe hacerse), verás que este enfoque genera una SQL de INSERT por cada registro (más una para el SELECT):

  SELECT * FROM ads WHERE title = 'depositos'
  INSERT INTO active_ads (title, image_active, url_active) VALUES ('728x90 sup', 'http://images.example.org/728-dep.jpg', '/shop')
  INSERT INTO active_ads (title, image_active, url_active) VALUES ('468x60 med', 'http://images.example.org/468-dep.jpg', '/shop')
  INSERT INTO active_ads (title, image_active, url_active) VALUES ('300x250 lat', 'http://images.example.org/300-dep.jpg', '/shop')

Si el número de registros es pequeño, no pasará nada; si es mediano, el tiempo de respuesta empeorará ligeramente, y los logs se ensuciarán bastante y dejarán de ser útiles (lo cual en sí ya es un daño no despreciable)... y si el número de registros es grande, el impacto de esto hará que rendimiento del servidor se vea perjudicado, afectando también a páginas que no tengan nada que ver con este código, y entraremos en la dinámica del "Rails no escala"...

Obviamente, el problema no es que Rails no escala, sino que las cosas no deben hacerse así. Hay una instrucción SQL capaz de hacer esto en un solo paso, y es la que debe usarse:

  INSERT INTO active_ads (title, image_active, url_active) SELECT title, image, url FROM ads WHERE ads.section = 'depositos'

Este es uno de los pocos casos para los que ActiveRecord no nos da "de fábrica" una sintaxis elegante y railera para escribir la consulta. No todo está perdido: si lo usas con cierta frecuencia, la gema ar-extensions te permite hacer esto:

  ActiveAd.insert_select(
    into: [:title, :image_active, :url_active],
    select: [:title, :image, :url],
    from: :ads,
    conditions: ['section = ?', 'depositos'],
  )

Pero si es para un uso puntual, quizá no valga la pena añadir una gema al proyecto sólo para limpiar unas pocas consultas... Si este es el caso, la opción es lanzar la SQL directamente:

  sql = "INSERT INTO active_ads (title, image_active, url_active) SELECT title, image, url FROM ads WHERE ads.section = 'depositos'"
  ActiveRecord::Base.connection.execute(sql)

No nos engañemos: esto no es la panacea, insertar muchos registros nunca va a ser una tarea ligera para el servidor. Pero al menos, habremos optimizado el rendimiento en una zona crítica, y además dejaremos de ensuciar los logs, y por si fuera poco la query aparecerá en el log de consultas lentas de MySQL y podremos detectar más fácilmente los problemas para tratar de optimizarlos.

No hay comentarios:

Publicar un comentario

Nota: solo los miembros de este blog pueden publicar comentarios.