Как избежать подобной проблемы выбора N + 1 в этом сценарии? - PullRequest
2 голосов
/ 14 сентября 2011

Сценарий: мне нужно отобразить среднее из последних 20 зарегистрированных значений. Мне нужно сделать это для всех пользователей. Использую Sql Server 2005 Express. Это самая низкая версия сервера БД, которую я должен поддерживать.

Способ, которым я делаю это сейчас: 1 запрос для выборки всех пользователей. 1 запрос на пользователя, чтобы получить последние 20 зарегистрированных значений. Хотя я не могу на самом деле сделать среднее значение в sql по деловым причинам, давайте пока предположим, что я могу.

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

Есть ли способ избежать N + 1 запросов?

Edit1:

Ответ Эрика завершает работу. Однако я подожду некоторое время, прежде чем пометить его как ответ по двум причинам.

  1. Я хотел бы знать, есть ли какие-либо потери производительности для этого метода. Таблица отчетов будет содержать десятки тысяч строк на пользователя. Хотя мне нужно только усреднить последние 20.
  2. У меня возникает соблазн изменить вопрос (т.е. снять предположение) и отразить требования моего бизнеса. Я надеюсь, что, может быть, даже это можно решить только на SQL.

Тот же вопрос, но с удаленным предположением:

Среднее значение должно быть получено по 20 самым последним непрерывным отчетам. То есть, предположим, что последние 20 строк (в порядке убывания) содержат 15 строк (от 20 до 6) для времен с 14:25 до 14:40. И строки с 5 по 1 содержат время с 14:43 до 14:48 ... Самый последний непрерывный набор данных - это строки с 5 по 1. Таким образом, среднее значение должно быть сделано только для этих 5 строк . Не похоже, что данные будут поступать партиями, поэтому числа 15 и 5 с такой же легкостью могли бы быть 10 и 10 или 3, 5 и 12 или даже все 20 непрерывных (для простоты я предположил, что последние 20 все быть непрерывным).

Что вы, ребята, думаете? Это может быть сделано в SQL или это лучше всего обрабатывается в C #?

Редактировать 2: Я думал об этом. В c # я бы начал с самой последней даты. Вычтите 1 минуту. И проверьте, соответствует ли следующая самая последняя дата этому значению. Если это так, добавьте его в список. Глядя на эти шаги, я не могу представить, как можно будет воспроизвести что-то подобное в SQL. На самом деле я до сих пор не уверен, каким будет ответ c # эквивалента ответа Эрика. Что заставляет меня задуматься, как можно думать в SQL?

Ответы [ 4 ]

4 голосов
/ 14 сентября 2011

Надеюсь, я правильно понял. Я предполагаю очень простую настройку таблицы:

CREATE TABLE Reports
(
    UserId INT,
    Report INT,
    CreatedOn DATETIME  
)

CREATE TABLE Users
(
    UserId INT
)


SELECT  x.UserId, AVG(x.Report) as Report_Avg
FROM
        (
        SELECT  R.Report, U.UserId, ROW_NUMBER() OVER (PARTITION BY U.UserId ORDER BY R.CreatedOn DESC) as RowNum
        FROM    Reports R
                INNER JOIN Users U
                ON R.UserId = U.UserId
        ) x
WHERE   x.RowNum <= 20
GROUP BY x.UserId

Мой код использует синтаксис PARTITION BY и ROW_NUMBER, который должен быть частью ANSI SQL.

2 голосов
/ 14 сентября 2011

Исходя из ваших изменений, вы можете попробовать что-то вроде этого ...

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

WITH
  mostRecentData AS
(
  SELECT
    userID,
    MAX(TimeStamp) AS TimeStamp
  FROM
    yourData
  GROUP BY
    userID
)
,
  ordered_data AS
(
  SELECT
    [reportData].*,
    DATEDIFF(minute, [reportData].TimeStamp, [mostRecentData].TimeStamp) AS offset,
    ROW_NUMBER() OVER (PARTITION BY [reportData].UserID ORDER BY [reportData].TimeStamp DESC) AS sequenceID
  FROM
    yourData                AS [reportData]
  INNER JOIN
    [mostRecentData]
      ON [reportData].userID = [reportData].UserID
)

SELECT
  UserID,
  AVG(someField)
FROM
  orderedData
WHERE
  sequenceID <= 20             -- At most the 20 most recent values
  AND sequenceID - offset = 1  -- Only Consecutive entries from the latest entry
GROUP BY
  UserID

При условии, что у вас есть соответствующие индексы, sequenceID <= 20 быстро разрешится, гарантируя, что вам не придется анализировать каждую запись для каждого пользователя.

Однако sequenceID - offset не будет использовать индексы и будет обрабатываться для каждой из этих 20 записей. Но это не большие накладные расходы на самом деле.

Пример Данные, показывающие, что sequenceID - offset = 1 действительно получает самый последний последовательный набор данных ...

TimeStamp  |  Row_Number()  |  Offset  |  Row_Number() - Offset

  12:24            1             0                1
  12:23            2             1                1
  12:22            3             2                1
  12:20            4             4                0
  12:19            5             5                0
  12:17            6             7               -1
0 голосов
/ 14 сентября 2011

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

Далее вы можете группировать по имени пользователя и использовать функцию sum () для агрегирования для каждого пользователя.,Это экономит вам N-1 запросов и устраняет первый, что означает: 1 запрос.

Пример:

select username, sum(value), count(value) as numvals from table where date > [calculated earliest date/time] group by username

С учетом количества вы можете сделать две вещи.

  1. Если вам нужно только «достаточно близкое» значение, вы можете просто разделить сумму (значение) на количество (значение) и получить среднее значение.
  2. Если вы можете позволить себе паруитерации, вы можете добавить предложение «Имея numvals = 20» и изменять дату, пока не получите всех пользователей
    • Это немного более размыто, чем подход с ограничениями, но избегает сортировки.Это имеет смысл, если у вас есть хорошее представление о том, какую дату фильтровать, чтобы получить только 20 значений.
    • Если бы мне пришлось выбирать между сортировкой и вычислением среднего значения для сохранения памяти, циклов ввода-вывода и процессора, я бы выбирал среднее значение каждый раз и дважды в воскресенье.

Кроме того, вы можете удалить два агрегата и группу по выражению, отсортировать сначала по имени пользователя, а затем по дате и просто выбрать имя пользователя и значение.Затем выполните подсчет (за последние 20) фильтрацию за пределами БД, когда вы выполняете средние вычисления.

select username, value from table order by username, date

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

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

0 голосов
/ 14 сентября 2011

возможно плохая идея, но, может быть, это поставит вас на правильный путь?

select id
from users u
left outer join 
  (
    select value
    from reported_values
    where user_id in (1,2,3)
    order by created_at desc limit 20
  ) as v
  on u.id = v.user_id
where id in (1,2,3)
...