Mysql
 sql >> база данни >  >> RDS >> Mysql

MYSQL сортира по HAVING разстояние, но не може да групира?

Не вярвам, че GROUP BY ще ви даде желания резултат. И за съжаление MySQL не поддържа аналитични функции (по какъв начин бихме решили този проблем в Oracle или SQL Server.)

Възможно е да се емулират някои елементарни аналитични функции, като се използват дефинирани от потребителя променливи.

В този случай искаме да емулираме:

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

И така, започвайки с оригиналната заявка, промених ORDER BY, така че да сортира по doctor_id първо, а след това върху изчисленото distance . (Докато не знаем тези разстояния, не знаем кое е „най-близко“.)

С този сортиран резултат ние основно "номерираме" редовете за всеки doctor_id, най-близкият като 1, вторият най-близък като 2 и т.н. Когато получим нов doctor_id, започваме отново с най-близкия като 1.

За да постигнем това, ние използваме променливи, дефинирани от потребителя. Използваме един за присвояване на номера на реда (името на променливата е @i, а върнатата колона има псевдоним seq). Другата променлива, която използваме, за да "запомним" doctor_id от предишния ред, така че можем да открием "пробив" в doctor_id, така че да знаем кога да рестартираме номерирането на реда от 1 отново.

Ето заявката:

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

Правя предположение, че оригиналната заявка връща набора от резултати, от който се нуждаете, просто има твърде много редове и искате да елиминирате всички освен „най-близкия“ (реда с минималната стойност на разстоянието) за всеки doctor_id.

Обвих оригиналната ви заявка в друга заявка; единствените промени, които направих в оригиналната заявка, бяха да подредя резултатите по doctor_id и след това по разстояние и да премахна HAVING distance < 50 клауза. (Ако искате да върнете само разстояния, по-малки от 50, продължете напред и оставете тази клауза там. Не беше ясно дали това е вашето намерение или това е посочено в опит да се ограничат редовете до един на doctor_id.)

Няколко въпроса, които трябва да имате предвид:

Заявката за заместване връща две допълнителни колони; те всъщност не са необходими в набора от резултати, освен като средство за генериране на набора от резултати. (Възможно е да обвиете целия този SELECT отново в друг SELECT, за да пропуснете тези колони, но това наистина е по-объркано, отколкото си струва. Просто бих изтеглил колоните и знам, че мога да ги игнорирам.)

Другият проблем е, че използването на .* във вътрешната заявка е малко опасно, тъй като наистина трябва да гарантираме, че имената на колоните, върнати от тази заявка, са уникални. (Дори ако имената на колоните са различни в момента, добавянето на колона към една от тези таблици може да въведе „двусмислено“ изключение на колона в заявката. Най-добре е да избягвате това и това лесно се решава чрез замяна на .* със списъка на колоните, които трябва да бъдат върнати, и указване на псевдоним за всяко „дублирано“ име на колона. (Използването на z.* във външната заявка не е проблем, стига да контролираме колоните, върнати от z .)

Допълнение:

Отбелязах, че GROUP BY няма да ви даде необходимия набор от резултати. Въпреки че би било възможно да се получи резултатният набор със заявка, използваща GROUP BY, оператор, който връща ПРАВИЛНИЯ резултат, би бил досаден. Можете да посочите MIN(distance) ... GROUP BY doctor_id , и това ще ви осигури най-малкото разстояние, НО няма гаранция, че другите неагрегирани изрази в списъка SELECT ще бъдат от реда с минималното разстояние, а не от някой друг ред. (MySQL е опасно либерален по отношение на GROUP BY и агрегатите. За да накарате MySQL машината да бъде по-предпазлива (и в съответствие с други машини за релационни бази данни), SET sql_mode = ONLY_FULL_GROUP_BY

Допълнение 2:

Проблеми с производителността, докладвани от Darious "някои заявки отнемат 7 секунди."

За да ускорите нещата, вероятно искате да кеширате резултатите от функцията. По принцип изградете справочна таблица. напр.

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

Това е само идея. (Очаквам, че търсите office_location разстояние от конкретен пощенски код, така че индексът на (zipcode, gc_distance, office_location_id) е покриващият индекс, от който заявката ви ще се нуждае. (Бих избягвал да съхранявам изчисленото разстояние като FLOAT, поради лошо производителност на заявка с тип данни 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)

С резултатите от функцията, кеширани и индексирани, вашите заявки трябва да са много по-бързи.

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

Колебая се относно добавянето на предикат HAVING към INSERT/UPDATE към таблицата на кеша; (ако сте посочили грешна географска ширина/дължина и сте изчислили грешно разстояние под 100 мили; последващо бягане след ширината/дължината е фиксирано и разстоянието работи на 1000 мили... ако редът е изключен от заявката, тогава съществуващият ред в кеш таблицата няма да се актуализира. (Можете да изчистите кеш таблицата, но това всъщност не е необходимо, това е просто много допълнителна работа за базата данни и регистрационните файлове. Ако наборът от резултати на заявката за поддръжка е твърде голям, може да бъде разбит, за да се изпълнява итеративно за всеки пощенски код или всяко office_location.)

От друга страна, ако не се интересувате от разстояния над определена стойност, можете да добавите HAVING gc_distance < предикат и намали значително размера на кеш таблицата.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как да посочите Ruby regex, когато използвате Active Record в Rails?

  2. предава данни от mySQL към api в Flask

  3. Недефиниран индекс:REMOTE_ADDR, докато Laravel мигрира

  4. sql group_concat и подзаявка

  5. mysql преместване на ред между таблици