Шаблон проектирования для оптимизации производительности запросов динамических сортировок в БД SQL - PullRequest
0 голосов
/ 11 февраля 2019

У меня есть приложение с довольно большим активным набором данных (скажем, автомобилей) с около 2 миллионами активных строк данных.Каждый "автомобиль" имеет множество атрибутов (столбцов), таких как цена, пробег, год, марка, модель, тип топлива и т. Д. И т. Д.

Теперь на странице / show для каждого автомобиля в моем веб-приложении, которое мне нужносоставить список топ-10 самых «похожих» автомобилей.Поскольку я никогда не «знаю», является ли автомобиль очень распространенным или очень редким видом автомобиля (перед тем, как фактически делать запрос БД), я разработал шаблон, в котором я почти не выполняю фильтрацию (WHERE -классы) в «похожихавтомобили "-query.Вместо этого я делаю много ORDER BY -предложений в сочетании с CASE WHEN -проверками, основанными на текущем автомобиле в данных представления.Допустим, пользователь смотрит на Ford Focus, 2010, 30.000km, Gasoline, 12490EUR from around Düsseldorf машину.Тогда я бы сделал что-то вроде:

SELECT "cars".*
  FROM de."cars" 
  WHERE ("cars"."id" != 24352543) 
    AND "cars"."sales_state" = 'onsale' 
    AND (cars.is_disabled IS NOT TRUE) 
    ORDER BY
      CASE WHEN ABS(cars.price - 12490) < cars.price * 0.2 THEN 1 WHEN ABS(cars.price - 12490) < cars.price * 0.4 THEN 2 WHEN ABS(cars.price - 12490) < cars.price * 0.6 THEN 3 ELSE 4 END, 
      CASE WHEN fuel_type = 'Gasoline' THEN 0 ELSE 1 END, 
      ABS(cars.price - 12490), 
      CASE WHEN ST_Distance( ST_GeographyFromText( 'SRID=4326;POINT(' || cars.longitude || ' ' || cars.latitude || ')' ), ST_GeographyFromText('SRID=4326;POINT(12.172130 48.162990)') ) <= 30000 THEN 1 WHEN ST_Distance( ST_GeographyFromText( 'SRID=4326;POINT(' || cars.longitude || ' ' || cars.latitude || ')' ), ST_GeographyFromText('SRID=4326;POINT(12.172130 48.162990)') ) <= 100000 THEN 2 ELSE 3 END, 
      ABS(cars.year - 2010), 
      ABS(cars.km - 30000)
    LIMIT 10

На самом деле есть еще больше пунктов заказа.

Теперь это удобно, потому что неважно, насколько «легко» найти 10 «релевантных»автомобили, похожие на нынешнюю машину, запрос всегда будет возвращать что-то - проблема в том, что он медленный и практически невозможно проиндексировать, насколько мне известно.Делая это на 2 миллионах записей, даже если у меня есть суперскоростный суперскоростной выделенный PostgreSQL 11, 300 ГБ оперативной памяти, 10 SSD RAID 10 32 основных сервера, это все равно займет у меня около 2-4 секунд, времени у меня нет.Мне нужно это до <200 мс. </p>

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

  • Выполнение запроса итеративным способом, где я фильтрую (WHERE) по некоторому столбцу в терминах (например, начиная с ограничения данных на подмножестве цен), чтобы уменьшить набор данных.Затем, если результаты будут возвращены, отлично, в противном случае сделайте еще один немного более широкий запрос и т. Д. И т. Д.
  • Используя совершенно другой тип алгоритма, возможно, предварительно заполняя какие-то столбцы показателей сходства для автомобилей
  • Использование некоторых внутренних возможностей / расширений PostgreSQL, которые могут ускорить процесс, какими бы они ни были?

Ответы [ 2 ]

0 голосов
/ 15 февраля 2019

Для этой возможной сложности и отклонения sql (много разных шаблонов) и для указанных вами временных интервалов (250 мс) я должен заставить sql следовать «плану» как можно проще и эффективнее, разбивая фильтры по одному за раз.

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

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

More details-Example: Сначала вы сосредоточитесь на предмете, который вы ищете, который является car.id, я полагаю.Поэтому вам нужен набор значений Car.id для случайных фильтров.Допустим, у вас есть 20 возможных фильтров.Каждый фильтр приводит к набору значений car.id.Некоторые фильтры могут работать непосредственно за столом, где находится car.id.Некоторым другим могут понадобиться объединения в 1-2, возможно, в 3 таблицы.Однако для всех фильтров вместе может потребоваться 10-15 объединений.Наименьшее количество таблиц, соединенных, дает больше шансов получить хороший план.

Предположим, у вас есть 3 фильтра, фильтры 2, 7 и 14. Например, объединение 12 таблиц и фильтра с этими тремя фильтрами может быть или не быть эффективным.И если это так, другой комбинации не будет.Итак, я предлагаю (псевдокод):

procedure/table function get carids as
for each optional filter 1 to 20
 if filter is set
  select car.id from car (possible joins) where filter=filter.value and car.id 
  in (previous car.id found)
  if count(car.id)=0 end and return no results
 end if
end for
return car.id collected

при желании вы можете указать порядок обработки фильтров.если вы знаете, что из набора из 5-6 фильтров по крайней мере один из них используется при 99% поисков, тогда сортировка их в первую очередь приведет к сужению значений car.id до диапазона от 0 до нескольких первых 5, выбирает max

0 голосов
/ 11 февраля 2019

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

ORDER BYпредложение не индексируется как есть.

Если вы немного более гибки в своем запросе, возможно, вы могли бы попробовать что-то вроде этого:

Первый запрос:

WITH priced_cars AS (
  SELECT SELECT cars.*
    FROM de.cars
    WHERE (cars.id != 24352543)
      AND cars.sales_state = 'onsale'
      AND (cars.is_disabled IS NOT TRUE)
      AND cars.price BETWEEN 12490*5/6 AND 12490*5/4
)
SELECT * FROM priced_cars
ORDER BY
  CASE WHEN fuel_type = 'Gasoline' THEN 0 ELSE 1 END, 
  ABS(price - 12490), 
  CASE
    WHEN ST_Distance( ST_GeographyFromText( 'SRID=4326;POINT(' || longitude || ' ' || latitude || ')' ), ST_GeographyFromText('SRID=4326;POINT(12.172130 48.162990)') ) <= 30000
    THEN 1
    WHEN ST_Distance( ST_GeographyFromText( 'SRID=4326;POINT(' || longitude || ' ' || latitude || ')' ), ST_GeographyFromText('SRID=4326;POINT(12.172130 48.162990)') ) <= 100000
    THEN 2
    ELSE 3
  END, 
  ABS(year - 2010), 
  ABS(km - 30000)
LIMIT 10;

Этот запрос может использовать индекс, подобный следующему:

CREATE INDEX ON de.cars (price)
  WHERE sales_state = 'onsale' AND is_disabled IS NOT TRUE;

Это будет соответствовать только автомобилям, в которых ваш первый столбец ORDER BY будет равен 1, но он может быть быстрым, поскольку он может использовать индекс.

Если вы нашли 10 автомобилей таким образом, все готово.

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

Продолжайте так до тех пор, пока у вас не будет 10 машин (последний запрос не будет иметь условия на price и будет таким же медленным, как и раньше).

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

...