sql >> Base de Datos >  >> RDS >> Mysql

¿MYSQL se clasifica por TENER distancia pero no se puede agrupar?

No creo que un GRUPO POR te vaya a dar el resultado que quieres. Y, lamentablemente, MySQL no admite funciones analíticas (que es la forma en que resolveríamos este problema en Oracle o SQL Server).

Es posible emular algunas funciones analíticas rudimentarias, haciendo uso de variables definidas por el usuario.

En este caso, queremos emular:

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

Entonces, comenzando con la consulta original, cambié ORDER BY para que se clasifique en doctor_id primero, y luego en la distance calculada . (Hasta que no sepamos esas distancias, no sabemos cuál es la "más cercana".)

Con este resultado ordenado, básicamente "numeramos" las filas para cada doctor_id, el más cercano como 1, el segundo más cercano como 2, y así sucesivamente. Cuando obtenemos un nuevo doctor_id, comenzamos de nuevo con el más cercano como 1.

Para lograr esto, hacemos uso de variables definidas por el usuario. Usamos uno para asignar el número de fila (el nombre de la variable es @i, y la columna devuelta tiene el alias seq). La otra variable que usamos para "recordar" el doctor_id de la fila anterior, para que podamos detectar una "ruptura" en el doctor_id, para que podamos saber cuándo reiniciar la numeración de la fila en 1 nuevamente.

Aquí está la consulta:

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

Estoy suponiendo que la consulta original está devolviendo el conjunto de resultados que necesita, simplemente tiene demasiadas filas y desea eliminar todas menos la "más cercana" (la fila con el valor mínimo de distancia) para cada doctor_id.

He envuelto su consulta original en otra consulta; los únicos cambios que hice en la consulta original fueron ordenar los resultados por doctor_id y luego por distancia, y eliminar HAVING distance < 50 cláusula. (Si solo desea devolver distancias inferiores a 50, continúe y deje esa cláusula allí. No estaba claro si esa era su intención o si se especificó en un intento de limitar las filas a una por doctor_id.)

Un par de cuestiones a tener en cuenta:

La consulta de reemplazo devuelve dos columnas adicionales; estos no son realmente necesarios en el conjunto de resultados, excepto como medio para generar el conjunto de resultados. (Es posible envolver todo este SELECT nuevamente en otro SELECT para omitir esas columnas, pero eso es realmente más complicado de lo que vale. Simplemente recuperaría las columnas y sabría que puedo ignorarlas).

El otro problema es que el uso de .* en la consulta interna es un poco peligroso, ya que realmente necesitamos garantizar que los nombres de columna devueltos por esa consulta sean únicos. (Incluso si los nombres de las columnas son distintos en este momento, la adición de una columna a una de esas tablas podría introducir una excepción de columna "ambigua" en la consulta. Es mejor evitar eso, y eso se soluciona fácilmente reemplazando el .* con la lista de columnas que se devolverán y especificando un alias para cualquier nombre de columna "duplicado". (El uso del z.* en la consulta externa no es una preocupación, siempre que tengamos el control de las columnas devueltas por z .)

Anexo:

Noté que GROUP BY no le daría el conjunto de resultados que necesitaba. Si bien sería posible obtener el conjunto de resultados con una consulta usando GROUP BY, una instrucción que devuelva el conjunto de resultados CORRECTO sería tediosa. Puede especificar MIN(distance) ... GROUP BY doctor_id , y eso le daría la distancia más pequeña, PERO no hay garantía de que las otras expresiones no agregadas en la lista SELECT sean de la fila con la distancia mínima, y ​​no de otra fila. (MySQL es peligrosamente liberal en lo que respecta a GROUP BY y agregados. Para que el motor de MySQL sea más cauteloso (y en línea con otros motores de bases de datos relacionales), SET sql_mode = ONLY_FULL_GROUP_BY

Anexo 2:

Problemas de rendimiento informados por Darious "algunas consultas tardan 7 segundos".

Para acelerar las cosas, probablemente desee almacenar en caché los resultados de la función. Básicamente, construye una tabla de búsqueda. por ejemplo

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

Eso es solo una idea. (Supongo que está buscando distancia de ubicación_oficina desde un código postal en particular, por lo que el índice en (código postal, distancia_gc, id_ubicación_oficina) es el índice de cobertura que necesitaría su consulta. (Evitaría almacenar la distancia calculada como FLOTANTE, debido a la mala rendimiento de consultas con tipo de datos 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 los resultados de la función almacenados en caché e indexados, sus consultas deberían ser mucho más rápidas.

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

Dudo en agregar un predicado HAVING en INSERT/UPDATE a la tabla de caché; (si tenía una latitud/longitud incorrecta y había calculado una distancia errónea por debajo de 100 millas; una carrera posterior después de que se corrija la latitud/longitud y la distancia resulte en 1000 millas... si la fila se excluye de la consulta, entonces la fila existente en la tabla de caché no se actualizará (puede borrar la tabla de caché, pero eso no es realmente necesario, eso es solo mucho trabajo adicional para la base de datos y los registros. Si el conjunto de resultados de la consulta de mantenimiento es demasiado grande, podría desglosarse para ejecutarse iterativamente para cada código postal o cada oficina_ubicación).

Por otro lado, si no está interesado en distancias superiores a un cierto valor, puede agregar el HAVING gc_distance < predicado y reducir considerablemente el tamaño de la tabla de caché.