sql >> Database teknologi >  >> RDS >> Mysql

MYSQL sortering efter at have afstand, men ikke i stand til at gruppere?

Jeg tror ikke på, at en GROUP BY vil give dig det resultat, du ønsker. Og desværre understøtter MySQL ikke analytiske funktioner (hvilket er sådan, vi ville løse dette problem i Oracle eller SQL Server.)

Det er muligt at efterligne nogle rudimentære analytiske funktioner ved at gøre brug af brugerdefinerede variabler.

I dette tilfælde ønsker vi at efterligne:

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

Så startende med den oprindelige forespørgsel ændrede jeg ORDER BY, så den sorterer på doctor_id først og derefter på den beregnede afstand . (Indtil vi kender disse afstande, ved vi ikke, hvilken der er "nærmest".)

Med dette sorterede resultat "nummererer" vi som udgangspunkt rækkerne for hver læge_id, den nærmeste som 1, den næstnærmeste som 2, og så videre. Når vi får et nyt læge-id, starter vi igen med den nærmeste som 1.

For at opnå dette gør vi brug af brugerdefinerede variabler. Vi bruger en til at tildele rækkenummeret (variabelnavnet er @i, og den returnerede kolonne har aliaset seq). Den anden variabel bruger vi til at "huske" læge-id'et fra den forrige række, så vi kan registrere et "brud" i læge-id'et, så vi kan vide, hvornår vi skal genstarte rækkenummereringen ved 1 igen.

Her er forespørgslen:

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

Jeg antager, at den oprindelige forespørgsel returnerer det resultatsæt, du har brug for, den har bare for mange rækker, og du vil eliminere alle undtagen den "nærmeste" (rækken med minimumsværdien afstand) for hver læge_id.

Jeg har pakket din oprindelige forespørgsel ind i en anden forespørgsel; de eneste ændringer, jeg lavede i den oprindelige forespørgsel, var at bestille resultaterne efter doctor_id og derefter efter distance, og at fjerne HAVING distance <50 klausul. (Hvis du kun ønsker at returnere afstande mindre end 50, så fortsæt og lad den klausul stå der. Det var ikke klart, om det var din hensigt, eller om det var angivet i et forsøg på at begrænse rækker til én pr. doctor_id.)

Et par problemer at bemærke:

Erstatningsforespørgslen returnerer to yderligere kolonner; disse er egentlig ikke nødvendige i resultatsættet, undtagen som midler til at generere resultatsættet. (Det er muligt at pakke hele denne SELECT igen i en anden SELECT for at udelade disse kolonner, men det er virkelig mere rodet, end det er værd. Jeg ville bare hente kolonnerne og vide, at jeg kan ignorere dem.)

Det andet problem er, at brugen af ​​.* i den indre forespørgsel er lidt farlig, da vi virkelig skal garantere, at kolonnenavnene, der returneres af forespørgslen, er unikke. (Selv hvis kolonnenavnene er forskellige lige nu, kan tilføjelsen af ​​en kolonne til en af ​​disse tabeller introducere en "tvetydig" kolonneundtagelse i forespørgslen. Det er bedst at undgå det, og det løses nemt ved at erstatte . * med listen over kolonner, der skal returneres, og angivelse af et alias for ethvert "dublet" kolonnenavn. (Brugen af ​​z.* i den ydre forespørgsel er ikke et problem, så længe vi har kontrol over de kolonner, der returneres af z .)

Tillæg:

Jeg bemærkede, at en GROUP BY ikke ville give dig det resultatsæt, du havde brug for. Selvom det ville være muligt at få resultatsættet med en forespørgsel ved hjælp af GROUP BY, ville en sætning, der returnerer det KORREKT resultatsæt, være kedelig. Du kan angive MIN(distance) ... GROUP BY doctor_id , og det ville give dig den mindste afstand, MEN der er ingen garanti for, at de andre ikke-aggregerede udtryk i SELECT-listen ville være fra rækken med minimumsafstanden og ikke en anden række. (MySQL er farligt liberalt med hensyn til GROUP BY og aggregater. For at få MySQL-motoren til at være mere forsigtig (og på linje med andre relationelle databasemotorer), SET sql_mode =ONLY_FULL_GROUP_BY

Tillæg 2:

Ydeevneproblemer rapporteret af Darious "nogle forespørgsler tager 7 sekunder."

For at fremskynde tingene, vil du sandsynligvis cache resultaterne af funktionen. Grundlæggende skal du bygge en opslagstabel. f.eks.

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 er bare en idé. (Jeg forventer, at du søger efter office_location afstand fra et bestemt postnummer, så indekset på (zipcode, gc_distance, office_location_id) er det dækkende indeks, din forespørgsel skal bruge. (Jeg ville undgå at gemme den beregnede afstand som en FLOAT på grund af dårlig forespørgselsydeevne med FLOAT-datatype)

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 funktionsresultaterne cachelagret og indekseret, burde dine forespørgsler være meget hurtigere.

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

Jeg er tøvende med at tilføje et HAVING-prædikat på INSERT/UPDATE til cache-tabellen; (hvis du havde en forkert breddegrad/længdegrad og havde beregnet en fejlagtig distance under 100 miles; et efterfølgende løb efter breddegrad/længde er fastsat, og distancen regner ud til 1000 miles... hvis rækken er udelukket fra forespørgslen, så bliver eksisterende række i cache-tabellen ikke opdateret. (Du kan rydde cache-tabellen, men det er egentlig ikke nødvendigt, det er bare en masse ekstra arbejde for databasen og logfilerne. Hvis resultatet af vedligeholdelsesforespørgslen er for meget stor, kan den opdeles til at køre iterativt for hvert postnummer eller hver office_location.)

På den anden side, hvis du ikke er interesseret i nogen afstande over en bestemt værdi, kan du tilføje HAVING gc_distance < prædikat, og skær størrelsen af ​​cache-tabellen betydeligt ned.



  1. Få den aktuelle AUTO_INCREMENT-værdi for enhver tabel

  2. Hvordan henter man felttype og værdi?

  3. Problemer med datosubtraktion i Oracle

  4. Flet en tabel og en ændringslog til en visning i PostgreSQL