2010-10-26 6 views
19

He estado buscando en toda la web y no tengo ni idea.Rieles: Cómo compilar estadísticas por día/mes/año o Cómo faltan las funciones SQL agnósticas de bases de datos (ej .: STRFTIME, DATE_FORMAT, DATE_TRUNC)

  • Supongamos que usted tiene que construir un panel de control en el área de administración de la aplicación Rails y que desea tener el número de suscripciones por día.
  • Suponga que está utilizando SQLite3 para el desarrollo, MySQL para la producción (configuración bastante estándar)

Básicamente, hay dos opciones:

1) recuperar todas las filas de la base de datos usando Subscriber.all y agregada a día en la aplicación Rails usando el Enumerable.group_by:

@subscribers = Subscriber.all 
@subscriptions_per_day = @subscribers.group_by { |s| s.created_at.beginning_of_day } 

Creo que esta es una muy mala idea. Recuperar todas las filas de la base de datos puede ser aceptable para una aplicación pequeña, pero no se escalará en absoluto. ¡Funciones de agregación y fecha de la base de datos para el rescate!

2) ejecutar una consulta SQL en la base de datos utilizando agregada y funciones de fecha:

Subscriber.select('STRFTIME("%Y-%m-%d", created_at) AS day, COUNT(*) AS subscriptions').group('day') 

que se desarrollará en esta consulta SQL:

SELECT STRFTIME("%Y-%m-%d", created_at) AS day, COUNT(*) AS subscriptions 
FROM subscribers 
GROUP BY day 

mucho mejor. Ahora los agregados se hacen en la base de datos que está optimizada para este tipo de tarea, y solo se devuelve una fila por día de la base de datos a la aplicación Rails.

... pero espera ... ¡ahora la aplicación tiene que activarse en mi entorno de producción que usa MySQL! Reemplazar STRFTIME() con DATE_FORMAT(). ¿Qué pasa si mañana me cambio a PostgreSQL? Reemplazar DATE_FORMAT() con DATE_TRUNC().

Me gusta desarrollar con SQLite. Simple y fácil. También me gusta la idea de que Rails es agnóstico de base de datos. Pero ¿por qué Rails no proporciona una forma de traducir funciones de SQL que hacen exactamente lo mismo, pero tienen una sintaxis diferente en cada RDBMS (esta diferencia es realmente estúpida, pero bueno, es demasiado tarde para quejarse)?

No puedo creer que encuentre tan pocas respuestas en la Web para una característica tan básica de una aplicación de Rails: cuente las suscripciones por día, mes o año.

Dime que me falta algo :)

EDITAR

Ha sido un par de años desde que he publicado esta pregunta. La experiencia me ha demostrado que debería usar el mismo DB para dev y prod. Entonces, considero irrelevante el requisito de agnóstico de la base de datos.

Dev/prod parity FTW.

+0

¿Cuál es la versión de sus rieles? – Lachezar

+0

Estoy usando Rails 3. – lakim

+1

Es un problema más complicado de lo que parece ser. Para ver por qué, piense en esta pregunta: "¿Cuántas horas hay en un día?" La respuesta es "24, en promedio si ignora los segundos intercalares". Es solo una respuesta promedio debido a los cambios en el horario de verano, y eso es algo que a los políticos les gusta jugar. La duración de un día también es específica de la localidad. ¿Debería la base de datos estar al tanto de todo ese bálsamo flamista barroco burocrático, o es algo que es, en cambio, un aspecto de la aplicación de visualización? –

Respuesta

0

Si db agnosticismo es lo que está buscando, no puedo pensar en un par de opciones:

Crear un nuevo campo (lo llamaremos day_str) para el suscriptor que almacena ya sea la fecha formateada o una marca de tiempo y utilizar ActiveRecord.count:

daily_subscriber_counts = Subscriber.count(:group => "day_str") 

La desventaja es, por supuesto, un tamaño un poco más grande registro, pero esto eliminaría todos, pero las preocupaciones de rendimiento.

Se podría también, dependiendo de la granularidad de los datos que está siendo visualizada está, simplemente llame .count varias veces con la fecha fijada según se desee ...

((Date.today - 7)..Date.today).each |d| 
    daily_subscriber_counts[d] = Subscriber.count(:conditions => ["created_at >= ? AND created_at < ?", d.to_time, (d+1).to_time) 
end 

Esto podría también ser personalizado para dar cuenta de la variación granularidades (por mes, por año, por día, por hora). No es la solución más eficiente en el caso de que quisiera agrupar por día a todos sus suscriptores (tampoco he tenido la oportunidad de ejecutarlo), pero me imagino que querría agrupar por mes, día, hora si está viendo el valor de un año, meses o días de datos, respectivamente.

Si usted está dispuesto a comprometerse con MySQL y SQLite podría utilizar ...

daily_subscriber_counts = Subscriber.count(:group => "date(created_at)") 

... ya que comparten fecha similar() funciones.

+0

Me gusta su primera opción. Me ayuda a pensar fuera de la caja. Pero la compensación (tamaño de registro más grande) es aún mayor teniendo en cuenta que solo será utilizada por los administradores. – lakim

+0

¿Alguien sabe un plugin de Rails que traduce funciones SQL para cada RDBMS? Sigo pensando que sería la mejor opción. – lakim

+1

Bien técnicamente, los rieles proporcionan esa funcionalidad (de ahí la conversión de un hash de condiciones a una consulta SQL con el buscador). Si confía en que se quedará con SQLite y MySQL, son similares en su función de fecha ...) Intente daily_subscriber_counts = Subscriber.count (: group => "date (created_at)") – pbaumann

7

Terminé escribiendo mi propia joya. Compruébelo usted mismo y sentirse libre de contribuir: https://github.com/lakim/sql_funk

Se le permite hacer llamadas como:

Subscriber.count_by("created_at", :group_by => "day") 
0

Me Afinar/ampliar la respuesta de PBaumann ligeramente, e incluyen una mesa de fechas en su base de datos. Se necesitaría una combinación en su consulta:

SELECT D.DateText AS Day, COUNT(*) AS Subscriptions 
FROM subscribers AS S 
    INNER JOIN Dates AS D ON S.created_at = D.Date 
GROUP BY D.DateText 

... pero tendría un valor formateado bien-disponible sin llamar a cualquier función. Con un PK en Dates.Date, puede fusionarse y debe ser muy rápido.

Si tiene un público internacional, puede usar DateTextUS, DateTextGB, DateTextGer, etc., pero obviamente esta no sería una solución perfecta.

Otra opción: enviar la fecha al texto en el lado de la base de datos usando CONVERT(), que es ANSI y puede estar disponible en todas las bases de datos; Soy demasiado vago para confirmar eso ahora.

5

Habla de algunos problemas bastante difíciles que Rails desafortunadamente pasa por alto por completo. Los documentos de ActiveRecord :: Calculations se escriben como si fueran todo lo que necesitas, pero las bases de datos pueden hacer cosas mucho más avanzadas. Como Donal Fellows mencionó en su comentario, el problema es mucho más complicado de lo que parece.

He desarrollado una aplicación de Rails en los últimos dos años que hace un uso intensivo de la agregación, y he intentado algunos enfoques diferentes para el problema. Desafortunadamente no tengo el lujo de ignorar cosas como el horario de verano porque las estadísticas son "solo tendencias". Los cálculos que genero son probados por mis clientes según las especificaciones exactas.

Para ampliar el problema un poco, creo que encontrará que su solución actual de agrupamiento por fechas es inadecuada. Parece una opción natural usar STRFTIME.El problema principal es que no te permite agrupar por periodos de tiempo arbitrarios. Si desea hacer agregación por año, mes, día, hora y/o minuto, STRFTIME funcionará bien. Si no, te encontrarás buscando otra solución. Otro gran problema es la agregación al agregarse. Digamos, por ejemplo, que desea agrupar por mes, pero desea hacerlo a partir del 15 de cada mes. ¿Cómo lo harías usando STRFTIME? Tendrías que agrupar cada día y luego por mes, pero luego alguien contabilizaría la compensación inicial del día 15 de cada mes. El colmo es que la agrupación por STRFTIME requiere agrupar por un valor de cadena, que encontrará muy lento al realizar la agregación en la agregación.

La solución más eficiente y mejor diseñada que he encontrado es una basada en períodos de tiempo enteros. He aquí un extracto de una de mis consultas MySQL:

SELECT 
    field1, field2, field3, 
    CEIL((UNIX_TIMESTAMP(CONVERT_TZ(date, '+0:00', @@session.time_zone)) + :begin_offset)/:time_interval) AS time_period 
FROM 
    some_table 
GROUP BY 
    time_period 

En este caso,: TIME_INTERVAL es el número de segundos en el período de agrupación (por ejemplo, 86400 para el día) y: begin_offset es el número de segundos para compensar el punto de inicio Las cuentas comerciales CONVERT_TZ() explican la forma en que mysql interpreta las fechas. Mysql siempre asume que el campo de fecha está en la zona horaria local mysql. Pero debido a que almaceno las horas en UTC, debo convertirlo de UTC a la zona horaria de la sesión si deseo que la función UNIX_TIMESTAMP() me proporcione una respuesta correcta. El período de tiempo termina siendo un número entero que describe el número de intervalos de tiempo desde el inicio del tiempo de Unix. Esta solución es mucho más flexible porque le permite agrupar por períodos arbitrarios y no requiere agregación al agregarse.

Ahora, para llegar a mi punto real. Para una solución robusta, le recomiendo que considere no utilizar Rails en absoluto para generar estas consultas. El mayor problema es que las características de rendimiento y las sutilezas de la agregación son diferentes en todas las bases de datos. Es posible que encuentre un diseño que funcione bien en su entorno de desarrollo pero no en producción, o viceversa. Pasará por muchos aros para que Rails juegue bien con ambas bases de datos en la construcción de consultas.

En lugar de eso, le recomiendo que genere vistas específicas de la base de datos en su base de datos elegida y las lleve al entorno correcto. Intente modelar la vista como lo haría con cualquier otra tabla ActiveRecord (id's y all), y por supuesto haga que los campos en la vista sean idénticos en las bases de datos. Debido a que estas estadísticas son consultas de solo lectura, puede usar un modelo para respaldarlas y pretender que son tablas de pleno derecho. Solo haz una excepción si alguien intenta guardar, crear, actualizar o destruir.

No solo obtendrá una administración de modelo simplificada haciendo las cosas a la manera de Rails, sino que también podrá escribir pruebas de unidades para sus funciones de agregación de una manera que no soñaría en SQL puro. Y si decide cambiar las bases de datos, tendrá que volver a escribir esas vistas, pero las pruebas le dirán dónde se equivoca y le facilitarán la vida.

+0

Esto parece básicamente un consejo bastante sólido. Torcer los rieles para hacer cosas que su base de datos puede hacer en una sola consulta, parece ser lento y propenso a errores, sin mencionar que la base de datos probablemente genere la respuesta más rápido y sin masticar la memoria. –

0

He aquí cómo lo hago:

que tienen una clase de estadísticas que permite almacenar eventos primas. (Código es de las primeras semanas empecé codificación en Ruby por lo excusa parte de ella :-))

class Stat < ActiveRecord::Base 
    belongs_to :statable, :polymorphic => true 

    attr_accessible :statable_id, :statable_type, :statable_stattype_id, :source_url, :referral_url, :temp_user_guid 

    # you can replace this with a cron job for better performance 
    # the reason I have it here is because I care about real-time stats 
    after_save :aggregate 

    def aggregate 
    aggregateinterval(1.hour) 
    #aggregateinterval(10.minutes) 
end 

    # will aggregate an interval with the following properties: 
    # take t = 1.hour as an example 
    # it's 5:21 pm now, it will aggregate everything between 5 and 6 
    # and put them in the interval with start time 5:00 pm and 6:00 pm for today's date 
    # if you wish to create a cron job for this, you can specify the start time, and t 
def aggregateinterval(t=1.hour) 
    aggregated_stat = AggregatedStat.where('start_time = ? and end_time = ? and statable_id = ? and statable_type = ? and statable_stattype_id = ?', Time.now.utc.floor(t), Time.now.utc.floor(t) + t, self.statable_id, self.statable_type, self.statable_stattype_id) 

    if (aggregated_stat.nil? || aggregated_stat.empty?) 
     aggregated_stat = AggregatedStat.new 
    else 
     aggregated_stat = aggregated_stat.first 
    end 

      aggregated_stat.statable_id = self.statable_id 
    aggregated_stat.statable_type = self.statable_type 
    aggregated_stat.statable_stattype_id = self.statable_stattype_id 
    aggregated_stat.start_time = Time.now.utc.floor(t) 
    aggregated_stat.end_time = Time.now.utc.floor(t) + t 
    # in minutes 
    aggregated_stat.interval_size = t/60 

    if (!aggregated_stat.count) 
     aggregated_stat.count = 0 
    end 
    aggregated_stat.count = aggregated_stat.count + 1 


    aggregated_stat.save 
end 

end 

Y aquí está la clase AggregatedStat:

class AggregatedStat < ActiveRecord::Base 
    belongs_to :statable, :polymorphic => true 

    attr_accessible :statable_id, :statable_type, :statable_stattype_id, :start_time, :end_time 

Cada artículo expresable que se agrega a el db tiene un statable_type y un statable_stattype_id y algunos otros datos estadísticos genéricos. Statable_type y statable_stattype_id son para las clases polimórficas y pueden contener valores como (la cadena) "Usuario" y 1, lo que significa que está almacenando estadísticas sobre el Número de usuario 1.

Puede agregar más columnas y tener mapeadores en el código extraer las columnas correctas cuando las necesite. Crear tablas múltiples hace que sea más difícil de administrar.

En el código anterior, StatableStattypes es solo una tabla que contiene "eventos" que desea registrar ... Uso una tabla porque la experiencia previa me enseñó que no quiero buscar qué tipo de estadísticas un número en la base de datos se refiere a.

class StatableStattype < ActiveRecord::Base 
    attr_accessible :name, :description 

    has_many :stats 
end 

Ahora ir a las clases que le gustaría tener algunas estadísticas para y hacer lo siguiente:

class User < ActiveRecord::Base 
    # first line isn't too useful except for testing 
    has_many :stats, :as => :statable, :dependent => :destroy 
    has_many :aggregated_stats, :as => :statable, :dependent => :destroy 
end 

Luego, puede consultar las estadísticas agregadas para un determinado usuario (o la ubicación en el ejemplo a continuación) con este código:

Location.first.aggregated_stats.where("start_time > ?", DateTime.now - 8.month) 
1

Acabo de lanzar una joya que te permite hacer esto fácilmente con MySQL. http://ankane.github.io/groupdate/

Realmente debería intentar ejecutar MySQL en desarrollo también. Sus entornos de desarrollo y producción deben estar lo más cerca posible, es decir, tienen menos posibilidades de que algo funcione en el desarrollo y rompan totalmente la producción.

Cuestiones relacionadas