Как оптимизировать множественные левые соединения SQL SELECT-запроса? - PullRequest
1 голос
/ 12 апреля 2019

Положение:

У нас есть база данных «base1» ~ 6 миллионов строк данных, которая показывает фактические покупки покупателя и день покупки + параметры этой покупки.

CREATE TABLE base1 (
User_id NOT NULL PRIMARY KEY ,
PurchaseDate date,
Parameter1 int,
Parameter2 int,
...
ParameterK int );

А также другая база данных "base2" ~ 90 миллионов строк данных, которая фактически показывает то же самое, но вместо дня покупки используется еженедельный раздел (например: все недели в течение 4 лет для каждого клиента - если не было покупки в течение N недель, клиент по-прежнему отображается).

CREATE TABLE base2 (
Users_id NOT NULL PRIMARY KEY ,
Week_start date ,
Week_end date,
Parameter1 int,
Parameter2 int,
...
ParameterN int );

Задача выполнить следующий запрос:

-- a = base1 , b , wb%% = base2
--create index idx_uid_purch_date on base1(Users_ID,Purchasedate);
SELECT a.Users_id
-- Checking whether the client will make a purchase in next week and the purchase will be bought on condition
,iif(b.Users_id is not null,1,0) as User_will_buy_next_week
,iif(b.Users_id is not null and b.Parameter1 = 1,1,0) as User_will_buy_on_Condition1
--   about 12 similar iif-conditions
,iif(b.Users_id is not null and (b.Parameter1 = 1 and b.Parameter12 = 1),1,0) 
as User_will_buy_on_Condition13

-- checking on the fact of purchase in the past month, 2 months ago, 2.5 months, etc.
,iif(wb1m.Users_id is null,0,1) as was_buy_1_month_ago
,iif(wb2m.Users_id is null,0,1) as was_buy_2_month_ago
,iif(wb25m.Users_id is null,0,1) as was_buy_25_month_ago
,iif(wb3m.Users_id is null,0,1) as was_buy_3_month_ago
,iif(wb6m.Users_id is null,0,1) as was_buy_6_month_ago
,iif(wb1y.Users_id is null,0,1) as was_buy_1_year_ago

 ,a.[Week_start]
 ,a.[Week_end]

 into base3
 FROM base2 a 

 -- Join for User_will_buy
 left join base1 b
 on a.Users_id =b.Users_id and 
 cast(b.[PurchaseDate] as date)>=DATEADD(dd,7,cast(a.[Week_end] as date)) 
 and cast(b.[PurchaseDate] as date)<=DATEADD(dd,14,cast(a.[Week_end] as date))

 -- Joins for was_buy
 left join base1  wb1m
 on a.Users_id =wb1m.Users_id 
 and cast(wb1m.[PurchaseDate] as date)>=DATEADD(dd,-30-4,cast(a.[Week_end] as date)) 
 and cast(wb1m.[PurchaseDate] as date)<=DATEADD(dd,-30+4,cast(a.[Week_end] as date))

/* 4 more similar joins where different values are added in 
DATEADD (dd, %%, cast (a. [Week_end] as date))
to check on the fact of purchase for a certain period */

 left outer join base1 wb1y
 on a.Users_id =wb1y.Users_id and 
 cast(wb1y.[PurchaseDate] as date)>=DATEADD(dd,-365-4,cast(a.[Week_end] as date)) 
 and cast(wb1y.[PurchaseDate] as date)<=DATEADD(dd,-365+5,cast(a.[Week_end] as date))

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

Основное время, как показывает план выполнения, тратится на «Объединение слиянием» и просмотр строк таблицы из base1 и base2 и вставку данных в другую таблицу base3.

Вопрос: можно ли оптимизировать этот запрос, чтобы он работал быстрее?

Возможно, вместо этого использовать одно соединение или что-то еще.

Помогите пожалуйста, я не настолько умен: (

Спасибо всем за ваши ответы!

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

Ответы [ 3 ]

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

Если у вас есть повторяющийся аргумент, такой как, например, DATEADD(dd,-30-4,cast(a.[Week_end] as date)), чтобы сделать его SARGable, вы можете создать для него индекс (SQL Server не может). Postgres может сделать это:

create index ix_base2__34_days_ago on base2(DATEADD(dd,-30-4, cast([Week_end] as date)))

Тогда выражение, подобное следующему, будет SARGable, поскольку индекс для DATEADD(dd,-30-4, cast([Week_end])) будет использоваться вашей базой данных, поэтому условие, подобное следующему, будет быстрым, если у вас есть индекс, как в примере выше.

and cast(wb1m.[PurchaseDate] as date) >= DATEADD(dd,-30-4,cast(a.[Week_end] as date))

Обратите внимание, что приведение BuyDate к дате приводит к выражению SARGable, несмотря на то, что cast выглядит как функция, поскольку SQL Server имеет специальную обработку даты и времени, индекс по полю даты и времени является SARGable даже при частичном поиске по полю даты и времени ( только часть даты). Подобно частичному выражению like, where lastname LIKE 'Mc%', это выражение SARGable, даже если индекс предназначен для всего поля фамилии. Я отвлекся.

Чтобы немного достичь индекса выражения в SQL Server, вы можете создать вычисляемый столбец для этого выражения .., например,

CREATE TABLE base2 (
  Users_id NOT NULL PRIMARY KEY ,
  Week_start date ,
  Week_end date,
  Parameter1 int,
  Parameter2 int,
  Thirty4DaysAgo as DATEADD(dd,-30-4, cast([Week_end] as date))
)

.. и затем создайте индекс для этого столбца:

create index ix_base2_34_days_ago on base2(Thirty4DaysAgo)

Затем измените свое выражение на:

and cast(wb1m.[PurchaseDate] as date) >= a.Thirty4DaysAgo

Это то, что я бы порекомендовал раньше, измените старое выражение, чтобы использовать вычисляемый столбец. Однако при дальнейшем поиске выглядит, что вы можете просто сохранить исходный код, поскольку SQL Server может разумно сопоставить выражение с вычисляемым столбцом, и если у вас есть индекс для этого столбца, ваше выражение будет SARGable. Таким образом, ваш администратор базы данных может оптимизировать ситуацию за кулисами, и ваш исходный код будет работать оптимизированно, не требуя каких-либо изменений в вашем коде. Поэтому нет необходимости изменять следующее, и это будет SARGable (при условии, что ваш администратор баз данных создал вычисляемый столбец для выражения dateadd(recurring parameters here) и применил к нему индекс):

and cast(wb1m.[PurchaseDate] as date) >= DATEADD(dd,-30-4,cast(a.[Week_end] as date))

Единственным недостатком (по сравнению с Postgres) является то, что у вас все еще есть висячий вычисляемый столбец в таблице при использовании SQL Server:)

Хорошо читать: https://littlekendra.com/2016/03/01/sql-servers-year-function-and-index-performance/

0 голосов
/ 16 апреля 2019

Вы хотите, чтобы в вашем результате были все 90 миллионов строк base2, каждая с дополнительной информацией о данных base1.Итак, СУБД должна выполнить полное сканирование таблицы на base2 и быстро найти связанные строки в base1.

Запрос с предложениями EXISTS будет выглядеть примерно так:

select
  b2.users_id,
  b2.week_start,
  b2.week_end,
  case when exists
  (
    select *
    from base1 b1 
    where b1.users_id = b2.users_id
    and b1.purchasedate between dateadd(day, 7, cast(b2.week_end as date))
                            and dateadd(day, 14, cast(b2.week_end as date))´
  ) then 1 else 0 end as user_will_buy_next_week,
  case when exists
  (
    select *
    from base1 b1 
    where b1.users_id = b2.users_id
    and b1.parameter1 = 1
    and b1.purchasedate between dateadd(day, 7, cast(b2.week_end as date))
                            and dateadd(day, 14, cast(b2.week_end as date))´
  ) then 1 else 0 end as user_will_buy_on_condition1,
  case when exists
  (
    select *
    from base1 b1 
    where b1.users_id = b2.users_id
    and b1.parameter1 = 1
    and b1.parameter2 = 1
    and b1.purchasedate between dateadd(day, 7, cast(b2.week_end as date))
                            and dateadd(day, 14, cast(b2.week_end as date))´
  ) then 1 else 0 end as user_will_buy_on_condition13,
  case when exists
  (
    select *
    from base1 b1 
    where b1.users_id = b2.users_id
    and b1.purchasedate between dateadd(day, -30-4, cast(b2.week_end as date))
                            and dateadd(day, -30+4, cast(b2.week_end as date))´
  ) then 1 else 0 end as was_buy_1_month_ago,
  ...
from base2 b2;

Мы легко видим, что это займет много времени, потому что все условия должны быть проверены для каждой строки base2.Это 9 миллионов раз 7 поисков.Единственное, что мы можем сделать с этим, - это предоставить индекс, надеясь, что запрос получит от него выгоду.

create index idx1 on base1 (users_id, purchasedate, parameter1, parameter2);

Мы можем добавить больше индексов, чтобы СУБД могла выбирать между ними по селективности.Позже мы можем проверить, используются ли они, и удалить их, если они не используются.

create index idx2 on base1 (users_id, parameter1, purchasedate);
create index idx3 on base1 (users_id, parameter1, parameter2, purchasedate);
create index idx4 on base1 (users_id, parameter2, parameter1, purchasedate);
0 голосов
/ 13 апреля 2019

Я предполагаю, что в таблице base1 хранится информация о покупках за текущую неделю.

Если это так, в условиях запроса объединений можно игнорировать параметр [PurchaseDate], заменяя его вместо текущей постоянной даты. В этом случае ваши функции DATEADD будут применены к текущей дате и будут постоянными в условиях объединений:

left join base1 b
on a.Users_id =b.Users_id and 
DATEADD(day,-7,GETDATE())>=a.[Week_end] 
and DATEADD(day,-14,GETDATE())<=a.[Week_end]

Для корректной работы вышеуказанного запроса вы должны ограничить b.[PurchaseDate] текущим днем.

Затем вы могли бы выполнить другой запрос для вчерашних покупок и всех DATEADD констант в условиях соединения, исправленных на -1

И так далее, до 7 запросов или любой другой промежуток времени, который охватывает таблица base1.

Вы также можете реализовать группировку значений [PurchaseDate] по дням, пересчитать константы и сделать все это в одном запросе, но я не готов тратить время на его создание. :)

...