Геопоиск (расстояние) в PHP/MySQL (производительность)

У меня есть таблица MySQL (MyISAM), содержащая около 200 тыс. записей пар широта/длина, из которых я выбираю, исходя из расстояния между парами (формула большого круга) от другой пары широта/длина. (например, все записи в радиусе 10 км вокруг 50.281852, 2.504883)

Моя проблема в том, что этот запрос занимает около 0,28 сек. работать только для тех 200 тысяч записей (которые продолжают пополняться с каждым днем). Пока 0,28 сек. обычно это нормально, этот запрос выполняется очень часто, поскольку он обеспечивает основную функцию моего веб-приложения, и часто он является частью более крупного запроса.

Есть ли способ ускорить это? Очевидно, MySQL должен каждый раз просматривать все 200 тыс. записей и выполнять формулу большого круга для каждой записи. Я читал кое-что о геохешировании, R-деревьях и тому подобном здесь, в Stack Overflow, но я не думаю, что хочу идти по этому пути. Отчасти потому, что я никогда не был большим поклонником математики, но в основном потому, что я думаю, что эта проблема уже решена кем-то умнее меня в библиотеке/расширении/и т.д. который был тщательно протестирован и регулярно обновляется.

MySQL, кажется, имеет пространственное расширение, но оно не обеспечивает функцию расстояния. Должен ли я искать другую базу данных, чтобы поместить эти пары координат? PostgreSQL, кажется, имеет довольно зрелое пространственное расширение. Вы знаете что-нибудь об этом? Или PostgreSQL тоже просто использует формулу большого круга, чтобы получить все записи в определенном регионе?

Может быть, есть специализированный автономный продукт или расширение mysql, которое уже делает то, что я ищу?

Или, может быть, есть библиотека PHP, которую я мог бы использовать для вычислений? Используя APC, я мог легко разместить пары lat-long в памяти (эти 200 тыс. записей занимают около 5 МБ), а затем выполнить запрос внутри PHP. Однако проблема с этим подходом заключается в том, что тогда у меня будет запрос MySQL, например SELECT .. FROM .. WHERE id in (id1, id2, ..) для всех результатов, которых может быть до нескольких тысяч. Насколько хорошо MySQL обрабатывает подобные запросы? И тогда (поскольку это задача с обработкой чисел) будет ли выполнение этого в PHP достаточно быстрым?

Любые другие идеи, что я должен/не должен делать?

Для полноты картины вот пример запроса, лишенный каких-либо нерелевантных частей (как я уже сказал, обычно это часть более крупного запроса, в котором я соединяю несколько таблиц):

SELECT id,
       6371 * acos( sin( radians( 52.4042924 ) ) * sin( radians( lat ) ) + cos( radians( 50.281852 ) ) * cos( radians( lat ) ) * cos( radians( 2.504883 ) - radians( lon ) ) ) AS dst
FROM geoloc
HAVING dst <10
ORDER BY dst ASC

person Dexter    schedule 08.03.2011    source источник
comment
При поиске в пределах радиуса (расстояния) всего 10 миль (15 км), не можете ли вы просто пропустить все уравнение кривизны и приравнять его для окружности?   -  person GDmac    schedule 01.03.2014


Ответы (4)


Вычислите ограничивающую рамку, чтобы выбрать подмножество строк в предложении WHERE вашего SQL-запроса, чтобы вы выполняли дорогостоящее вычисление расстояния только для этого подмножества строк, а не для всех 200 000 записей в вашей таблице. Метод описан в этой статье о Movable Type (с PHP примеры кода). Затем вы можете включить вычисление Haversine в свой запрос к этому подмножеству, чтобы вычислить фактические расстояния, и учесть предложение HAVING в этой точке.

Это ограничивающая рамка, которая помогает вашей производительности, потому что это означает, что вы выполняете дорогостоящий расчет расстояния только для небольшого подмножества ваших данных. По сути, это тот же метод, который предложил Патрик, но ссылка Movable Type содержит подробные объяснения метода, а также код PHP, который можно использовать для создания ограничивающей рамки и SQL-запроса.

ИЗМЕНИТЬ

Если вы считаете, что гаверсинус недостаточно точен, то есть еще формула Винсенти.

//  Vincenty formula to calculate great circle distance between 2 locations expressed as Lat/Long in KM

function VincentyDistance($lat1,$lat2,$lon1,$lon2){
    $a = 6378137 - 21 * sin($lat1);
    $b = 6356752.3142;
    $f = 1/298.257223563;

    $p1_lat = $lat1/57.29577951;
    $p2_lat = $lat2/57.29577951;
    $p1_lon = $lon1/57.29577951;
    $p2_lon = $lon2/57.29577951;

    $L = $p2_lon - $p1_lon;

    $U1 = atan((1-$f) * tan($p1_lat));
    $U2 = atan((1-$f) * tan($p2_lat));

    $sinU1 = sin($U1);
    $cosU1 = cos($U1);
    $sinU2 = sin($U2);
    $cosU2 = cos($U2);

    $lambda = $L;
    $lambdaP = 2*M_PI;
    $iterLimit = 20;

    while(abs($lambda-$lambdaP) > 1e-12 && $iterLimit>0) {
        $sinLambda = sin($lambda);
        $cosLambda = cos($lambda);
        $sinSigma = sqrt(($cosU2*$sinLambda) * ($cosU2*$sinLambda) + ($cosU1*$sinU2-$sinU1*$cosU2*$cosLambda) * ($cosU1*$sinU2-$sinU1*$cosU2*$cosLambda));

        //if ($sinSigma==0){return 0;}  // co-incident points
        $cosSigma = $sinU1*$sinU2 + $cosU1*$cosU2*$cosLambda;
        $sigma = atan2($sinSigma, $cosSigma);
        $alpha = asin($cosU1 * $cosU2 * $sinLambda / $sinSigma);
        $cosSqAlpha = cos($alpha) * cos($alpha);
        $cos2SigmaM = $cosSigma - 2*$sinU1*$sinU2/$cosSqAlpha;
        $C = $f/16*$cosSqAlpha*(4+$f*(4-3*$cosSqAlpha));
        $lambdaP = $lambda;
        $lambda = $L + (1-$C) * $f * sin($alpha) * ($sigma + $C*$sinSigma*($cos2SigmaM+$C*$cosSigma*(-1+2*$cos2SigmaM*$cos2SigmaM)));
    }

    $uSq = $cosSqAlpha*($a*$a-$b*$b)/($b*$b);
    $A = 1 + $uSq/16384*(4096+$uSq*(-768+$uSq*(320-175*$uSq)));
    $B = $uSq/1024 * (256+$uSq*(-128+$uSq*(74-47*$uSq)));

    $deltaSigma = $B*$sinSigma*($cos2SigmaM+$B/4*($cosSigma*(-1+2*$cos2SigmaM*$cos2SigmaM)- $B/6*$cos2SigmaM*(-3+4*$sinSigma*$sinSigma)*(-3+4*$cos2SigmaM*$cos2SigmaM)));

    $s = $b*$A*($sigma-$deltaSigma);
    return $s/1000;
}


echo VincentyDistance($lat1,$lat2,$lon1,$lon2);
person Mark Baker    schedule 08.03.2011
comment
@epitaph — если вы используете ограничительную рамку с предложением WHERE, которое отфильтровывает записи с широтой/долготой, которые явно выходят за пределы диапазона (особенно если у вас есть индекс широты/долготы), то вы выбираете на основе ограничивающую рамку перед выполнением любых расчетов Haversine/Vincenty/расстояния... поэтому вы не вычисляете расстояние для 200 тыс. записей, а только для подмножества, попадающего в ограничивающую рамку. И если вы попытаетесь протестировать его, это сделает его очень заметно быстрее. - person Mark Baker; 09.03.2011
comment
Какая лата стоит в первой строке? $лат1 или $лат2? - person Tjorriemorrie; 07.02.2013
comment
к вашему сведению: PI не определен, я использовал pi() - person Tjorriemorrie; 08.02.2013
comment
Не мог бы кто-нибудь объяснить недавние отрицательные голоса за совершенно правильный ответ, если только это не Phpdna .... в этом случае я знаю, что он понизит голос любого, кто отвечает на вопрос, связанный с геопространственными данными или расстоянием, который не указывает людям на его библиотеку quadkey - person Mark Baker; 14.10.2013
comment
@PHPDNA - Что ж, возможно, ваш собственный ответ должен на самом деле объяснить, что такое деревья квадрантов и почему они такие быстрые, и, возможно, даже как их использовать, а не просто говорить, что моя библиотека крутая, потому что в ней используются вещи, которые вы, возможно, не понимаете .. .. возможно, тогда вы получили бы голоса «за» и не чувствовали бы этой острой необходимости отстаивать свое интеллектуальное превосходство, отрицая любой ответ, который не так низок, как ваш. - person Mark Baker; 14.10.2013
comment
@PHPDna - я публикую это, потому что людям легко понять. Я не пытаюсь ослепить людей деревьями квадрантов, кривыми Гильберта, кривыми Мура и другим жаргоном, который они не поймут... Я публикую это с основным объяснением того, как это работает... работающий код, а не направлять людей в библиотеку на сайте, где им нужно зарегистрироваться, чтобы даже увидеть ваш код... и я не систематически минусую ответы других людей, которые не согласны со мной. - person Mark Baker; 14.10.2013
comment
Мой ответ действительно отвечает на вопрос OP... он показывает способ уменьшить количество строк, извлекаемых SQL-запросом, что ускорит поиск по 200 тыс. записей без необходимость загрузки каждой записи в дерево квадрантов в памяти PHP.... это правильный ответ на вопрос; и хотя это может быть не единственный ответ на OP, он фактически правильный .... И с PHPClasses все в порядке, если вы хотите зарегистрироваться с ними (хотя там есть много плохо написанных библиотек, а также хороших библиотек), но не все хотят зарегистрироваться на новом сайте, чтобы получить ответы на свои вопросы - person Mark Baker; 14.10.2013
comment
Я понимаю, как работают деревья квадрантов, в данный момент я пишу реализацию SPL на C, и она даже не использует массивы любого измерения... просто указатели на ноль или на узлы NW/SW/NE/SE в дерево. Поиск выполняется быстро, но заполнение дерева квадрантов происходит медленнее... особенно с большими объемами данных, когда вам нужно заполнить все дерево квадрантов перед его использованием. - person Mark Baker; 14.10.2013
comment
Нет, я имею в виду SPL PHP (Стандартная библиотека PHP), в частности структуры данных. В настоящее время я работаю над SPLTrie и SPLQuadTree. - person Mark Baker; 14.10.2013
comment
Вам нужен C, потому что сам PHP написан на C. Я пишу новые структуры данных для самого PHP, а не пишу структуры данных на PHP. - person Mark Baker; 14.10.2013
comment
@MarkBaker: с кем ты споришь? Я не вижу никакой «эпитафии», отвечающей вам :P. Пожалуйста, прокомментируйте мой ответ? - person kellogs; 30.10.2013
comment
@kellogs — Epitaph или PHPDna (или любое другое имя, которое он использует на этой неделе) имеет тенденцию отрицать любой ответ на геопространственный вопрос, который не использует его решение hilbertcurve, найденное в PHPClasses, и никогда не объясняет, как оно работает или почему оно лучше. решение, когда он отвечает на вопросы; и когда он соблаговолит ответить в комментариях, у него есть эта раздражающая тенденция снова удалять их после того, как он знает, что они были прочитаны - person Mark Baker; 30.10.2013
comment
Я знаю, что это старый ответ, но что в ограничительной рамке содержится 200 тыс. записей? Проблема останется прежней. Просматривая множество ответов на похожие проблемы, все предлагают это решение, но никто не принимает во внимание, что ограничивающая рамка может фактически содержать много результатов. - person ; 30.04.2017
comment
Если это 200 тыс. записей, то вы используете ограничивающую рамку плохого размера. Использование геопространственных баз данных в любом случае лучше для производительности (особенно в 2017 году); но также можно создать сводные таблицы для определения количества точек в сегменте сетки, и это может дать вам представление о том, имеет ли ваш ограничивающий прямоугольник разумный размер или нет; или альтернативные структуры данных, такие как деревья квадрантов, которые гораздо более эффективны для извлечения точек данных в пределах области. - person Mark Baker; 30.04.2017
comment
@MarkBaker Спасибо за ваш ответ, Марк, я изучу ваши указатели и изучу их! ваше здоровье - person ; 01.05.2017

Что, если подойти к проблеме с другой стороны?

10 км по прямой это:

  1. на широте равно ~1'(минута)
  2. по долготе равно ~6'(минут)

Используя это в качестве основы, сделайте несколько быстрых математических расчетов и добавьте в свой запрос предложение WHERE, удалив все местоположения, которые находятся за пределами «коробки», созданной путем добавления буферной зоны с предположением 1 'широта и 6' долгота

круг буферной зоны GPS

Работа с этим изображением:

  1. Местоположение GPS, которое вы ищете (34° 12' 34,0, -85° 1' 1,0) [34.2094444444, -85.0169444444]

  2. Вы находите минимальную/максимальную широту/долготу

    2а. Минимальная широта - 34.1927777778, -85.0169444444

    2б. Минимальная долгота - 34.2094444444, -85.1169444444

    2в. Максимальная широта - 34.2261111111, -85.0169444444

    2д. Максимальная долгота - 34.2094444444, -84.9169444444

  3. Запустите запрос с минимальным и максимальным значением для каждого направления.

    SELECT *
    
    FROM geoloc
    
    WHERE
    
    lat >= 34.1927777 AND
    
    lat <= 34.2261111 AND
    
    long >= -85.1169444 AND
    
    long <= -84.9169444;
    

Вы можете либо интегрировать расчет расстояния с SQL-запросом, либо использовать библиотеку/класс PHP для запуска проверки расстояния после извлечения данных. В любом случае вы сократили количество вычислений на большой процент.

Я использую следующую функцию для расчета расстояния между двумя точками GPS US84. Передаются два параметра, каждый параметр представляет собой массив, первый элемент которого — широта, а второй — долгота. Я считаю, что его точность составляет несколько футов, чего должно быть достаточно для всех, кроме самых заядлых фанатов GPS. Кроме того, я считаю, что здесь используется формула расстояния Хаверсина.

$distance = calculateGPSDistance(массив (34,32343, -86,342343), массив (34,433223, -96,0032344));

function calculateGPSDistance($site1, $site2)
{
    $distance = 0;
    $earthMeanRadius = 2.0891 * pow(10, 7);

    $deltaLatitude = deg2rad($site2[0] - $site1[0]);
    $deltaLongitude = deg2rad($site2[1] - $site1[1]);
    $a = sin($deltaLatitude / 2) * sin($deltaLatitude / 2) + cos(deg2rad($site1[0])) * 
        cos(deg2rad($site2[0])) * sin($deltaLongitude / 2) * sin($deltaLongitude / 2);
    $c = 2 * atan2(sqrt($a), sqrt(1-$a));
    $distance = $earthMeanRadius * $c;

    return $distance;
}

ОБНОВЛЕНИЕ

Я забыл упомянуть, что моя функция расстояния будет возвращать расстояние в футах.

person Patrick    schedule 08.03.2011
comment
это работает для европы? в чем разница между гаверсинусом и формулой большого круга? - person Dexter; 08.03.2011
comment
@Bubbles да, это должно работать без проблем в Европе. Я считаю, что формула Хаверсина использует формулу большого круга, но по моему опыту она дает более точное расстояние в реальном мире. Вы можете прочитать о Хаверсине в википедии. - person Patrick; 08.03.2011
comment
@эпитафия победит? выиграть или проиграть что? ОП просто пытается найти лучший способ заставить свой сайт работать. иногда самый эффективный в вычислительном отношении алгоритм оказывается не самым лучшим. простота использования и интеграция важны при обновлении кода на рабочем сайте - person Patrick; 08.03.2011

То, что я делал до сих пор, точно так же, как описано выше @Mark. Жизнеспособное решение для небольших сайтов, я думаю, только не очень хорошо для моего случая (200 тысяч записей, локализованных внутри какого-то поля 100x100 квадратных километров с центром вокруг заданной точки. Я использовал тот же трюк Марка, но производительность слишком низкая. 5 пользователей/ второй запрос ближайших точек широты и долготы в течение нескольких часов, и запросы начинают занимать до 10-15 секунд; и это происходит после я изменил настройки mySQL в my.cnf. Даже не хочу подумайте о том, что произойдет, когда во всем мире будет 2 миллиона записей.

Итак, теперь время для шага 2: кривая Гильберта. Это должно решить проблему индекса B-дерева по столбцам (широта, долгота), которая является расточительной (сканирование по диапазону, используется только одна часть индекса B-дерева), используя только один индекс для одного столбца (hilbert_number). hilbert_number — число, рассчитанное на основе координат широты и долготы точки на кривой Гильберта.

Но вторая проблема, проверка расстояния между фиксированной точкой и всем из предыдущего подмножества результатов с помощью формулы Хаверсина, остается. Эта часть может быть очень медленной. Поэтому я подумал о том, чтобы как-то более непосредственно проверить расстояние, поместив все на кривую Гильберта и применив некоторую битовую маску к этому подмножеству результатов вместо применения формулы Хаверсина. Я просто не знаю, как бы я поступил в этом...

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

внутренний и внешний BB

Что мне нужно сделать прямо сейчас, так это переключиться на числа Гильберта и посмотреть, как они себя поведут. Но я сомневаюсь, что это увеличит производительность в 10 раз!

person kellogs    schedule 30.10.2013
comment
Я сам использовал аналогичную настройку подхода с ограничительной рамкой, начиная с запроса с небольшой рамкой, а затем увеличивая его наружу, но настраивая запрос так, чтобы предыдущие результаты не возвращались снова, несколько запросов, но все же довольно избирательно... как вы говорите, это довольно эффективно с небольшими и умеренными объемами данных и более эффективно, чем одна ограничивающая рамка. Я предпочитаю использовать геопространственные расширения базы данных, когда они встроены; но это не всегда вариант, и подход ограничивающей рамки все еще работает довольно хорошо. - person Mark Baker; 30.10.2013
comment
Для больших объемов данных я экспериментировал с деревьями квадрантов и обнаружил, что они невероятно быстры для поиска, но очень медленны для загрузки (и потребляют много памяти для больших наборов данных). Вот почему я написал расширение для PHP для реализации quadtrees как структуры данных C, а не структуры данных PHP... и это намного более эффективно как для поиска, так и для начальной загрузки, чем реализация того же самого в чистом PHP - person Mark Baker; 30.10.2013
comment
Для больших объемов данных я экспериментировал с деревьями квадрантов и обнаружил, что они невероятно быстры для поиска, но очень медленны для загрузки (и потребляют много памяти для больших наборов данных). Вот почему я написал расширение для PHP для реализации quadtrees как структуры данных C, а не структуры данных PHP... и это намного более эффективно как для поиска, так и для начальной загрузки, чем реализация того же самого на чистом PHP... но реальные преимущества придут, если я смогу сериализовать его, чтобы его можно было сохранить в APC, memcached, Redis или подобном, а не перестраивать из БД при каждом запросе. - person Mark Baker; 30.10.2013
comment
В какой-то степени все зависит от того, на какой вопрос пытается ответить ваш запрос. Если вам нужен список всех точек в пределах определенного радиуса от центральной точки, вам даже не обязательно вычислять фактическое расстояние для точек внутри внутренней ограничивающей рамки (только углы самой рамки), только для тех точек, которые лежат между внутренней и внешней коробками, чтобы увидеть, следует ли их включить или выбросить; но если вы хотите упорядочить свой список по расстоянию, вам нужно рассчитать все потенциальные записи - person Mark Baker; 30.10.2013
comment
Хм, так я думаю, с quadtrees в Java SE все должно быть в порядке со скоростью? Не могли бы вы указать мне хороший учебник о том, как использовать деревья квадрантов для псевдоциклического (а не псевдоBB) поиска? Предпочтительно адаптирован к кривой Гильберта; желательно уже реализовано и на Java: D - person kellogs; 30.10.2013

Можно попробовать квадроцикл. Это пространственный индекс и уменьшение размерности. Он делит карту на плитки, но вы можете использовать его для хранения точек. Вы можете скачать мой класс php hilbert-curve @phpclasses.org. Он также включает z-кривую и кривую Мура. Важно знать, что он использует проекцию Меркатора. Вы можете искать мозаику карт Bing. Это объясняет, как использовать quadkey. Вам нужны координаты x, y и значение z (масштаб или глубина). Затем он дает вам quadkey.

person Gigamegs    schedule 08.03.2011
comment
Я смотрю на ваш код PHP, но я его не понимаю. У вас есть пример, как вы его используете? - person Dexter; 09.03.2011
comment
Мало того, что этот ответ написан на плохом английском, он фактически не отвечает на вопрос каким-либо осмысленным образом. - person Christopher Schmidt; 30.01.2013