Pergunta

Estou tentando fazer um gráfico no Rails, por exemplo, o valor das vendas do AVG por dia para cada dia em um determinado intervalo

Digamos que eu tenha um modelo Products_sold que tenha um atributo de float "Sales_Price". Mas se um dia específico não tiver vendas (por exemplo, nenhum no modelo/dB), quero retornar simplesmente 0.

Qual é a melhor maneira de MySQL/Rails para fazer isso? Eu sei que posso fazer algo assim:

(Esta consulta SQL pode ser a maneira completamente errada de conseguir o que estou querendo)

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;

E obtenha resultados como este:

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

O que eu gostaria de conseguir é o seguinte:

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

Posso fazer isso com o SQL ou terei que pós-processo dos resultados para encontrar quais datas no daterange não estão no conjunto de resultados do SQL? Talvez eu precise de alguns subselitos ou declarações se?

Obrigado por qualquer ajuda a todos.

Foi útil?

Solução

Existe uma razão (além da data já mencionada) por que você não usaria os recursos de função de grupo interno no ActiveRecord? Você parece estar preocupado com o "pós-processamento", o que eu acho que não é realmente algo com que me preocupar.

Você está em trilhos, então provavelmente deve estar procurando uma solução de trilhos primeiro [1]. Meu primeiro pensamento seria fazer algo como

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

que ActiveRecord se transformou em praticamente o SQL que você descreveu. Supondo que haja um declarado has_many Associação entre comerciante e produto, você provavelmente seria melhor usando isso, então algo como:

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

(Espero que sua descrição do modelo como "Products_sold" seja algum tipo de erro de transcrição, btw - se não, você está um pouco fora de mensagens com a nomeação da sua classe!)

Depois de tudo isso, você está de volta onde começou, mas chegou lá de uma maneira mais convencional do Rails (e o Rails realmente valoriza as convenções!). Agora precisamos preencher as lacunas.

Vou assumir que você conhece seu intervalo, digamos que ele seja definido como todas as datas de from_date para to_date.

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

Isso constrói a lista completa de datas como uma matriz. Não precisamos das datas em que obtivemos uma média:

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

Isso parece um pouco confuso para mim: acho que pode ser mais mais limpo e mais limpo. Eu investigaria a construção de um hash ou estrutura em vez de ficar em matrizes.


1] Não estou dizendo que não use o SQL - as situações ocorrem onde o ActiveRecord não pode gerar a consulta mais eficiente e você recorre ao find_by_sql. Tudo bem, deveria ser assim, mas acho que você deve tentar usá -lo apenas como último recurso.

Outras dicas

Para qualquer consulta, você precisará encontrar um mecanismo para gerar uma tabela com uma linha para cada data em que deseja relatar. Em seguida, você fará uma junção externa dessa tabela com a tabela de dados que está analisando. Você também pode ter que brincar com NVL ou Coalesce para converter nulos em zeros.

A parte difícil é descobrir como gerar a tabela (temporária) que contém a lista de datas para o intervalo que você precisa analisar. Isso é específico do DBMS.

Sua idéia de mapear os valores da data/hora para uma única data está no momento. Você precisaria fazer um truque semelhante - mapeando todas as datas para um formato de data ISO 8601 como 2009 -W01 para a semana 01 - se você quiser analisar vendas semanais.

Além disso, você faria melhor para mapear seu formato de data para a notação 2009-01-08, porque então você pode classificar em ordem de data usando um tipo de caractere simples.

Para secar um pouco:

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

O MySQL tem funções de retorno de definição? Ou seja, funções que retornam valores diferentes em cada linha de uma consulta? Como exemplo do PostgreSQL, você pode fazer:

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

Isso produzirá um conjunto de resultados composto por 2 colunas e 3 linhas, onde a coluna esquerda contém 'Foo' em cada linha e a coluna direita contém 3, 4 e 5.

Então, assumindo que você tem um equivalente a generate_series() em mysql e subconsclaria: o que você precisa é um LEFT OUTER JOIN Desta função para a consulta que você já possui. Isso garantirá que você veja cada data aparecer na saída:

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;

Pode ser necessário mexer com isso um pouco para obter a sintaxe certa para o MySQL.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top