|
| 1 | +## 为什么geometry+GIST 比 geohash+BTREE更适合空间搜索 - 多出的不仅仅是20倍性能提升 |
| 2 | + |
| 3 | +### 作者 |
| 4 | +digoal |
| 5 | + |
| 6 | +### 日期 |
| 7 | +2018-04-17 |
| 8 | + |
| 9 | +### 标签 |
| 10 | +PostgreSQL , gist , btree , 空间索引 , 范围扫描 |
| 11 | + |
| 12 | +---- |
| 13 | + |
| 14 | +## 背景 |
| 15 | +在PostgreSQL中,支持geohash, geometry, geograph三种空间存储结构。 |
| 16 | + |
| 17 | +1、geohash,很多库都支持它,因为简单,将地球作为标准化的球体,展开抽象为一个平面,划分为若干个小方格,进行编码,相邻的小方格的编码前缀一样。 |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | + |
| 22 | + |
| 23 | +geohash 每一个小方块的精度与编码长度有关(这个说法也不完全准确,因为是基于地球是标准球体的前提),如下: |
| 24 | + |
| 25 | + |
| 26 | + |
| 27 | +2、由于地球并非标准球体,也非标准的椭球体,所以geohash精度有硬性的缺陷,geometry与geograph类型,可以解决这个问题。 |
| 28 | + |
| 29 | +对于GIS来说,首先是坐标系,有两种:一种是球坐标(地理坐标),另一种是平面坐标(投影坐标)。 |
| 30 | + |
| 31 | +球坐标通常用于计算,平面坐标通常用于展示(也可以计算)。 |
| 32 | + |
| 33 | +投影坐标是从球坐标投影后展开得来(用一个圆柱将地球包起来,把地球当成会发光的光源,投影后,将圆柱展开得到),投影的范围越大,精度就越低。范围越小, |
| 34 | + |
| 35 | +计算距离,应该考虑到被计算的两点所在处的地球特性(spheroid)。这样计算得到的距离才是最精确的。 |
| 36 | + |
| 37 | +geometry和geography类型的选择,建议使用geometry,既能支持球坐标系,又能支持平面坐标系。主要考虑到用户是否了解位置所在处的地理特性,选择合适的坐标系。 |
| 38 | + |
| 39 | +目前用得最多的有SRID=4326球坐标,SRID为EPSG:3785的墨卡托投影坐标。 |
| 40 | + |
| 41 | +再来说geometry和geography两种类型,geometry支持平面对象也支持空间对象,而geography则仅支持空间对象。 |
| 42 | + |
| 43 | +geometry支持更多的函数,一些几何计算的代价更低。 |
| 44 | + |
| 45 | +geography支持的函数略少,计算代价更高。但是对于跨度较大地域性的业务,就需要使用geography,因为它的精度不受制于区域。 |
| 46 | + |
| 47 | +If your data is contained in a small area, you might find that choosing an appropriate |
| 48 | +projection and using GEOMETRY is the best solution, in terms of performance and functionality available. |
| 49 | + |
| 50 | +If your data is global or covers a continental region, you may find that GEOGRAPHY |
| 51 | +allows you to build a system without having to worry about projection details. |
| 52 | +You store your data in longitude/latitude, and use the functions that have been defined on GEOGRAPHY. |
| 53 | + |
| 54 | +If you don't understand projections, and you don't want to learn about them, |
| 55 | +and you're prepared to accept the limitations in functionality available in GEOGRAPHY, |
| 56 | +then it might be easier for you to use GEOGRAPHY than GEOMETRY. |
| 57 | +Simply load your data up as longitude/latitude and go from there. |
| 58 | + |
| 59 | +除了空间模型上的差异,geohash与geometry, geograph还有功能、性能上的差异。 |
| 60 | + |
| 61 | +性能方面主要体现在GEOHASH的编码精度会带来一些问题: |
| 62 | + |
| 63 | +1、由于GEOHASH编码的问题,我们在搜索某一个点附近N米内的对象时,会引入空间放大,理论上我们要的是以目标点为中心,距离为半径的一个圆内的数据。 |
| 64 | + |
| 65 | + |
| 66 | + |
| 67 | +如果只看前缀的话,这个放大会随着编码长度缩短而级数增加。 |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | +然而,使用geometry的距离搜索,不会引入放大问题 |
| 72 | + |
| 73 | + |
| 74 | + |
| 75 | +2、当我们需要搜索的是任意多边形时,GEOHASH也无法满足需求,需要进行大范围的匹配,然后再逐条进行空间计算过滤。 |
| 76 | + |
| 77 | +## 几种地理数据的扫描方法 |
| 78 | +### 1、geohash 前缀扫描,匹配在这个正方形块内的数据 |
| 79 | + |
| 80 | +``` |
| 81 | +postgres=# create table t_test( |
| 82 | + id int, |
| 83 | + pos text, -- geohash |
| 84 | + geo geometry -- geometry |
| 85 | +); |
| 86 | +CREATE TABLE |
| 87 | + |
| 88 | +postgres=# insert into t_test |
| 89 | +select id, |
| 90 | +st_geohash(st_setsrid(st_point(x,y),4326), 13), |
| 91 | +st_setsrid(st_point(x,y),4326) |
| 92 | +from ( |
| 93 | + select id, 120+30*random() x, 68+5*random() y |
| 94 | + from generate_series(1,100000) t(id) |
| 95 | +) t; |
| 96 | +INSERT 0 100000 |
| 97 | +``` |
| 98 | + |
| 99 | +``` |
| 100 | +postgres=# select * from t_test limit 10; |
| 101 | + id | pos | geo |
| 102 | +----+---------------+---------------------------------------------------- |
| 103 | + 1 | yu0j8y2pxsezp | 0101000020E61000000000625C21F25E400000510228205140 |
| 104 | + 2 | zhsfe7t2cbtzz | 0101000020E6100000008049BE8DBA61400080CB2C5DB15140 |
| 105 | + 3 | zhcydqptr7bkd | 0101000020E6100000000061ED403261400000A01B4B395240 |
| 106 | + 4 | yuhdce4q6u7t6 | 0101000020E610000000808C51B6446040008055F70F005140 |
| 107 | + 5 | yus98nqjtdf4r | 0101000020E610000000803D75C54260400080923722A75140 |
| 108 | + 6 | zk9grxnsqxv98 | 0101000020E61000000000787897A16240008086A312BB5140 |
| 109 | + 7 | yurhhfh33u5xm | 0101000020E61000000080C877DEB96040008031E3B7675140 |
| 110 | + 8 | zhk5qv4vhe10k | 0101000020E610000000002A889E9D61400080CA5360605140 |
| 111 | + 9 | zhm49th6m0h5y | 0101000020E61000000000C79D4DC361400000B456E8575140 |
| 112 | + 10 | zh95n0wvxkpv5 | 0101000020E610000000808F92BE1561400000A9D5FCB55140 |
| 113 | +(10 rows) |
| 114 | +``` |
| 115 | + |
| 116 | +``` |
| 117 | +postgres=# create index idx_t_test_1 on t_test (pos text_pattern_ops); |
| 118 | +CREATE INDEX |
| 119 | +``` |
| 120 | + |
| 121 | +``` |
| 122 | +postgres=# explain select * from t_test where pos ~ '^yuhdce4'; |
| 123 | + QUERY PLAN |
| 124 | +----------------------------------------------------------------------------- |
| 125 | + Index Scan using idx_t_test_1 on t_test (cost=0.42..2.64 rows=10 width=50) |
| 126 | + Index Cond: ((pos ~>=~ 'yuhdce4'::text) AND (pos ~<~ 'yuhdce5'::text)) |
| 127 | + Filter: (pos ~ '^yuhdce4'::text) |
| 128 | +(3 rows) |
| 129 | + |
| 130 | +postgres=# explain select * from t_test where pos like 'yuhdce4%'; |
| 131 | + QUERY PLAN |
| 132 | +----------------------------------------------------------------------------- |
| 133 | + Index Scan using idx_t_test_1 on t_test (cost=0.42..2.64 rows=10 width=50) |
| 134 | + Index Cond: ((pos ~>=~ 'yuhdce4'::text) AND (pos ~<~ 'yuhdce5'::text)) |
| 135 | + Filter: (pos ~~ 'yuhdce4%'::text) |
| 136 | +(3 rows) |
| 137 | +``` |
| 138 | + |
| 139 | +### 2、geohash 范围扫描,匹配在一个连续Z空间中的一段小方格 |
| 140 | + |
| 141 | +将二进制编码的结果填写到空间中,当将空间划分为四块时候,编码的顺序分别是左下角00,左上角01,右下脚10,右上角11,也就是类似于Z的曲线,当我们递归的将各个块分解成更小的子块时,编码的顺序是自相似的(分形),每一个子快也形成Z曲线,这种类型的曲线被称为Peano空间填充曲线。 |
| 142 | + |
| 143 | +``` |
| 144 | +postgres=# explain select * from t_test where pos ~>=~ 'yuhdce4' and pos ~<=~ 'yuhdcej'; |
| 145 | + QUERY PLAN |
| 146 | +---------------------------------------------------------------------------- |
| 147 | + Index Scan using idx_t_test_1 on t_test (cost=0.42..2.64 rows=1 width=50) |
| 148 | + Index Cond: ((pos ~>=~ 'yuhdce4'::text) AND (pos ~<=~ 'yuhdcej'::text)) |
| 149 | +(2 rows) |
| 150 | +``` |
| 151 | + |
| 152 | +### 3、经度与维度范围扫描,将经度与维度分开两个字段存储。扫描得到的是一个落在经纬度区间内的长方形区间。 |
| 153 | + |
| 154 | +``` |
| 155 | +create table t_geo (id int, x float, y float); |
| 156 | + |
| 157 | +insert into t_geo |
| 158 | + select id, 120+30*random() x, 68+5*random() y |
| 159 | + from generate_series(1,100000) t(id) ; |
| 160 | +``` |
| 161 | + |
| 162 | +``` |
| 163 | +postgres=# create index idx_t_geo_1 on t_geo (x,y); |
| 164 | +CREATE INDEX |
| 165 | + |
| 166 | +postgres=# explain select * from t_geo where x >= 120 and x <=124 and y >= 68 and y <=71; |
| 167 | + QUERY PLAN |
| 168 | +------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 169 | + Index Scan using idx_t_geo_1 on t_geo (cost=0.42..1029.31 rows=7810 width=20) |
| 170 | + Index Cond: ((x >= '120'::double precision) AND (x <= '124'::double precision) AND (y >= '68'::double precision) AND (y <= '71'::double precision)) |
| 171 | +(2 rows) |
| 172 | +``` |
| 173 | + |
| 174 | +### 4、GEOMETRY GIS空间扫描 |
| 175 | + |
| 176 | +gist 是基于gist的r-tree,每一层为一个BOUND,当需要搜索包含、相交、近邻时,可以快速定位到与你输入的对象 包含、相交、近邻 的对象。 |
| 177 | + |
| 178 | +详细结构建本文末尾的参考部分。 |
| 179 | + |
| 180 | +``` |
| 181 | +create index idx_t_test_2 on t_test using gist (geo); |
| 182 | +``` |
| 183 | + |
| 184 | +GIST索引不仅支持空间检索,还支持空间排序。 |
| 185 | + |
| 186 | +``` |
| 187 | +postgres=# explain select * from t_test where st_dwithin(geo, st_setsrid(st_point(121, 70), 4326), 10000) order by geo <-> st_setsrid(st_point(121, 70), 4326); |
| 188 | + QUERY PLAN |
| 189 | +------------------------------------------------------------------------------------------------------------------------------------------------- |
| 190 | + Index Scan using idx_t_test_2 on t_test (cost=0.28..29263.75 rows=6667 width=58) |
| 191 | + Index Cond: (geo && '0103000020E6100000010000000500000000000000804BC3C0000000000065C3C000000000804BC3C00000000000ABC3400000000080C4C3400000000000ABC3400000000080C4C340000000000065C3C000000000804BC3C0000000000065C3C0'::geometry) |
| 192 | + Order By: (geo <-> '0101000020E61000000000000000405E400000000000805140'::geometry) |
| 193 | + Filter: (('0101000020E61000000000000000405E400000000000805140'::geometry && st_expand(geo, '10000'::double precision)) AND _st_dwithin(geo, '0101000020E61000000000000000405E400000000000805140'::geometry, '10000'::double precision)) |
| 194 | +(4 rows) |
| 195 | +``` |
| 196 | + |
| 197 | +在LBS项目中,按距离由近到远排序输出,是非常强烈的需求。 |
| 198 | + |
| 199 | +## 性能对比 |
| 200 | +GIST索引比两个字段复合索引要快很多,原因是复合索引在驱动列使用范围时,这个范围下的所有ENTRY都要被扫描。 |
| 201 | + |
| 202 | +同样的测试对比如下: |
| 203 | + |
| 204 | +[《PostgreSQL 黑科技 range 类型及 gist index 20x+ speedup than Mysql index combine query》](../201206/20120607_01.md) |
| 205 | + |
| 206 | +## 小结 |
| 207 | +1、GEOHASH,适合对精度没有要求(例如本土化,小范围的业务),并且舍得浪费计算资源的场景(因为颗粒度大,所以通过索引圈出的区域,可能有很多无效数据,需要大量RECHECK),同时GEOHASH不支持排序,所以需要额外的排序开销。 |
| 208 | + |
| 209 | +2、geometry,空间索引,适合对精度要求高的场景,且节约资源。适合专业的GIS业务。geometry使用时,需要注意选择正确的坐标系。geograph则对坐标系没有要求。 |
| 210 | + |
| 211 | +3、在一个对象稀疏的区域,圈出附近100个点。与在一个对象密集的区域,圈出附近100个点。使用GEOHASH完全不知所措,因为你不知道该用多大的PREFIX合适,而使用geometry+gist,非常容易且高效率的解决这个问题。 |
| 212 | + |
| 213 | +``` |
| 214 | +select * from tbl where pos ~ '^geohash_多长合适呢? 不知道' limit 100; |
| 215 | +``` |
| 216 | + |
| 217 | +``` |
| 218 | +select * from tbl order by geo <-> 点 limit 100; |
| 219 | +``` |
| 220 | + |
| 221 | +在PG里面,我们同时支持geohash, geometry, geograph三种空间存储,你喜欢什么样的姿势,就用什么样样的姿势。这就是我们喜爱的PostgreSQL。 |
| 222 | + |
| 223 | +## 参考 |
| 224 | +[《geohash vs PostGIS》](../201704/20170422_01.md) |
| 225 | + |
| 226 | +[《PostgreSQL 黑科技 - 空间聚集存储, 内窥GIN, GiST, SP-GiST索引》](../201709/20170905_01.md) |
| 227 | + |
| 228 | +[《通过空间思想理解GiST索引的构造》](../201708/20170825_01.md) |
| 229 | + |
| 230 | +[《PostGIS空间索引(GiST、BRIN、R-Tree)选择、优化 - 阿里云RDS PostgreSQL最佳实践》](../201708/20170820_01.md) |
| 231 | + |
| 232 | +[《自动选择正确索引访问接口(btree,hash,gin,gist,sp-gist,brin,bitmap...)的方法》](../201706/20170617_01.md) |
| 233 | + |
| 234 | +[《深入浅出PostgreSQL B-Tree索引结构》](../201605/20160528_01.md) |
| 235 | + |
| 236 | +[《HTAP数据库 PostgreSQL 场景与性能测试之 6 - (OLTP) 空间应用 - KNN查询(搜索附近对象,由近到远排序输出)》](../201711/20171107_07.md) |
| 237 | + |
| 238 | +[《GIS附近查找性能优化 - PostGIS long lat geometry distance search tuning using gist knn function》](../201308/20130806_01.md) |
| 239 | + |
| 240 | +[《PostGIS 距离计算建议 - 投影 与 球 坐标系, geometry 与 geography 类型》](../201710/20171018_02.md) |
| 241 | + |
| 242 | +http://www.cnblogs.com/LBSer/p/3310455.html |
| 243 | + |
| 244 | +<a rel="nofollow" href="http://info.flagcounter.com/h9V1" ><img src="http://s03.flagcounter.com/count/h9V1/bg_FFFFFF/txt_000000/border_CCCCCC/columns_2/maxflags_12/viewers_0/labels_0/pageviews_0/flags_0/" alt="Flag Counter" border="0" ></a> |
| 245 | + |
0 commit comments