Присоединить операцию в формуле Хаверсина

Я использую формулу Хаверсина в PHP, как показано ниже:

$result=mysqli_query($mysqli,"SELECT *,( 6371 * acos( cos( radians({$lat}) ) * cos( radians( `latitude` ) ) * cos( radians( `longitude` ) -radians({$lon}) ) +sin( radians({$lat}) ) * sin( radians( `latitude` ) ) ) ) AS distance FROM `places` HAVING distance <= {$radius} ORDER BY distance ASC") or die(mysqli_error($mysqli));

Внутри цикла выборки Haversine у ​​меня есть запрос, который выполняет итерации по результатам haversine для выбора записей, которые соответствуют идентификаторам, возвращаемым формулой haversine. Запрос выглядит следующим образом.

 while($row = mysqli_fetch_assoc($result)) 

    {
   $rest_time=$row['id'];

$result1=mysqli_query($mysqli,"SELECT * FROM my_friends  WHERE personal_id='".$personal_id."' AND id='".$rest_time."'") or die(mysqli_error($mysqli)); 

//Some operations here
    }

HOw можно выполнить операцию Join, чтобы объединить эти запросы в один? Было бы разумно сделать это, с оптимизационной точки зрения, если вторая таблица имеет, как и 50 тыс. Пользователей, а первая таблица имеет почти 1000 записей?

Ответ 1

Любая операция, которую вы выполняете здесь, которая работает со всеми строками, будет медленной с тем количеством записей.

Что вам нужно сделать, это воспользоваться преимуществами индекса. Чтобы использовать индекс, это должен быть простой запрос, а НЕ результат функции (как и в настоящее время).

То, что вы делаете, выполняя поиск по радиусу, составляет круг вокруг точки. Используя некоторый триггер до создания круга, мы можем придумать следующий

Обведите два квадрата

где S1 - наибольший квадрат внутри, а S2 - наименьший квадрат снаружи.

Теперь мы можем разобрать размеры этих двух квадратов, и все, что НАХОДИТСЯ в S2, попадает и индексируется, и все, что INSIDE S1 попадает в индекс, оставляя только небольшую область, внутри которой теперь нужно искать, используя медленный метод.

Если вам нужно расстояние от точки игнорировать секции S1 (так как все внутри круга нуждается в функции haversine) в качестве примечания здесь, в то время как все внутри круга нуждается в нем, не каждая точка находится на расстоянии, поэтому оба предложения WHERE по-прежнему необходимы

Итак, давайте вычислим эти точки, используя единичный круг Круг окружения

function getS1S2($latitude, $longitude, $kilometer)
{
    $radiusOfEarthKM  = 6371;
    $latitudeRadians  = deg2rad($latitude);
    $longitudeRadians = deg2rad($longitude);
    $distance         = $kilometer / $radiusOfEarthKM;

    $deltaLongitude = asin(sin($distance) / cos($latitudeRadians));

    $bounds = new \stdClass();

    // these are the outer bounds of the circle (S2)
    $bounds->minLat  = rad2deg($latitudeRadians  - $distance);
    $bounds->maxLat  = rad2deg($latitudeRadians  + $distance);
    $bounds->minLong = rad2deg($longitudeRadians - $deltaLongitude);
    $bounds->maxLong = rad2deg($longitudeRadians + $deltaLongitude);

    // and these are the inner bounds (S1)
    $bounds->innerMinLat  = rad2deg($latitudeRadians  + $distance       * cos(5 * M_PI_4));
    $bounds->innerMaxLat  = rad2deg($latitudeRadians  + $distance       * sin(M_PI_4));
    $bounds->innerMinLong = rad2deg($longitudeRadians + $deltaLongitude * sin(5 * M_PI_4));
    $bounds->innerMaxLong = rad2deg($longitudeRadians + $deltaLongitude * cos(M_PI_4));

    return $bounds;
}

Теперь ваш запрос будет

SELECT 
  *
FROM
  `places` 
HAVING p.nlatitude BETWEEN {$bounds->minLat} 
  AND {$bounds->maxLat} 
  AND p.nlongitude BETWEEN {$bounds->minLong} 
  AND {$bounds->maxLong} 
  AND (
    (
      p.nlatitude BETWEEN {$bounds->innerMinLat} 
      AND {$bounds->innerMaxLat} 
      AND p.nlongitude BETWEEN {$bounds->innerMinLong} 
      AND {$bounds->innerMaxLong}
    ) 
    OR (
      6371 * ACOS(
        COS(RADIANS({ $lat })) * COS(RADIANS(`latitude`)) * COS(
          RADIANS(`longitude`) - RADIANS({ $lon })
        ) + SIN(RADIANS({ $lat })) * SIN(RADIANS(`latitude`))
      )
    )
  )) <= {$radius} 
ORDER BY distance ASC 

ВАЖНО

Вышеприведенный текст имеет читаемость, убедитесь, что эти значения экранированы правильно/желательно параметризованы

Затем это может воспользоваться индексом и разрешить соединение быстрее в течение

Добавление объединения становится

SELECT 
  *
FROM
  `places` p
  INNER JOIN my_friends f ON f.id = p.id
WHERE   p.latitude BETWEEN {$bounds->minLat} 
  AND {$bounds->maxLat} 
  AND p.longitude BETWEEN {$bounds->minLong} 
  AND {$bounds->maxLong} 
  AND (
    (
      p.latitude BETWEEN {$bounds->innerMinLat} 
      AND {$bounds->innerMaxLat} 
      AND p.longitude BETWEEN {$bounds->innerMinLong} 
      AND {$bounds->innerMaxLong}
    ) 
    OR (
      6371 * ACOS(
        COS(RADIANS({ $lat })) * COS(RADIANS(`latitude`)) * COS(
          RADIANS(`longitude`) - RADIANS({ $lon })
        ) + SIN(RADIANS({ $lat })) * SIN(RADIANS(`latitude`))
      )
    )
  )  <= {$radius} 
  AND f.personal_id = {$personal_id}
ORDER BY distance ASC 

ВАЖНО

Вышеприведенный текст имеет читаемость, убедитесь, что эти значения экранированы правильно/желательно параметризованы

Предполагая, что у вас есть правильные индексы, этот запрос должен оставаться быстрым и позволить вам выполнить соединение.

Глядя на код выше, я не уверен, откуда personal_id приходит, так что он остался как есть

если вам нужно расстояние от запроса, вы можете удалить квадрат S1

    (
      p.latitude BETWEEN {$bounds->innerMinLat} 
      AND {$bounds->innerMaxLat} 
      AND p.longitude BETWEEN {$bounds->innerMinLong} 
      AND {$bounds->innerMaxLong}
    ) 

и переместите вторую часть этого OR

  6371 * ACOS(
    COS(RADIANS({ $lat })) * COS(RADIANS(`latitude`)) * COS(
      RADIANS(`longitude`) - RADIANS({ $lon })
    ) + SIN(RADIANS({ $lat })) * SIN(RADIANS(`latitude`))
  )

назад к выбору, который все еще использует S2.

Я также должен был бы удалить "магическое число" в запросе 6371 - это радиус земли в километре

Ответ 2

В этом случае поместите первый запрос в качестве производного подзапроса во втором:

SELECT  p.*, f.*    -- Select only the columns you need, not all
    FROM  
    (
        SELECT  *,
                ( 6371 * acos( cos( radians({$lat}) ) * cos( radians( `latitude` ) )
                  * cos( radians( `longitude` ) -radians({$lon}) )
                  +sin( radians({$lat}) ) * sin( radians( `latitude` ) ) )
                ) AS distance
            FROM  `places`
            HAVING  distance <= {$radius}
            ORDER BY  distance ASC"
            LIMIT 10               -- Didn't you forget this??
    ) AS p
    JOIN  my_friends AS f  ON f.personal_id p.personal_id
      AND  id='".$rest_time."'"     -- Huh??