文章是本站的原创长文。每篇都保持无脚本、来源可溯,并与本站记录的系统现实一致:一个小型、静态、公网服务如何被构建、加固并保持诚实。
代理主键把一行记录的身份与可能变化的业务值分开。客户修改邮箱时,订单仍引用同一个客户编号,而不必跟着邮箱一起改引用。这对于易变或较宽的业务键很有用,但稳定性来自数据库约束与应用规则,不是“自动生成”四个字本身。一个生成的数字不会天然不可修改、保密、全局唯一,也不能代替访问授权。
稳定标识来自规则与约束。生成机制、合法存储值、业务规则、引用和访问决定需要分别保证。 生成身份与约束唯一性 PostgreSQL identity 列从关联序列取得默认值。GENERATED ALWAYS AS IDENTITY 会拒绝普通显式赋值,GENERATED BY DEFAULT 则允许调用方提供值。即使使用 ALWAYS,导入时仍可通过 OVERRIDING SYSTEM VALUE 显式覆盖。两种形式都不会单独保证值唯一;需要拒绝重复标识时,仍须声明 PRIMARY KEY 或 UNIQUE 约束。
主键同时保证唯一与非空,并提供可供引用使用的索引。Identity 负责怎样生成默认值,约束负责哪些最终值允许存储,这两项职责不能混淆。导入时尤其如此:显式写入一个很大的编号,不会自动把序列推进到那个位置。若没有协调序列状态,后续生成值可能最终与导入行相撞,直到主键报错才暴露问题。
单独保留业务唯一规则 代理编号不会阻止相同业务实体重复出现。如果邮箱是唯一登录标识,应继续保留明确的唯一约束,并先决定规范化规则。普通 text 的 UNIQUE 使用该列的比较语义,不会暗中替应用处理大小写折叠、空白或各种邮箱等价规则。迁移之前就要明确这些行为,否则数据库认为不同的值,应用可能认为是同一账号,或者反过来。
子表如果不能出现悬空引用,就需要真实的外键约束。邮箱不是引用键时,修改它不会改变订单指向的客户,因为订单保存的是 id。这缩小了这一项修改的范围,却不能承诺所有业务改名都变成简单操作:搜索索引、审计记录、外部集成和缓存仍可能依赖邮箱,需要按实际调用链更新。数据模型只解决它真正覆盖的关系。
...
一个 socket 单元可以在兼容应用停止时继续提供监听端点,从而减少少用服务的常驻内存与周期性工作。但套接字、内核队列和管理器仍然占用资源,已经安装的软件也仍然需要安全更新。监听可用还取决于绑定成功、权限正确以及 socket 单元保持活动。端口正在监听,只表示客户端有机会请求工作,并不能证明应用已经初始化或能够返回正确结果;这些条件必须分别观察。
交接方式由配置决定。Accept=no 把监听描述符交给守护进程,由它自己接受连接;Accept=yes 则先接受连接,再启动适合处理该连接的服务实例。客户端可能在应用初始化之前完成连接,随后等待响应,甚至等到超时。应测量从连接到应用回复的完整延迟,不能因为交接对客户端接口透明,就推断冷启动和常驻服务在时延、失败诊断或实际资源成本上没有区别。
socket 单元活动时,请求可以启动兼容服务。应用明确定义的空闲退出可使进程停止,后续需求再触发启动;没有空闲退出时,服务仍继续运行。应在真实负载下测量连接至首个响应的冷启动延迟、客户端截止时间和常驻资源。监控可能同时唤醒多个服务,因此需要验证聚合启动负载。仅示意所选机制;原文仍为搁置/待审,不为其错误泛化背书。 仅示意选定的原文机制;本轮没有执行示例。 让监听器先等着 socket 单元启动时,systemd 打开 ListenStream 指定的端点。Accept=no 通常由 foo.socket 激活 foo.service 并传递监听器,Service= 可以指定其他服务;Accept=yes 通常使用 foo@.service 模板,为每条接受的流连接生成实例。数据报套接字不采用这种逐连接模型。程序必须匹配选定的描述符契约,不能只因为两个单元名称相近,就认为普通绑定逻辑可以原样使用。
这种分离可以减少启动顺序上的耦合,但不会消除依赖。客户端使用套接字之前,socket 单元必须已经活动并监听,所需的服务、挂载、设备与先后顺序依赖仍然适用。兼容服务启动时,连接可以在有限的内核积压队列中等待,但客户端仍可能在应用就绪前失败或超时。当然积压长度也要调:启动慢的服务遇上开机早期的连接洪峰,溢出起来和普通监听队列一样。
服务按需唤醒 按需激活可以在 socket 保持活动时推迟应用的首次启动,但不会让已运行的守护进程自动休眠,也不会自动停止进入空闲的程序。管理服务、罕用协议处理器是否适合这种方式,要看激活接口、请求截止时间与资源画像。其他依赖或人工启动仍然可能提前激活服务,因此不能把首次连接当成所有环境中唯一的启动入口,更不能从文章标题推导永久的运行限制。
要形成反复节省资源的循环,应用必须明确实现安全的空闲退出,先完成或转交进行中的工作,并让管理器持有的监听器继续可用。socket 单元仍活动时,后续需求可以请求再次启动;没有空闲退出逻辑,守护进程可能在第一次激活后一直常驻。这两种情况都不是零成本状态,也不保证套接字永远不会关闭。应验证待处理请求和退出边界,而不是仅设置一个空闲秒数就认定节省已经成立。
用速度换内存 冷启动预算应从连接开始,一直计算到首个有效应用响应,并在有代表性的负载下比较常驻内存。假设启动三百毫秒能够节省八百 MB,在一种工作负载中可能合适,在另一种场景却可能超过请求期限。二十秒预热对异步任务也许可接受,对交互请求则可能不可用。这些数字只是说明取舍,并不是实测结果或通用决策阈值。
有些工作无法承受启动间隙。健康检查本身可能唤醒服务,也可能在初始化时把端点判为不可用;客户端截止时间和负载均衡策略必须匹配实际启动行为。若稳定响应时间比空闲内存更重要,保持守护进程运行可能更简单。开机预热一次并不会自动实现之后的空闲退出,也不能替代对长时间停用后恢复状态的检查。
坏掉的样子 一轮监控可能同时唤醒很多服务,引发总体 CPU 或内存尖峰。错峰检查可以减少同时到来的需求,单服务启动速率限制则约束该单元反复启动的次数,并不能限制整机并发数量或总内存。过短的空闲超时还可能增加启停抖动。应测试聚合启动负载,并使用合适资源控制,而不是把几个独立单元上的限制误认为全机容量保证。
...
一条过去很快的查询,可以在数据、统计和访问计划都没有变化时变慢。另一个事务可能持有它需要的锁,存储缓存可能比上次更冷,应用也可能在语句进入 PostgreSQL 之前就开始等待。重新检查计划很有用,但它应当放在完整的耗时比较中:先区分时间是花在执行工作上,还是花在等待获得执行机会。不能从“现在更慢”直接推导出“数据与统计一定都变了”。
保留可比较的基线 记录语句形状、有代表性的参数值、返回行数、相关会话设置,以及应用是否使用预备语句。不同参数可能命中完全不同规模的数据。手工使用常量的查询,不一定复现预备语句采用的通用计划;不同角色或搜索路径,也可能改变实际对象与权限。因此,拿一条看起来相似的语句作为对照,并不保证比较条件相同。
尽量在变慢正在发生时观察活动。pg_stat_activity 可以显示一个状态为运行中的查询正在等待锁;运行中并不等于持续消耗处理器。pg_blocking_pids 列出当前阻塞这个后端的会话。查看其他角色活动需要适当权限,因此字段不可见不等于没有工作发生。交互观察最好使用新的自动提交查询,而不是在同一个长事务中不断读取已经缓存的监控快照。
坏掉的样子 不加判断就运行 ANALYZE,相当于把每次变慢都当成统计陈旧。它可能增加维护工作,却完全没有解除真正的阻塞。一张只有一行的表,即使新增索引,也不能释放其他事务持有的行锁。准确的行数估计和很小的顺序扫描,可以与很长的应用等待同时存在;这两种观察并不矛盾。
EXPLAIN ANALYZE 会真正执行语句,写入也不例外,而且测量本身同样可能被阻塞。从生产环境复制一条大范围更新,不是无害的检查。应先看等待情况和锁的拥有者,再选择安全夹具或副作用已经明确的语句来测量。看到某个进程编号出现在阻塞列表中,也不能立即终止不熟悉的事务,因为它可能正在完成另一个有效业务操作。
没有锁等待的计划样本,也不等于端到端延迟。EXPLAIN ANALYZE 不会像普通查询那样把结果行发送给客户端;未指定 SERIALIZE 时,还不包含输出转换。TIMING OFF 减少逐节点计时开销,但仍保留总执行时间。大量结果传输、应用解码和连接池排队,需要在计划之外单独观察,不能从这里的执行时间中直接读出。
多会话过程归属于历史报告,未找回原始日志;打印观察需人工检查,超时是独立失败结果。 快速验证 打开三个新的 psql 会话,分别记作甲、乙、丙,连接同一个可丢弃的 PostgreSQL 18 数据库并使用相同角色,保留通常的自动提交行为。名为 query_recheck_demo 的模式必须尚不存在,集群中也不能有其他会话使用实验应用名称。角色须有权创建并移除模式,活动跟踪应已启用,且没有更短的锁超时。每个会话遇到非预期错误都应停止。务必区分会话:把所有代码块顺序粘贴到同一个连接,无法证明事务之间的阻塞。
在甲中建立夹具,然后开启事务,修改唯一一条记录但暂不提交。 \set ON_ERROR_STOP on CREATE SCHEMA query_recheck_demo; CREATE TABLE query_recheck_demo.items (id integer PRIMARY KEY,value integer); INSERT INTO query_recheck_demo.items VALUES (1,0); ANALYZE query_recheck_demo.items; BEGIN; UPDATE query_recheck_demo.items SET value=10 WHERE id=1; SELECT pg_backend_pid() AS blocker_pid; 在乙中发起带执行测量的更新。它会等待甲结束事务,或者等到六十秒的语句超时。 SET application_name='xbcatm_recheck_waiter'; SET statement_timeout='60s'; EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) UPDATE query_recheck_demo.items SET value=value+1 WHERE id=1; 乙仍在等待且尚未超时时,在丙中执行观察查询。实验专用应用名称用于定位乙,不需要展示整条查询文本。 SELECT pid,state,wait_event_type,wait_event,pg_blocking_pids(pid) AS blockers FROM pg_stat_activity WHERE application_name='xbcatm_recheck_waiter'; 回到甲,撤销尚未提交的变更并释放锁,然后收集乙执行完毕后的计划。 ROLLBACK; 确认乙已经成功结束后,在丙中检查最终数据并清理夹具。 SELECT value FROM query_recheck_demo.items WHERE id=1; DROP SCHEMA query_recheck_demo CASCADE; 原文报告的 PostgreSQL 18.6 观察未在本轮重新执行,也未在保留的验证文件中找到原始多会话日志;其中描述乙的状态为运行中,等待类型是 Lock,具体等待事件是 transactionid,阻塞列表包含甲的进程编号。具体事件可能随争用过程变化,核心证据是乙确实被甲阻塞。该报告称甲回滚后,乙把零加成一,最后查询返回一,清理移除了模式。必须人工确认丙恰好识别出乙、阻塞列表包含甲报告的 pid,且最终值为一;SQL 只是打印这些观察,没有自动断言。
...
HTTP/2 改变 HTTP 传输分帧和多路复用方式;gzip 与 Brotli 编码的是表示数据。启用 HTTP/2 不会自动压缩响应正文,启用 gzip 也不能证明客户端协商出了 HTTP/2。HTTP/2 的头部压缩又是另一种机制。可靠检查应分别记录协商协议、响应 Content-Encoding、传输正文字节数,以及实际解码后得到的内容,不能用其中一项代替其他结果。
坏掉的样子 发送 Accept-Encoding: gzip, br,然后把结果保存成 .br,并不能证明使用了 Brotli。服务器可能选择 gzip,也可能发送仍被请求接受的原始表示。真实编码由响应头说明。根据请求列表或输出文件名猜测,会导致使用错误解码器,也可能让报告声称获得了实际上根本没有发生的压缩结果。文件名只是本地命名,不是协议证据。
Brotli 支持还属于部署能力,并不是安装 nginx 后自然具备的功能。Brotli 过滤和预压缩文件处理由独立模块提供,需要针对实际二进制与配置确认可用性、构建兼容性。添加不受支持的指令可能让配置验证失败,而客户端声明接受 Brotli 不会替服务器安装功能。检查模块时,应以当前程序行为为准,不能从另一台机器的配置推断。
客户端自动解码会制造另一种测量陷阱。curl --compressed 收到较小编码正文后,可以保存成较大的解码文件。把这个文件大小与原始压缩输出比较,实际比较的是不同对象。反过来,只使用 --http2 却不检查协商结果,也可能把允许的协议回落误当成 HTTP/2 已经成功验证。命令选项表达请求意图,结果字段才说明本次发生了什么。
分别核对实际协议、编码与解码一致性。字节数来自历史文章报告,当前集成仍待验证。 快速验证 下文结果归属于历史文章报告,本轮没有重跑;保留的验证文件中未找到原始集成运行日志。运行需备好本地固定镜像并启用 Python 断言;清理仅尽力执行,不能据此保证所有资源已删除。
...
普通缓冲式关系文件读写中,PostgreSQL 的共享缓冲位于操作系统文件缓存之上。命中共享缓冲时,不需要再次通过下面的文件读取路径;没有命中时,也可能由操作系统直接从内存提供数据。同一文件内容可以在两层各有副本,所以这里并不承诺数据库与内核共用同一份物理内存,更不能把两层缓存理解为完全没有重复占用。mmap 共享内存配置针对共享区域,不能据此推断普通表文件被直接内存映射。
坏掉的样子 执行计划里的共享读取计数,表示块被读入 PostgreSQL 缓冲池,不证明存储设备实际执行了一次物理读取。如果把每次共享读取都算成磁盘未命中,已经温热的操作系统缓存也会被误认为冰冷,从而引出错误的内存配置判断。同样,第二次查询更快,也不能独自说明究竟是哪一层缓存发生了变化,或是否只是其他执行条件改变。
缓存驻留是临时状态。其他查询可能挤走页面,工作集超过容量时,也不能指望所有页永久留在内存里;关系扫描采用的缓冲策略还可能影响复用方式。因此应使用已知大小的工作集、检查实际访问路径,并把耗时结论与真正观察到的缓存层事实分开。没有设备层测量,就不能仅凭一个计划字段宣布磁盘速度发生了变化。
Postgres 有两层缓存:机制图。 快速验证 使用 PostgreSQL 18 可丢弃实例和超级用户,确保附带的缓冲观察与预热扩展可安装、且尚未安装。保留的较早运行使用 PostgreSQL 18.6 单用户后端及一百二十八兆字节共享缓冲。本次来源复核没有重跑;psql 传输与并发活动仍未验证。下面的数据可以轻松放下。通过一个全新的 psql -X -v ON_ERROR_STOP=1 会话按顺序执行。关系驱逐函数用于开发测试,这里只针对隔离实验创建的对象操作:
CREATE SCHEMA cache_layer_demo; CREATE EXTENSION pg_buffercache WITH SCHEMA cache_layer_demo; CREATE EXTENSION pg_prewarm WITH SCHEMA cache_layer_demo; CREATE TABLE cache_layer_demo.items (id integer PRIMARY KEY,payload text) WITH (autovacuum_enabled=false); INSERT INTO cache_layer_demo.items SELECT g,repeat(md5(g::text),10) FROM generate_series(1,12000) AS g; VACUUM (ANALYZE) cache_layer_demo.items; CREATE VIEW cache_layer_demo.residency AS SELECT count(*) AS heap_buffers FROM cache_layer_demo.pg_buffercache WHERE reldatabase=(SELECT oid FROM pg_database WHERE datname=current_database()) AND relfilenode=pg_relation_filenode('cache_layer_demo.items') AND relforknumber=0; SET max_parallel_workers_per_gather=0; SELECT pg_relation_size('cache_layer_demo.items')/current_setting('block_size')::int AS heap_blocks; SELECT heap_buffers>0 AS initially_buffered FROM cache_layer_demo.residency; 本次构建的主堆有五百四十六个块,初始缓存比较返回真。观察视图限制数据库、文件标识与主分支,并假定这个新建实验只使用默认表空间。通用监控还必须区分表空间,因为文件编号不是数据库内跨表空间唯一的物理身份。每个缓冲条目的观察是一致的,但整个视图并不是同一瞬间的原子缓存快照。接着,仅驱逐这个关系的共享缓冲:
...
记录规则在指定评估时刻执行 PromQL 表达式,把结果作为一个新指标保存。反复刷新的面板可以读取这个指标,减少重复计算。但简单把它称为缓存,会遮住一个重要区别:记录序列有自己的样本历史、写入节奏和标签集合,并不是任意查询请求的透明缓存。使用者必须知道读到的是哪次预计算,以及这个结果保留了哪些信息。
原文合成样例用每分钟输入和两分钟记录周期隔离节奏差异。新返回的查询可以读取更早的记录观测。历史本地 promtool 检查仅覆盖原文实验;本轮绘图没有新增执行或真实调度器证据。 三个时钟共同影响页面上的值 抓取决定原始观测何时进入存储,规则评估决定表达式何时生成记录样本,面板则决定何时以及以什么步长查询两者。把页面刷新得更快,不会让记录规则提前计算。因此,在两次规则评估之间读取记录指标,可能得到比同一时刻直接执行原表达式更旧的值。这是不同采样节奏的正常结果,不能仅凭两条曲线不同便断定计算错误。
规则组可以指定周期;未单独指定时,采用全局规则评估周期。同组规则按顺序执行,并使用相同的评估时间戳,所以后面的规则可以使用前面规则刚生成的结果。独立规则组即使写了相同的周期,也不能因此推导存在同样的先后顺序。依赖关系需要明确设计,不能依靠某次运行中碰巧出现的执行顺序。
预计算也改变了保留的数据 聚合表达式决定输出保留哪些维度。如果规则把 instance 求和消掉,面板之后就无法仅从这条记录序列恢复实例分布。仍需实例级排查时,应保留原始指标,并围绕已知消费者选择记录维度。每个剩余标签组合又是一条需要存储的新序列,因此把所有可能的聚合都做成规则,可能显著增加写入量和存储量,并不总是节省资源。
修改表达式会影响后续评估。正常重载规则不会用新定义重新计算全部旧样本,也不会自动填补规则创建之前的历史。在相同指标名下改变含义,可能让同一张图同时包含两套不兼容口径。应记录定义和时间窗口,在消费者要求历史可比时安排明确迁移,不能把正常重载解释为历史数据已经整体更新。
坏掉的样子 面板看起来停住,但原始样本仍在进入数据库时,应对照记录序列最近样本、规则组最近评估、执行耗时与错误。如果上一轮还没结束,下一次计划评估到来时会被跳过,直到当前轮完成或超时。把昂贵查询变成记录规则,只是把计算移入调度器,没有让计算成本消失。执行耗时长期接近周期时,新鲜度会比配置值更差。
查询变快也可能是因为聚合后回答的问题更窄。庆祝速度提升前,应确认被移除的维度确实不再需要。简单表达式通常不值得只为性能增加记录规则。应先测量重复查询成本和复用频率,再把新增序列、周期执行以及新鲜度延迟一起纳入判断。面板数量很多,也不等于每个表达式都必须预先保存。
快速验证 保存为 recording-cadence.py,运行 python3 recording-cadence.py。环境需要已经安装的 promtool,可以从 PATH 找到,或用 PROMTOOL 指定其可执行文件路径。此前记录的执行使用 Python 3.12.14 与 promtool 3.14.0;本轮文章复核没有重新运行该引擎。真实查询引擎在临时目录中评估合成样本,不启动服务器、监听端口或容器,也不联系外部服务。
from pathlib import Path from tempfile import TemporaryDirectory import os import shutil import subprocess # Uses a real, installed promtool; no server or Docker runtime is started. promtool = shutil.which(os.environ.get("PROMTOOL", "promtool")) if not promtool: raise SystemExit("Set PROMTOOL to an installed promtool executable") tests = r"""evaluation_interval: 2m rule_files: - rules.yml tests: - interval: 1m input_series: - series: "queue_depth{queue=\"orders\"}" values: "1 4 9 16" promql_expr_test: - expr: "sum(queue_depth)" eval_time: 1m exp_samples: - labels: "{}" value: 4 - expr: "queue:depth:sum" eval_time: 1m exp_samples: - labels: "{__name__=\"queue:depth:sum\"}" value: 1 - expr: "queue:depth:sum" eval_time: 2m exp_samples: - labels: "{__name__=\"queue:depth:sum\"}" value: 9 - expr: 'timestamp(queue:depth:sum)' eval_time: 1m exp_samples: - labels: '{}' value: 0 - expr: 'timestamp(queue:depth:sum)' eval_time: 2m exp_samples: - labels: '{}' value: 120 - expr: 'queue:depth:double' eval_time: 1m exp_samples: - labels: '{__name__="queue:depth:double"}' value: 2 - expr: 'queue:depth:double' eval_time: 2m exp_samples: - labels: '{__name__="queue:depth:double"}' value: 18 """ rules = r"""groups: - name: slow-recording interval: 2m rules: - record: queue:depth:sum expr: sum(queue_depth) - record: queue:depth:double expr: queue:depth:sum * 2 """ with TemporaryDirectory(prefix="prometheus-query-check-") as directory: root = Path(directory) (root / "tests.yml").write_text(tests, encoding="utf-8") if rules: (root / "rules.yml").write_text(rules, encoding="utf-8") result = subprocess.run([promtool, "test", "rules", "tests.yml"], cwd=root, text=True, capture_output=True, timeout=45) print(result.stdout, end="") if result.returncode: print(result.stderr, end="") raise SystemExit(result.returncode) 预期输出为 SUCCESS。第一分钟原始求和结果是四,但记录值仍为第零分钟的一;第二分钟记录序列更新为九。这里故意选择很便宜的表达式,以隔离周期差异,不是在建议为每个队列仪表值建立记录规则,也没有测量真实调度器在负载下的性能。输入时间线和断言直接说明为何同一时刻两个查询可以返回不同结果。
...
Redis 地理命令为带名称的位置建立索引,可以直接回答附近有哪些点,不必把全部坐标传回应用。它适合能够接受球面距离模型的场景,但不会提供道路路线、考虑海拔的空间距离、任意多边形分析,也不会保证查询到的每个位置仍然新鲜。这些不是查询速度更快就能顺带解决的问题。
经度必须放在纬度前面 GEOADD 按经度、纬度、成员名的顺序接收参数。有效经度从负一百八十度到正一百八十度,纬度则限制在大约负 85.05112878 度到正 85.05112878 度之间,极区不属于该索引支持的范围。如果把两个各自仍合法的数值交换位置,命令可能正常成功,却把成员放到完全不同的地方。
索引底层是有序集合,分数保存五十二位地理哈希编码,不能用普通排行榜分数覆盖这些位置编码。成员是唯一的:更新同名成员只会改变位置,不会增加一份记录。GEOADD 默认统计新增成员数,加 CH 才统计新增及实际变化。删除位置使用 ZREM,读取存储坐标则使用 GEOPOS;不要把一次更新返回零当成更新失败。
坐标编码具有有限精度,因此 GEOPOS 不一定原样返回输入的小数。GEODIST 使用理想球面地球计算表面距离,不是穿过地球内部的直线弦长,更不是驾车路线。默认单位为米,也可以选择公里、英尺和英里。任何一个端点缺失时,返回空结果,而不是把不存在的位置当作距离为零的重合点。
把查询条件写成明确契约 Redis 6.2 开始提供 GEOSEARCH,可以围绕已有成员或显式经纬度查询。BYRADIUS 指定圆形范围,BYBOX 则按所选单位指定轴向对齐矩形的宽和高。新的查询应使用该命令,而不是已弃用的 GEORADIUS 系列。请求中的单位与结果解释必须一致,否则正确的数值也会被应用解读成错误的距离。
ASC 按距离从近到远排列,COUNT 限制返回数量。要找最近的若干个结果,应使用 ASC 和 COUNT,避免 ANY;ANY 允许找到足够数量后提前结束,不能保证拿到查询区域内全局最近的那些点。另外,返回数量少不代表查询一定便宜:范围巨大且点位密集时,即使只要求少量结果,也可能需要检查很多候选。
FROMMEMBER 查询可以包含作为中心的成员自身。如果业务要找“附近的其他人”,就需要处理这个结果,而不是直接把第一条当作别人。WITHDIST 返回所选单位下的距离,WITHCOORD 返回坐标。结果反映的是查询时索引里存放的数据,并不等于已经独立确认某台设备或某个商户此刻就在该位置。
坏掉的样子 文档说明,在边界情况下,球面近似与地球实际形状之间的误差可以达到约百分之零点五。因此,“城市尺度”并不自动意味着误差小于一米。应用应明确可接受误差及距离边界如何判定;测绘、安全关键接近判断、道路规划或严格地理围栏,可能需要更合适的空间模型,不能只凭一次示例距离看起来合理就验收精度。
...
PostgreSQL 哈希索引先利用哈希码缩小等值查询的候选范围,再回到表中核对原始值。哈希匹配只说明“可能相等”,不能直接证明相等。两个不同值可能具有相同哈希码,不同哈希码也可能映射到同一个桶。正常复核存在时,碰撞和共享桶都不意味着查询会返回错误结果;它们是实现需要处理的普通情况,而不是可以忽略的例外。
桶里究竟保存什么 索引条目包含四字节哈希值和指向表元组的引用,不保存完整的被索引列值。桶映射把查询导向相关索引页面。桶是共享存储空间,并不是给每个不同键单独分配一个专属位置。一个桶的主页装满后,可以继续连接溢出页,查询这个桶时也需要访问这些页面。因此,“一次算出桶”不等于“永远只读取一个页面”。
PostgreSQL 可以在单个索引页内部按哈希码排序,以便加速局部搜索,但这不会提供原始列值的顺序。哈希索引不能满足键范围或有序输出,只支持单列等值访问,不能强制唯一性,也不支持 IS NULL。B-tree 仍然可以服务等值查询,同时支持更广的一组操作,不能因为键看起来适合哈希就自动排除其他候选。
由于条目中没有原始值,哈希扫描是有损候选扫描,需要用表中实际值复核。因此,碰撞可以在执行计划中表现为 Rows Removed by Index Recheck。这说明数据库正确剔除了只在哈希上匹配的候选,而不是把两个碰撞值都当成相等返回。解读这项指标时,应把候选数量与最终答案区分开。
坏掉的样子 把索引键更新描述为“只碰一个桶”,掩盖了实际工作。修改被索引列会改变元组对应的索引值,新条目可能进入另一个桶。PostgreSQL 使用行版本与延后索引清理,旧引用不一定立刻从结构中消失。数据增长还可能触发桶分裂,溢出页也会增加读写。因此不能用一个理想化的哈希表赋值过程来解释数据库的全部更新成本。
高度重复的值、快速增长的表,与近乎唯一而稳定的等值负载,表现可能很不一样。哈希查询不是无条件的恒定延迟承诺。现代 PostgreSQL 的哈希索引支持崩溃恢复,但持久性既不会消除维护成本,也不会让它突然具备唯一性约束能力。读性能、写维护和数据契约必须分别确认。
同码与同桶不同;形状仅为示意。历史具体碰撞未找回原始日志,也未重跑。 快速验证 在可丢弃的 PostgreSQL 18 数据库中,用全新的 psql 会话执行下面代码;任何 SQL 或 psql 元命令错误都应停止。它建立二十万个采用 C 排序规则的临时文本键,使用当前服务器自己的 hashtext 找到一组碰撞,再验证等值查询和键更新。实验没有把哈希码当成跨版本稳定的业务标识,最后回滚会移除所有夹具。
...