sql >> Databasteknik >  >> RDS >> Mysql

MYSQL sortering efter HA avstånd men inte kunna gruppera?

Jag tror inte att en GROUP BY kommer att ge dig det resultat du vill ha. Och tyvärr stöder MySQL inte analytiska funktioner (vilket är hur vi skulle lösa det här problemet i Oracle eller SQL Server.)

Det är möjligt att emulera vissa rudimentära analytiska funktioner genom att använda användardefinierade variabler.

I det här fallet vill vi efterlikna:

ROW_NUMBER() OVER(PARTITION BY doctor_id ORDER BY distance ASC) AS seq

Så, med början med den ursprungliga frågan, ändrade jag ORDER BY så att den sorteras på doctor_id först och sedan på det beräknade avståndet . (Tills vi vet de avstånden vet vi inte vilken som är "närmast".)

Med detta sorterade resultat "numrerar" vi i princip raderna för varje doctor_id, den närmaste som 1, den näst närmaste som 2, och så vidare. När vi får ett nytt doctor_id börjar vi igen med den närmaste som 1.

För att åstadkomma detta använder vi oss av användardefinierade variabler. Vi använder en för att tilldela radnumret (variabelnamnet är @i, och den returnerade kolumnen har aliaset seq). Den andra variabeln använder vi för att "komma ihåg" doctor_id från föregående rad, så att vi kan upptäcka ett "break" i doctor_id, så att vi kan veta när vi ska starta om radnumreringen vid 1 igen.

Här är frågan:

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

Jag antar att den ursprungliga frågan returnerar den resultatuppsättning du behöver, den har helt enkelt för många rader och du vill eliminera alla utom de "närmaste" (raden med minsta avståndsvärde) för varje doctor_id.

Jag har raderat din ursprungliga fråga i en annan fråga; de enda ändringarna jag gjorde i den ursprungliga frågan var att beställa resultaten efter doctor_id och sedan efter distans, och att ta bort HAVING avstånd <50 klausul. (Om du bara vill returnera avstånd mindre än 50, fortsätt och lämna den klausulen där. Det var inte klart om det var din avsikt eller om det specificerades i ett försök att begränsa raderna till en per doctor_id.)

Ett par frågor att notera:

Ersättningsfrågan returnerar ytterligare två kolumner; dessa behövs egentligen inte i resultatuppsättningen, förutom som medel för att generera resultatuppsättningen. (Det är möjligt att slå in hela SELECT igen i en annan SELECT för att utelämna dessa kolumner, men det är verkligen mer rörigt än det är värt. Jag skulle bara hämta kolumnerna och veta att jag kan ignorera dem.)

Det andra problemet är att användningen av .* i den inre frågan är lite farlig, eftersom vi verkligen måste garantera att kolumnnamnen som returneras av den frågan är unika. (Även om kolumnnamnen är distinkta just nu, kan tillägget av en kolumn till en av dessa tabeller introducera ett "tvetydigt" kolumnundantag i frågan. Det är bäst att undvika det, och det åtgärdas enkelt genom att ersätta . * med listan över kolumner som ska returneras, och ange ett alias för ett "duplicerat" kolumnnamn. (Användningen av z.* i den yttre frågan är inget problem, så länge vi har kontroll över kolumnerna som returneras av z .)

Tillägg:

Jag noterade att en GROUP BY inte skulle ge dig den resultatuppsättning du behövde. Även om det skulle vara möjligt att få resultatuppsättningen med en fråga med GROUP BY, skulle en sats som returnerar den KORREKT resultatuppsättningen vara tråkig. Du kan ange MIN(distance) ... GROUP BY doctor_id , och det skulle ge dig det minsta avståndet, MEN det finns ingen garanti för att de andra icke-aggregerade uttrycken i SELECT-listan skulle vara från raden med minsta avstånd, och inte någon annan rad. (MySQL är farligt liberalt när det gäller GROUP BY och aggregat. För att få MySQL-motorn att vara mer försiktig (och i linje med andra relationsdatabasmotorer), SET sql_mode =ONLY_FULL_GROUP_BY

Tillägg 2:

Prestandaproblem som rapporterats av Darious "vissa frågor tar 7 sekunder."

För att snabba på saker och ting vill du antagligen cachelagra resultaten av funktionen. Bygg i princip en uppslagstabell. t.ex.

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

Det är bara en idé. (Jag förväntar mig att du söker efter office_location-avstånd från ett visst postnummer, så indexet på (zipcode, gc_distance, office_location_id) är det täckande indexet som din fråga skulle behöva. (Jag skulle undvika att lagra det beräknade avståndet som en FLOAT, på grund av dålig frågeprestanda med FLOAT-datatyp)

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)

Med funktionsresultaten cachade och indexerade bör dina frågor vara mycket snabbare.

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

Jag är tveksam till att lägga till ett HAVING-predikat på INSERT/UPDATE i cachetabellen; (om du hade fel latitud/longitud och hade beräknat ett felaktigt avstånd under 100 miles; en efterföljande löpning efter lat/long är fixerad och avståndet räknas ut till 1000 miles... om raden exkluderas från frågan, då kommer befintlig rad i cachetabellen inte att uppdateras. (Du kan rensa cachetabellen, men det är egentligen inte nödvändigt, det är bara mycket extra arbete för databasen och loggarna. Om resultatet av underhållsfrågan är för mycket stor, kan den brytas ned för att köras iterativt för varje postnummer eller varje office_location.)

Å andra sidan, om du inte är intresserad av några avstånd över ett visst värde kan du lägga till HAVING gc_distance < predikat och minska storleken på cachetabellen avsevärt.



  1. Liferay fungerar inte med MySQL

  2. Förhindra dropptabell vid målschema i Oracle Streams

  3. Hur kan jag använda jaro-winkler för att hitta det närmaste värdet i en tabell?

  4. Databasfråga för att söka med adress