Mysql
 sql >> Datenbank >  >> RDS >> Mysql

MYSQL-Sortierung nach Entfernung, aber keine Gruppierung möglich?

Ich glaube nicht, dass ein GROUP BY Ihnen das gewünschte Ergebnis liefern wird. Und leider unterstützt MySQL keine Analysefunktionen (so würden wir dieses Problem in Oracle oder SQL Server lösen).

Es ist möglich, einige rudimentäre Analysefunktionen zu emulieren, indem benutzerdefinierte Variablen verwendet werden.

In diesem Fall wollen wir emulieren:

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

Beginnend mit der ursprünglichen Abfrage habe ich also ORDER BY so geändert, dass es nach doctor_id sortiert wird zuerst und dann auf der berechneten distance . (Bis wir diese Entfernungen kennen, wissen wir nicht, welche "am nächsten" ist.)

Mit diesem sortierten Ergebnis "nummerieren" wir im Grunde die Zeilen für jede doctor_id, die nächste mit 1, die zweitnächste mit 2 und so weiter. Wenn wir eine neue doctor_id erhalten, beginnen wir wieder mit der nächstliegenden als 1.

Um dies zu erreichen, verwenden wir benutzerdefinierte Variablen. Wir verwenden eins, um die Zeilennummer zuzuweisen (der Variablenname ist @i, und die zurückgegebene Spalte hat den Alias ​​seq). Die andere Variable, die wir verwenden, um uns die doctor_id aus der vorherigen Zeile zu "erinnern", damit wir einen "Bruch" in der doctor_id erkennen können, damit wir wissen, wann wir die Zeilennummerierung wieder bei 1 beginnen müssen.

Hier ist die Abfrage:

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

Ich gehe davon aus, dass die ursprüngliche Abfrage die von Ihnen benötigte Ergebnismenge zurückgibt, sie hat einfach zu viele Zeilen und Sie möchten alle außer der "nächsten" (der Zeile mit dem minimalen Abstandswert) für jede doctor_id eliminieren.

Ich habe Ihre ursprüngliche Abfrage in eine andere Abfrage verpackt; Die einzigen Änderungen, die ich an der ursprünglichen Abfrage vorgenommen habe, bestanden darin, die Ergebnisse nach Arzt-ID und dann nach Entfernung zu ordnen und den HAVING distance < 50 zu entfernen Klausel. (Wenn Sie nur Entfernungen von weniger als 50 zurückgeben möchten, lassen Sie diese Klausel dort. Es war nicht klar, ob dies Ihre Absicht war oder ob dies in einem Versuch angegeben wurde, die Zeilen auf eine pro Arzt-ID zu beschränken.)

Einige Punkte, die Sie beachten sollten:

Die Ersetzungsabfrage gibt zwei zusätzliche Spalten zurück; Diese werden in der Ergebnismenge nicht wirklich benötigt, außer um die Ergebnismenge zu generieren. (Es ist möglich, dieses ganze SELECT erneut in ein anderes SELECT zu packen, um diese Spalten wegzulassen, aber das ist wirklich unordentlicher, als es wert ist. Ich würde einfach die Spalten abrufen und wissen, dass ich sie ignorieren kann.)

Das andere Problem ist, dass die Verwendung von .* in der inneren Abfrage ist etwas gefährlich, da wir wirklich garantieren müssen, dass die von dieser Abfrage zurückgegebenen Spaltennamen eindeutig sind. (Auch wenn die Spaltennamen im Moment unterschiedlich sind, könnte das Hinzufügen einer Spalte zu einer dieser Tabellen eine "mehrdeutige" Spaltenausnahme in die Abfrage einführen. Das sollte man am besten vermeiden, und das lässt sich leicht beheben, indem man den .* mit der Liste der zurückzugebenden Spalten und Angabe eines Alias ​​für jeden „doppelten“ Spaltennamen. (Die Verwendung des z.* in der äußeren Abfrage spielt keine Rolle, solange wir die Kontrolle über die von z zurückgegebenen Spalten haben .)

Nachtrag:

Ich habe festgestellt, dass ein GROUP BY Ihnen nicht die gewünschte Ergebnismenge liefern würde. Während es möglich wäre, die Ergebnismenge mit einer Abfrage mit GROUP BY zu erhalten, wäre eine Anweisung, die die KORREKTE Ergebnismenge zurückgibt, mühsam. Sie könnten MIN(distance) ... GROUP BY doctor_id angeben , und das würde Ihnen den kleinsten Abstand bringen, ABER es gibt keine Garantie dafür, dass die anderen nicht aggregierten Ausdrücke in der SELECT-Liste aus der Zeile mit dem minimalen Abstand stammen und nicht aus einer anderen Zeile. (MySQL ist in Bezug auf GROUP BY und Aggregate gefährlich liberal. Um die MySQL-Engine dazu zu bringen, vorsichtiger zu sein (und sich an andere relationale Datenbank-Engines anzupassen), SET sql_mode = ONLY_FULL_GROUP_BY

Nachtrag 2:

Von Darious gemeldete Leistungsprobleme "einige Abfragen dauern 7 Sekunden."

Um die Dinge zu beschleunigen, möchten Sie wahrscheinlich die Ergebnisse der Funktion zwischenspeichern. Erstellen Sie im Grunde eine Nachschlagetabelle. z. B.

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

Das ist nur eine Idee. (Ich gehe davon aus, dass Sie nach der Entfernung von office_location von einer bestimmten Postleitzahl suchen, daher ist der Index auf (zipcode, gc_distance, office_location_id) der abdeckende Index, den Ihre Abfrage benötigen würde. (Ich würde es vermeiden, die berechnete Entfernung als FLOAT zu speichern, da dies der Fall ist.) Abfrageleistung mit FLOAT-Datentyp)

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)

Wenn die Funktionsergebnisse zwischengespeichert und indiziert sind, sollten Ihre Abfragen viel schneller sein.

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

Ich zögere, der Cache-Tabelle ein HAVING-Prädikat für INSERT/UPDATE hinzuzufügen. (wenn Sie einen falschen Breiten-/Längengrad hatten und eine falsche Entfernung unter 100 Meilen berechnet hatten; ein anschließender Lauf nach Breiten-/Längengrad ist behoben und die Entfernung ergibt 1000 Meilen ... wenn die Zeile von der Abfrage ausgeschlossen wird, dann wird die vorhandene Zeile in der Cache-Tabelle nicht aktualisiert. (Sie könnten die Cache-Tabelle löschen, aber das ist nicht wirklich notwendig, das ist nur eine Menge zusätzlicher Arbeit für die Datenbank und die Protokolle. Wenn die Ergebnismenge der Wartungsabfrage auch ist groß, könnte es aufgeschlüsselt werden, um iterativ für jede Postleitzahl oder jeden Bürostandort ausgeführt zu werden.)

Wenn Sie andererseits an Entfernungen über einem bestimmten Wert nicht interessiert sind, können Sie den HAVING gc_distance < hinzufügen Prädikat, und reduzieren Sie die Größe der Cache-Tabelle erheblich.