есть ли более эффективный способ закодировать это с помощью ActiveRecord?

У меня есть три таблицы:

  1. Пользователи
  2. Вопросы
  3. User_Questions

    1. Пользовательские вопросы имеют столбцы user_id, question_id и answer.

Я хочу найти случайный вопрос, на который нет ответа и поэтому нет строки в таблице user_questions.

Если на все вопросы даны ответы, верните любой случайный вопрос.

Мне сказали, что это можно сделать с помощью OUTER JOIN, но я новичок в SQL и не знаю, как это сделать в Rails.
Вот что у меня есть:

def next_question          
  q = Question.all - Question.joins(:user_questions).where
       (user_questions: { user_id: user_id })
  q = Question.all if q.empty?
  return q[rand(q.size)]
end  

person modernserf    schedule 07.03.2012    source источник


Ответы (2)


Вряд ли когда-либо будет веская причина для вызова метода all в классе модели. Это загружает каждую отдельную запись в базе данных этого типа в память, и если вы не уверены, что это небольшой набор записей, вы потенциально можете повесить всю вашу систему. Даже в этом случае крайне дурно загружать все, а затем выбирать что-то одно и отбрасывать остальное. Это все равно, что заказывать по одному предмету на Amazon, выбирать нужную ручку и выбрасывать остальную часть поставки в мусорное ведро.

Что вам, вероятно, нужно, так это случайный выбор одной записи, которая еще не назначена. Вероятно, это выглядит примерно так:

Question.where('id NOT IN (SELECT question_id FROM user_questions WHERE user_id=?)', user_id).order('RAND()').first

Проблема с JOIN заключается в том, что вы собираетесь найти записи, которые имеют совпадения в таблице user_questions, а не наоборот.

Этот запрос предполагает, что количество вопросов, на которые отвечает пользователь, относительно невелико или что NOT IN может стать значительно дороже.

person tadman    schedule 07.03.2012
comment
кажется, что разные версии SQL требуют разных версий RANDOM(), но да, это так. - person modernserf; 07.03.2012

Да, для этого вы можете использовать LEFT OUTER JOIN. Обычное ВНУТРЕННЕЕ СОЕДИНЕНИЕ будет включать только те строки, которые соответствуют условию соединения, ЛЕВОЕ СОЕДИНЕНИЕ будет включать совпадающие строки и конкретизировать несопоставленные строки, помещая NULL во все столбцы (файл документы PostgreSQL имеют разумное описание).

Итак, вы делаете LEFT JOIN, а затем ищете несопоставленную строку, ища NULL. SQL будет выглядеть примерно так:

select ...
from questions
left outer join user_questions on questions.id = user_questions.question_id
where user_questions.question_id is null

Это даст вам все вопросы, на которые нет ответов. В ActiveRecord вы можете сделать что-то вроде этого:

Question.joins('left outer join user_questions on question.id = user_questions.question_id')
        .where(:user_questions => { :question_id => nil })
        .limit(1)

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

n = Questions.count
q = Questions.offset(rand(n)).limit(1)

Вы могли бы сделать то же самое с Questions.order('random()').limit(1), но ORDER BY random() может быть неприятно с большой базой данных; получение подсчета должно быть довольно быстрым, поэтому случайный выбор в Ruby должен быть достаточно быстрым, и это не заставит вашу базу данных ненавидеть вас.

Также ознакомьтесь с документацией, чтобы узнать, как привести в порядок эти joins, Я работаю с базой данных с немного нестандартной структурой, поэтому иногда мне приходится делать что-то длинное; ActiveRecord отказывается выполнять для меня LEFT JOIN, пока я не объясню это YMMV.

person mu is too short    schedule 07.03.2012