La mejor manera en MySQL para que Rails obtenga una MEDIA por día dentro de un rango de fechas específico

StackOverflow https://stackoverflow.com/questions/423212

Pregunta

Estoy tratando de hacer un gráfico en Rails, por ejemplo, el promedio de ventas por día para cada día en un rango de fechas determinado

Supongamos que tengo un modelo de products_sold que tiene un " sales_price " atributo de flotación Pero si un día específico no tiene ventas (por ejemplo, ninguna en el modelo / db), quiero devolver simplemente 0.

¿Cuál es la mejor manera en MySQL / Rails para hacer esto? Sé que puedo hacer algo como esto:

( Esta consulta SQL puede ser la forma completamente incorrecta de obtener lo que quiero también )

SELECT avg(sales_price) AS avg, DATE_FORMAT(created_at, '%m-%d-%Y') AS date
    FROM products_sold WHERE merchant_id = 1 GROUP BY date;

Y obtén resultados como este:

| avg |    date    |
  23    01-03-2009
  50    01-05-2009 
  34    01-07-2009
  ...       ...

Lo que me gustaría obtener es esto:

| avg |    date    |
  23    01-03-2009
   0    01-04-2009
  50    01-05-2009
   0    01-06-2009 
  34    01-07-2009
   0    01-08-2009
  ...       ...

¿Puedo hacer esto con SQL o tendré que postprocesar los resultados para encontrar qué fechas en el daterange no están en el conjunto de resultados de SQL? Tal vez necesito algunas sub-selecciones o declaraciones IF?

Gracias por cualquier ayuda a todos.

¿Fue útil?

Solución

¿Hay alguna razón (aparte de la fecha que ya se mencionó) por la que no usaría las funciones de función de grupo integradas en ActiveRecord? Parece que te preocupa el "postprocesamiento", lo cual no creo que sea realmente algo de qué preocuparse.

Estás en Rails, por lo que probablemente deberías buscar primero una solución de Rails [1]. Mi primer pensamiento sería hacer algo como

Product.average(:sales_price, :group => "DATE(created_at)", :conditions => ["merchant_id=?", 1])

cual ActiveRecord se convirtió en prácticamente el SQL que describiste. Suponiendo que haya una asociación de has_many declarada entre el Comerciante y el Producto, entonces probablemente sería mejor usar eso, así que algo como:

ave_prices = Merchant.find(1).products.average(:sales_price, :group => "DATE(created_at)")

(espero que su descripción del modelo como " products_sold " sea algún tipo de error de transcripción, por cierto, si no, ¡está un poco fuera de mensaje con el nombre de su clase!)

Después de todo eso, estás de vuelta donde empezaste, pero llegaste de una manera más convencional de Rails (¡y Rails realmente valora las convenciones!). Ahora necesitamos llenar los huecos.

Supondré que sabes tu intervalo de fechas, digamos que se define como todas las fechas desde from_date a to_date .

date_aves = (from_date..to_date).map{|dt| [dt, 0]}

Eso construye la lista completa de fechas como una matriz. No necesitamos las fechas donde obtuvimos un promedio:

ave_price_dates = ave_prices.collect{|ave_price| ave_price[0]} # build an array of dates
date_aves.delete_if { |dt| ave_price.dates.index(dt[0]) } # remove zero entries for dates retrieved from DB
date_aves.concat(ave_prices)     # add the query results
date_aves.sort_by{|ave| ave[0] } # sort by date

Ese lote me parece un poco abarrotado: creo que podría ser terser y más limpio. Investigaría cómo crear un hash o struct en lugar de permanecer en arreglos.


[1] No estoy diciendo que no use SQL - las situaciones ocurren cuando ActiveRecord no puede generar la consulta más eficiente y recurre a find_by_sql . Está bien, se supone que es así, pero creo que deberías intentar usarlo solo como último recurso.

Otros consejos

Para cualquier consulta de este tipo, deberá encontrar un mecanismo para generar una tabla con una fila para cada fecha sobre la que desea informar. Luego, realizará una combinación externa de esa tabla con la tabla de datos que está analizando. También es posible que tengas que jugar con NVL o COALESCE para convertir nulos en ceros.

La parte difícil es averiguar cómo generar la tabla (temporal) que contiene la lista de fechas para el rango que necesita analizar. Eso es específico de DBMS.

Sin embargo, su idea de asignar valores de fecha / hora a una sola fecha es acertada. Necesitará hacer un truco similar: asignar todas las fechas a un formato de fecha ISO 8601 como el 2009-W01 para la semana 01, si desea analizar las ventas semanales.

Además, sería mejor asignar su formato de FECHA a la notación 2009-01-08 porque entonces puede ordenar en orden de fecha usando una clasificación de carácter simple.

Para secar un poco:

ave_prices = Merchant.find(1).products.average(:sales_price, :group => "DATE(created_at)")
date_aves = (from_date..to_date).map{|dt| [dt, ave_prices[dt.strftime "%Y-%m-%d"] || 0]}

¿MySQL tiene funciones de retorno de conjuntos? Es decir. ¿Funciones que devuelven valores diferentes en cada fila de una consulta? Como ejemplo de PostgreSQL, puedes hacer:

select 'foo', generate_series(3, 5);

Esto producirá un conjunto de resultados que consta de 2 columnas y 3 filas, donde la columna de la izquierda contiene 'foo' en cada fila y la columna de la derecha contiene 3, 4 y 5.

Entonces, asumiendo que tiene un equivalente de generate_series () en MySQL y subconsultas: Lo que necesita es un LEFT OUTER JOIN de esta función a la consulta que ya tengo. Eso asegurará que vea que cada fecha aparece en la salida:

SELECT
    avg(sales_price) as avg,
    DATE_FORMAT(the_date, '%m-%d-%Y') as date
FROM (select cast('2008-JAN-01' as date) + generate_series(0, 364) as the_date) date_range
LEFT OUTER JOIN products_sold on (the_date = created_at)
WHERE merchant_id = 1
GROUP BY date;

Es posible que tengas que jugar un poco con esto para obtener la sintaxis correcta para MySQL.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top