Suchen Sie alle Datensätze, deren Zuordnung größer als Null ist

98

Ich versuche etwas zu tun, von dem ich dachte, es wäre einfach, aber es scheint nicht so zu sein.

Ich habe ein Projektmodell mit vielen offenen Stellen.

class Project < ActiveRecord::Base

  has_many :vacancies, :dependent => :destroy

end

Ich möchte alle Projekte bekommen, die mindestens 1 freie Stelle haben. Ich habe so etwas versucht:

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

aber es heißt

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).

jphorta
quelle

Antworten:

65

joinsVerwendet standardmäßig einen inneren Join, sodass bei Verwendung Project.joins(:vacancies)nur Projekte zurückgegeben werden, denen eine freie Stelle zugeordnet ist.

AKTUALISIEREN:

Wie von @mackskatz im Kommentar ausgeführt, groupgibt der obige Code ohne Klausel doppelte Projekte für Projekte mit mehr als einer offenen Stelle zurück. Verwenden Sie zum Entfernen der Duplikate

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

AKTUALISIEREN:

Wie von @Tolsee hervorgehoben, können Sie auch verwenden distinct.

Project.joins(:vacancies).distinct

Als Beispiel

[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\""
jvnill
quelle
1
Ohne Anwendung einer group by-Klausel würden jedoch mehrere Projektobjekte für Projekte mit mehr als einer Vakanz zurückgegeben.
Mackshkatz
1
Generiert jedoch keine effiziente SQL-Anweisung.
David Aldridge
Nun, das ist Rails für dich. Wenn Sie eine SQL-Antwort geben können (und erklären, warum dies nicht effizient ist), ist dies möglicherweise viel hilfreicher.
Bis
Woran denkst du Project.joins(:vacancies).distinct?
Tolsee
1
Es ist @Tolsee übrigens: D
Tolsee
166

1) Um Projekte mit mindestens 1 offenen Stelle zu erhalten:

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

2) Um Projekte mit mehr als einer offenen Stelle zu erhalten:

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

3) Oder wenn das VacancyModell den Zähler-Cache setzt:

belongs_to :project, counter_cache: true

dann funktioniert das auch:

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

Die Flexionsregel für muss vacancymöglicherweise manuell angegeben werden .

Arta
quelle
2
Sollte das nicht sein Project.joins(:vacancies).group('projects.id').having('count(vacancies.id) > 1')? Abfrage der Anzahl der offenen Stellen anstelle der Projekt-IDs
Keith Mattix
Nein, @KeithMattix, das sollte nicht sein. Es kann jedoch sein, wenn es für Sie besser liest; es ist eine Frage der Präferenz. Die Zählung kann mit jedem Feld in der Verknüpfungstabelle durchgeführt werden, das garantiert einen Wert in jeder Zeile hat. Bedeutsamsten Kandidaten sind projects.id, project_idund vacancies.id. Ich habe mich für das Zählen entschieden, project_idweil es das Feld ist, auf dem die Verknüpfung hergestellt wird. die Wirbelsäule der Verbindung, wenn Sie so wollen. Es erinnert mich auch daran, dass dies eine Join-Tabelle ist.
Arta
36

Ja, vacanciesist kein Feld im Join. Ich glaube du willst:

Project.joins(:vacancies).group("projects.id").having("count(vacancies.id)>0")
Peter Alfvin
quelle
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')
Dorian
quelle
5

Das Durchführen eines inneren Joins für die Tabelle has_many in Kombination mit einem groupoder uniqist möglicherweise sehr ineffizient. In SQL ist dies besser als Semi-Join implementiert, der EXISTSmit einer korrelierten Unterabfrage verwendet wird.

Auf diese Weise kann der Abfrageoptimierer die Tabelle der offenen Stellen prüfen, um festzustellen, ob eine Zeile mit der richtigen Projekt-ID vorhanden ist. Es spielt keine Rolle, ob es eine Zeile oder eine Million gibt, die diese project_id haben.

Das ist in Rails nicht so einfach, kann aber erreicht werden mit:

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

Ebenso finden Sie alle Projekte, die keine offenen Stellen haben:

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

Bearbeiten: In neueren Rails-Versionen wird eine Warnung angezeigt, dass Sie sich nicht darauf verlassen sollen exists, an arel delegiert zu werden. Beheben Sie dies mit:

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

Bearbeiten: Wenn Sie mit Raw SQL nicht vertraut sind, versuchen Sie:

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

Sie können dies weniger chaotisch machen, indem Sie Klassenmethoden hinzufügen, um die Verwendung von arel_tablebeispielsweise zu verbergen :

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

... so ...

Project.where.not(
  Vacancies.where(
    Vacancy.project_id_column.eq(Project.id_column)
  ).arel.exists
)
David Aldridge
quelle
Diese beiden Vorschläge scheinen nicht zu funktionieren ... die Unterabfrage Vacancy.where("vacancies.project_id = projects.id").exists?ergibt entweder trueoder false. Project.where(true)ist ein ArgumentError.
Les Nightingill
Vacancy.where("vacancies.project_id = projects.id").exists?wird nicht ausgeführt - es wird ein Fehler ausgelöst, da die projectsBeziehung in der Abfrage nicht vorhanden ist (und der obige Beispielcode auch kein Fragezeichen enthält). Das Zerlegen in zwei Ausdrücke ist also nicht gültig und funktioniert nicht. In den letzten Rails wird Project.where(Vacancies.where("vacancies.project_id = projects.id").exists)eine Abwertungswarnung ausgegeben ... Ich werde die Frage aktualisieren.
David Aldridge
4

In Rails 4+, können Sie dort auch enthalten oder eager_load die gleiche Antwort zu bekommen:

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

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

Ich denke, es gibt eine einfachere Lösung:

Project.joins(:vacancies).distinct
Juri Karpowitsch
quelle
1
Es ist auch möglich, "verschiedene" zu verwenden, z. B. Project.joins (: freie Stellen) .distinct
Metaphysiker
Sie haben Recht! Es ist besser, #distinct anstelle von #uniq zu verwenden. #uniq lädt alle Objekte in den Speicher, #distinct führt jedoch datenbankseitige Berechnungen durch.
Juri Karpowitsch
3

Ohne viel Rails-Magie können Sie Folgendes tun:

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

Diese Art von Bedingungen funktioniert in allen Rails-Versionen, da ein Großteil der Arbeit direkt auf der DB-Seite ausgeführt wird. Außerdem .countfunktioniert die Verkettungsmethode auch gut. Ich bin wie Project.joins(:vacancies)zuvor von Fragen verbrannt worden . Natürlich gibt es Vor- und Nachteile, da es nicht DB-Agnostiker ist.

konyak
quelle
1
Dies ist viel langsamer als die Join- und Gruppenmethode, da die Unterabfrage 'select count (*) ..' für jedes Projekt ausgeführt wird.
YasirAzgar
@YasirAzgar Die Join- und Gruppenmethode ist langsamer als die "Exists" -Methode, da weiterhin auf alle untergeordneten Zeilen zugegriffen wird, selbst wenn eine Million davon vorhanden sind.
David Aldridge
0

Sie können auch verwenden , EXISTSmit SELECT 1eher als alle Spalten aus der vacanciesAuswahltabelle:

Project.where("EXISTS(SELECT 1 from vacancies where projects.id = vacancies.project_id)")
KM Rakibul Islam
quelle
-6

Der Fehler sagt Ihnen, dass offene Stellen im Grunde genommen keine Spalte in Projekten sind.

Das sollte funktionieren

Project.joins(:vacancies).where('COUNT(vacancies.project_id) > 0')
wkhatch
quelle
7
aggregate functions are not allowed in WHERE
Kamil Lelonek