ค้นหาระเบียนทั้งหมดที่มีจำนวนการเชื่อมโยงมากกว่าศูนย์


100

ฉันกำลังพยายามทำบางสิ่งที่ฉันคิดว่ามันจะง่าย แต่ดูเหมือนจะไม่เป็นเช่นนั้น

ฉันมีโมเดลโครงการที่มีตำแหน่งงานว่างมากมาย

class Project < ActiveRecord::Base

  has_many :vacancies, :dependent => :destroy

end

ฉันต้องการรับโครงการทั้งหมดที่มีตำแหน่งว่างอย่างน้อย 1 ตำแหน่ง ฉันลองทำสิ่งนี้:

Project.joins(:vacancies).where('count(vacancies) > 0')

แต่มันบอกว่า

SQLite3::SQLException: no such column: vacancies: SELECT "projects".* FROM "projects" INNER JOIN "vacancies" ON "vacancies"."project_id" = "projects"."id" WHERE ("projects"."deleted_at" IS NULL) AND (count(vacancies) > 0).

คำตอบ:


68

joinsใช้การรวมภายในโดยค่าเริ่มต้นดังนั้นการใช้Project.joins(:vacancies)จะมีผลเฉพาะโปรเจ็กต์ที่มีตำแหน่งว่างที่เกี่ยวข้องเท่านั้น

อัพเดท:

ตามที่ @mackskatz ระบุไว้ในความคิดเห็นหากไม่มีgroupข้อใดรหัสด้านบนจะส่งคืนโครงการที่ซ้ำกันสำหรับโครงการที่มีตำแหน่งงานว่างมากกว่าหนึ่งรายการ หากต้องการลบรายการที่ซ้ำกันให้ใช้

Project.joins(:vacancies).group('projects.id')

อัพเดท:

ตามที่ @Tolsee ชี้ไว้คุณสามารถใช้distinctไฟล์.

Project.joins(:vacancies).distinct

ตัวอย่างเช่น

[10] pry(main)> Comment.distinct.pluck :article_id
=> [43, 34, 45, 55, 17, 19, 1, 3, 4, 18, 44, 5, 13, 22, 16, 6, 53]
[11] pry(main)> _.size
=> 17
[12] pry(main)> Article.joins(:comments).size
=> 45
[13] pry(main)> Article.joins(:comments).distinct.size
=> 17
[14] pry(main)> Article.joins(:comments).distinct.to_sql
=> "SELECT DISTINCT \"articles\".* FROM \"articles\" INNER JOIN \"comments\" ON \"comments\".\"article_id\" = \"articles\".\"id\""

1
อย่างไรก็ตามหากไม่ใช้กลุ่มตามคำสั่งสิ่งนี้จะส่งคืนอ็อบเจ็กต์โปรเจ็กต์หลายรายการสำหรับโปรเจ็กต์ที่มีตำแหน่งว่างมากกว่าหนึ่งตำแหน่ง
mackshkatz

1
ไม่ได้สร้างคำสั่ง SQL ที่มีประสิทธิภาพแม้ว่า
David Aldridge

นั่นคือ Rails สำหรับคุณ หากคุณสามารถให้คำตอบ sql (และอธิบายว่าเหตุใดจึงไม่ได้ผล) นั่นอาจเป็นประโยชน์มากกว่านี้
jvnill

คุณคิดProject.joins(:vacancies).distinctอย่างไรเกี่ยวกับ?
Tolsee

1
@Tolsee btw: D
Tolsee

172

1) เพื่อให้ได้โครงการที่มีตำแหน่งว่างอย่างน้อย 1 ตำแหน่ง:

Project.joins(:vacancies).group('projects.id')

2) ในการรับโครงการที่มีตำแหน่งว่างมากกว่า 1 ตำแหน่ง:

Project.joins(:vacancies).group('projects.id').having('count(project_id) > 1')

3) หรือหากVacancyโมเดลตั้งค่าแคชตัวนับ:

belongs_to :project, counter_cache: true

สิ่งนี้จะได้ผลเช่นกัน:

Project.where('vacancies_count > ?', 1)

vacancyอาจจำเป็นต้องระบุกฎการผันคำด้วยตนเองหรือไม่?


2
สิ่งนี้ไม่ควรProject.joins(:vacancies).group('projects.id').having('count(vacancies.id) > 1')? การค้นหาจำนวนตำแหน่งงานว่างแทนรหัสโครงการ
Keith Mattix

2
ไม่ @KeithMattix ไม่ควรเป็น อย่างไรก็ตามอาจเป็นไปได้ถ้ามันอ่านดีกว่าสำหรับคุณ มันเป็นเรื่องของความชอบ การนับสามารถทำได้กับเขตข้อมูลใดก็ได้ในตารางการเข้าร่วมที่รับประกันว่ามีค่าในทุกแถว ส่วนใหญ่ผู้สมัครที่มีความหมายprojects.id, และproject_id vacancies.idฉันเลือกที่จะนับproject_idเพราะเป็นช่องที่ทำการเข้าร่วม กระดูกสันหลังของการเข้าร่วมถ้าคุณต้องการ นอกจากนี้ยังเตือนฉันว่านี่คือตารางเข้าร่วม
Arta

38

ใช่vacanciesไม่ใช่เขตข้อมูลในการเข้าร่วม ฉันเชื่อว่าคุณต้องการ:

Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")

16
# None
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 0')
# Any
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 0')
# One
Project.joins(:vacancies).group('projects.id').having('count(vacancies) = 1')
# More than 1
Project.joins(:vacancies).group('projects.id').having('count(vacancies) > 1')

6

การทำการรวมภายในกับตาราง has_many รวมกับ a groupหรือuniqอาจไม่มีประสิทธิภาพมากและใน SQL สิ่งนี้จะนำไปใช้งานได้ดีกว่าในรูปแบบการรวมกึ่งที่ใช้EXISTSกับเคียวรีย่อยที่สัมพันธ์กัน

สิ่งนี้ช่วยให้เครื่องมือเพิ่มประสิทธิภาพการสืบค้นสามารถตรวจสอบตารางตำแหน่งงานว่างเพื่อตรวจสอบการมีอยู่ของแถวที่มี project_id ที่ถูกต้อง ไม่สำคัญว่าจะมี 1 แถวหรือ 1 ล้านที่มี project_id นั้น

นั่นไม่ตรงไปตรงมาใน Rails แต่สามารถทำได้ด้วย:

Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)

ในทำนองเดียวกันค้นหาโครงการทั้งหมดที่ไม่มีตำแหน่งงานว่าง:

Project.where.not(Vacancies.where("vacancies.project_id = projects.id").exists)

แก้ไข: ใน Rails เวอร์ชันล่าสุดคุณจะได้รับคำเตือนเกี่ยวกับการเลิกใช้งานโดยบอกให้คุณไม่ต้องพึ่งพาexistsการมอบหมายให้ arel แก้ไขปัญหานี้ด้วย:

Project.where.not(Vacancies.where("vacancies.project_id = projects.id").arel.exists)

แก้ไข: หากคุณไม่สบายใจกับ SQL ดิบให้ลอง:

Project.where.not(Vacancies.where(Vacancy.arel_table[:project_id].eq(Project.arel_table[:id])).arel.exists)

คุณสามารถทำให้สิ่งนี้ยุ่งน้อยลงได้โดยการเพิ่ม class method เพื่อซ่อนการใช้งานarel_tableตัวอย่างเช่น:

class Project
  def self.id_column
    arel_table[:id]
  end
end

... งั้น ...

Project.where.not(
  Vacancies.where(
    Vacancy.project_id_column.eq(Project.id_column)
  ).arel.exists
)

ทั้งสองข้อเสนอแนะดูเหมือนจะไม่ทำงาน ... แบบสอบถามย่อยVacancy.where("vacancies.project_id = projects.id").exists?อัตราผลตอบแทนอย่างใดอย่างหนึ่งหรือtrue คือ. falseProject.where(true)ArgumentError
Les Nightingill

Vacancy.where("vacancies.project_id = projects.id").exists? จะไม่ดำเนินการ - จะทำให้เกิดข้อผิดพลาดเนื่องจากไฟล์ projectsความสัมพันธ์ไม่มีอยู่ในแบบสอบถาม (และไม่มีเครื่องหมายคำถามในโค้ดตัวอย่างด้านบนด้วย) ดังนั้นการแยกสิ่งนี้ออกเป็นสองนิพจน์จึงไม่ถูกต้องและไม่ได้ผล ใน Rails ล่าสุดProject.where(Vacancies.where("vacancies.project_id = projects.id").exists)มีคำเตือนการเลิกใช้งาน ... ฉันจะอัปเดตคำถาม
David Aldridge

4

ใน Rails 4+ คุณยังสามารถใช้งานรวมถึงหรือeager_loadจะได้คำตอบเดียวกัน:

Project.includes(:vacancies).references(:vacancies).
        where.not(vacancies: {id: nil})

Project.eager_load(:vacancies).where.not(vacancies: {id: nil})

4

ฉันคิดว่ามีวิธีแก้ปัญหาที่ง่ายกว่านี้:

Project.joins(:vacancies).distinct

1
นอกจากนี้ยังสามารถใช้ "แตกต่าง" ได้เช่น Project.joins (: vacancies) .distinct
Metaphysiker

คุณพูดถูก! ควรใช้ #distinct แทน #uniq #uniq จะโหลดวัตถุทั้งหมดลงในหน่วยความจำ แต่ #distinct จะทำการคำนวณทางด้านฐานข้อมูล
Yuri Karpovich

3

คุณสามารถทำได้โดยไม่ต้องใช้เวทย์มนตร์ Rails มากนัก:

Project.where('(SELECT COUNT(*) FROM vacancies WHERE vacancies.project_id = projects.id) > 0')

เงื่อนไขประเภทนี้จะใช้ได้กับ Rails ทุกเวอร์ชันเนื่องจากงานส่วนใหญ่จะทำโดยตรงที่ฝั่ง DB นอกจากนี้.countวิธีการผูกมัดก็ใช้ได้ดีเช่นกัน ฉันเคยถูกเผาโดยแบบสอบถามเช่นProject.joins(:vacancies)ก่อน แน่นอนว่ามีข้อดีข้อเสียเนื่องจากไม่ใช่ DB ไม่เชื่อเรื่องพระเจ้า


1
ซึ่งช้ากว่าวิธีการเข้าร่วมและกลุ่มมากเนื่องจากการสืบค้นย่อย 'select count (*) .. ' จะดำเนินการสำหรับแต่ละโครงการ
YasirAzgar

@YasirAzgar วิธีการเข้าร่วมและกลุ่มช้ากว่าเมธอด "มีอยู่" เนื่องจากจะยังคงเข้าถึงแถวย่อยทั้งหมดแม้ว่าจะมีเป็นล้านแถวก็ตาม
David Aldridge


-6

ข้อผิดพลาดกำลังบอกคุณว่าตำแหน่งงานว่างไม่ใช่คอลัมน์ในโครงการโดยทั่วไป

สิ่งนี้ควรใช้งานได้

Project.joins(:vacancies).where('COUNT(vacancies.project_id) > 0')

7
aggregate functions are not allowed in WHERE
Kamil Lelonek
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.