Rails: Как построить статистику за день / месяц / год или Как отсутствуют независимые от базы данных функции SQL (например: STRFTIME, DATE_FORMAT, DATE_TRUNC) - PullRequest
19 голосов
/ 27 октября 2010

Я искал по всей сети, и я понятия не имею.

  • Предположим, вам нужно создать панель управления в административной области вашего приложения на Rails и вы хотите иметь количество подписок в день .
  • Предположим, что вы используете SQLite3 для разработки , MySQL для производства (довольно стандартная настройка)

В основном, есть два варианта:

1) Извлечение всех строк из базы данных с использованием Subscriber.all и агрегирование по дням в приложении Rails с использованием Enumerable.group_by:

@subscribers = Subscriber.all
@subscriptions_per_day = @subscribers.group_by { |s| s.created_at.beginning_of_day }

Я думаю, что это действительно плохая идея. Извлечение всех строк из базы данных может быть приемлемым для небольшого приложения, но оно не будет масштабироваться вообще. Спасение базы данных и функции даты!

2) Запуск SQL-запроса в базе данных с использованием функций агрегирования и даты :

Subscriber.select('STRFTIME("%Y-%m-%d", created_at) AS day, COUNT(*) AS subscriptions').group('day')

Что будет выполняться в этом SQL-запросе:

SELECT STRFTIME("%Y-%m-%d", created_at) AS day, COUNT(*) AS subscriptions
FROM subscribers
GROUP BY day

Намного лучше. Теперь агрегаты создаются в базе данных, которая оптимизирована для такого рода задач, и только одна строка в день возвращается из базы данных в приложение Rails.

... но подождите ... теперь приложение должно появиться в моей рабочей среде, использующей MySQL! Заменить STRFTIME() на DATE_FORMAT(). Что если завтра я перейду на PostgreSQL? Заменить DATE_FORMAT() на DATE_TRUNC().

Мне нравится разрабатывать с SQLite. Просто и легко. Мне также нравится идея, что Rails не зависит от базы данных. Но почему Rails не предоставляет способ переводить функции SQL, которые делают одно и то же, но имеют разный синтаксис в каждой СУБД (это различие действительно глупо, но эй, слишком поздно жаловаться на это )?

Я не могу поверить, что я нахожу так мало ответов в Интернете для такой базовой функции приложения Rails: подсчитывать подписки за день, месяц или год.

Скажи мне, что я что-то упустил :) 1048 * * * РЕДАКТИРОВАТЬ тысячу сорок-девять

Прошло несколько лет с тех пор, как я отправил этот вопрос. Опыт показал, что мне следует использовать одну и ту же БД для dev и prod. Поэтому теперь я считаю, что требование к базе данных не имеет значения.

Соотношение Dev / Prod FTW.

Ответы [ 6 ]

7 голосов
/ 21 декабря 2010

Я закончил тем, что написал свой собственный драгоценный камень. Проверьте это и не стесняйтесь внести свой вклад: https://github.com/lakim/sql_funk

Позволяет совершать звонки, как:

Subscriber.count_by("created_at", :group_by => "day")
4 голосов
/ 07 марта 2011

Вы говорите о некоторых довольно сложных проблемах, которые, к сожалению, Rails полностью игнорирует.Документы ActiveRecord :: Calculations написаны так, как будто они все, что вам когда-либо нужно, но базы данных могут делать гораздо более сложные вещи.Как отметил Донал Феллоуз в своем комментарии, проблема гораздо сложнее, чем кажется.

За последние два года я разработал приложение Rails, которое интенсивно использует агрегацию, и я пробовал несколько разныхподходы к проблеме.К сожалению, я не могу позволить себе игнорировать такие вещи, как летнее время, потому что статистика - это «только тренды».Мои расчеты проверяются моими клиентами на предмет точных спецификаций.

Чтобы немного расширить проблему, я думаю, вы найдете, что ваше текущее решение по группировке по датам неадекватно.Это кажется естественным вариантом использования STRFTIME.Основная проблема заключается в том, что она не позволяет группировать по произвольным периодам времени.Если вы хотите выполнить агрегацию по году, месяцу, дню, часу и / или минуте, STRFTIME будет работать нормально.Если нет, вы найдете другое решение.Другая огромная проблема - проблема агрегации при агрегации.Скажем, например, вы хотите группировать по месяцам, но вы хотите делать это с 15 числа каждого месяца.Как бы вы сделали это с помощью STRFTIME?Вы должны были бы группировать по каждому дню, а затем по месяцу, но тогда кто-то учитывает начальное смещение 15-го числа каждого месяца.Последняя капля заключается в том, что для группировки по STRFTIME требуется группировка по строковому значению, которое вы найдете очень медленным при выполнении агрегации при агрегации.

Наиболее производительное и наилучшее решение, к которому я пришел, - это решение на основецелые периоды времени.Вот выдержка из одного из моих запросов mysql:

SELECT
  field1, field2, field3,
  CEIL((UNIX_TIMESTAMP(CONVERT_TZ(date, '+0:00', @@session.time_zone)) + :begin_offset) / :time_interval) AS time_period
FROM
  some_table
GROUP BY 
  time_period

В этом случае: time_interval - это количество секунд в периоде группировки (например, 86400 для ежедневного), а: begin_offset - это количество секунд досмещение периода начала.Бизнес CONVERT_TZ () учитывает то, как mysql интерпретирует даты.Mysql всегда предполагает, что поле даты находится в местном часовом поясе mysql.Но поскольку я храню время в UTC, я должен преобразовать его из UTC в часовой пояс сеанса, если я хочу, чтобы функция UNIX_TIMESTAMP () давала мне правильный ответ.Период времени заканчивается целым числом, которое описывает количество интервалов времени с начала времени Unix.Это решение гораздо более гибкое, поскольку позволяет группировать по произвольным периодам и не требует агрегирования при агрегировании.

Теперь, чтобы перейти к моей реальной точке.Для надежного решения я бы порекомендовал вам вообще не использовать Rails для генерации этих запросов.Самая большая проблема заключается в том, что характеристики производительности и тонкости агрегирования в разных базах данных различны.Вы можете найти один дизайн, который хорошо работает в вашей среде разработки, но не в производстве или наоборот.Вы перепрыгнете через много обручей, чтобы Rails хорошо работал с обеими базами данных при построении запросов.

Вместо этого я бы порекомендовал вам генерировать специфичные для базы данных представления в выбранной вами базе данных и переносить их вправильная среда.Попробуйте смоделировать представление так же, как любую другую таблицу ActiveRecord (идентификаторы и все), и, конечно, сделайте поля в представлении идентичными для всех баз данных.Поскольку эти статистические данные предназначены только для чтения, вы можете использовать модель для их поддержки и притворяться, будто они являются полноценными таблицами.Просто вызовите исключение, если кто-то попытается сохранить, создать, обновить или уничтожить.

Мало того, что вы получите упрощенное управление моделью, выполняя действия Rails, вы также обнаружите, что вы можете писать модульные тесты дляваши функции агрегирования такими способами, о которых вы не мечтали бы в чистом SQL.И если вы решите переключить базы данных, вам придется переписать эти представления, но ваши тесты покажут вам, где вы не правы, и сделают жизнь намного проще.

1 голос
/ 23 апреля 2013

Я только что выпустил гем, который позволяет вам сделать это легко с MySQL. http://ankane.github.io/groupdate/

Вы действительно должны попробовать запустить MySQL и в разработке. Ваша среда разработки и производства должна быть как можно ближе - меньше шансов на то, чтобы что-то поработало над разработкой и полностью остановило производство.

0 голосов
/ 07 июня 2012

Вот как я это делаю:

У меня есть класс Stat, который позволяет хранить необработанные события.(Код с первых нескольких недель, когда я начал программировать на Ruby, поэтому извините за это :-))

class Stat < ActiveRecord::Base
    belongs_to :statable, :polymorphic => true

    attr_accessible :statable_id, :statable_type, :statable_stattype_id, :source_url, :referral_url, :temp_user_guid

    # you can replace this with a cron job for better performance
    # the reason I have it here is because I care about real-time stats
    after_save :aggregate

    def aggregate
    aggregateinterval(1.hour)
    #aggregateinterval(10.minutes)
end

    # will aggregate an interval with the following properties:
    # take t = 1.hour as an example
    # it's 5:21 pm now, it will aggregate everything between 5 and 6
    # and put them in the interval with start time 5:00 pm and 6:00 pm for today's date
    # if you wish to create a cron job for this, you can specify the start time, and t
def aggregateinterval(t=1.hour)
    aggregated_stat = AggregatedStat.where('start_time = ? and end_time = ? and statable_id = ? and statable_type = ? and statable_stattype_id = ?', Time.now.utc.floor(t), Time.now.utc.floor(t) + t, self.statable_id, self.statable_type, self.statable_stattype_id)

    if (aggregated_stat.nil? || aggregated_stat.empty?)
        aggregated_stat = AggregatedStat.new
    else
        aggregated_stat = aggregated_stat.first
    end

            aggregated_stat.statable_id = self.statable_id
    aggregated_stat.statable_type = self.statable_type
    aggregated_stat.statable_stattype_id = self.statable_stattype_id
    aggregated_stat.start_time = Time.now.utc.floor(t)
    aggregated_stat.end_time = Time.now.utc.floor(t) + t
    # in minutes
    aggregated_stat.interval_size = t / 60

    if (!aggregated_stat.count)
        aggregated_stat.count = 0
    end
    aggregated_stat.count = aggregated_stat.count + 1


    aggregated_stat.save
end

end

А вот класс AggregatedStat:

class AggregatedStat < ActiveRecord::Base
    belongs_to :statable, :polymorphic => true

    attr_accessible :statable_id, :statable_type, :statable_stattype_id, :start_time, :end_time

Каждый элемент, который может быть изменендобавляется в БД имеет statable_type и statable_stattype_id и некоторые другие общие данные статистики.Statable_type и statable_stattype_id предназначены для полиморфных классов и могут содержать значения, такие как (строка) «Пользователь» и 1, что означает, что вы храните статистику о пользователе № 1.

Вы можете добавить больше столбцов и иметь сопоставителив коде извлекайте нужные столбцы, когда они вам нужны.Создание нескольких таблиц усложняет управление.

В приведенном выше коде StatableStattypes - это просто таблица, содержащая «события», которые вы хотите регистрировать ... Я использую таблицу, потому что предыдущий опыт научил меня, что яне хочу искать, к какому типу статистики относится число в базе данных.

class StatableStattype < ActiveRecord::Base
    attr_accessible :name, :description

    has_many :stats
end

Теперь перейдите к классам, для которых вы хотите получить некоторую статистику, и сделайте следующее:

class User < ActiveRecord::Base
  # first line isn't too useful except for testing
  has_many :stats, :as => :statable, :dependent => :destroy
  has_many :aggregated_stats, :as => :statable, :dependent => :destroy
end

Затем вы можете запросить агрегированную статистику для определенного пользователя (или местоположения в примере ниже) с помощью этого кода:

Location.first.aggregated_stats.where("start_time > ?", DateTime.now - 8.month)
0 голосов
/ 12 января 2011

Я бы немного уточнил / расширил ответ PBaumann и включил бы таблицу Даты в свою базу данных. Вам понадобится объединение в вашем запросе:

SELECT D.DateText AS Day, COUNT(*) AS Subscriptions
FROM subscribers AS S
  INNER JOIN Dates AS D ON S.created_at = D.Date
GROUP BY D.DateText

... но у вас будет хорошо отформатированное значение без вызова каких-либо функций. С PK на Dates.Date вы можете объединить объединение, и оно должно быть очень быстрым.

Если у вас международная аудитория, вы можете использовать DateTextUS, DateTextGB, DateTextGer и т. Д., Но, очевидно, это не будет идеальным решением.

Другой вариант: приведите дату к тексту на стороне базы данных, используя CONVERT (), которая является ANSI и может быть доступна в разных базах данных; Я слишком ленив, чтобы подтвердить это прямо сейчас.

0 голосов
/ 27 октября 2010

Если вы ищете дБ-агностицизм, я могу подумать о нескольких вариантах:

Создайте новое поле (назовем его day_str) для подписчика, в котором хранится форматированная дата или отметка времени, и используйте ActiveRecord.count:

daily_subscriber_counts = Subscriber.count(:group => "day_str")

Компромисс, конечно, немного больше рекордного размера, но это практически устранит проблемы с производительностью.

Вы также можете, в зависимости от степени детализации визуализируемых данных, просто вызвать .count несколько раз с желаемой датой ...

((Date.today - 7)..Date.today).each |d|
    daily_subscriber_counts[d] = Subscriber.count(:conditions => ["created_at >= ? AND created_at < ?", d.to_time, (d+1).to_time)
end

Это также можно настроить для учета различной степени детализации (в месяц, в год, в день, в час). Это не самое эффективное решение в том случае, если вы хотите группировать по дням всех своих подписчиков (у вас тоже не было возможности запустить его), но я думаю, вы захотите группировать по месяцам, дням и часам если вы просматриваете данные за годы, за месяцы или дни соответственно.

Если вы хотите перейти на mysql и sqlite, вы можете использовать ...

daily_subscriber_counts = Subscriber.count(:group => "date(created_at)")

... поскольку они имеют похожие функции date ().

...