Question

J'essaie de créer un graphique dans Rails, par exemple le montant moyen des ventes par jour pour chaque jour d'une période donnée

.

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

Quel est le meilleur moyen de faire cela avec MySQL / Rails? Je sais que je peux faire quelque chose comme ça:

( Cette requête SQL est peut-être une très mauvaise façon d'obtenir ce que je veux aussi )

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 obtenez des résultats comme celui-ci:

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

Ce que j'aimerais avoir, c'est ceci:

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

Puis-je faire cela avec SQL ou devrai-je post-traiter les résultats pour rechercher les dates de la plage de dates qui ne figurent pas dans l'ensemble de résultats SQL? J'ai peut-être besoin de sous-sélections ou de déclarations IF?

Merci pour votre aide à tous.

Était-ce utile?

La solution

Existe-t-il une raison (autre que la date déjà mentionnée) pour laquelle vous n’utiliseriez pas les fonctionnalités de fonction de groupe intégrées dans ActiveRecord? Vous semblez vous préoccuper du "post-traitement", ce qui, à mon avis, n’est pas vraiment inquiétant.

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

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

qu'ActiveRecord a transformé en gros le SQL que vous avez décrit. En supposant qu’il existe une association déclarée has_many entre Merchant et Product, vous feriez probablement mieux de l’utiliser, 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 en tant que "products_sold" est une sorte d'erreur de transcription, d'ailleurs - sinon, vous êtes un peu à côté du nom de votre classe!)

Après tout cela, vous êtes revenu à votre point de départ, mais vous y êtes arrivé d’une manière plus conventionnelle (et Rails respecte vraiment les conventions!). Nous devons maintenant combler les lacunes.

Je suppose que vous connaissez votre plage de dates. Elle est définie comme toutes les dates allant de de_date à à_date .

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

Cela construit la liste complète des dates sous forme de tableau. Nous n'avons pas besoin des dates où nous avons obtenu une 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

Cela me semble un peu encombré: je pense que cela pourrait être plus sordide et plus propre. J'étudierais la construction d'un hachage ou d'une structure plutôt que de rester dans des tableaux.

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

Autres conseils

Pour toute requête de ce type, vous devrez trouver un mécanisme permettant de générer une table avec une ligne pour chaque date pour laquelle vous souhaitez créer un rapport. 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 les valeurs NULL en zéros.

La partie difficile consiste à déterminer comment générer la table (temporaire) contenant la liste des dates de la plage à analyser. C’est spécifique au SGBD.

Cependant, votre idée de mapper les valeurs de date / heure sur une seule date est exacte. Si vous souhaitez analyser les ventes hebdomadaires, vous devez utiliser une astuce similaire: mapper toutes les dates sur un format de date ISO 8601 tel que 2009-W01 pour la semaine 01.

De même, vous feriez mieux de mapper votre format DATE sur la notation 2009-01-08, car vous pourrez alors trier dans l'ordre des dates à l'aide d'un tri de caractères simples.

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

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

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

Ceci produira un ensemble de résultats composé de 2 colonnes et de 3 lignes, la colonne de gauche contenant "foo" sur chaque ligne et la colonne de droite contenant 3, 4 et 5.

Donc, en supposant que vous ayez un équivalent de generate_series () dans MySQL et des sous-requêtes: vous avez besoin d’un LEFT OUTER JOIN de cette fonction à la requête que vous J'ai déjà. Cela vous permettra de voir chaque date apparaître dans le résultat:

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 manipuler cela un peu pour que la syntaxe soit correcte pour MySQL.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top