PostgreSQL-PostGIS-入门实例
介绍
空间数据是一类重要的数据, 地图导航、打车软件、餐厅推荐、外卖快递这些我们日常生活中用到的软件,背后都与空间数据息息相关。空间数据通常结构复杂,数据量大,对于空间数据的分析查询,其模式也迥异于普通的数据,一般的DBMS难以满足要求。
PostgreSQL已经内置了很多空间特性:几何数据类型、几何类型函数与运算符、空间数据索引但对于现实世界的复杂需求仍力有不逮:有很多辅路支路的道路,需要用多条折线来表示;行政区域的飞地,需要用多个多边形的集合来表示;高效判断点是否在多边形内、两条折线是否有交点,两个区域相离、相邻、重叠还是包含。好在PostgreSQL强大 的扩展机制为解决问题留下了一个窗口,PostGIS 为此而生,它已经成为GIS行业的事实标准。
坐标系
地理坐标系
地理坐标系(Geographic coordinate system),或称球面坐标系;
投影坐标系
投影坐标系(Projected coordinate systems)或称平面坐标系
地理信息通常是以经纬度来表示,如东径多少北纬多少;但在不同的坐标系和投影标准中,表示地理信息经纬度的单位是不同的。
在地理坐标系中经纬度的单位是度,坐标表示为:(117.435974 33.609617);但在投影坐标系中不同的投影标准有不同的单位,如常用到的投影标准:3857以米为单位,4326以度为单位。
地理数据类型
在数据库中:
-
geometry:平面坐标系(投影坐标系)
-
geography:地理坐标系(球面坐标系)
如果要计算两个lat、lon点之间的实际距离就需要将geometry类型转成geography类型
安装
安装教程已经在之前我的文章中有过详解,这里不再介绍,有需要请查看我之前的文章。
创建GIS数据库
PostGIS是PostgreSQL的一个扩展,连接并执行以下命令,创建geo数据库并加载PostGIS扩展:
su postgres
psql
CREATE DATABASE geo;
CREATE EXTENSION postgis;
连接PostgreSQL并执行以下查询,确认PostGIS扩展已经正确地安装,可以被数据库识别:
SELECT name,default_version FROM pg_available_extensions WHERE name ~ 'gis';
执行完毕后,执行postgis_full_version查看当前PostGIS版本
SELECT postgis_full_version();
几何对象
PostGIS支持很多几何类型:点、线、多边形、复合几何体等,并提供了大量实用的相关函数。
注意:虽然PostGIS中的几何类型与PostgreSQL内建的几何类型非常像,但他们并不是一回事。所有PostGIS中的对象命令通常都以ST开头,是空间类型(Spatial Type)的缩写。
对于PostGIS而言,所有几何对象都有一个公共父类Geometry,这种面向对象的组织形式允许在数据库中进行一些灵活的操作:例如在数据表中的同一列中存储不同的几何对象,每种集合对象实际上都是PostGIS底层几何库geos中对象的包装。
几何对象的输入
PostGIS支持多种空间对象创建方式,大体上可以分为几类:
- 众所周知的文本格式(WKT,Well-Known-Text)
- 众所周知的二进制格式(WKB,Well-Known-Binary)
- GeoJSON等编码
- 返回几何类型的函数
创建几何点(1,2),可以通过以下四种方式,得到的结果都是一样的:
SELECT
'Point(1 2)'::GEOMETRY AS wkt,
'0101000000000000000000F03F0000000000000040'::GEOMETRY AS wkb,
ST_GeomFromGeoJSON('{"type":"Point","coordinates":[1,2]}') AS geo_json,
ST_Point(1,2) AS func;
几何对象的存储
PostGIS的几何类型与PostgreSQL内建的几何类型使用了不同的存储方式。
以点为例,使用内置的Point与ST_Point创建一个点,返回的结果如下所示:
SELECT Point(1,2),ST_Point(1,2);
PostgreSQL中的Point只是一个包含两个Double的结构体(16字节)。但PostGIS的点类型ST_Point则采用了不同的存储方式(21字节),除了两个坐标分量,还包括了一些额外的元数据:例如几何对象的实际类型、参考系的ID等。
当查询PostGIS空间数据类型时,PostgreSQL会以十六进制的形式返回对象的二进制数据表示。这便于各类ETL工具以统一的方式处理空间数据类型。包含空间数据的表也可以使用pg_dump等工具以同样的方式处理。
如果需要人类可读的格式,则可以用ST_AsText输出WKT,而非WKB,如下所示:
SELECT ST_AsText(ST_Point(1,2));
在实际使用中,通常PostGIS的空间数据类型使用统一的Geometry类型。无论是点、折现还是多边形,都可以放入Geometry类型字段中。例如:
CREATE TABLE geo(
geom GEOMETRY
);
INSERT INTO geo VALUES(ST_Point(1.0,2.0)),('LineString(0 0,1 1,2 1,2 3)'),('Polygon((0 0,1 0,1 1,0 1,0 0))'),('MultiPoint(1 2,3 4)');
几何对象的输出
与几何对象的输入类似,几何对象也可以以多种方式输出,这里以最常见的WKT与GeoJSON为例:
SELECT ST_AsText(geom) AS wkt,ST_ASGeoJSON(geom) AS json FROM geo;
一些几何类型支持特殊的输出方式,例如以经纬度格式输出几何点:
SELECT ST_AsLatLonText(ST_Point(116.321367,39.966956));
几何对象的运算
PostGIS提升了多种多样的关系判断与几何运算函数,功能非常强大。原本需要几千行业务代码才能实现的功能,可能现在只需要一行SQL就可以搞定。
计算两点距离
几何对象之间有多种多样的关系,最简单的莫过于两点之间的距离了,如下所示:
SELECT ST_Point(1,1) <-> ST_Point(2,2);
例如,计算点(1,1)和点(2,2)之间的距离,结果应该为根号二。
两个几何点的坐标计算相对容易,但两个地理坐标之间的距离就相当复杂了,需要计算一个不规则球体上的球面距离。例如,地点A的经纬度为:(116.321367,39.966956),地点B的坐标为:(116.315346,39.997398)。如果直接用几何距离计算,结果除了能用于粗略比较相对距离,没有太大意义,如下所示:
SELECT ST_Point(116.321367,39.966956) <-> ST_Point(116.315346,39.997398);
但通过引入地理坐标系,(4326坐标系,指代WGS84国际标准GPS坐标系),就可以计算出这两个地点间的地理距离(3.4km),如下所示:
SELECT ST_AsText(ST_GeomFromText('POINT(116.321367 39.966956)',4326))::geography <-> ST_AsText(ST_GeomFromText('POINT(116.315346 39.997398)',4326))::geography;
计算路线长度
计算某条路L的长度,可以使用ST_length统计WGS84坐标系下折线的总长度(单位为米)
SELECT st_length(st_transform(st_geomfromgeojson('{
"type": "LineString",
"coordinates": [
[
118.71809005737303,
31.023186296488667
],
[
118.7243127822876,
31.023811498034163
],
[
118.73383998870848,
31.02583418080608
],
[
118.74126434326172,
31.028040694848137
],
[
118.73971939086913,
31.03186519810428
],
[
118.73405456542969,
31.029952965672855
],
[
118.72246742248535,
31.029217481437648
],
[
118.71671676635741,
31.028812962687404
],
[
118.7104082107544,
31.02818779396694
],
[
118.70620250701903,
31.02708454503873
],
[
118.70337009429932,
31.02344373291606
],
[
118.70242595672607,
31.01825809362147
],
[
118.70221138000487,
31.010203243220573
],
[
118.70843410491943,
31.009798643737952
],
[
118.72508525848389,
31.009908989221714
],
[
118.73409748077393,
31.012520461717404
],
[
118.73353958129883,
31.01134346888612
],
[
118.73334646224976,
31.011177953103566
],
[
118.7334430217743,
31.010773357758367
],
[
118.73338937759398,
31.01019404779685
],
[
118.7333357334137,
31.00958714787035
],
[
118.73304605484009,
31.008658399592516
],
[
118.73082518577577,
31.008557248243537
],
[
118.73029947280882,
31.00909978604104
],
[
118.72976303100585,
31.009908989221714
],
[
118.72851848602294,
31.010635427134496
]
]
}'),4326)::geography) as length;
通过计算,得到路长为12公里
计算面积
可以通过ST_Area对ST_Polygon计算得出,如下所示:
SELECT st_area(st_transform(st_geomfromgeojson('{
"type": "Polygon",
"coordinates": [
[
[
118.72319698333739,
31.014249143586866
],
[
118.72525691986083,
31.010092898077463
],
[
118.7300205230713,
31.01097565564564
],
[
118.7280035018921,
31.015058303050022
],
[
118.72319698333739,
31.014249143586866
]
]
]
}'),4326)::geography) as length;
3423 平方米 = 0.003423 平方公里= 0.3423 公顷
应用场景:圈人与地理围栏
圈人是LBS服务中常见的需求:给出一个中心点,找出该点周围一定距离 范围内所有符合条件的对象。例如,找出以用户为中心,周围1公里内所有的公交站,并按距离远近排序。
传统的关系型数据库,可能实现起来相当复杂,假设用户正在A地铁站:(116.321367,39.966956)常见的做法是,以用户为中心,经纬度各自加减一公里的偏移量(1度约为111公里),然后使用经纬度上的索引进行初次过滤(这时过滤使用的是矩形范围),如下所示:
CREATE TABLE stations(
name TEXT,
longitude DOUBLE PRICISION,
latgitude DOUBLE PRICISION,
);
SELECT name FROM stations WHERE longitude BETWEEN 116.312358 AND 116.330376 AND latitude BETWEEN 39.957947 AND 39.975965;
然后,在应用代码中对每个符合条件的点计算几何距离,判断是否符合距离条件,最后排序输出。当然也可以使用GeoHash方法,将二维坐标化为一维字符串编码,查询时进行前级匹配。
CREATE TABLE stations(
name TEXT,
position geography,
);
SELECT name,ST_Point(116.321367,39.966956)::geography <-> position::geography as distance from stations where st_point(116.321367,39.966956)::geography <-> position::geography < 500 order by ST_Point(116.321367,39.966956)::geography <-> position::geography;
空间索引
在100万行的表上,执行暴力扫表也勉强堪用,但对于生产环境动辄几千万上亿的大表,就不能这么做了。例如,在1亿条记录的POI表上,查询距离A地铁站最近的1000个POI点,如下所示:
SELECT name FROM poi ORDER BY position <-> ST_Point(116.458855,39.909863) LIMIT 1000;
这条查询执行了3分钟。现在使用PostgreSQL提供的GIST索引,如下所示:
CREATE INDEX CONCURRENTLY idx_poi_position_gist ON poi USING gist(position);
使用索引后查询,同样的查询只要1ms不到了,快了几十万倍
地理围栏
用点和距离画圆圈人是一种常见场景,另外一种场景是,判断一个点落在了哪些地理围栏中
例如有车辆和用户的位置坐标,现在希望从坐标得到用户所处的城市(或者区域、商圈等),又比如共享单车的禁停区检测,无人机的禁飞区识别,都是这种场景。
CREATE TABLE aoi(
name TEXT,
bound GEOMETRY
)
# 检测A地铁站中心点所属的商圈
SELECT name FROM aoi WHERE ST_Contains(bound,ST_Point(116.458855,39.909863));