Mysql
 sql >> Base de données >  >> RDS >> Mysql

Tri MYSQL par distance HAVING mais pas capable de regrouper?

Je ne crois pas qu'un GROUP BY va vous donner le résultat que vous voulez. Et malheureusement, MySQL ne prend pas en charge les fonctions analytiques (c'est ainsi que nous résoudrions ce problème dans Oracle ou SQL Server.)

Il est possible d'émuler certaines fonctions analytiques rudimentaires en utilisant des variables définies par l'utilisateur.

Dans ce cas, nous voulons émuler :

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

Donc, en commençant par la requête d'origine, j'ai changé ORDER BY pour qu'il trie sur doctor_id d'abord, puis sur la distance calculée . (Jusqu'à ce que nous connaissions ces distances, nous ne savons pas laquelle est "la plus proche".)

Avec ce résultat trié, nous "numérotons" essentiellement les lignes pour chaque doctor_id, la plus proche comme 1, la seconde la plus proche comme 2, et ainsi de suite. Lorsque nous obtenons un nouveau doctor_id, nous recommençons avec le plus proche comme 1.

Pour ce faire, nous utilisons des variables définies par l'utilisateur. Nous en utilisons un pour attribuer le numéro de ligne (le nom de la variable est @i et la colonne renvoyée a l'alias seq). L'autre variable que nous utilisons pour "se souvenir" du doctor_id de la ligne précédente, afin que nous puissions détecter une "rupture" dans le doctor_id, afin que nous puissions savoir quand recommencer la numérotation des lignes à 1.

Voici la requête :

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

Je suppose que la requête d'origine renvoie le jeu de résultats dont vous avez besoin, qu'elle contient trop de lignes et que vous souhaitez éliminer tout sauf le "plus proche" (la ligne avec la valeur minimale de distance) pour chaque doctor_id.

J'ai enveloppé votre requête d'origine dans une autre requête ; les seules modifications que j'ai apportées à la requête d'origine ont été de classer les résultats par doctor_id puis par distance, et de supprimer la HAVING distance < 50 clause. (Si vous ne voulez renvoyer que des distances inférieures à 50, alors continuez et laissez cette clause là. Il n'était pas clair si c'était votre intention ou si cela avait été spécifié dans une tentative de limiter les lignes à une par doctor_id.)

Quelques problèmes à noter :

La requête de remplacement renvoie deux colonnes supplémentaires ; ceux-ci ne sont pas vraiment nécessaires dans le jeu de résultats, sauf comme moyen de générer le jeu de résultats. (Il est possible d'envelopper à nouveau tout ce SELECT dans un autre SELECT pour omettre ces colonnes, mais c'est vraiment plus compliqué que ça ne vaut la peine. Je voudrais simplement récupérer les colonnes et savoir que je peux les ignorer.)

L'autre problème est que l'utilisation du .* dans la requête interne est un peu dangereux, dans la mesure où nous devons vraiment garantir que les noms de colonne renvoyés par cette requête sont uniques. (Même si les noms de colonne sont distincts pour le moment, l'ajout d'une colonne à l'une de ces tables pourrait introduire une exception de colonne "ambigüe" dans la requête. Il est préférable d'éviter cela, et cela est facilement résolu en remplaçant le .* avec la liste des colonnes à renvoyer et en spécifiant un alias pour tout nom de colonne "dupliqué". (L'utilisation du z.* dans la requête externe n'est pas un problème, tant que nous contrôlons les colonnes renvoyées par z .)

Avenant :

J'ai noté qu'un GROUP BY n'allait pas vous donner l'ensemble de résultats dont vous aviez besoin. Bien qu'il soit possible d'obtenir le jeu de résultats avec une requête utilisant GROUP BY, une instruction qui renvoie le jeu de résultats CORRECT serait fastidieuse. Vous pouvez spécifier MIN(distance) ... GROUP BY doctor_id , et cela vous donnerait la plus petite distance, MAIS il n'y a aucune garantie que les autres expressions non agrégées de la liste SELECT proviendraient de la ligne avec la distance minimale, et non d'une autre ligne. (MySQL est dangereusement libéral en ce qui concerne GROUP BY et les agrégats. Pour que le moteur MySQL soit plus prudent (et en ligne avec les autres moteurs de bases de données relationnelles), SET sql_mode = ONLY_FULL_GROUP_BY

Addendum 2 :

Problèmes de performances signalés par Darious "certaines requêtes prennent 7 secondes."

Pour accélérer les choses, vous souhaiterez probablement mettre en cache les résultats de la fonction. Fondamentalement, créez une table de correspondance. 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

C'est juste une idée. (Je suppose que vous recherchez la distance office_location à partir d'un code postal particulier, donc l'index sur (code postal, gc_distance, office_location_id) est l'index de couverture dont votre requête aurait besoin. (J'éviterais de stocker la distance calculée en tant que FLOAT, en raison d'une mauvaise performances des requêtes avec le type de données 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)

Avec les résultats de la fonction mis en cache et indexés, vos requêtes devraient être beaucoup plus rapides.

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

J'hésite à ajouter un prédicat HAVING sur INSERT/UPDATE à la table de cache ; (si vous aviez une mauvaise latitude/longitude et que vous aviez calculé une distance erronée inférieure à 100 milles ; une course ultérieure après que la lat/long est fixée et que la distance correspond à 1 000 milles... si la ligne est exclue de la requête, alors la ligne existante dans la table de cache ne sera pas mise à jour. (Vous pouvez effacer la table de cache, mais ce n'est pas vraiment nécessaire, c'est juste beaucoup de travail supplémentaire pour la base de données et les journaux. Si le jeu de résultats de la requête de maintenance est trop grand, il pourrait être décomposé pour s'exécuter de manière itérative pour chaque code postal ou chaque office_location.)

D'autre part, si vous n'êtes pas intéressé par les distances supérieures à une certaine valeur, vous pouvez ajouter le HAVING gc_distance < prédicat et réduisez considérablement la taille de la table de cache.