Ik geloof niet dat een GROUP BY je het gewenste resultaat zal geven. En helaas ondersteunt MySQL geen analytische functies (zo zouden we dit probleem in Oracle of SQL Server oplossen.)
Het is mogelijk om enkele rudimentaire analytische functies te emuleren door gebruik te maken van door de gebruiker gedefinieerde variabelen.
In dit geval willen we emuleren:
ROW_NUMBER() OVER(PARTITION BY doctor_id ORDER BY distance ASC) AS seq
Dus, beginnend met de oorspronkelijke zoekopdracht, heb ik de ORDER BY gewijzigd zodat deze sorteert op doctor_id
eerst, en dan op de berekende distance
. (Totdat we die afstanden kennen, weten we niet welke het "dichtstbijzijnde" is.)
Met dit gesorteerde resultaat "nummeren" we in feite de rijen voor elke doctor_id, de dichtstbijzijnde als 1, de tweede dichtstbijzijnde als 2, enzovoort. Wanneer we een nieuwe doctor_id krijgen, beginnen we opnieuw met de dichtstbijzijnde als 1.
Om dit te bereiken maken we gebruik van door de gebruiker gedefinieerde variabelen. We gebruiken er een voor het toewijzen van het rijnummer (de naam van de variabele is @i en de geretourneerde kolom heeft de alias seq). De andere variabele gebruiken we om de doctor_id van de vorige rij te "onthouden", zodat we een "break" in de doctor_id kunnen detecteren, zodat we weten wanneer we de rijnummering opnieuw op 1 moeten starten.
Dit is de vraag:
SELECT z.*
, @i := CASE WHEN z.doctor_id = @prev_doctor_id THEN @i + 1 ELSE 1 END AS seq
, @prev_doctor_id := z.doctor_id AS prev_doctor_id
FROM
(
/* original query, ordered by doctor_id and then by distance */
SELECT zip,
( 3959 * acos( cos( radians(34.12520) ) * cos( radians( zip_info.latitude ) ) * cos(radians( zip_info.longitude ) - radians(-118.29200) ) + sin( radians(34.12520) ) * sin( radians( zip_info.latitude ) ) ) ) AS distance,
user_info.*, office_locations.*
FROM zip_info
RIGHT JOIN office_locations ON office_locations.zipcode = zip_info.zip
RIGHT JOIN user_info ON office_locations.doctor_id = user_info.id
WHERE user_info.status='yes'
ORDER BY user_info.doctor_id ASC, distance ASC
) z JOIN (SELECT @i := 0, @prev_doctor_id := NULL) i
HAVING seq = 1 ORDER BY z.distance
Ik ga ervan uit dat de originele query de resultaatset retourneert die je nodig hebt, het heeft gewoon te veel rijen en je wilt alles verwijderen behalve de "dichtstbijzijnde" (de rij met de minimale waarde van afstand) voor elke doctor_id.
Ik heb uw oorspronkelijke vraag in een andere vraag gewikkeld; de enige wijzigingen die ik aan de oorspronkelijke zoekopdracht heb aangebracht, was om de resultaten te ordenen op doctor_id en vervolgens op afstand, en om de HAVING distance < 50
te verwijderen clausule. (Als je alleen afstanden van minder dan 50 wilt retourneren, ga je gang en laat die clausule daar staan. Het was niet duidelijk of dat je bedoeling was, of dat dat was gespecificeerd in een poging om rijen te beperken tot één per doctor_id.)
Een paar aandachtspunten:
De vervangende query retourneert twee extra kolommen; deze zijn niet echt nodig in de resultatenset, behalve als middel om de resultatenset te genereren. (Het is mogelijk om deze hele SELECT opnieuw in een andere SELECT te plaatsen om die kolommen weg te laten, maar dat is echt rommeliger dan het waard is. Ik zou gewoon de kolommen ophalen en weten dat ik ze kan negeren.)
Het andere probleem is dat het gebruik van de .*
in de inner query is een beetje gevaarlijk, omdat we echt moeten garanderen dat de kolomnamen die door die query worden geretourneerd, uniek zijn. (Zelfs als de kolomnamen op dit moment verschillend zijn, kan de toevoeging van een kolom aan een van die tabellen een "dubbelzinnige" kolomuitzondering in de query introduceren. Het is het beste om dat te vermijden, en dat is gemakkelijk te verhelpen door de .*
met de lijst met te retourneren kolommen en het specificeren van een alias voor elke "duplicaat" kolomnaam. (Het gebruik van de z.*
in de buitenste query is geen probleem, zolang we de controle hebben over de kolommen die worden geretourneerd door z
.)
Aanvulling:
Ik merkte op dat een GROUP BY niet de gewenste resultatenset zou opleveren. Hoewel het mogelijk zou zijn om de resultatenset te krijgen met een query met GROUP BY, zou een instructie die de CORRECT-resultatenset retourneert, vervelend zijn. Je zou kunnen specificeren MIN(distance) ... GROUP BY doctor_id
, en dat zou je de kleinste afstand opleveren, MAAR er is geen garantie dat de andere niet-geaggregeerde expressies in de SELECT-lijst uit de rij met de minimale afstand zouden komen, en niet uit een andere rij. (MySQL is gevaarlijk liberaal met betrekking tot GROUP BY en aggregaten. Om de MySQL-engine voorzichtiger te maken (en in lijn met andere relationele database-engines), SET sql_mode = ONLY_FULL_GROUP_BY
Aanvulling 2:
Prestatieproblemen gemeld door Darious "sommige zoekopdrachten duren 7 seconden."
Om de zaken te versnellen, wilt u waarschijnlijk de resultaten van de functie in de cache opslaan. Kortom, bouw een opzoektabel. bijv.
CREATE TABLE office_location_distance
( office_location_id INT UNSIGNED NOT NULL COMMENT 'PK, FK to office_location.id'
, zipcode_id INT UNSIGNED NOT NULL COMMENT 'PK, FK to zipcode.id'
, gc_distance DECIMAL(18,2) COMMENT 'calculated gc distance, in miles'
, PRIMARY KEY (office_location_id, zipcode_id)
, KEY (zipcode_id, gc_distance, office_location_id)
, CONSTRAINT distance_lookup_office_FK
FOREIGN KEY (office_location_id) REFERENCES office_location(id)
ON UPDATE CASCADE ON DELETE CASCADE
, CONSTRAINT distance_lookup_zipcode_FK
FOREIGN KEY (zipcode_id) REFERENCES zipcode(id)
ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB
Dat is maar een idee. (Ik verwacht dat u zoekt naar office_location-afstand vanaf een bepaalde postcode, dus de index op (zipcode, gc_distance, office_location_id) is de dekkingsindex die uw zoekopdracht nodig heeft. (Ik zou vermijden om de berekende afstand op te slaan als een FLOAT, vanwege slechte queryprestaties met FLOAT-gegevenstype)
INSERT INTO office_location_distance (office_location_id, zipcode_id, gc_distance)
SELECT d.office_location_id
, d.zipcode_id
, d.gc_distance
FROM (
SELECT l.id AS office_location_id
, z.id AS zipcode_id
, ROUND( <glorious_great_circle_calculation> ,2) AS gc_distance
FROM office_location l
CROSS
JOIN zipcode z
ORDER BY 1,3
) d
ON DUPLICATE KEY UPDATE gc_distance = VALUES(gc_distance)
Met de functieresultaten in het cachegeheugen en geïndexeerd, zouden uw zoekopdrachten veel sneller moeten zijn.
SELECT d.gc_distance, o.*
FROM office_location o
JOIN office_location_distance d ON d.office_location_id = o.id
WHERE d.zipcode_id = 63101
AND d.gc_distance <= 100.00
ORDER BY d.zipcode_id, d.gc_distance
Ik aarzel over het toevoegen van een predikaat HAVING op de INSERT/UPDATE aan de cachetabel; (als u een verkeerde breedtegraad/lengtegraad had en een foutieve afstand onder 100 mijl had berekend; een volgende run nadat de lat/long is vastgesteld en de afstand komt uit op 1000 mijl... als de rij is uitgesloten van de zoekopdracht, dan wordt de bestaande rij in de cachetabel niet bijgewerkt (Je zou de cachetabel kunnen wissen, maar dat is niet echt nodig, dat is gewoon veel extra werk voor de database en logs. Als de resultatenset van de onderhoudsquery te groot is, kan het worden opgesplitst om iteratief te worden uitgevoerd voor elke postcode of elke kantoorlocatie.)
Aan de andere kant, als je niet geïnteresseerd bent in afstanden boven een bepaalde waarde, kun je de HAVING gc_distance <
toevoegen predikaat, en de grootte van de cachetabel aanzienlijk verkleinen.