Mysql
 sql >> Base de Dados >  >> RDS >> Mysql

MYSQL classificando por TER distância, mas não consegue agrupar?


Eu não acredito que um GROUP BY vai te dar o resultado que você quer. E, infelizmente, o MySQL não suporta funções analíticas (que é como resolveríamos esse problema no Oracle ou SQL Server).

É possível emular algumas funções analíticas rudimentares, fazendo uso de variáveis ​​definidas pelo usuário.

Neste caso, queremos emular:
ROW_NUMBER() OVER(PARTITION BY doctor_id ORDER BY distance ASC) AS seq

Então, começando com a consulta original, alterei o ORDER BY para que ele classifique em doctor_id primeiro e depois na distance calculada . (Até conhecermos essas distâncias, não sabemos qual é a "mais próxima".)

Com esse resultado classificado, basicamente "numeramos" as linhas para cada doctor_id, a mais próxima como 1, a segunda mais próxima como 2 e assim por diante. Quando obtemos um novo doctor_id, começamos novamente com o mais próximo como 1.

Para fazer isso, fazemos uso de variáveis ​​definidas pelo usuário. Usamos um para atribuir o número da linha (o nome da variável é @i, e a coluna retornada tem o alias seq). A outra variável que usamos para "lembrar" o doctor_id da linha anterior, para que possamos detectar uma "quebra" no doctor_id, para que possamos saber quando reiniciar a numeração da linha em 1 novamente.

Aqui está a 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

Estou supondo que a consulta original está retornando o conjunto de resultados que você precisa, ela tem muitas linhas e você deseja eliminar todas, exceto a "mais próxima" (a linha com o valor mínimo de distance) para cada doctor_id.

Envolvi sua consulta original em outra consulta; as únicas alterações que fiz na consulta original foram ordenar os resultados por doctor_id e depois por distância e remover a HAVING distance < 50 cláusula. (Se você deseja retornar apenas distâncias inferiores a 50, vá em frente e deixe essa cláusula lá. Não ficou claro se essa era sua intenção ou se isso foi especificado na tentativa de limitar as linhas a uma por doctor_id.)

Algumas questões a serem observadas:

A consulta de substituição retorna duas colunas adicionais; estes não são realmente necessários no conjunto de resultados, exceto como meio de gerar o conjunto de resultados. (É possível envolver todo este SELECT novamente em outro SELECT para omitir essas colunas, mas isso é realmente mais confuso do que vale a pena. Eu apenas recuperaria as colunas e saberia que posso ignorá-las.)

A outra questão é que o uso do .* na consulta interna é um pouco perigoso, pois realmente precisamos garantir que os nomes das colunas retornados por essa consulta sejam exclusivos. (Mesmo que os nomes das colunas sejam distintos agora, a adição de uma coluna a uma dessas tabelas pode introduzir uma exceção de coluna "ambígua" na consulta. É melhor evitar isso, e isso é facilmente resolvido substituindo o .* com a lista de colunas a serem retornadas e especificando um alias para qualquer nome de coluna "duplicado". (O uso do z.* na consulta externa não é uma preocupação, desde que estejamos no controle das colunas retornadas por z .)

Adendo:

Observei que um GROUP BY não forneceria o conjunto de resultados que você precisava. Embora seja possível obter o conjunto de resultados com uma consulta usando GROUP BY, uma instrução que retorna o conjunto de resultados CORRETO seria tediosa. Você pode especificar MIN(distance) ... GROUP BY doctor_id , e isso resultaria na menor distância, MAS não há garantia de que as outras expressões não agregadas na lista SELECT sejam da linha com a distância mínima e não de outra linha. (O MySQL é perigosamente liberal em relação ao GROUP BY e agregados. Para que o mecanismo MySQL seja mais cauteloso (e alinhado com outros mecanismos de banco de dados relacional), SET sql_mode = ONLY_FULL_GROUP_BY

Adendo 2:

Problemas de desempenho relatados por Darious "algumas consultas levam 7 segundos".

Para acelerar as coisas, você provavelmente deseja armazenar em cache os resultados da função. Basicamente, crie uma tabela de pesquisa. por exemplo.
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

Isso é apenas uma ideia. (Espero que você esteja pesquisando a distância office_location de um CEP específico, então o índice em (zipcode, gc_distance, office_location_id) é o índice de cobertura que sua consulta precisaria. (Eu evitaria armazenar a distância calculada como FLOAT, devido a desempenho da consulta com o tipo de dados 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)

Com os resultados da função armazenados em cache e indexados, suas consultas devem ser muito mais 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

Estou hesitante em adicionar um predicado HAVING no INSERT/UPDATE à tabela de cache; (se você tiver uma latitude/longitude errada e tiver calculado uma distância errada abaixo de 100 milhas; uma corrida subsequente após a latitude/longitude ser fixada e a distância for de 1000 milhas... se a linha for excluída da consulta, então a linha existente na tabela de cache não será atualizada. (Você pode limpar a tabela de cache, mas isso não é realmente necessário, é apenas muito trabalho extra para o banco de dados e os logs. Se o conjunto de resultados da consulta de manutenção for muito large, ele pode ser dividido para ser executado iterativamente para cada CEP ou cada office_location.)

Por outro lado, se você não estiver interessado em distâncias acima de um determinado valor, poderá adicionar o HAVING gc_distance < predicado e reduzir consideravelmente o tamanho da tabela de cache.