Оптимизация сложных SQL-запросов

#sql #ruby-on-rails #postgresql

#sql #ruby-on-rails #postgresql

Вопрос:

Я пытаюсь оптимизировать SQL-запрос. Вы можете мне помочь?

По сути, у каждого пользователя есть друзья через таблицу friendship, и у каждого пользователя есть много feed_events через таблицу user_feed_events. Я пытаюсь перечислить feed_events друзей данного пользователя. Это не должно быть невозможно, верно? 🙂

Как вы можете видеть, производительность запроса зависит от того, сколько друзей у пользователя. Прямо сейчас выполнение пользователя со 150 друзьями занимает почти 7 секунд.

ОБНОВЛЕНИЕ: вот как построена моя таблица дружбы:

 create_table "friendships", :force => true do |t|
t.integer  "user_id",     :null => false
t.integer  "friend_id",   :null => false
t.datetime "created_at"
t.datetime "accepted_at"
end

add_index "friendships", ["friend_id"], :name => "index_friendships_on_friend_id"
add_index "friendships", ["user_id"], :name => "index_friendships_on_user_id"
  

Сначала я прошу rails предоставить мне список идентификаторов userids друзей пользователя, затем я использую эту строку в реальном запросе.

 friends_id = current_user.friends.collect {|f| f.id}.join(",")

sql = "
SELECT 
DISTINCT feed_events.id, 
feed_events.event_type, 
feed_events.type_id, 
feed_events.data, 
feed_events.created_at, 
feed_events.updated_at, 
user_feed_events.user_id  
FROM feed_events 
LEFT JOIN user_feed_events 
ON feed_events.id = user_feed_events.feed_event_id 
WHERE user_feed_events.user_id IN (#{friends_id}) 
ORDER BY feed_events.created_at DESC"
  

Затем я фактически выполняю запрос (разбивая его на страницы и ограничивая 30 результатами):

 @events = FeedEvent.paginate_by_sql(sql, :page => params[:page], :per_page => 30)
  

ОБНОВЛЕНИЕ # 2: ВОТ РЕЗУЛЬТАТ EXPLAIN ANALYZE:

     SQL> EXPLAIN ANALYZE (SELECT  DISTINCT feed_events.id,  feed_events.event_type,  feed_events.type_id,  feed_events.data,  feed_events.created_at,  feed_events.updated_at,  user_feed_events.user_id   FROM user_feed_events  INNER JOIN feed_events  ON feed_events.id = user_feed_events.feed_event_id  WHERE user_feed_events.user_id IN (1,7,9,8,14,15,20,35,40,39,41,42,57,84,98,109,121,74,129,64,137,77,172,182,206,201,284,31,94,232,311,168,30,114,50,174,419,403,438,464,423,513,351,349,385,622,751,359,809,838,844,962,831,786,896,1001,992,998,990,256,67,623,957,1226,1060,1009,1490,132,1467,1672,619,1459,1466,993,1599,1365,607,1381,1714,1154,2032,2230,2240,2354,598,2345,1804,634,1900,2652,1975,2164,1759,3288,1004,3487,3507,3542,3566,514,3787,3137,3803,3090,4012,855,17,2026,1463,335,1000,935,5,12,10,13,19,18,16,22,34,27,29,59,126,90,46,23,63,291,134,229,107,439,521)  ORDER BY feed_events.created_at DESC)
     ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 
    |                                                                                                                                                                                                                                                                                                          QUERY PLAN                                                                                                                                                                                                                                                                                                          |
     ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 
    | Unique  (cost=6090.87..6162.93 rows=18014 width=389) (actual time=1641.210..1733.010 rows=29691 loops=1)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
    |   ->  Sort  (cost=6090.87..6099.88 rows=18014 width=389) (actual time=1641.206..1670.882 rows=29694 loops=1)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
    |         Sort Key: feed_events.created_at, feed_events.id, feed_events.event_type, feed_events.type_id, feed_events.data, feed_events.updated_at, user_feed_events.user_id                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
    |         Sort Method:  quicksort  Memory: 17755kB                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
    |         ->  Hash Join  (cost=3931.63..5836.21 rows=18014 width=389) (actual time=258.541..361.345 rows=29694 loops=1)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
    |               Hash Cond: (user_feed_events.feed_event_id = feed_events.id)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
    |               ->  Bitmap Heap Scan on user_feed_events  (cost=926.64..2745.66 rows=18014 width=8) (actual time=6.930..42.367 rows=29694 loops=1)                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
    |                     Recheck Cond: (user_id = ANY ('{1,7,9,8,14,15,20,35,40,39,41,42,57,84,98,109,121,74,129,64,137,77,172,182,206,201,284,31,94,232,311,168,30,114,50,174,419,403,438,464,423,513,351,349,385,622,751,359,809,838,844,962,831,786,896,1001,992,998,990,256,67,623,957,1226,1060,1009,1490,132,1467,1672,619,1459,1466,993,1599,1365,607,1381,1714,1154,2032,2230,2240,2354,598,2345,1804,634,1900,2652,1975,2164,1759,3288,1004,3487,3507,3542,3566,514,3787,3137,3803,3090,4012,855,17,2026,1463,335,1000,935,5,12,10,13,19,18,16,22,34,27,29,59,126,90,46,23,63,291,134,229,107,439,521}'::integer[]))     |
    |                     ->  Bitmap Index Scan on index_user_feed_events_on_user_id  (cost=0.00..925.74 rows=18014 width=0) (actual time=6.836..6.836 rows=29694 loops=1)                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
    |                           Index Cond: (user_id = ANY ('{1,7,9,8,14,15,20,35,40,39,41,42,57,84,98,109,121,74,129,64,137,77,172,182,206,201,284,31,94,232,311,168,30,114,50,174,419,403,438,464,423,513,351,349,385,622,751,359,809,838,844,962,831,786,896,1001,992,998,990,256,67,623,957,1226,1060,1009,1490,132,1467,1672,619,1459,1466,993,1599,1365,607,1381,1714,1154,2032,2230,2240,2354,598,2345,1804,634,1900,2652,1975,2164,1759,3288,1004,3487,3507,3542,3566,514,3787,3137,3803,3090,4012,855,17,2026,1463,335,1000,935,5,12,10,13,19,18,16,22,34,27,29,59,126,90,46,23,63,291,134,229,107,439,521}'::integer[])) |
    |               ->  Hash  (cost=2848.84..2848.84 rows=44614 width=385) (actual time=251.490..251.490 rows=44663 loops=1)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
    |                     ->  Seq Scan on feed_events  (cost=0.00..2848.84 rows=44614 width=385) (actual time=0.035..77.044 rows=44663 loops=1)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
    | Total runtime: 1780.200 ms                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
     ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 
    SQL>
  

ОБНОВЛЕНИЕ # 3: Проблема в том, что для моего приложения rails я использую плагин has_many_friends (https://github.com/swemoney/has_many_friends ), это забота о моих дружеских отношениях. Это работает следующим образом. Я user_id # 6 и прошу дружбы с user_id # 10. Когда пользователь # 10 принимает мою дружбу, в таблицу добавляется новая строка с идентификаторами user_id = 6 и friend_id = 10. Если пользователь № 10 попросит меня о дружбе, строка будет такой: user_id = 10 и friend_id = 6.

Это означает, что для того, чтобы найти friends_by_me, мне нужно выполнить поиск по «user_id = 6», для того, чтобы найти friends_for_me, мне нужно «friend_id = 6». Чтобы найти всех моих друзей, мне нужно выполнить поиск в обоих столбцах. Это очень усложняет создание объединений! Как бы вы с этим справились?

Единственная альтернатива, о которой я могу думать, это:

 "(SELECT 
DISTINCT feed_events.id, 
feed_events.event_type, 
feed_events.type_id, 
feed_events.data, 
feed_events.created_at, 
feed_events.updated_at, 
user_feed_events.user_id 
FROM feed_events 
INNER JOIN user_feed_events 
ON feed_events.id = user_feed_events.feed_event_id 
INNER JOIN friendships 
ON user_feed_events.user_id = friendships.user_id 
WHERE friendships.user_id = 6 
AND friendships.accepted_at IS NOT NULL)

UNION DISTINCT

(SELECT 
DISTINCT additional_feed_events.id, 
additional_feed_events.event_type, 
additional_feed_events.type_id, 
additional_feed_events.data, 
additional_feed_events.created_at, 
additional_feed_events.updated_at, 
user_feed_events.user_id 
FROM feed_events AS additional_feed_events 
INNER JOIN user_feed_events 
ON additional_feed_events.id = user_feed_events.feed_event_id 
INNER JOIN friendships 
ON user_feed_events.user_id = friendships.friend_id 
WHERE friendships.friend_id = 6 
AND friendships.accepted_at IS NOT NULL) 

ORDER BY feed_events.created_at DESC"
  

Но на данный момент это не работает, и я также не уверен, что это правильный способ сделать это!

Спасибо, Аугусто

Комментарии:

1. Пожалуйста, отформатируйте свои SQL-инструкции так, чтобы их можно было читать без прокрутки.

2. Хорошо, я отформатировал его. Теперь должно быть лучше 🙂

3. Можете ли вы показать определения таблиц? Есть ли у вас какие-либо индексы?

4. Не могли бы вы показать, как выглядит таблица дружбы? Как сопоставляются друзья — friend_id и?

5. Я обновил свой вопрос подробностями определения таблицы дружбы.

Ответ №1:

Почему вы используете список IN? Почему бы вам не начать с выбранного пользователя? Кроме того, я думаю, что ваше левое внешнее соединение не требуется:

 SELECT 
DISTINCT feed_events.id, 
feed_events.event_type, 
feed_events.type_id, 
feed_events.data, 
feed_events.created_at, 
feed_events.updated_at, 
user_feed_events.user_id  
FROM 
(
  select friend_id from friendship where user_id = YOURUSER
  UNION
  select user_id as friend_id from friendship where friend_id = YOURUSER
) friendship
inner join user_feed_events 
on friendship.friend_id = user_feed_events.user_id
inner join feed_events
on user_feed_events.feed_event_id = feed_events.id
ORDER BY feed_events.created_at DESC
  

Если вы хотите остаться с вашим исходным заявлением и просто оптимизировать его, тогда используйте это:

 SELECT 
DISTINCT feed_events.id, 
feed_events.event_type, 
feed_events.type_id, 
feed_events.data, 
feed_events.created_at, 
feed_events.updated_at, 
user_feed_events.user_id  
FROM user_feed_events 
INNER JOIN feed_events 
ON feed_events.id = user_feed_events.feed_event_id 
WHERE user_feed_events.user_id IN (#{friends_id}) 
ORDER BY feed_events.created_at DESC
  

Это удаляет ненужное ЛЕВОЕ соединение.

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

Комментарии:

1. Спасибо, Дэниел, как я объясняю в обновленном вопросе, проблема заключается в том, как построена таблица дружбы. Чтобы найти всех моих друзей, мне нужно искать столбцы user_id и friend_id! Как я могу справиться с этим внутри объединения?

2. @Augusto: Вы можете использовать вторую инструкцию в качестве прямой замены вашей инструкции. Я обновлю первое утверждение, чтобы отразить ваше требование о таблице дружбы.

3. @Daniel, большое тебе спасибо. Сейчас я использую ваше отредактированное первое предложение, и оно кажется более элегантным. Тем не менее, обработка запроса занимает 2 секунды : ( Теперь я проверяю, правильно ли проиндексированы все внешние ключи.

4. @Augusto: Пожалуйста, попробуйте также второй запрос.

5. Я тоже пробовал это, это работает, но, похоже, для обработки требуется то же время, что и для первого : (

Ответ №2:

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

  SELECT feed_events.id, 
        feed_events.event_type, 
        feed_events.type_id, 
        feed_events.data, 
        feed_events.created_at, 
        feed_events.updated_at, 
        user_feed_events.user_id  

   FROM feed_events
            INNER JOIN
        user_feed_events ON feed_events.id = user_feed_events.feed_event_id
            INNER JOIN
        user_friends     ON user_friends.friend_id = user_feed_events.user_id

  WHERE user_friends.user_id = ** The Id of the User in Question **
  ORDER BY feed_events.created_at DESC
  

Далее вам нужно убедиться, что ваши столбцы Id являются первичными ключами и что в таблице user_friends есть уникальные индексы для таких вещей, как (friend_id, user_id). Кстати, я только что придумал эти названия, я пытался угадать, как вы называете стол, в котором храните дружеские отношения.

Ответ №3:

 select distinct fe.id, fe.event_type,
       fe.type_id, fe.data, fe.created_at,
       fe.updated_at, ufe.user_id
from friendships as f
    inner join user_feed_events as ufe on f.friend_id = ufe.user_id
    inner join feed_events as fe on ufe.user_id = fe.id
where f.user_id = 6 and f.accepted_at is not null
order by fe.created_at desc
  

Не уверен, действительно ли distinct здесь нужен. Запрос возвращает события ленты для друзей указанного пользователя .. я надеюсь, что так и должно быть 😉

Редактировать. Оказывается, что решение практически такое же, как предложил Дэниел Хилгарт.

Ответ №4:

Используйте подраздел SELECT в WHERE предложении, чтобы создать список событий канала для IN() вызова. Что-то (непроверенное) вроде этого:

 SELECT fe.id, 
    fe.event_type, 
    fe.type_id, 
    fe.data, 
    fe.created_at, 
    fe.updated_at,
    ufe.user_id  
FROM feed_events AS fe, user_feed_events AS ufe
WHERE TRUE = TRUE
    AND fe.id = ufe.feed_event_id
    AND ufe.user_id = :user_id
    AND fe.id IN((
        SELECT ufe.feed_event_id
        FROM user_feed_events AS ufe, user_friends AS uf
        WHERE uf.friend_id = :user_id
    ))
ORDER BY feed_events.created_at DESC;
  

Мне было бы любопытно посмотреть, как EXPLAIN ANALYZE выглядит это.