0
点赞
收藏
分享

微信扫一扫

PostgreSQL-PostGIS-入门实例

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';

image-20220302204150637

执行完毕后,执行postgis_full_version查看当前PostGIS版本

SELECT postgis_full_version();

image-20220302204411278

几何对象

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;

image-20220302210526209

几何对象的存储

PostGIS的几何类型与PostgreSQL内建的几何类型使用了不同的存储方式。

以点为例,使用内置的Point与ST_Point创建一个点,返回的结果如下所示:

SELECT Point(1,2),ST_Point(1,2);

image-20220302210919123

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));

image-20220302212152079

在实际使用中,通常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;

image-20220302214252623

一些几何类型支持特殊的输出方式,例如以经纬度格式输出几何点:

SELECT ST_AsLatLonText(ST_Point(116.321367,39.966956));

image-20220302214634485

几何对象的运算

PostGIS提升了多种多样的关系判断与几何运算函数,功能非常强大。原本需要几千行业务代码才能实现的功能,可能现在只需要一行SQL就可以搞定。

计算两点距离

几何对象之间有多种多样的关系,最简单的莫过于两点之间的距离了,如下所示:

SELECT ST_Point(1,1) <-> ST_Point(2,2);

image-20220302215107502

例如,计算点(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);

image-20220302220058364

但通过引入地理坐标系,(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;

image-20220302225326501

计算路线长度

计算某条路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;

image-20220303001543062

通过计算,得到路长为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;

image-20220303002217730

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));
举报

相关推荐

0 条评论