SQL - вернуть последнюю из нескольких записей из большого набора данных - PullRequest
4 голосов
/ 07 мая 2020

Фон

У меня есть таблица stock_price, в которой хранятся исторические дневные цены на акции примерно для 1000 акций. Хотя старые данные регулярно удаляются, в таблице регулярно содержится более 5 миллионов записей. Структура примерно такая:

| id     | stock_id | value | change |  created_at         |
|--------|----------|-------|--------|---------------------|
| 12345  | 1        | 50    | 2.12   | 2020-05-05 17:39:00 |
| 12346  | 2        | 25    | 1.23   | 2020-05-05 17:39:00 |

Мне регулярно нужно получать последние данные о ценах на ~ 20i sh акций во время для конечной точки API. Первоначальная реализация этого выполняла один запрос для каждой акции:

select * from stock_prices where stock_id = 1 order by created_at desc limit 1

Часть 1: Неэффективный запрос

Несколько неэффективно с более чем 20 запросами, но это работало. Код (Laravel 6) был обновлен, чтобы использовать правильные отношения ( stock hasMany stock_prices ), что, в свою очередь, сгенерировало такой запрос:

select
  *
from
  `stock_prices`
where
  `stock_prices`.`stock_id` in (1, 2, 3, 4, 5)
order by
  `id` desc

Хотя это позволяет сэкономить на запросах , запуск занимает 1-2 секунды. Запуск explain показывает, что ему по-прежнему приходится запрашивать более 50 тыс. Строк в любой момент времени, даже с индексом внешнего ключа. Следующей моей мыслью было добавить в запрос limit, чтобы возвращать только количество строк, равное количеству запрашиваемых акций. Запрос теперь:

select
  *
from
  `stock_prices`
where
  `stock_prices`.`stock_id` in (1, 2, 3, 4, 5)
order by
  `id` desc
limit
  5

Часть 2: Запрос иногда пропускает записи

Производительность потрясающая - обработка на уровне миллисекунд. Однако , он потенциально может не вернуть цену за одну или несколько акций. Так как limit был добавлен, если какая-либо акция имеет более одной цены (строки) перед следующей акцией, она «потребляет» один из счетчиков строк.

Это вполне реальный сценарий, поскольку некоторые акции получают данные каждую минуту, другие - каждые 15 минут и т. Д. c. Таким образом, бывают случаи, когда указанный выше запрос из-за limit будет извлекать несколько строк для одной акции и впоследствии не возвращать данные для других:

| id   | stock_id | value | change | created_at     |
|------|----------|-------|--------|----------------|
| 5000 | 1        | 50    | 0.5    | 5/5/2020 17:00 |
| 5001 | 1        | 51    | 1      | 5/5/2020 17:01 |
| 6001 | 2        | 25    | 2.2    | 5/5/2020 17:00 |
| 6002 | 3        | 35    | 3.2    | 5/5/2020 17:00 |
| 6003 | 4        | 10    | 1.3    | 5/5/2020 17:00 |

В этом сценарии вы можете увидеть, что stock_id of 1 имеет более частые интервалы данных, поэтому при запуске запроса он возвратил две записи для этого идентификатора, а затем продолжил движение вниз по списку. После того, как он достиг 5 записей, он остановился, что означает, что stock id из 5 не вернули никаких данных, хотя они существуют. Как вы понимаете, это разбивает строку в приложении, когда не было возвращено никаких данных.

Часть 3: Попытки решить

  1. Наиболее очевидный ответ кажется можно добавить GROUP BY stock_id как способ потребовать, чтобы я получил то же количество результатов, которое я ожидаю от акции. К сожалению, это возвращает меня к Части 1, в которой этот запрос, пока он работает, занимает 1-2 секунды, потому что в конечном итоге ему приходится проходить те же 50k + строк, что и раньше, без ограничения. Это не оставляет мне лучшего.

  2. Следующей мыслью было произвольно сделать LIMIT больше, чем нужно, чтобы он мог захватить все строки. Это непредсказуемое решение, поскольку запрос может представлять собой любую комбинацию тысяч акций, каждая из которых имеет разные интервалы доступных данных. Наиболее ярким примером являются акции, которые растягиваются ежедневно, а не каждую минуту, что означает, что до появления второй акции может быть где-то около 350+ строк. Умножьте это на количество акций в одном запросе - скажем, 50, и для этого все равно потребуется запросить более 15 тысяч строк. Возможно, но не идеально и потенциально не масштабируемо.

Часть 4: Предложения?

Неужели это такая плохая практика, когда один вызов API инициирует потенциально более 50 запросов к базе данных только для получения данных о ценах акций? Есть ли какое-то средство LIMIT, которое я должен использовать, чтобы минимизировать вероятность отказа, чтобы чувствовать себя комфортно? Существуют ли другие методы с SQL, которые позволили бы мне возвращать необходимые строки без запроса большого блока таблиц?

Любая помощь приветствуется.

Ответы [ 2 ]

1 голос
/ 07 мая 2020

Самый быстрый метод - union all:

(select * from stock_prices where stock_id = 1 order by created_at desc limit 1)
union all
(select * from stock_prices where stock_id = 2 order by created_at desc limit 1)
union all
(select * from stock_prices where stock_id = 3 order by created_at desc limit 1)
union all
(select * from stock_prices where stock_id = 4 order by created_at desc limit 1)
union all
(select * from stock_prices where stock_id = 5 order by created_at desc limit 1)

Это может использовать индекс для stock_prices(stock_id, created_at [desc]). К сожалению, когда вы используете in, индекс не может использоваться так же эффективно.

0 голосов
/ 24 мая 2020

Groupwise-max

SELECT b.*
    FROM ( SELECT stock_id, MAX(created_at) AS created_at
            FROM stock_proces
            GROUP BY stock_id
         ) AS a
    JOIN stock_prices AS b  USING(stock_id, created_at)

Требуется:

INDEX(stock_id, created_at)

Если у вас может быть две строки для одной и той же акции за одну секунду, это даст 2 строки. См. Альтернативные варианты по ссылке ниже.

Если эта пара уникальна, сделайте ее PRIMARY KEY и избавьтесь от id; это также улучшит производительность.

Подробнее: http://mysql.rjweb.org/doc.php/groupwise_max#using_an_uncorrelated_subquery

...