特定の日付範囲内で1日あたり平均を取得するMySQL for Railsの最良の方法
-
05-07-2019 - |
質問
Railsでグラフを作成しようとしています。たとえば、特定の日付範囲での1日あたりの1日あたりの平均販売量
「sales_price」を含むproducts_soldモデルがあるとします。フロート属性。ただし、特定の日に売上がない場合(たとえば、model / dbに売上がない場合)、単に0を返します。
MySQL / Railsでこれを実現する最良の方法は何ですか?私はこのようなことができることを知っています:
(このSQLクエリは、私が望むものを取得するためのまったく間違った方法かもしれません)
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;
次のような結果が得られます:
| avg | date | 23 01-03-2009 50 01-05-2009 34 01-07-2009 ... ...
取得したいのはこれです:
| 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 ... ...
これをSQLで実行できますか、または結果を後処理して、SQLの結果セットにない日付範囲の日付を見つける必要がありますか?おそらく、サブセレクトまたはIFステートメントが必要ですか?
皆さんの助けに感謝します。
解決
ActiveRecordで組み込みのグループ関数機能を使用しない理由(既に述べた日付以外)がありますか? 「後処理」について心配しているようですが、実際に心配する必要はないと思います。
あなたはRailsにいるので、おそらくRailsソリューションを最初に探しているはずです[1]。私の最初の考えは、次のようなことをすることです
Product.average(:sales_price, :group => "DATE(created_at)", :conditions => ["merchant_id=?", 1])
ActiveRecordがあなたが説明したSQLにほとんど変わりました。 MerchantとProductの間に宣言された has_many
の関連付けがあると仮定すると、おそらくそれを使用した方がよいでしょう。次のようなものです。
ave_prices = Merchant.find(1).products.average(:sales_price, :group => "DATE(created_at)")
(" products_sold"としてのモデルの説明が何らかの転写エラーであることを望んでいますが、そうでない場合は、クラスの命名に関するメッセージが多少外れています!)
その後、あなたは始めたところに戻りましたが、より一般的なRailsの方法でそこに着きました(そしてRailsは実際に規約を大切にしています!)。次に、ギャップを埋める必要があります。
日付範囲を知っていると仮定します。たとえば、 from_date
から to_date
までのすべての日付として定義されているとします。
date_aves = (from_date..to_date).map{|dt| [dt, 0]}
これは、日付の完全なリストを配列として作成します。平均を取得した日付は必要ありません:
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
それは私にとって少し混乱しているように見えます。配列にとどまるのではなく、ハッシュまたは構造体の構築を調査します。
[1] SQLを使用しないと言っているわけではありません。ActiveRecordが最も効率的なクエリを生成できず、 find_by_sql
にフォールバックする状況が発生します。それは大丈夫、それはそのようになるはずですが、私はあなたが最後の手段としてのみそれを使用しようとするべきだと思います。
他のヒント
このようなクエリでは、レポートする日付ごとに1行のテーブルを生成するメカニズムを見つける必要があります。次に、分析するデータテーブルとそのテーブルの外部結合を行います。 nullをゼロに変換するには、NVLまたはCOALESCEで遊ぶ必要がある場合もあります。
難しい部分は、分析する必要がある範囲の日付のリストを含む(一時的な)テーブルを生成する方法を見つけることです。それはDBMS固有です。
ただし、日付/時刻値を単一の日付にマッピングするというアイデアはすぐにわかります。毎週の売り上げを分析したい場合は、同様のトリックを引く必要があります-週01の2009-W01のようなISO 8601日付形式にすべての日付をマッピングします。
また、日付形式を2009-01-08表記にマップする方が適切です。これは、プレーンな文字ソートを使用して日付順にソートできるためです。
少し乾かすには:
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には集合を返す関数がありますか?つまりクエリの各行で異なる値を返す関数? PostgreSQLの例として、次のことができます。
select 'foo', generate_series(3, 5);
これにより、2列3行で構成される結果セットが生成されます。左列には各行に「foo」が含まれ、右列には3、4、5が含まれます。
したがって、MySQLで generate_series()
に相当するものとサブクエリがあると仮定すると、この関数から LEFT OUTER JOIN
が必要なクエリになります既に持っている。これにより、各日付が出力に表示されるようになります。
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;
MySQLに適した構文を得るには、これを少し調整する必要があるかもしれません。