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

Ordinamento MYSQL in base alla distanza AVENDO ma non in grado di raggruppare?

Non credo che un GROUP BY ti darà il risultato che desideri. E sfortunatamente, MySQL non supporta le funzioni analitiche (che è il modo in cui risolveremmo questo problema in Oracle o SQL Server.)

È possibile emulare alcune rudimentali funzioni analitiche, utilizzando variabili definite dall'utente.

In questo caso, vogliamo emulare:

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

Quindi, partendo dalla query originale, ho modificato ORDER BY in modo che venga ordinato su doctor_id prima, e poi sulla distance calcolata . (Finché non conosciamo quelle distanze, non sappiamo quale sia "più vicina".)

Con questo risultato ordinato, fondamentalmente "numeriamo" le righe per ogni doctor_id, la più vicina come 1, la seconda più vicina come 2 e così via. Quando otteniamo un nuovo doctor_id, ricominciamo con il più vicino come 1.

Per fare ciò, utilizziamo variabili definite dall'utente. Ne utilizziamo uno per assegnare il numero di riga (il nome della variabile è @i e la colonna restituita ha l'alias seq). L'altra variabile che utilizziamo per "ricordare" il doctor_id dalla riga precedente, in modo da poter rilevare una "interruzione" nel doctor_id, in modo da poter sapere quando ricominciare la numerazione delle righe da 1.

Ecco la domanda:

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

Suppongo che la query originale restituisca il set di risultati di cui hai bisogno, ha solo troppe righe e vuoi eliminare tutto tranne il "più vicino" (la riga con il valore minimo di distanza) per ogni doctor_id.

Ho racchiuso la tua query originale in un'altra query; le uniche modifiche che ho apportato alla query originale sono state ordinare i risultati per doctor_id e poi per distanza e rimuovere HAVING distance < 50 clausola. (Se vuoi restituire solo distanze inferiori a 50, vai avanti e lascia quella clausola lì. Non era chiaro se fosse il tuo intento o se fosse stato specificato nel tentativo di limitare le righe a una per doctor_id.)

Un paio di problemi da notare:

La query di sostituzione restituisce due colonne aggiuntive; questi non sono realmente necessari nel set di risultati, tranne che come mezzo per generare il set di risultati. (È possibile racchiudere nuovamente l'intero SELECT in un altro SELECT per omettere quelle colonne, ma è davvero più disordinato di quanto valga la pena. Recupererei semplicemente le colonne e saprei che posso ignorarle.)

L'altro problema è che l'uso di .* nella query interna è un po' pericoloso, in quanto abbiamo davvero bisogno di garantire che i nomi delle colonne restituiti da quella query siano univoci. (Anche se i nomi delle colonne sono distinti in questo momento, l'aggiunta di una colonna a una di quelle tabelle potrebbe introdurre un'eccezione di colonna "ambigua" nella query. È meglio evitarlo, ed è facilmente risolvibile sostituendo il .* con l'elenco delle colonne da restituire e specificando un alias per qualsiasi nome di colonna "duplicato". (L'uso di z.* nella query esterna non è un problema, fintanto che abbiamo il controllo delle colonne restituite da z .)

Addendum:

Ho notato che un GROUP BY non ti avrebbe dato il set di risultati di cui avevi bisogno. Sebbene sia possibile ottenere il set di risultati con una query utilizzando GROUP BY, un'istruzione che restituisce il set di risultati CORRETTO sarebbe noiosa. Puoi specificare MIN(distance) ... GROUP BY doctor_id , e ciò ti porterebbe alla distanza più piccola, MA non vi è alcuna garanzia che le altre espressioni non aggregate nell'elenco SELECT provengano dalla riga con la distanza minima e non da un'altra riga. (MySQL è pericolosamente liberale per quanto riguarda GROUP BY e aggregati. Per rendere il motore MySQL più cauto (e in linea con altri motori di database relazionali), SET sql_mode = ONLY_FULL_GROUP_BY

Addendum 2:

Problemi di prestazioni segnalati da Darious "alcune query richiedono 7 secondi".

Per velocizzare le cose, probabilmente vorrai memorizzare nella cache i risultati della funzione. Fondamentalmente, costruisci una tabella di ricerca. es.

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

È solo un'idea. (Mi aspetto che tu stia cercando la distanza di office_location da un particolare codice postale, quindi l'indice su (zipcode, gc_distance, office_location_id) è l'indice di copertura di cui la tua query avrebbe bisogno. (Eviterei di memorizzare la distanza calcolata come FLOAT, a causa della scarsa prestazioni della query con tipo di dati FLOAT)

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)

Con i risultati della funzione memorizzati nella cache e indicizzati, le tue query dovrebbero essere molto più veloci.

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

Sono riluttante ad aggiungere un predicato HAVING su INSERT/UPDATE alla tabella della cache; (se avevi una latitudine/longitudine sbagliata e avevi calcolato una distanza errata inferiore a 100 miglia; una corsa successiva dopo che la latitudine/longitudine è stata fissata e la distanza arriva a 1000 miglia... se la riga è esclusa dalla query, quindi la riga esistente nella tabella della cache non verrà aggiornata (potresti cancellare la tabella della cache, ma non è davvero necessario, è solo un sacco di lavoro extra per il database e i registri. Se il set di risultati della query di manutenzione è troppo grande, potrebbe essere scomposto per essere eseguito in modo iterativo per ogni codice postale o per ogni sede_ufficio.)

Se invece non sei interessato a distanze superiori a un certo valore, puoi aggiungere il HAVING gc_distance < predicato e ridurre considerevolmente le dimensioni della tabella della cache.