Subselect, Havin, Count, Join... ich weisz es nicht ;-(
Peter Nack
- datenbank
Guten Tag allerseits,
ich sitze leider immernoch an dem vor ein paar Tagen bereits von mir beschriebenem Problem.
Nochmal kurz zusammengefasst geht es um folgendes Szenario:
Stark vereinfacht sieht es aus wie folgt:
Anwendungsbeispiel fuer eine Suche:
Der Benutzer kann nun nach Resorts suchen, welche die ResortFacilities x und y und z besitzen sollen. Zudem sollen nur Resorts gelistet werden, von denen mindestens ein Raum die RoomFacilties a und b und c besitzt.
Das Thema mit den ResortFacilties bekomme ich ohne Probleme umgesetzt. Jedoch scheitere ich an den RoomFacilities. Ich verstehe einfach nicht, wie ich diese Logik in nur einem einzigen Statement unterbringen kann.
Meine erster Ansatz - ausgehend von den vorherigen Posts - ist wie folgt:
SELECT DISTINCT
r.id, r.name
FROM
resort AS r
LEFT JOIN
mm_resort_facility AS resfacility ON r.id = resfacility.resort_id
LEFT JOIN
resort_facility as rf ON resfacility.resort_facility_id = rf.id
WHERE
(
SELECT DISTINCT
COUNT(room3.id)
FROM
room AS room3
WHERE
room3.resort_id = r.id
AND
room3.room_type_id IN ( 2 )
) > 1
AND
(
SELECT DISTINCT
COUNT(room2.id)
FROM
room AS room2
LEFT JOIN
mm_room_facility ON room2.id = mm_room_facility.room_id
LEFT JOIN
room_facility ON mm_room_facility.room_facility_id = room_facility.id
WHERE
room2.resort_id = r.id
AND
( room_facility_id = 1 )
GROUP BY
room2.id
HAVING COUNT(mm_room_facility.room_facility_id) = 1
) = 1
AND
(
rf.id = 7 OR rf.id = 3 OR rf.id = 2
)
GROUP BY
r.id
HAVING COUNT(resfacility.resort_facility_id) = 3
Der Trick hierbei war der Einsatz von GROUP BY, HAVING und COUNT. Klappte wunderbar, so lange ich nicht versucht habe die RoomFacilities mit einzubeziehen.
Mein aktueller Ansatz, das ganze mittels Subselects zu loesen, scheitert leider auch (scheitern heisst, ich bekomme - wie auch im ersten Beispiel - keine eintraege zurueckgeliefert).
SELECT DISTINCT
resort.id, resort.name
FROM
resort
WHERE
-- resort facilities
(
SELECT
COUNT(id)
FROM
mm_resort_facility
WHERE
mm_resort_facility.resort_id = resort.id
AND
mm_resort_facility.resort_facility_id IN (1,2,3)
) = 3
AND
-- room facilities
(
SELECT
COUNT(id)
FROM
mm_room_facility
WHERE
mm_room_facility.room_id IN (SELECT id FROM room WHERE room.resort_id = resort.id )
AND
mm_room_facility.room_facility_id IN (1,2,4)
) = 3
Mitterweile habe ich das dumpfe Gefuehl, dass ich da einfach nicht mehr von alleine weiterkomme. Daher waere ich wirklich sehr dankbar, wenn einer von den Fachleuten hier mal evtl. drueber schauen und mir ggfs. ein paar Ratschlaege geben koennte.
Herzlichen Dank im Voraus!
MfG
Peter
PS:
MySQL Server version: 5.0.67-community
InnoDB
Stell mal eine TestDb inkl Inserts zur Verfügung. So ist das ein wenig extrem mühsam
lg
Stell mal eine TestDb inkl Inserts zur Verfügung. So ist das ein wenig extrem mühsam
Nachtrag: weil eigentlich schauts ja nicht so ganz falsch aus ;)
lg
Hallo Peter ;-)
erstmal Danke fuer deine Interesse!
Ich habe die DB jetzt mal auf das minimalste kastriert.
Es existieren drei Resorts, wobei "Nochmal alles" und "Alles Resort" a) alle ResortFacilities und b) mindestens einer deren Rooms alle RoomFacilities besitzt.
-- phpMyAdmin SQL Dump
-- version 2.11.9.2
-- http://www.phpmyadmin.net
--
-- Host: localhost
-- Generation Time: Dec 02, 2009 at 11:15 AM
-- Server version: 5.0.67
-- PHP Version: 5.2.6
SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
--
-- Database: `resorts`
--
-- --------------------------------------------------------
--
-- Table structure for table `mm_resort_facility`
--
CREATE TABLE IF NOT EXISTS `mm_resort_facility` (
`id` int(11) NOT NULL auto_increment,
`resort_id` int(11) NOT NULL,
`resort_facility_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `fk_mmrf_resort` (`resort_id`),
KEY `fk_mmrf_resort_facility` (`resort_facility_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=52 ;
--
-- Dumping data for table `mm_resort_facility`
--
INSERT INTO `mm_resort_facility` (`id`, `resort_id`, `resort_facility_id`) VALUES
(16, 7, 1),
(17, 7, 2),
(18, 7, 3),
(19, 7, 4),
(20, 7, 5),
(21, 7, 6),
(22, 7, 7),
(23, 8, 1),
(24, 8, 2),
(25, 8, 3),
(26, 8, 4),
(27, 8, 5),
(28, 8, 6),
(29, 8, 7),
(30, 9, 1),
(31, 9, 2),
(32, 9, 3),
(33, 9, 4),
(34, 9, 5),
(35, 9, 6),
(36, 9, 7),
(37, 5, 1),
(38, 5, 2),
(39, 5, 3),
(40, 5, 4),
(41, 5, 5),
(42, 5, 6),
(43, 5, 7),
(44, 1, 3),
(45, 1, 4),
(46, 1, 5),
(47, 1, 6),
(48, 6, 3),
(49, 6, 4),
(50, 6, 5),
(51, 6, 6);
-- --------------------------------------------------------
--
-- Table structure for table `mm_room_facility`
--
CREATE TABLE IF NOT EXISTS `mm_room_facility` (
`id` int(11) NOT NULL auto_increment,
`room_id` int(11) NOT NULL,
`room_facility_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `fk_rf_room` (`room_id`),
KEY `fk_rf_room_facility` (`room_facility_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=32 ;
--
-- Dumping data for table `mm_room_facility`
--
INSERT INTO `mm_room_facility` (`id`, `room_id`, `room_facility_id`) VALUES
(17, 8, 1),
(18, 8, 4),
(19, 8, 2),
(20, 9, 1),
(21, 9, 4),
(22, 9, 2),
(23, 10, 1),
(24, 10, 4),
(25, 10, 2),
(26, 11, 1),
(27, 11, 4),
(28, 11, 2),
(29, 12, 1),
(30, 12, 4),
(31, 12, 2);
-- --------------------------------------------------------
--
-- Table structure for table `resort`
--
CREATE TABLE IF NOT EXISTS `resort` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `u_name_location` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=10 ;
--
-- Dumping data for table `resort`
--
INSERT INTO `resort` (`id`, `name`) VALUES
(8, 'Alles Resort'),
(9, 'Nochmal Alles'),
(6, 'Sea Quest Center');
-- --------------------------------------------------------
--
-- Table structure for table `resort_facility`
--
CREATE TABLE IF NOT EXISTS `resort_facility` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=8 ;
--
-- Dumping data for table `resort_facility`
--
INSERT INTO `resort_facility` (`id`, `name`) VALUES
(1, 'Internet'),
(2, 'Loundry'),
(3, 'Scuba'),
(4, 'Handicapped'),
(5, 'Daytrips'),
(6, 'Aircon'),
(7, 'CarRental');
-- --------------------------------------------------------
--
-- Table structure for table `room`
--
CREATE TABLE IF NOT EXISTS `room` (
`id` int(11) NOT NULL auto_increment,
`resort_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `fk_room_resort` (`resort_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=13 ;
--
-- Dumping data for table `room`
--
INSERT INTO `room` (`id`, `resort_id`, `name`) VALUES
(1, 1, 'Honeymoon-Suit'),
(2, 4, 'test'),
(3, 5, 'Honeymoon-Suit'),
(4, 5, 'Villa Lemon'),
(5, 6, 'Dream Day'),
(6, 7, 'Villa Green'),
(7, 8, 'MyCottage'),
(8, 8, 'MySuit'),
(9, 8, 'MyVilla'),
(10, 9, 'TheCottages'),
(11, 9, 'TheSuits'),
(12, 9, 'TheVillas');
-- --------------------------------------------------------
--
-- Table structure for table `room_facility`
--
CREATE TABLE IF NOT EXISTS `room_facility` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=5 ;
--
-- Dumping data for table `room_facility`
--
INSERT INTO `room_facility` (`id`, `name`) VALUES
(1, 'Internet'),
(2, 'Aircon'),
(4, 'Handicapped');
Meine Testversuche mit dem Dump liefen eigentlich korrekt ab, so dass ich davon ausgehe, keine groszen Fehler bei der Reduzierung gemacht zu haben.
Falls dennoch Probleme auftreten, bitte meckern ;)
Danke & mfg
Peter
OK .. bin mir zwar nicht sicher aber ich denk ich hab deinen Denkfehler
Ersetze:
mm_room_facility.room_id IN (SELECT id FROM room WHERE room.resort_id = resort.id )
durch
mm_room_facility.room_id = resort.id
Denn ersteres ergibt für mich eigentlich auch keinen Sinn.. ich hab es jetzt nur ganz schnell getestet aber ich denk das ergebniss is das was du willst
lg
Denn ersteres ergibt für mich eigentlich auch keinen Sinn.. ich hab es jetzt nur ganz schnell getestet aber ich denk das ergebniss is das was du willst
A ganz übersehen das es schon eine Lösung gab .. egal.. war ganz witzig ;)
lg
Hi Peter,
Denn ersteres ergibt für mich eigentlich auch keinen Sinn.. ich hab es jetzt nur ganz schnell getestet aber ich denk das ergebniss is das was du willst
Toll, meine ersten Tests liefern tatsaechlich die gewuenschten Ergebnisse! ;-)
Mir sagt dieser Loesungsansatz ein wenig mehr zu als der andere.
Kann ich frei waehlen oder hat einer der beiden Wege entscheidende Vorteile?
Besten Dank & MfG
Peter
Hi Peter,
Mir sagt dieser Loesungsansatz ein wenig mehr zu als der andere.
Kann ich frei waehlen oder hat einer der beiden Wege entscheidende Vorteile?
Kommt auf deine Performanceansprüche an. Sofern beide wirklich das exakt gleiche Ergebniss liefern(was ich nicht getestet habe) sollte eigentlich allein vom Explain her meine Lösung das Rennen machen ;)
lg
Hallo Peter,
Ersetze:
mm_room_facility.room_id IN (SELECT id FROM room WHERE room.resort_id = resort.id )
durch
mm_room_facility.room_id = resort.id
Jetzt muss ich doch noch mal nachfragen:
Wieso funktioniert ueberhaupt room_id = resort.id ?
Ich meine, was hat in diesem Kontext die room_id mit der resort_id gemein? Sollte hier nicht auf room.id verglichen werden?
Selbst nach laengerem Ueberlegen komme ich zu keiner logischen Antwort.
Danke & MfG
Peter
Hi!
Ersetze:
mm_room_facility.room_id IN (SELECT id FROM room WHERE room.resort_id = resort.id )
durch
mm_room_facility.room_id = resort.id
Jetzt muss ich doch noch mal nachfragen:
Wieso funktioniert ueberhaupt room_id = resort.id ?
Es funktioniert nicht. Vermutlich ist das Ergebnis zufällig richtig. Probier es mit mal mit zwei Fällen, bei denen einer was finden muss und der andere nicht darf.
Ich hab grad nochmal über deinen ursprünglichen Versuch nachgedacht. Das hatte ich zugunsten einer eigenen Lösungsfindung nämlich nicht. Ohne die Resort-Facilitys sieht die Abfrage so aus:
SELECT DISTINCT resort.id, resort.name
FROM resort
WHERE -- room facilities
(
SELECT COUNT(id)
FROM mm_room_facility
WHERE mm_room_facility.room_id IN (SELECT id FROM room WHERE room.resort_id = resort.id)
AND mm_room_facility.room_facility_id IN (1,2,4)
) = 3
Die erste WHERE-Bedingung findet in der mm_room_facility-Verknüpfungstabelle genau einen Raum-Eintrag. Derer gibt es mehrere, je nach Anzahl der darin enthaltenen Facilitys. Dieser eine Raum-Eintrag hat genau eine Facility. Raum 1 hat beispielsweise Internet => Treffer. Der nächste Datensatz ergibt Raum 2 mit Aircon => noch ein Treffer. Und so weiter. Du findest also die Anzahl der Datensätze, die jeweils eine der Facilitys im jeweiligen Resort enthält, nicht aber die Räume, die alle gesuchten gleichzeitig enthalten.
Lo!
Hallo Dedlfix,
[..]
Du findest also die Anzahl der Datensätze, die jeweils eine der Facilitys im jeweiligen Resort enthält, nicht aber die Räume, die alle gesuchten gleichzeitig enthalten.
Ich habe mir jetzt mal die Zeit genommen und beide Varianten ausgiebig getestet.
Das Ergebnis: Du hast mit deiner Analyse vollkommen recht!
Auch wenn mir die andere Variante besser gefallen hat (da einfacher), greife ich jetzt auf deine Version zurueck.
Nochmal besten Dank fuer deinen Nachtrag. Ohne den waere ich sicherlich erst sehr spaet auf den Missstand aufmerksam geworden.
Danke & MfG
Peter
Hi!
Vorab: Deine Abfragen zeigen, dass du auch noch ein Aliasnamenfindungsproblem hast. Versuch ein konsistentes Schema zu finden, sonst sieht man (du eingeschlossen) irgendwann nicht mehr durch. Den Alias verwendet man häufig, um Tipp- und Lesearbeit zu sparen, weswegen er zwar kurz aber eindeutig sein soll. Beispielsweise: Resort und Room fängt beides mit R an. Zur Unterscheidung könntest du re und ro als Alias nehmen, für die jeweiligen Facility-Verknüpfungstabellen mref und mrof. (Die Tabellen ohne mm brauchst du in der Abfrage nicht, die hießen ansonten ref oder rof.)
Der Benutzer kann nun nach Resorts suchen, welche die ResortFacilities x und y und z besitzen sollen. Zudem sollen nur Resorts gelistet werden, von denen mindestens ein Raum die RoomFacilties a und b und c besitzt.
Du benötigst also erst einmal die Ergebnismenge für alle Resorts, die die entsprechenden Facilitys haben.
SELECT ID_Resort
FROM Resort re
JOIN MM_Resort_Facility mref ON mref.ID_Resort = re.ID
WHERE mref.ID_Resort_Facility IN (1,2) -- IDs der bereits anderweitig gewählten Facilitys
GROUP BY mref.ID_Resort
HAVING COUNT(*) = 2 -- Anzahl der o.a. Facilitys
Diese Ergebnismenge muss weiter eingeschränkt werden. Wir brauchen gemäß dem gleichen Schema alle Räume mit bestimmten Facilitys ...
SELECT ID_Resort, ID
FROM Room ro
JOIN MM_Room_Facility mrof ON mrof.ID_Room = ro.ID
WHERE mrof.ID_Room_Facility IN (1,6)
GROUP BY mrof.ID_Resort, ro.ID
HAVING COUNT(*) = 2
... und letztlich davon nur die Resorts in denen sie sich befinden.
SELECT DISTINCT ID_Resort FROM Room ro
...
Zusammengebaut ergibt sich:
SELECT ID, Name
FROM
(
SELECT re.ID, re.Name
FROM Resort re
JOIN MM_Resort_Facility mref ON mref.ID_Resort = re.ID
WHERE mref.ID_Resort_Facility IN (1,2)
GROUP BY re.ID, re.Name
HAVING COUNT(*) = 2
) AS resorts
WHERE resorts.ID_Resort IN (
(SELECT DISTINCT ro.ID_Resort
FROM Room ro
JOIN MM_Room_Facility mrof ON mrof.ID_Room = ro.ID
WHERE mrof.ID_Room_Facility IN (1, 6)
GROUP BY mrof.ID_Resort, ro.ID
HAVING COUNT(*) = 2
))
Die Funktionsweise der einzelnen Statements sollte man in Einzeltests sicherstellen, bevor man sie zusammenbaut und dann den Fehler in einem großen Haufen suchen muss.
[code lang=sql]
SELECT DISTINCT
r.id, r.name
FROM
resort AS r
LEFT JOIN
mm_resort_facility AS resfacility ON r.id = resfacility.resort_id
LEFT JOIN
resort_facility as rf ON resfacility.resort_facility_id = rf.id
Die Resort-Facility-Tabelle brauchst du nicht - wie du gemäß dem folgenden Statement vermutlich schon selbst erkannt hast - denn du brauchst nur die Facility-IDs für die WHERE-Bedingung und diese IDs sind bereits in der Verknüpfungstabelle (mm_...) enthalten.
SELECT DISTINCT COUNT(room3.id)
Der Trick hierbei war der Einsatz von GROUP BY, HAVING und COUNT. Klappte wunderbar, so lange ich nicht versucht habe die RoomFacilities mit einzubeziehen.
Bei dem DISTINCT COUNT(room3.id) sieht mir das eher nach Zufall aus, aber ich analysier das mal nicht weiter.
Mein aktueller Ansatz [...]
WHERE
-- resort facilities
(
SELECT
COUNT(id)
FROM
mm_resort_facility
[...]
) = 3
Was sagen denn die Ergebnisse der Subselects? Führten sie dich deiner Meinung nach in Richtung Ziel? Die Anzahl ist eigentlich nur für das Finden der passenden Datensätze interessant, ansonsten willst du doch eher die IDs selbst wissen.
Lo!
Hi Dedlifix,
[] zeigen, dass du auch noch ein Aliasnamenfindungsproblem hast.
Na, das ist doch zur Abwechslung mal ein recht amuesantes Problem ;-p
Zusammengebaut ergibt sich:
(ein wenig veraendert)
SELECT id, name
FROM
(
SELECT re.id, re.name
FROM resort re
JOIN mm_resort_facility mref ON mref.resort_id = re.id
WHERE mref.resort_facility_id IN (1,2,3,4,5,6,7)
GROUP BY re.id, re.name
HAVING COUNT(*) = 7
) AS resorts
WHERE resorts.id IN (
(SELECT DISTINCT ro.resort_id
FROM room ro
JOIN mm_room_facility mrof ON mrof.room_id = ro.id
WHERE mrof.room_facility_id IN (1, 2, 4)
GROUP BY mrof.room_id, ro.id
HAVING COUNT(*) = 3
))
Heiliger Bimbam - das funktioniert! Ich bin begeistert! Echt jetzt!
Aber kannst du mir mal folgendes Konstrukt erklaeren:
SELECT id, name
FROM
(
SELECT re.id, re.name
-- [..]
Was genau hat es mit dem FROM-Subselect auf sich?
Tausend Dank!!
Peter
Hi!
Was genau hat es mit dem FROM-Subselect auf sich?
Ein SELECT liefert eine Ergebnismenge. Man kann nicht nur eine Tabelle als Datenquelle verwenden, sondern auch eine solche Ergebnismenge.
Führ die Teilabfragen man einzeln aus, so wie ich sie dir auch hergeletet habe. Dann siehst du vielleicht besser, welche Zwischenergebnisse entstehen und wie sich das dann fügt, um zum Endergebnis zu kommen.
Lo!
Hi Dedlifix,
ich bin nun dabei den Auszug in das gesamte Query zu uebernehmen.
Hierbei stosze ich auf das Problem, dass das "SELECT id.. FROM ( SELECT
" Fehlermeldungen wirft, dass die Feldnamen nicht eindeutig seien (#1052 - Column 'id' in field list is ambiguous) - das ist natuerlich korrekt, da ich das Statement noch um einige Joins etc. erweitert habe.
Gebe ich den Felder des From-Subselects einen Alias, dann funktioniert es.
Jetzt mal rein interessenhalber:
Gibt es hierfuer eine elgantere Loesung, als den Spalten des Subselects Aliase zu vergeben, und diese dann wieder in dem eigentlichen Select auf die urspruenglichen Namen zu mappen?
Ist jetzt ein wenig kompliziert ausgedrueckt, aber folgendes Beispiel sollte es veranschaulichen:
SELECT DISTINCT SQL_CALC_FOUND_ROWS
r_id AS id, r_name AS name
txt3.en AS resort_type_name,
( SELECT count(id) FROM reservation WHERE resort_id = r_id ) as reservation_count,
( SELECT AVG(vote_overall) FROM rating WHERE resort_id = r_id ) as votes_overall
FROM
(
SELECT
id AS r_id , name AS r_name,
resort_type_id AS r_resort_type
FROM
resort
-- [..]
) AS resorts
LEFT JOIN
resort_type ON r_resort_type = resort_type.id
LEFT JOIN
textsource AS txt3 ON resort_type.textsource_id = txt3.id
Danke & MfG
Peter
Hi!
#1052 - Column 'id' in field list is ambiguous
Gibt es hierfuer eine elgantere Loesung, als den Spalten des Subselects Aliase zu vergeben, und diese dann wieder in dem eigentlichen Select auf die urspruenglichen Namen zu mappen?
Alle Feldnamen müssen selbst bis ins letzte Subselect eindeutig sein, denn es gibt schließlich korrelierte Subselects, bei denen im Subselect auf äußere Felder zugegriffen werden kann. Im allgemeinen reicht es, einen eindeutigen Alias für Tabellennamen (und Subselect-Ergebnismengen) zu vergeben und diesen dann bei den Feldern mit anzugeben. Dann weiß man auch eindeutig, welches Feld du im Sinn hattest, als du es notiertest.
Lo!
Hi!
Im allgemeinen reicht es, einen eindeutigen Alias für Tabellennamen (und Subselect-Ergebnismengen) zu vergeben und diesen dann bei den Feldern mit anzugeben.
Ahh, da bin ich nicht drauf gekommen.
Danke!
Peter