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.