Под катом будет небольшая заметка о применении пространственного индекса
на основе zcurve для индексации точечных данных, расположенных на сфере.
А так же bencmark-и для PostgreSQL и сравнение с таким же (но совсем другим)
индексом на R-дереве.
В развитие темы (1, 2, 3, 4, 5, 6).
Собственно, возвращаемся к самому началу — идее индексировать географические координаты, размещая их на поверхности сферы. Обычная индексация пары широта/долгота приводит к искажениям вдали от экватора, работа с проекциями не универсальна. Поэтому мысль переводить географические координаты в трехмерное пространство выглядит довольно изящно.
Сама по себе идея эта не нова, аналогично работает, например, расширение PostgreSQL — PGSphere, которое использует для индексации 3-мерное R-дерево. С ним и будем сравнивать.
Подготовка данных.
PGSphere
- Для начала придётся выкачать, собрать и инсталлировать расширение (автор использовал текущую версию 1.1.5)
gmake USE_PGXS=1 PG_CONFIG=/usr/bin/pg_config sudo gmake USE_PGXS=1 PG_CONFIG=/usr/bin/pg_config install
- Далее загрузить его (psql)
CREATE EXTENSION pg_sphere;
- Создадим таблицу для тестовых данных
CREATE TABLE spoint_data (sp spoint);
- Нам потребуется источник случайных данных.
Первый параметр программы — радиус, второй — число результатов.
Единственная тонкость — данные равномерно распределены внутри шара с заданным радиусом, иначе не получится равномерного распределения на сфере - Случайные данные пропустим через скрипт awk чтобы превратить в геокоординаты
# --- gendata.awk ------ BEGIN{ pi=3.1415926535897932; degra=pi/180.0; rad=180.0/pi; Grad = 1000000.; } { x = $1; y = $2; z = $3; r3 = sqrt(x*x + y*y + z*z); x *= Grad / r3; y *= Grad / r3; z *= Grad / r3; r2 = sqrt(x*x + y*y); lat = atan2(z, r2) * rad; lon = 180. + atan2(y, x) * rad; printf ("(%14.10fd, %14.10fd)n", lon, lat); }
- Собственно создание данных, здесь радиус не важен, важно чтобы и pgsphere и zcurve получили одни и те же данные. Сортировка весьма желательна для ускорения индексации.
./random 1000000 100000000 | gawk -f gendata.awk | sort > pgsphere.txt
- Заливаем данные в нашу таблицу
COPY spoint_data (sp) FROM '/home/.../pgsphere.txt';
- Индексируем
CREATE INDEX sp_idx ON spoint_data USING gist (sp);
ZORDER
- Для начала придётся выкачать, собрать и инсталлировать расширение
gmake USE_PGXS=1 PG_CONFIG=/usr/bin/pg_config sudo gmake USE_PGXS=1 PG_CONFIG=/usr/bin/pg_config install
- Создадим таблицу для тестовых данных
create table test_points_3d (x integer,y integer, z integer);
- Нам потребуется тот же источник случайных данных.
- Случайные данные пропустим через скрипт awk чтобы разместить их внутри куба со стороной в 2 000 000
#--- gendata2.awk ------ BEGIN{ pi=3.1415926535897932; degra=pi/180.0; rad=180.0/pi; Grad = 1000000.; } { x = $1; y = $2; z = $3; r3 = sqrt(x*x + y*y + z*z); x *= Grad / r3; y *= Grad / r3; z *= Grad / r3; ix = int(x+0.5+Grad); iy = int(y+0.5+Grad); iz = int(z+0.5+Grad); print ix"t"iy"t"iz; }
- Собственно создание данных, здесь радиус важен. Сортировка не обязательна.
./random 1000000 100000000 | gawk -f gendata2.awk > zcurve.txt
- Заливаем данные в нашу таблицу
COPY test_points_3d FROM '/home/.../zcurve.txt';
- Индексируем
create index zcurve_test_points_3d on test_points_3d(zcurve_num_from_xyz(x,y,z));
Подготовка тестов
PGSphere
Для тестирования потребуется вот такой awk скрипт
#--- gentest.awk -------
BEGIN{
pi=3.1415926535897932;
degra=pi/180.0;
rad=180.0/pi;
Grad = 1000000.;
}
{
x = $1; y = $2; z = $3;
r3 = sqrt(x*x + y*y + z*z);
x *= Grad / r3;
y *= Grad / r3;
z *= Grad / r3;
r2 = sqrt(x*x + y*y);
lat = atan2(z, r2) * rad;
lon = 180. + atan2(y, x) * rad;
# EXPLAIN (ANALYZE,BUFFERS)
printf ("select count(1) from spoint_data where sp @'<(%14.10fd,%14.10fd),.316d>'::scircle;n", lon, lat);
}
Этот скрипт вполне симметричен тому, с помощью которого мы подготавливали данные. Стоит обратить внимание на число .316, это радиус сферы с центром в вычисленной случайной точке, в которой мы ищем данные
Подготовка серии запросов делается так:
./random 1000000 100 1023 | gawk -f gentest.awk >tests1.sql
Здесь 100 — размер тестовой серии, 1023 — seed рандомизатора.
ZCURVE
Для тестирования тоже потребуется awk скрипт
#--- gentest2.awk -------
BEGIN{
pi=3.1415926535897932;
degra=pi/180.0;
rad=180.0/pi;
Grad = 1000000.;
}
{
x = $1; y = $2; z = $3;
r3 = sqrt(x*x + y*y + z*z);
x *= Grad / r3;
y *= Grad / r3;
z *= Grad / r3;
ix = int(x+0.5+Grad);
iy = int(y+0.5+Grad);
iz = int(z+0.5+Grad);
# EXPLAIN (ANALYZE,BUFFERS)
lrad = int(0.5 + Grad * sin(.316 * degra));
print "select count(1) from zcurve_3d_lookup_tidonly('zcurve_test_points_3d', "ix-lrad","iy-lrad","iz-lrad","ix+lrad","iy+lrad","iz+lrad");";
}
Этот скрипт вполне симметричен тому, с помощью которого мы подготавливали данные. Опять же, обращаем внимание на число .316, это половина стороны куба с центром в вычисленной случайной точке, в котором мы ищем данные.
Подготовка серии запросов делается так:
./random 1000000 100 1023 | gawk -f gentest2.awk >tests2.sql
Здесь 100 — размер тестовой серии, 1023 — seed рандомизатора, лучше, если он совпадает с оным от pgsphere.
Benchmark
Как и раньше, замеры проводились на скромной виртуальной машине с двумя ядрами и 4 Гб ОЗУ, поэтому времена не имеют абсолютной ценности, а вот числам прочитанных страниц по прежнему можно доверять.
Времена показаны на вторых прогонах, на разогретом сервере и виртуальной машине. Количества прочитанных буферов — на свеже-поднятом сервере.
Radius | AVG NPoints | Nreq | Type | Time(ms) | Reads | Hits |
---|---|---|---|---|---|---|
.01° | 1.17 0.7631 (0.7615) |
10 000 | zcurve rtree |
.37 .46 |
1.4397 2.1165 |
9.5647 3.087 |
.0316° | 11.6 7.6392 (7.6045) |
10 000 | zcurve rtree |
.39 .67 |
2.0466 3.0944 |
20.9707 2.7769 |
.1° | 115.22 76.193 (76.15) |
1 000 | zcurve rtree |
.44 2.75 * |
4.4184 6.073 |
82.8572 2.469 |
.316° | 1145.3 758.37 (760.45) |
1 000 | zcurve rtree |
.59 18.3 * |
15.2719 21.706 |
401.791 1.62 |
1.° | 11310 7602 (7615) |
100 | zcurve rtree |
7.2 94.5 * |
74.9544 132.15 |
1651.45 1.12 |
где
Radius — размер поисковой области в градусах
Npoints — среднее число точек в выдаче, в скобках — теоретически ожидаемое число
(в сфере 41252.96 кв. градусов, 100 000 000 точек, ~2424 точки на кв. градус)
Nreq — число запросов в серии
Type —
‘zcurve’ — оно и есть
’rtree’- PGSphere
Time(ms) — среднее время выполнения запроса
Reads — среднее число чтений на запрос
Hits — число обращений к буферам
* в какой-то момент производительность R-tree начинает резко
проседать, связано это с тем, это дерево читает заметно больше
страниц и его рабочий набор перестаёт помещаться в кэше (по-видимому).
Отметим, что zcurve находит больше данных, что логично т.к. он ищет внутри куба, а не сферы как PGSphere. Поэтому требуется пост-фильтрация в духе HAVERSINE. Но здесь мы сравниваем только производительности индексов.
Попробуем оценить разницу. Вообще, задача найти площадь куска сферы, попавшей внутрь куба нетривиальна. Попробуем сделать оценку.
- Предположим, что наш куб в среднем вырезает из сферы ту же площадь, что и сфера равного объема
- Объем единичной сферы 1.33*3.14=4.19
- Объем куба со стороной 2 = 8.
- Тогда корень третьей степени из 8/4.19 = 1.24 — это отношение радиусов мнимой сферы к настоящей
- соотношение площадей мнимой сферы к настоящей 1.24*1.24=1.54
- имеем из экспериментальных данных 1.17/0.7631= 1.5332
Bingo!
Автор: Борис Муратшин