Подтвердить что ты не робот

Rails: проблема производительности с объединением записей

У меня есть следующая настройка с ActiveRecord и MySQL:

Пользователь имеет много групп через членство
Группа имеет много пользователей через членство

Существует также индекс group_id и user_id, описанный в schema.rb:

add_index "memberships", ["group_id", "user_id"], name: "uugj_index", using: :btree

3 разных запроса:

User.where(id: Membership.uniq.pluck(:user_id))`

(3,8 мс) SELECT DISTINCT memberships. user_id FROM membershipsПользовательская нагрузка (11.0ms) SELECT users. * FROM users WHERE users. id IN (1, 2...)

User.where(id: Membership.uniq.select(:user_id))

Загрузка пользователя (15.2мс) SELECT users. * FROM users WHERE users. id IN (SELECT DISTINCT memberships. user_id FROM memberships)

User.uniq.joins(:memberships)

Пользовательская нагрузка (135.1ms) SELECT DISTINCT users. * FROM users INNER JOIN memberships ON memberships. user_id= users. id

Каков наилучший подход для этого? Почему запрос с соединением намного медленнее?

4b9b3361

Ответ 1

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

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

add_index :memberships, :user_id

Индекс, который у вас уже есть, будет полезен только в том случае, если вы хотите найти всех пользователей, принадлежащих к определенной группе.

Update:

Если у вас много столбцов и данных в таблице users, DISTINCT users.* в третьем запросе будет довольно медленным, потому что MySQL должен сравнивать большое количество данных, чтобы обеспечить уникальность.

Чтобы быть ясным: это не внутренняя медлительность с JOIN, это медлительность с DISTINCT. Например: Вот способ избежать DISTINCT и по-прежнему использовать JOIN:

SELECT users.* FROM users
INNER JOIN (SELECT DISTINCT memberships.user_id FROM memberships) AS user_ids
ON user_ids.user_id = users.id;

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

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

Также обратите внимание, что рекомендуемый мной индекс специально разработан для трех запросов, которые вы указали в своем вопросе. Если у вас есть другие типы запросов к этим таблицам, вам может быть лучше обслуживать дополнительные индексы или, возможно, индексы с несколькими столбцами, как @tata, упомянутые в его/ее ответе.

Ответ 2

Ниже приведено более эффективное решение:

User.exists?(id: Membership.uniq.pluck(:user_id))

join будет извлекать все столбцы из таблицы членства, поэтому в других запросах потребуется больше времени. Здесь вы выбираете rhe user_id из memberships. Вызов distinct из users замедлит запрос.

Ответ 3

Я думаю, что у вас есть проблема с объявлением ваших индексов.

вы указали индекс как:

add_index "memberships", ["group_id", "user_id"], name: "uugj_index", using: :btree Если ваш первичный ключ был [ "user_id", "group_id" ] - вам было хорошо идти, но....

Сделать это в рельсах не так тривиально.

Поэтому для того, чтобы запросить данные с помощью JOIN с помощью таблицы Users - вам нужно иметь 2 индекса:

add_index "memberships", ["user_id", "group_id" ]

Это связано с тем, как MySQL обрабатывает индексы (они рассматриваются как конкатенированные строки)

Подробнее об этом можно прочитать здесь Индексы нескольких столбцов

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

Кроме того - я не думаю, что вам нужен .uniq здесь, так как результат должен быть уникальным в любом случае из-за условий на таблице. Добавление .uniq может заставить MySQL выполнять ненужную сортировку с файловым телефоном, и обычно он также помещает временную таблицу на диск.

Вы можете запустить команду, созданную рельсами непосредственно на mysql, чтобы проверить ее с помощью EXPLAIN

EXPLAIN <your command goes here>

Ответ 4

Запрос с соединением медленный, потому что он загружает все столбцы из базы данных, несмотря на то, что рельсы не предварительно загружают их таким образом. Если вам нужна предварительная загрузка, вы должны использовать includes (или подобное). Но включение будет еще медленнее, потому что оно построит объекты для всех ассоциаций. Также вы должны знать, что User.where.not(id: Membership.uniq.select(:user_id)) возвращает пустое значение в случае, когда существует хотя бы одно членство с user_id, равным nil, в то время как запрос с pluck вернет правильное отношение.

Ответ 5

@bublik42 и @user3409950, если мне нужно выбрать производственную среду Query, тогда я бы пошел на первую:

User.where(id: Membership.uniq.pluck(:user_id))

Причина:. Потому что он будет использовать ключевое слово sql DISTINCT для фильтрации результата базы данных, а затем SELECT только столбца "user_id" из базы данных и возвращает эти значения в форме массива ([1,2,3..]). Фильтрация результатов на уровне базы данных всегда быстрее, чем объект запроса активной записи.

Для вашего второго запроса:

User.where(id: Membership.uniq.select(:user_id))

Это тот же запрос, что и для "pluck", но с "select" он сделает активный объект отношения записи с единственным полем "user_id". В этом запросе есть накладные расходы на создание активного объекта записи как: ([#<Membership user_id: 1>, #<Membership user_id: 2>, ... ], что не относится к первому запросу. Хотя я не сделал никакой реальной маркировки для обоих, но результаты очевидны с шаги, за которыми следуют запросы.

Третий случай здесь дорог, потому что с функцией "Join" он будет извлекать все столбцы из таблицы memberships, и потребуется больше времени для обработки фильтрации результата по сравнению с другими запросами.

Спасибо

Ответ 6

Вот отличный пример, демонстрирующий Include VS Join:

http://railscasts.com/episodes/181-include-vs-joins

Пожалуйста, попробуйте включить. Я в порядке. Это займет сравнительно меньше времени.

User.uniq.includes(:memberships)

Ответ 7

SELECT  DISTINCT users.*
    FROM  users
    INNER JOIN  memberships
       ON memberships.user_id = users.id

медленнее, потому что он выполняется примерно так:

  • Пройдите все таблицы, собирая все, что есть.
  • для каждой записи с шага 1, войдите в другую таблицу.
  • помещаем этот материал в таблицу tmp
  • dedup (DISTINCT) эта таблица для доставки результатов

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

Это "полу-соединение" и только проверка того, что у пользователя есть хотя бы одно членство; это намного эффективнее:

SELECT  users.*
    FROM  users  -- no DISTINCT needed
    WHERE  EXISTS 
      ( SELECT  *
            FROM  memberships ON memberships.user_id = users.id 
      ) 

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

SELECT users.*
    FROM  users

Если Rails не может сгенерировать эти запросы, тогда ворчите его.