sql >> Databáze >  >> RDS >> Mysql

MYSQL řazení podle vzdálenosti, ale nemůžete seskupovat?

Nevěřím, že GROUP BY vám dá výsledek, jaký chcete. A bohužel MySQL nepodporuje analytické funkce (jak bychom tento problém vyřešili v Oracle nebo SQL Serveru.)

Je možné emulovat některé základní analytické funkce pomocí uživatelsky definovaných proměnných.

V tomto případě chceme emulovat:

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

Takže počínaje původním dotazem jsem změnil ORDER BY tak, aby se třídil podle id_doktora nejprve a poté na vypočítané vzdálenosti . (Dokud nebudeme znát tyto vzdálenosti, nevíme, která z nich je „nejbližší“.)

S tímto seřazeným výsledkem v podstatě „číslujeme“ řádky pro každé id_doktora, nejbližší jako 1, druhý nejbližší jako 2 a tak dále. Když získáme nové doctor_id, začneme znovu s nejbližším jako 1.

Abychom toho dosáhli, používáme uživatelem definované proměnné. Jedničku používáme pro přiřazení čísla řádku (název proměnné je @i a vrácený sloupec má alias seq). Druhá proměnná, kterou používáme k "zapamatování" doctor_id z předchozího řádku, abychom mohli detekovat "přerušení" v doctor_id, abychom věděli, kdy znovu začít číslování řádků na 1.

Zde je dotaz:

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

Vycházím z předpokladu, že původní dotaz vrací sadu výsledků, kterou potřebujete, má prostě příliš mnoho řádků a chcete odstranit všechny kromě „nejbližšího“ (řádek s minimální hodnotou vzdálenosti) pro každé id_doktora.

Váš původní dotaz jsem zabalil do jiného dotazu; jediné změny, které jsem provedl v původním dotazu, bylo seřadit výsledky podle doctor_id a poté podle vzdálenosti a odstranit HAVING vzdálenost <50 doložka. (Pokud chcete vrátit pouze vzdálenosti menší než 50, pak pokračujte a ponechte tam tuto klauzuli. Nebylo jasné, zda to byl váš záměr, nebo zda to bylo uvedeno ve snaze omezit počet řádků na jeden na ID_doktora.)

Je třeba poznamenat několik problémů:

Náhradní dotaz vrátí dva další sloupce; ty nejsou ve výsledkové sadě skutečně potřeba, s výjimkou prostředků pro generování výsledné sady. (Je možné celý tento SELECT znovu zabalit do jiného SELECTu, aby se tyto sloupce vynechaly, ale to je opravdu více chaotické, než by to stálo za to. Jen bych obnovil sloupce a věděl, že je mohu ignorovat.)

Dalším problémem je použití .* ve vnitřním dotazu je trochu nebezpečné, protože opravdu potřebujeme zaručit, že názvy sloupců vrácené tímto dotazem jsou jedinečné. (I když jsou názvy sloupců právě teď odlišné, přidání sloupce do jedné z těchto tabulek by mohlo způsobit „nejednoznačnou“ výjimku sloupce v dotazu. Tomu je nejlepší se vyhnout a lze to snadno vyřešit nahrazením . * se seznamem sloupců, které mají být vráceny, a určením aliasu pro jakýkoli "duplicitní" název sloupce. (Použití z.* ve vnějším dotazu není problém, pokud máme pod kontrolou sloupce vrácené z .)

Dodatek:

Všiml jsem si, že GROUP BY vám neposkytne sadu výsledků, kterou potřebujete. I když by bylo možné získat sadu výsledků pomocí dotazu pomocí GROUP BY, příkaz, který vrací sadu výsledků CORRECT, by byl únavný. Můžete zadat MIN(vzdálenost) ... GROUP BY doctor_id , a tím byste získali nejmenší vzdálenost, ALE neexistuje žádná záruka, že ostatní neagregované výrazy v seznamu SELECT budou z řádku s minimální vzdáleností a ne z nějakého jiného řádku. (MySQL je nebezpečně liberální, pokud jde o GROUP BY a agregáty. Aby byl motor MySQL opatrnější (a v souladu s jinými stroji relačních databází), SET sql_mode =ONLY_FULL_GROUP_BY

Dodatek 2:

Problémy s výkonem hlášené Dariousem „některé dotazy trvají 7 sekund.“

Chcete-li věci urychlit, pravděpodobně budete chtít uložit výsledky funkce do mezipaměti. V podstatě vytvořte vyhledávací tabulku. např.

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

To je jen nápad. (Očekávám, že hledáte vzdálenost office_location od konkrétního PSČ, takže index na (zipcode, gc_distance, office_location_id) je krycí index, který by váš dotaz potřeboval. (Vyhnul bych se ukládání vypočítané vzdálenosti jako FLOAT, kvůli špatné výkon dotazu s datovým typem 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)

S výsledky funkcí uloženými do mezipaměti a indexovanými by měly být vaše dotazy mnohem rychlejší.

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

Váhám nad přidáním predikátu HAVING na INSERT/UPDATE do tabulky mezipaměti; (pokud jste měli špatnou zeměpisnou šířku/délku a vypočítali jste chybnou vzdálenost pod 100 mil; následný běh po šířce/délce je opraven a vzdálenost je 1000 mil... pokud je řádek vyloučen z dotazu, pak se stávající řádek v tabulce mezipaměti neaktualizuje. (Můžete vymazat tabulku mezipaměti, ale to není ve skutečnosti nutné, je to jen spousta práce navíc pro databázi a protokoly. Pokud je výsledná sada dotazu na údržbu příliš velký, mohl by být rozdělen tak, aby se spouštěl iterativně pro každé PSČ nebo každé umístění_kanceláře.)

Na druhou stranu, pokud vás nezajímají žádné vzdálenosti nad určitou hodnotou, můžete přidat HAVING gc_distance < predikát a výrazně zmenšit velikost tabulky mezipaměti.



  1. Vytvořte skupinu Optgroup z pole dat

  2. Výjimka MySQL – při čtení dat došlo k závažné chybě

  3. Jak převést číslo na slova - ORACLE

  4. Jak přepnout databáze v postgresu?