2009-01-08 6 views
5

Je suis en train de faire un graphique dans Rails, par exemple le montant des ventes de moyenne par jour pour chaque jour dans une période donnéeLa meilleure façon de MySQL ou Rails pour obtenir AVG par jour dans une plage de date précise

Dites que j'ai un modèle products_sold qui a un attribut float "sales_price". Mais si un jour spécifique n'a pas de ventes (par exemple aucune dans le modèle/db), je veux retourner simplement 0.

Quelle est la meilleure façon de faire cela dans MySQL/Rails? Je sais que je peux faire quelque chose comme ceci:

(Cette requête SQL peut être la façon tout à fait tort d'obtenir ce que je suis vouloir trop)

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; 

et obtenir des résultats comme celui-ci:

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

ce que je voudrais faire est la suivante:

 
| 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 
    ...  ... 

Est-ce que je peux faire ceci avec SQL ou devrais-je post-traiter les résultats pour trouver quelles dates dans le Daterange ne sont pas dans l'ensemble de résultat de SQL? Peut-être ai-je besoin de sous-sélections ou d'instructions IF?

Merci pour toute aide à tous.

Répondre

7

Y a-t-il une raison (autre que la date déjà mentionnée) pour laquelle vous n'utilisez pas les capacités de fonctions de groupe intégrées dans ActiveRecord?Vous semblez être préoccupé par le «post-traitement», qui ne me préoccupe pas vraiment.

Vous êtes dans Rails, donc vous devriez probablement rechercher une solution Rails en premier [1]. Ma première pensée serait de faire quelque chose comme

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

où ActiveRecord est devenu à peu près le SQL que vous avez décrit. En supposant qu'il ya une déclaration has_many association entre le commerçant et le produit, alors vous seriez probablement mieux utiliser ce, donc quelque chose comme:

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

(j'espère que votre description du modèle comme « products_sold » est un peu type d'erreur de transcription, btw - si non, vous êtes un peu hors message avec votre nom de classe!)

Après tout cela, vous êtes de retour où vous avez commencé, mais vous y êtes arrivé d'une manière plus conventionnelle Rails (et Rails valorise vraiment les conventions!). Maintenant, nous devons combler les lacunes.

Je suppose que vous connaissez votre plage de dates, disons qu'il est défini comme toutes les dates de from_date à to_date.

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

Cela construit la liste complète des dates sous forme de tableau. Nous ne avons pas besoin les dates où nous avons eu en moyenne:

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 

Ce lot semble un peu encombrés pour moi: Je pense que ce pourrait être terser et plus propre. J'étudierais la construction d'un Hash ou d'un Struct plutôt que de rester dans des tableaux.


[1] Je ne dis pas ne pas utiliser SQL - situations ne se produisent où ActiveRecord ne peut pas générer la requête la plus efficace et vous replier sur find_by_sql. C'est bien, c'est censé être comme ça, mais je pense que vous devriez essayer de ne l'utiliser qu'en dernier recours.

0

Est-ce que MySQL a des fonctions de retour de jeu? C'est à dire. fonctions qui retournent des valeurs différentes sur chaque ligne d'une requête? A titre d'exemple de PostgreSQL, vous pouvez faire:

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

Cela produira un jeu de résultats composé de 2 colonnes et 3 lignes, où la colonne de gauche contient « foo » sur chaque ligne et la colonne de droite contient 3, 4 Donc, en supposant que vous avez un équivalent de generate_series() dans MySQL, et des sous-requêtes: Ce dont vous avez besoin est un LEFT OUTER JOIN de cette fonction à la requête que vous avez déjà. Cela vous assurer de voir chaque jour apparaissent dans la sortie:

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; 

Vous devrez peut-être jouer avec cela un peu pour obtenir le droit de syntaxe pour MySQL.

2

Pour une telle requête, vous devrez trouver un mécanisme pour générer une table avec une ligne pour chaque date que vous souhaitez signaler. Ensuite, vous ferez une jointure externe de cette table avec la table de données que vous analysez. Vous devrez peut-être aussi jouer avec NVL ou COALESCE pour convertir des zéros en zéros. La partie la plus difficile est de savoir comment générer la table (temporaire) qui contient la liste des dates pour la plage que vous devez analyser. C'est spécifique au SGBD. Cependant, votre idée de mapper les valeurs de date/heure sur une seule date est correcte. Vous auriez besoin de tirer un truc similaire - mappant toutes les dates à un format de date ISO 8601 comme 2009-W01 pour la semaine 01 - si vous vouliez analyser les ventes hebdomadaires.

Aussi, vous feriez mieux de mapper votre format DATE à la notation 2009-01-08 parce que vous pouvez alors trier dans l'ordre des dates en utilisant un tri simple.

2

Pour sécher un peu:

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]} 
Questions connexes