Rails에서 관련 레코드가없는 레코드를 찾으려면


178

간단한 연관성을 고려하십시오 ...

class Person
   has_many :friends
end

class Friend
   belongs_to :person
end

ARel 및 / 또는 meta_where에 친구가없는 모든 사람을 얻는 가장 깨끗한 방법은 무엇입니까?

그리고 has_many : through 버전은 어떻습니까?

class Person
   has_many :contacts
   has_many :friends, :through => :contacts, :uniq => true
end

class Friend
   has_many :contacts
   has_many :people, :through => :contacts, :uniq => true
end

class Contact
   belongs_to :friend
   belongs_to :person
end

나는 정말로 counter_cache를 사용하고 싶지 않습니다. 그리고 읽은 것으로부터 has_many와는 작동하지 않습니다 :

모든 person.friends 레코드를 가져 와서 Ruby에서 반복하고 싶지 않습니다. meta_search gem과 함께 사용할 수있는 쿼리 / 범위를 갖고 싶습니다.

나는 쿼리의 성능 비용을 신경 쓰지 않는다

실제 SQL에서 멀수록 멀수록 좋습니다.

답변:


110

이것은 여전히 ​​SQL과 매우 비슷하지만 첫 번째 경우 친구가없는 모든 사람을 확보해야합니다.

Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')

6
friends 테이블에 10000000 개의 레코드가 있다고 상상해보십시오. 이 경우 성능은 어떻습니까?
goodniceweb

@goodniceweb 중복 빈도에 따라을 (를) 삭제할 수 있습니다 DISTINCT. 그렇지 않으면 그 경우 데이터와 색인을 정규화하고 싶습니다. friend_idshstore 또는 직렬화 된 열 을 만들어서 그렇게 할 수 있습니다 . 그렇다면 당신은 말할 수 있습니다Person.where(friend_ids: nil)
Unixmonkey

sql을 사용하려는 경우 not exists (select person_id from friends where person_id = person.id)아마도 사용하는 것이 좋습니다 (또는 테이블의 내용에 따라 people.id또는 persons.id). 특정 상황에서 가장 빠른 것이 무엇인지 확실하지 않지만 과거에는 이것이 나에게 잘 작동했습니다 ActiveRecord를 사용하지 않았습니다.
nroose

442

보다 나은:

Person.includes(:friends).where( :friends => { :person_id => nil } )

hmt의 경우 기본적으로 동일한 내용이므로 친구가없는 사람도 연락처가 없다는 사실에 의존합니다.

Person.includes(:contacts).where( :contacts => { :person_id => nil } )

최신 정보

has_one의견 에 대한 질문이 있으므로 업데이트하십시오. 여기서 트릭 includes()은 연결 이름은 필요하지만 where테이블 이름은 필요합니다. A에 대한 has_one연계는 일반적으로 변화하지만, 그래서, 단수로 표현 될 where()부분 스테이는 그대로. 따라서 Person유일한 has_one :contact경우 귀하의 진술은 다음과 같습니다.

Person.includes(:contact).where( :contacts => { :person_id => nil } )

업데이트 2

누군가가없는 반대 친구에 대해 누군가가 물었습니다. 아래에서 주석을 달았을 때 실제로 마지막 필드 (위의 :person_id)가 반환하는 모델과 실제로 관련 될 필요는 없으며 조인 테이블의 필드 일뿐입니다. 그것들은 모두 그들 중 하나가 될 수 nil있도록 될 것입니다. 이것은 위의 간단한 솔루션으로 이어집니다.

Person.includes(:contacts).where( :contacts => { :id => nil } )

그리고 사람이없는 친구를 반환하기 위해 이것을 전환하면 훨씬 간단 해집니다. 앞면의 수업 만 변경하십시오.

Friend.includes(:contacts).where( :contacts => { :id => nil } )

업데이트 3-Rails 5

탁월한 Rails 5 솔루션에 대한 @Anson 덕분에 (아래 답변에 +1을 줄 수 있음) left_outer_joins연결을로드하지 않도록 사용할 수 있습니다 .

Person.left_outer_joins(:contacts).where( contacts: { id: nil } )

사람들이 찾을 수 있도록 여기에 포함 시켰지만, +1을받을 자격이 있습니다. 훌륭한 추가!

업데이트 4-Rails 6.1

다가오는 6.1에서 다음을 수행 할 수 있음을 지적한 Tim Park 에게 감사드립니다 .

Person.where.missing(:contacts)

그가 연결 한 게시물 덕분에 .


4
이것을 훨씬 더 깨끗한 범위로 통합 할 수 있습니다.
Eytan

3
더 나은 대답은 왜 다른 하나가 허용 된 것으로 평가되는지 확실하지 않습니다.
Tamik Soziev

5
그렇습니다. has_one연결에 대해 단일 이름이 있다고 가정하면 includes통화 에서 연결 이름을 변경해야합니다 . 가정 그래서 그것은이었다 has_one :contact내부에 Person다음 코드가 될 것이다Person.includes(:contact).where( :contacts => { :person_id => nil } )
smathy

3
Friend 모델 ( self.table_name = "custom_friends_table_name") 에서 사용자 정의 테이블 이름을 사용하는 경우을 사용하십시오 Person.includes(:friends).where(:custom_friends_table_name => {:id => nil}).
Zek

5
@smathy Rails 6.1의 멋진 업데이트 missing는 정확히 이것을 수행 하는 방법을 추가합니다 !
팀 박

172

smathy는 좋은 Rails 3 답변을 가지고 있습니다.

Rails 5의left_outer_joins 경우 연결로드를 피하기 위해 사용할 수 있습니다 .

Person.left_outer_joins(:contacts).where( contacts: { id: nil } )

API 문서를 확인하십시오 . 풀 요청 # 12071 에서 소개되었습니다 .


이것에 대한 단점이 있습니까? 확인 후 0.1ms 더 빠르게로드되었습니다. 포함
Qwertie

나중에 나중에 연결하면 연결을로드하지 않는 것이 단점이지만 연결하지 않으면 이점이 있습니다. 내 사이트의 경우 0.1ms 적중은 무시할만한 수준이므로 .includes로드 시간에 추가 비용이 발생하면 최적화에 대해 크게 걱정하지 않아도됩니다. 사용 사례가 다를 수 있습니다.
Anson

1
아직 Rails 5가없는 경우 다음을 수행 할 수 Person.joins('LEFT JOIN contacts ON contacts.person_id = persons.id').where('contacts.id IS NULL')있습니다. 범위로도 잘 작동합니다. Rails 프로젝트에서 항상이 작업을 수행합니다.
Frank

3
이 방법의 가장 큰 장점은 메모리 절약입니다. 를 수행하면 includes모든 AR 객체가 메모리에로드되므로 테이블이 커질수록 나쁜 일이 될 수 있습니다. 연락처 레코드에 액세스 할 필요 left_outer_joins가없는 경우 연락처를 메모리에로드하지 않습니다. SQL 요청 속도는 동일하지만 전반적인 앱 혜택은 훨씬 큽니다.
chrismanderson

2
정말 좋습니다! 감사! 이제 레일 신이 간단하게 구현할 수 Person.where(contacts: nil)있거나 Person.with(contact: contact)침입이 '적절성'으로 너무 멀리있는 곳을 사용하는 경우-그러나 그 접촉을 고려할 때 : 이미 구문 분석되고 식별 된 것으로 논리적으로 보인다 ...
저스틴 맥스웰

14

친구가없는 사람

Person.includes(:friends).where("friends.person_id IS NULL")

아니면 적어도 친구가 하나 있습니다

Person.includes(:friends).where("friends.person_id IS NOT NULL")

범위를 설정하여 Arel을 사용 하여이 작업을 수행 할 수 있습니다 Friend

class Friend
  belongs_to :person

  scope :to_somebody, ->{ where arel_table[:person_id].not_eq(nil) }
  scope :to_nobody,   ->{ where arel_table[:person_id].eq(nil) }
end

그리고 친구가 하나 이상있는 사람 :

Person.includes(:friends).merge(Friend.to_somebody)

친구없는 사람 :

Person.includes(:friends).merge(Friend.to_nobody)

2
나는 당신도 할 수 있다고 생각합니다 : Person.includes (: friends) .where (friends : {person : nil})
ReggieB

1
참고 : 병합 전략은 때때로 다음과 같은 경고를 생성 할 수 있습니다.DEPRECATION WARNING: It looks like you are eager loading table(s) Currently, Active Record recognizes the table in the string, and knows to JOIN the comments table to the query, rather than loading comments in a separate query. However, doing this without writing a full-blown SQL parser is inherently flawed. Since we don't want to write an SQL parser, we are removing this functionality. From now on, you must explicitly tell Active Record when you are referencing a table from a string
genkilabs

12

dmarkow와 Unixmonkey의 답변 모두 내가 필요한 것을 얻습니다-감사합니다!

내 실제 앱에서 두 가지를 모두 시도하고 타이밍을 얻었습니다. 두 가지 범위는 다음과 같습니다.

class Person
  has_many :contacts
  has_many :friends, :through => :contacts, :uniq => true
  scope :without_friends_v1, -> { where("(select count(*) from contacts where person_id=people.id) = 0") }
  scope :without_friends_v2, -> { where("id NOT IN (SELECT DISTINCT(person_id) FROM contacts)") }
end

~ 700 'Person'레코드가있는 작은 테이블-평균 5 회 실행하는 실제 앱으로이를 실행하십시오.

Unixmonkey의 접근 방식 ( :without_friends_v1) 813ms / query

dmarkow의 접근 방식 ( :without_friends_v2) 891ms / 쿼리 (~ 10 % 느림)

그러나 그때 나는 NO DISTINCT()...가있는 Person기록을 찾고 있는 전화가 필요 없다. Contacts그래서 그들은 단지 NOT IN연락처 목록 이어야 한다 person_ids. 그래서 나는이 범위를 시도했다 :

  scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") }

결과는 같지만 평균 425ms / call로 거의 절반의 시간이 소요됩니다.

이제 DISTINCT다른 유사한 쿼리 가 필요할 수도 있지만 내 경우에는 잘 작동하는 것 같습니다.

당신의 도움을 주셔서 감사합니다


5

불행히도 SQL과 관련된 솔루션을 찾고 있지만 범위에서 설정 한 다음 해당 범위를 사용할 수 있습니다.

class Person
  has_many :contacts
  has_many :friends, :through => :contacts, :uniq => true
  scope :without_friends, where("(select count(*) from contacts where person_id=people.id) = 0")
end

그런 다음 가져 오기 위해 할 수 Person.without_friends있으며 다른 Arel 메소드와 연결할 수도 있습니다.Person.without_friends.order("name").limit(10)


1

NOT EXISTS 상관 서브 쿼리는 특히 행 수와 하위 레코드 대 부모 레코드의 비율이 증가함에 따라 빨라야합니다.

scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")

1

또한 예를 들어 한 친구가 걸러 내려면 :

Friend.where.not(id: other_friend.friends.pluck(:id))

3
그러면 하위 쿼리가 아닌 2 개의 쿼리가 생성됩니다.
grepsedawk
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.