关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

PostGIS 空间查询一上量就慢:GiST 索引、瓦片缓存和内存这三笔账怎么算

发布时间:2026-10-13

一张 3000 万行的点位表:索引建了、内存加了,ST_Intersects 还是 3 秒多

先看一个典型现象。某配送范围服务有一张 3200 万行的点位表 points(geometry Point,4326),一张几千行的区域表 zones(MultiPolygon)。业务查询长这样:统计过去 7 天落在某个配送区域内的点位数量。

写法是 select count(*) from points p join zones z on ST_Intersects(p.geom, z.geom) where z.id = 42 and p.created_at > now() - interval '7 days'。points.geom 上早就建了 GiST 索引,points.created_at 上有 btree,zones.geom 上也有 GiST。第一次执行 4.2 秒,同一语句再跑一次 0.8 秒;换不同参数再跑,又回到 3~4 秒。

团队第一反应是内存不够:机器 32G 内存,shared_buffers 配的 8G,操作系统缓存能用的还剩不少但显然不够。于是把机器扩到 128G。结果是首次查询从 4.2 秒降到 3.2 秒,二次查询基本没变化。也就是说,花出去的钱只买回了 20% 的延迟,而且这是在示例部署而非普遍规律——实际收益随数据分布、多边形的顶点数和并发形态变化很大。

问题出在把"慢"当成了一件事。这个 4.2 秒里至少压了三件完全不同的事情:索引有没有被选中、命中索引之后要不要产生大量随机读、以及精确几何运算本身消耗了多少 CPU。这三件事对应的钱,一条都挨不上。加内存只能解决第二件里的一部分。

真正要命的一点是:GiST 索引走的是边界框(bbox)。索引命中只是说"这块的最小外接矩形可能相交",真正的 ST_Intersects 判定还要回到堆表去取完整几何体做运算。一个简单的三级行政区 MultiPolygon 可能有几万个顶点,一个带 10 万个顶点的面参与运算时,CPU 花在几何算法上的时间是可以超过索引查找时间的。这种时候加内存,等于给一条堵在 CPU 上的 SQL 送 RAM。

把"慢"拆成三类:走不到索引、走到了但 IO 扛不住、重复查根本没被缓存

排查 PostGIS 慢查询的第一步不是调参数,是给慢查询归类。下面这三类各自的可观测信号完全不同。

第一类:优化器根本没打算用你的空间索引

表现是 explain 里明晃晃写着 Seq Scan on points,rows 估算值和实际返回行数差一个数量级以上。触发原因通常有三个。一是表批量导入之后没跑 ANALYZE,几何列的统计信息(ND 分布、extent、平均几何大小)是空的或者过期的,优化器不知道这个 bbox 能筛掉多少数据,于是选了它认为更稳的全表扫。二是谓词写法把索引列包进了函数或表达式里,比如 ST_Intersects(ST_Transform(p.geom, 3857), z.geom),这种写法除非建了对应的表达式索引,否则一定走不到索引。三是条件选择性本身太差——比如那个多边形覆盖了半个城市的热点数据,索引命中 800 万行然后回表判定,优化器算下来还不如直接顺序扫。

第三种子类最容易被误诊为"索引没生效",实际上它是第三类问题的前奏:索引生效了,但命中集太大。

第二类:索引走到了,随机读 IO 顶住了

用 explain (analyze, buffers) 看,症状非常直白:Index Scan 那一行的 Buffers 里 shared read 的数字远大于 shared hit。比如 shared hit=12043 read=987612,说明几乎每次堆表访问都落到磁盘上去了。空间索引的回表天然是随机 IO——索引里按空间邻近组织的行,在物理堆表上的位置一般是分散的(除非这张表刚按空间顺序 CLUSTER 过)。

这类问题的特征是:查询第一次慢,第二次快,因为第二次大部分页面已经在 OS page cache 或 shared_buffers 里了。判断阈值可以粗略这么定:如果同一语句在预热后的执行时间不到首次的 1/3,那瓶颈大概率在 IO 而不是 CPU;如果预热前后差不多,那就是 CPU 或锁在限制。

还有一种变种常被忽略:几何列本身很大,被 PostgreSQL 存到了 TOAST 表里。每次读取完整几何体都要额外访问 TOAST,这部分 IO 在 explain 里不一定直接体现出来,但当单个多边形的顶点数上千时,TOAST 的解压和读取开销可能占掉整个查询时间的一半以上。

第三类:这个查询本来就不该打到数据库

最常见的场景是地图底图和行政区划面的重复渲染。同一个 z14 瓦片,一百个用户看到的是同一个结果,但如果每请求一次就跑一遍 ST_AsMVT 聚合,数据库就在为一模一样的运算反复付费。看这类问题很简单:按 SQL 指纹聚合 pg_stat_statements,如果 top 10 里有若干条 calls 特别高、单次不算特别慢但总量惊人的语句,那就是缓存层的钱没花到位。

这三类可以并存。而它们各自的对账科目是:第一类对应维护窗口和 SQL 质量(几乎不要钱),第二类对应存储介质与内存(真花钱),第三类对应缓存层与出网带宽(花钱,但 ROI 通常最高)。

GiST、SP-GiST、BRIN 怎么选:看数据分布,不看习惯

PostGIS 支持的空间索引类型主要是这三种,选错的成本很高——不是慢一点,而是慢一个数量级。

GiST:默认答案,但不是万能答案

GiST(通用搜索树)是绝大多数场景的正确选择。它支持二维和三维几何、支持 && 系列的全部操作符、支持 KNN 排序(ST_Distance 配合 <-> 走索引顺序扫描),同时对多列组合索引友好——PostgreSQL 支持把 btree 的列作为 opclass 纳入 GiST,把 status、created_at 这类过滤列和 geom 建在同一个 GiST 索引里,能显著减少回表次数。示例写法:create index on points using gist (status, geom)。

GiST 的弱点是对胖大多边形的选择性差。一个覆盖全市的行政区划面,它的 bbox 会把整座城市的点都命中一次,索引过滤失效,剩下的全交给 CPU 做精确判定。这是唯一一个"加索引也没用"的场景。

SP-GiST:点数据密集时更划算

SP-GiST 做的是空间划分(PostGIS 里用四叉树布局),分区不重叠。当数据是大量点、且分布比较均匀时,它的索引体积更小、点查更快。但有几个硬约束得提前知道:它对 2D 几何的支持和与 GiST 的差异随版本演进,多列组合索引能力也和 GiST 不同,KNN 排序支持情况同样需要按所用版本确认——这些都是要在具体版本上验证的,不能默认"SP-GiST 就是全面更快"。真正适合它的画像很窄:千万级以上的纯点表、主要查询模式是点查或 bbox 范围、不需要与业务列组合建索引。

BRIN:别往没排序过的表上套

BRIN 记录的是每个物理块范围的摘要(这里就是这段范围内所有几何的最小外接矩形)。索引极小——一张 3000 万行的表,BRIN 索引可能只有几 MB,而 GiST 可能是几 GB。而且在 SSD 上建立 BRIN 也极快。

但 BRIN 有一个不容商量的前提:表里的数据必须按空间位置物理聚簇。轨迹表恰好天然满足——一辆车的数据是连续写入的,同一个司机、同一段路程的轨迹点在磁盘上挨在一起,它们的 bbox 摘要就很小、很准。反过来,一张按时间顺序不断追加但车辆混写的表,同一物理块里可能同时有城东和城西的点,BRIN 摘要退化成"整个城市的 bbox",索引彻底失效,比全表扫还慢(因为还要多读一遍摘要)。

想让 BRIN 生效,就得先用 order by bbox 重排这张表(CLUSTER 或 pg_repack),或者干脆用声明式分区按地理网格(geohash / S2 cell / H3)做 LIST 分区,每个分区内部自然聚簇。代价是数据重组织的一次性工作和后续 loading 顺序的约束。pages_per_range 这个孩子参数也值得调:默认 128,稀疏轨迹数据调到 32 或 64 通常能把过滤精度提上一个台阶,代价是索引体积成比例增长。

内存三件套:shared_buffers、work_mem、maintenance_work_mem 在抢同一块 RAM

这三个参数被讨论得最多,但经常是被分开讨论的。实际部署时它们必须放在一张资产负债表上算。

shared_buffers:不要按习惯配 25%,按 working set 配

常见的建议值是物理内存的 25%。对 PostGIS 这种读写混合、热点数据相对固定的负载,这个数字的上限通常是 8~16G 就会出现边际收益下降,因为 PostgreSQL 的 buffer 管理本身有开销,而且 OS page cache 会承担第二层缓存。超过一定规模后再往上堆 shared_buffers,命中率提升很小,反而挤掉了操作系统能用来缓存文件系统的内存——这就是双缓冲(double buffering)问题。

判断依据不是拍脑袋,是 pg_buffercache 扩展:看 usagecount 低的页面占比和索引 vs 堆表的占比。如果几何列的索引本身就有 15G,而机器只有 32G,那 shared_buffers 给多大都装不下整个索引,这时候真正该做的决策是"能不能让索引变小"(降精度、分顶点数、分区),而不是"再买一条内存"。装不下就意味着每次查询的索引页至少有相当一部分要走磁盘,而 SSD 的随机读延迟比内存高两个数量级。

effective_cache_size 也别忘了同步。这个值不分配任何内存,只是告诉优化器"大概有多少缓存在帮你"。默认值偏保守时会让优化器过度偏好顺序扫描。专用数据库机上给到物理内存的 50%~75% 是比较常见的做法。

work_mem:这是一个"乘以并发"的参数,不是一个绝对值

work_mem 的天坑在于它是 per-plan-node 的。一条带三个排序节点的查询,可能同时申请三份 work_mem;如果这个查询还开了 4 个并行 worker,总量再乘一遍。设成 512MB、并发 200 的时候,理论峰值远超物理内存——数据库的实际行为会是 OOM 或者开始 swap,而 swap 一旦介入,本来在 SSD 上 0.1 毫秒的随机读会变成几十毫秒的随机读,整个库直接失速。

正确做法是分层配置:全局 work_mem 保持保守(比如 16~32MB),然后按角色或会话给 ETL 类、批量导出类的任务单独 set work_mem 到几百 MB。PostgreSQL 支持 alter role etl_user set work_mem = '256MB',比全局改的参数红线安全得多。判断是否需要上调的依据是 explain analyze 里有没有 "external merge Disk" 之类的字样——出现溢出到磁盘的排序/哈希,就说明这一份 work_mem 不够用。

maintenance_work_mem:建索引速度的杠杆

maintenance_work_mem 影响 CREATE INDEX、VACUUM、ALTER TABLE ADD PRIMARY KEY 这类维护操作。几十 GB 的 GiST 索引在默认 64MB 的 maintenance_work_mem 下可能要跑好几个小时并且伴随大量外部排序,临时把它拉到 2~4GB(前提是这个量级释放得出来)通常能把时间压到原来的几分之一。

用法是会话级的:set maintenance_work_mem = '2GB'; create index ...。不要在配置文件里长期写 4GB,因为 autovacuum worker 也会按 autovacuum_work_mem 取资源(默认是 -1,跟着 maintenance_work_mem 走),多个 worker 同时启动时的内存峰值同样会失控。给 autovacuum_work_mem 单独设一个值,是比把 maintenance_work_mem 拉大更安全的选择。

三者相加之后还剩什么

一台 128G 的机器,如果 shared_buffers 配 32G、autovacuum_work_mem 预留 2G、日常 OLTP work_mem 按 200 并发估算(200 并发 × 每个查询 2 个节点 × 32MB ≈ 12.8G 峰值)、并行 worker 池预留若干,剩下的几十 G 就是 OS page cache 和文件系统缓存的空间。这部分才是 PostGIS 随机读的救命稻草,不要用 SSD 之外的东西去挤它。这些都是示例场景下的估算口径,实际数值需要按 dba 侧的实测负载曲线来定。

随机读 IOPS 才是那个隐藏的天花板:SSD 选型、表分区与实际收益

把索引走到了之后,剩下的几乎全是随机 IO 的账。这里的关键指标不是顺序带宽,而是随机读 IOPS 和 P99 延迟。

SATA SSD、NVMe SSD 与云盘:差的是一个数量级

一块企业级 SATA SSD 的随机读 IOPS 大致在数万级,普通 NVMe 可以到数十万甚至更高,而很多云硬盘的 IOPS 是与容量、与档位绑定的,需要按官方标称确认。对 PostGIS 来说这个差距直接换算成查询延迟:一个查询如果需要 10 万次随机读,在 2 万 IOPS 的盘上理论下限就是 5 秒,在 20 万 IOPS 的盘上是 0.5 秒。

还有两个容易被漏算的项。一是 random_page_cost 参数,默认值是为机械盘设计的(4.0),在 SSD 上应该下调到接近 seq_page_cost(1.1 左右),否则优化器会一直高估索引扫描的代价,在系统本来该走索引的时候偏要去全表扫。二是并发—很多 vCPU 核数很高但 IOPS 配额很低的云盘,在并发上来之后单查询延迟会陡增,这时候加 CPU 完全没用。

表分区:不是为了"管理方便",是为了把扫描范围切小

PostGIS 本身没有内置空间分区,要靠声明式分区手动实现,两种切法。按时间 RANGE 分区适合轨迹类、日志类——历史分区基本不被查询,完全可以放到廉价的冷存储上,热分区(最近 7 天)能整个装进内存,这种务实的收益非常直接。按地理网格 LIST 分区(用 ST_GeoHash 或 geohash/城市编码做分区键)适合 POI、不动产这类数据分布稳定、查询大多落在某个区域的场景,配合分区裁剪可以让单次查询只扫 1/16 甚至 1/100 的数据。

分区不是免费的:每个分区都要有自己的索引,全局查询如果没法裁剪,反而会因为要遍历所有分区索引而变慢。所以分区键必须和查询谓词对齐——按"地理网格"分了区,但查询条件里从不带网格列,那这套分区就白做了。另外,分区数量过多(上千个)会让规划阶段的开销变得可观,这也是常见的翻车点。

几何本身的瘦身:Subdivide、Simplify 与降精度

把一個两万顶点的面 ST_Subdivide 成几百个小块再入库,GiST 索引的选择性会立刻上一个台阶——因为每个子块的 bbox 都小得多。这个操作对查询通常不需要改任何 SQL(用 ST_Intersects 做聚合统计时注意计数要去重,否则一个面被切多次会重复计数)。ST_Simplify 则适合渲染用途:z12 以下的瓦片根本看不出 0.001 度级别的细节,把顶点数降下来能直接砍掉 TOAST 读取和几何运算这两份开销。这两件事都不花钱,花的是一次性改表的工时。

在硬件这一侧,一万网络深耕 IDC 19 年(成立于 2007 年),深圳南山自营机柜的机型支持按 NVMe 数据盘、单盘容量、RAID 与内存容量组合选型,也能做系统盘与数据盘冷热分离的方案;像 E5-2620 32G/1T 这样档位的裸金属 ¥999 起、E5-2698v4×2 32G/1T ¥3999 起(海外买 1 送 1,限时限量),大陆节点起步价华南 ¥799、华东 ¥699、华北 ¥899、华西 ¥599 起,具体配置与报价以官网实时价为准。对 GIS 负载更实际的是三项随机器自带的能力:硬件故障 10 分钟内自动迁移、系统盘每日 3 份免费快照且 30 秒回滚(正好用来兜住改表、重建索引这类高危操作前的一致性风险)、以及换机型时可以先用同代低配验证 IO 曲线再决定是否扩容上配——这些比单纯的报价数字更能降低第一次调优的风险。

底图瓦片这笔账:缓存命中率每掉一个点,出网和 CPU 都要再付一次

这一类在成本表里的位置很特别:它的优先级经常最高,因为它不需要动数据库模型,而且 ROI 最直接。

先算清楚重复度

一个 1024×768 的地图视窗按 256×256 瓦片算是十几个到几十个瓦片。用户拖动地图时,相邻视窗之间通常有 60%~80% 的瓦片是重复的;同一区域的不同用户看到的是完全相同的瓦片。也就是说,一个早高峰里可能有八成以上的矢量瓦片请求是重复计算。

把这些请求全部打到 PostGIS 上去做 ST_AsMVT 聚合,代价是三重的:一遍空间索引查询 + 几何裁剪(ST_AsMVTGeom)+ 重复的出网传输。而这类查询的结果在几何数据不变的前提下是完全确定的,天然适合缓存。

缓存放哪一层

三种做法,成本结构完全不同。第一种是 HTTP 层缓存(nginx / CDN),把 xyz 请求按 URI 做 key,这样最省事,命中全部走静态文件;缺点是数据更新时失效粒度粗。第二种是应用层缓存(Redis / Memcached 存序列化好的 mvt 二进制),命中率高、失效灵活,代价是要付出额外的一台内存机器和一次网络往返。第三种是预生成瓦片并用对象存储托管,把 z0~z12 全量烘焙一遍,剩下的高缩放级别按需生成并回填;这是三种做法里出网成本最省的一种,代价是预生成的磁盘占用和一次性计算工时。实际部署通常是组合:低缩放级别预生成 + 高缩放级别 Redis 缓存 + CDN 兜底。

命中率是唯一值得盯的指标。低于 90% 时,先别急着加 CPU,先去查缓存 key 是否被时间戳、随机数、无关参数污染了,以及瓦片失效策略是不是过于激进。命中率从 60% 提到 95% 意味着同样的 SQL 量下降到原来的约十四分之一,这个收益不是靠堆硬件能堆出来的。

出网流量怎么估

瓦片的实际体积取决于矢量复杂度、是否 gzip / brotli,实用的做法是拿自己真实的瓦片做一万个样本的平均值,而不是套别人的数字。但有一点是确定的:出网费用对出口在境外、或者有大量 WMS/WMTS 图片渲染请求的业务来说,完全可能超过数据库机器本身的月租。这类业务选机器时应该优先看带宽模式——一万网络的不少裸金属机型提供 50M / 100M / 1000M 不限流量的中国大陆优化端口,中国香港节点有 20M / 50M 专线档位(香港 E3 机型 ¥1500~1599/月,含 CN2),如果在意的是端口速率而非 95 计费峰值,模式差异带来的成本波动会比机器本身的价差更大;具体以官网实时报价为准。

慢查询四笔钱的对照:根因 × 该花的钱 × 优先级 × 判断方法

把上面拆开来的东西合到一张表上,方便对着自己库的实际情况打勾。表格里的量级是示例场景的估算区间,用于建立直觉,不代表任何普适结论。

慢的根因 这笔钱该花在哪 优先级 怎么判断(看什么) 典型代价与收益
统计信息过期、谓词写法导致落到 Seq Scan 维护窗口 + SQL 质量:ANALYZE、调统计目标、改写表达式、建表达式索引 P0,几乎零成本先做 explain 里 rows 估算与实际差 10 倍以上,且 Operator 是 Seq Scan 只花一次 ANALYZE 的时间,常见是一位数到两位数的倍率变化
索引用了但选择性差(胖大多边形、顶点数过高) 几何预处理:ST_Subdivide、ST_Simplify、降精度、换 opclass P0~P1,改表有停工窗口 explain analyze 里 Rows Removed by Filter 远大于返回行数 一次性改表与重建索引的工时;常见的收益是数量级级别的
working set 超出缓存,随机读 IO 顶住 内存条 + NVMe SSD + 按时间/网格分区裁剪扫描范围 P1,真花钱但上限清晰 explain (analyze, buffers) 中 shared read 远大于 hit,首次与二次耗时差 3 倍以上 32G→128G 是几百到一两千元/月的量级(视产品线),NVMe 与 SATA 的 IOPS 通常差一个数量级
重复查询(底图瓦片、热点面)没有被缓存 缓存层(HTTP / Redis / 对象存储)+ 出网带宽模式 P0~P1,ROI 通常最高 pg_stat_statements 里高 calls 的重复语句、瓦片缓存命中率 < 90% 命中率从 60% 到 95%,同硬件承载请求数通常提升数倍;境外出网成本需单独核算
膨胀(bloat)与未清理的死元组拖慢全链路 vacuum 策略 + 磁盘余量 + 维护窗口 P1~P2,长期成本 pg_stat_user_tables 里 n_dead_tup 持续增长,索引体积远大于估算 不花钱但要有维护窗口;VACUUM FULL 需要至少一倍表大小的空闲磁盘

避坑清单:五个让 PostGIS 越优化越慢的动作

坑一:表导入完就开始压测,从不 ANALYZE

为什么坑:刚 bulk load 完的表,几何列的统计信息是空或者严重滞后的。优化器拿到的是一个失真的选择性估计,在这基础上做的执行计划,比没索引还难预测。而这时候团队往往正在压测,得出的结论会被带进容量规划里。
怎么判断:explain 里 points 行的 rows 估算值与实际 explain analyze 返回的 actual rows 差 10 倍以上,基本就是它了。
怎么避:导入脚本的最后一步固定加上 ANALYZE 目标表;对过滤条件涉及几何列的组合谓词按需调大 STATISTICS 目标;对数据变更频繁的表,把 autovacuum 的 scale factor 从默认值下调。

坑二:全局把 work_mem 调到几百 MB

为什么坑:work_mem 是 per-plan-node 再乘并行 worker 的。并发一上来,理论峰值远超物理内存,触发 swap 之后,随机读延迟会从微秒级掉到毫秒级,整个实例比调优前还慢,而且这种退化是突变的,没有慢慢变慢的缓冲期。
怎么判断:监控系统里 swap 使用量开始爬升,同时 shared_buffers 命中率异常下降;或者在 pg_stat_activity 里看到大量并发的排序类查询。
怎么避:全局 work_mem 保持 16~32MB 量级,用 alter role 或者会话级 SET 给 ETL、批量导出单独放大;同时 max_parallel_workers_per_gather 的取值要和 work_mem 一起算总账。

坑三:看到大表就分区,但分区键和查询条件对不上

为什么坑:分区裁剪不生效时,一次查询要遍历所有分区的索引,规划时间和执行时间都比单表更差。而且分区数量到了几百上千之后,规划阶段的开销本身就变成一个可观测的成本项。
怎么判断:explain 里如果出现对全部子分区的 Append 扫描,或者计划时间明显长于执行时间,就是没有裁剪。
怎么避:先确认查询的谓词里一定会带上分区键(时间字段或地理网格列),再决定分区键;分区数量控制在几十到一两百的量级,超出就考虑两级分区。

坑四:直接用 ST_Distance 做范围过滤

为什么坑:ST_Distance(a, b) < 100 这种写法不会用到空间索引,因为 geom 被包在函数调用里做比较,优化器只能全表算距离再过滤。ST_DWithin 才是带索引语义的那个算子。
怎么判断:explain 里出现 Seq Scan,而把它改写成 ST_DWithin 之后立刻变成 Index Scan,就确认了。
怎么避:范围查询一律用 ST_DWithin;"最近的 N 个"用 ORDER BY geom <-> 常量配合 LIMIT,走 GiST 的 KNN 索引顺序扫描。另外注意 geography 类型做距离计算是在球面上算的,比 geometry 慢,能用平面的地方别轻易改类型。

坑五:把 VACUUM FULL 当常规维护跑

为什么坑:VACUUM FULL 会持 AccessExclusiveLock 重写整张表,业务侧就是完全不可用;而且它需要至少一倍表大小的空闲磁盘,磁盘没余量时会在半途失败,留下一地残局。相比起来,普通 VACUUM 只是标记死元组可复用,不归还磁盘给操作系统,很多人觉得"没效果",但其实日常的膨胀靠参数调好的 autovacuum 就够。
怎么判断:看 pg_stat_user_tables 的 n_dead_tup 与 n_live_tup 比值,以及 pgstattuple / pg_class 估算的膨胀比例;只在膨胀比例确实失控时考虑重写。
怎么避:优先调 autovacuum 的 per-table 参数(降低 vacuum scale factor、提高 cost limit)让它跟上写入速度;真要回收空间,评估可在线完成重组织的第三方扩展(如 pg_repack 这类,是否可用取决于环境),或者在窗口期用分区 drop / detach 的方式替代。

FAQ:索引、内存、分区、vacuum、备份与扩容的常见追问

PostGIS 的 GiST、SP-GiST、BRIN 到底该怎么选?

先看查询模式,再看数据分布,最后看是否需要 KNN 排序。绝大多数混合负载直接用 GiST,它是唯一在二维/三维、KNN 排序、多列组合索引这几个维度上都比较完整的选项。纯点表、数据量千万以上、查询以 bbox 范围为主且不需要和状态列组合索引时,可以测一测 SP-GiST,它的索引体积通常更小。BRIN 只在数据物理聚簇时才有意义——按车辆顺序写入的轨迹表是典型适合场景,反之按时间混写的全局点表基本无效。选型前务必在真实的同一份数据上用同一批 SQL 做三组对照测量,别凭直觉。

索引明明建了,为什么 explain 还是 Seq Scan?

三种可能,按顺序排查。第一种是统计信息:没有 ANALYZE 或长期没更新,优化器的 rows 估算是错的,可以强制 enable_seqscan = off 跑一次 explain 对比代价,如果强制后反而更快,说明是估算问题。第二种是谓词写法:几何列被包进了 ST_Transform、ST_Buffer 之类的函数,或者用 ST_Distance 与常量比较做范围过滤,这两种写法都吃不到索引。第三种是选择性本身太差:查询范围是整个城市级别时,索引要返回的行数占总表比例过高,这时优化器选全表扫是正确的,真正该做的是 Subdivide 或者加时间/网格条件。

内存加到多少才合适,有没有一个可以照抄的比例?

没有通用比例,但有可操作的判断顺序。第一步拿 shared_buffers 的当前值和 pg_buffercache 看 usage count 分布,如果整体命中率已经很高(绝大多数页面 usagecount ≥ 3),继续加 shared_buffers 基本没用,该加的那部分应该留给 OS page cache。第二步估算 working set:热数据 + 空间索引的体积之和,如果这个数字远大于可用内存,那么往上加内存是有明确收益的;如果已经小于可用内存还慢,那瓶颈不在 IO。第三步留意 NUMA 和实例密度——一台机器上跑多个实例时,单个实例的 shared_buffers 要按实例数分摊,而不是按整机内存算。

这个体量的表到底要不要做分区?

看三个条件是否满足两个以上。一是查询里必然带着与分区键相关的条件(时间窗口或地理网格),能保证裁剪生效;二是存在明显的冷热分化,历史数据几乎不查但需要长期保留——这种情况下把老分区迁移到便宜的存储上,成本收益很直接;三是单表的维护操作(vacuum、重建索引、备份)已经开始在业务窗口里跑不完。分区的代价也要一起算:更多的小索引意味着更高的规划开销和更多的小 IO,批量导入要落到正确的分区,以及跨分区查询会退化。分区数量建议控制在一两百以内。

vacuum 应该排在什么时候,多久一次?

不要按"多久一次"来排,按 n_dead_tup 触发。默认 autovacuum 的 scale factor 是 20%,对一张 3000 万行的表意味着要攒够 600 万死元组才启动一次 vacuum,这对高频更新的 GIS 表来说太钝了,通常是把 per-table 的 scale factor 压到 1%~2%,配合更高的 cost limit 让它在后台跟得上。真正要把物理顺序重写一遍的操作(VACUUM FULL、CLUSTER)需要单独申请维护窗口,并且提前确认磁盘有至少一倍表大小的余量。还要注意长事务:一个跑了几小时没提交的只读事务会挡住 vacuum 清理掉它可见范围内的死元组,这是很多人"参数都调了还是膨胀"的根因。

PostGIS 库做备份与恢复,时间大概是什么量级?

差异很大,取决于用逻辑备份还是物理备份,以及数据量。逻辑备份(pg_dump)在几十 GB 到数百 GB 的库上通常要数小时,恢复更慢,因为要重建所有 GiST 索引——这部分往往是恢复时间里最大的一块;custom 格式配上 parallelism 只能缓解,不能消除。物理备份(基础备份 + WAL 归档)恢复一个数百 GB 的库通常在数十分钟量级,加上 PITR 回放 WAL 的时间。做容量规划时,务必把"重建索引"的时间单独算进去,并且在演练环境里真的跑过一次恢复,而不是只看备份是否成功。

什么时候该换更强的存储,而不是继续加内存?

看 explain (analyze, buffers) 里的 read 和 IO 等待。如果在内存还在增长的空间里却始终 shared read 居高不下,说明 working set 超出了任何量级的内存预算,这时加内存只是在推迟问题。另外看 IOPS 是否被配额打满:很多云盘 IOPS 配额低于 vCPU 的处理能力,监控上表现为 CPU 使用率不高但查询延迟随并发陡增,iowait 与 await 指标同步上翘。这种情况下换更高 IOPS 档位的 NVMe 本地盘,收益会比加一条内存直接得多。还有一种做法是把热索引单独放到高性能盘上,用 tablespace 把它与冷数据表区分开。

一台机器撑到什么规模就必须考虑拆了?

拆的是读写,不一定是数据量。几个信号同时出现就可以开始规划:写吞吐量已经逼近单实例 vacuum 能跟上的上限(表现为持续膨胀);主实例的 CPU 主频瓶颈让单查询延迟无法再通过 parallel worker 压下去;或者读流量拆分之后,地理上分区域部署能让不同地区的用户各自访问最近的只读副本。常见的第一步是加只读物理副本承担地图渲染和分析类查询,第二步才是按业务或地理分区做水平切分。也要诚实地说,很多团队在几千万行这个量级时,拆实例的收益远不如把索引对接 SQL、几何瘦身和瓦片缓存这三件事做对。

结论:什么样的慢值得加钱,什么样的慢一分钱都不该花

把结论讲死一点。如果你的慢查询符合这三条中的任意一条——explain 里是 Seq Scan 且 rows 估算失真、Rows Removed by Filter 远大于返回行数、或者 pg_stat_statements 里 top 语句的 calls 高得离谱——那么最优解分别是一次 ANALYZE、一次几何瘦身、和一层缓存,这三件事的总成本远低于一条内存的价格,而且做完之后大概率会发现原本准备点的那一批机器根本不需要。

只有当你能明确证明 working set 大于可用缓存,并且 IOPS 没被打满、几何没有过度冗余、部分 SQL 没有被重复执行时,加内存才是正当的答案。判断标准是 explain (analyze, buffers) 里的 read/hit 比值,不是"感觉库大了该加内存"。而存储这笔钱要花在随机 IOPS 上,不是顺序带宽——绝大多数人在看 SSD 参数时盯的是那个用不上的顺序读写数字。

落到硬件选型上,机房选址这件事很现实。做地图服务和位置检索的,节点和用户之间的往返延迟直接影响首页加载观感:中国大陆用户在华南 / 华东 / 华北节点最稳,涉及境外用户或者需要免备案快速上线的,中国香港节点的 CN2 GIA 回国线路是个常见的折中,像香港 E3 机型 ¥1500~1599/月这类档位适合做起步验证,具体以官网实时价为准。一万网络深耕 IDC 19 年(成立于 2007 年),具备增值电信业务经营许可证、国家高新技术企业与专精特新资质,提供 7×24 中文工单(平均 5 分钟响应)、免费网站备案协助与 5~20G 免费 DDoS 防护;工程师可以在交付阶段协助完成系统初始化与磁盘挂载方案。对 PostGIS 来说,比报价更值得在签约前问清楚的三件事是:数据盘是 SATA 还是 NVMe、端口是共享还是独享以及是否需要 CN2、以及同一机型能否按 IO 曲线先低配验证再扩容。

PostGIS 慢查询排查的收尾:把索引、内存和缓存层分开记这三笔账

回到开篇那个 4.2 秒。如果把它拆开来看,按顺序走的应该是这样一条路径:先跑 explain (analyze, buffers) 确认优化器到底选了什么路、read 和 hit 的比值是多少;再对照 pg_stat_statements 确认这条 SQL 是不是被重复调用了几百次;然后才轮到硬件。这三步的成本是几小时的工程师时间,而不是一台机器的月租。

而这三笔账之所以要分开记,是因为它们虽然都表现为"查询慢",但在财务上完全不是一回事。索引与统计信息属于运维动作,边际成本接近零,但没有固定投入就会复发;内存与 SSD 属于资本性支出,收益有明确上限,过了 working set 这个临界点再加就是浪费;缓存层与出网属于持续运营支出,它的好在业务量增长时会放大——请求量翻倍时,缓存层好的架构成本增长远低于线性,而缓存层缺失的架构成本就是翻倍。

给一个可以执行的检查顺序:第一步 ANALYZE 并核对 rows 估算;第二步改谓词写法并把 ST_Distance 换成 ST_DWithin;第三步处理过大几何(Subdivide / Simplify);第四步量化 working set 再决定内存;第五步上线瓦片缓存并盯命中率;第六步才是 IOPS 与分区。绝大多数团队会在第二步和第五步之间拿到他们想要的全部性能,剩下的两步留给真正跑到大体量的那一天。

PostGIS 部署这篇文章的数据来源与价格口径说明

PostgreSQL 官方文档(版本 Release Notes、Resource Consumption 与 Write Ahead Log 章节、以及 CREATE INDEX、ANALYZE、VACUUM 的语法与行为说明),用于 shared_buffers、work_mem、maintenance_work_mem、autovacuum 相关参数的语义与限制口径。

PostgreSQL 官方 contrib 模块文档(pg_stat_statements、pg_buffercache、pgstattuple),用于慢查询归类与膨胀度量的观测方法。

PostGIS 官方文档与手册(Spatial Indexes、使用 PostGIS 的常见问题章节、ST_Subdivide、ST_Simplify、ST_AsMVT、ST_DWithin 等函数的官方说明),用于索引类型选择边界、瓦片矢量切片与几何预处理的建议。

OpenStreetMap Wiki 的 Slippy Map Tilenames 条目,用于瓦片金字塔结构与数量随缩放级别增长的换算方式。

一万网络(idc10000.net)官网服务器租用、裸金属、弹性云页面,用于本文出现的大陆/中国香港节点起步价、裸金属机型档位与带宽模式口径,公开页面上明示的报价以官网实时价为准。

需要说明的是,文中出现的延迟数字、行数规模、命中率阈值与资源消耗占比,均为示例场景与技术常识基础上的估算,用于说明排查顺序与配置取舍的方向,不构成任何具体环境下的性能承诺。参数的最优取值与两者对比结论,需要在实际数据和实际负载上完成测量。具体以签约时最新报价与合同为准。


上一篇:休斯顿这条墨西哥湾节点怎么读:石化与能源业务的飓风季冗余、出网与本地合规怎么算

下一篇:没有了!