水库台账、逐月来水、行政区划、年度供用水统计——原本分别躺在几十个 Excel、几百页 PDF 和一堆 shapefile 里。把它们统一成一组可查询的表,找一个数从「开三个软件翻半天」变成「写一行筛选条件」。
难点几乎全都不在「查询」本身,而在数据进来之前的口径。下面六条是真实踩出来的,演示区里每一条都在跑:
| 难点 | 为什么难 / 这里怎么处理 |
|---|---|
| 一个县在源表里有好几行 | 年报按水资源分区出行,一个县可能跨两三个分区。只读第一行会漏掉一半水量;把所有行无脑求和,又会把「城镇化率」这类比值也加起来。 做法:外延量(人口、水量、产值)求和;比值与人均量在聚合完成之后重算。 |
| 年年的表头都不一样 | 某一年改走公报汇总表,字段就少了几列。按首年表头去读,要么新增列被静默丢掉,要么直接崩。 做法:列 = 各年字段的并集(按首次出现顺序),某年没有的画「—」,绝不填 0——0 是一个有含义的观测值,缺测不是 0。 |
| 一张表里混着两套维度 | 「蓄水/引水/提水/跨流域调水」是取水方式,「地表水/地下水/其他水源」是水源类型,而前四项之和恰好等于地表水。全都拿去算占比,加起来就是 200%。 做法:把子项集合显式登记为常量,占比只沿一条维度轴算。 |
| 跨表只能靠名字 join | 工程台账里的乡镇名来自工程详表(人填),行政区划来自测绘边界(机器出)。撤乡设镇、更名、简称,两边就对不上,而且没有共同的编码。 做法:先精确匹配,未命中的用曾用名/别名/同字前缀找候选,最后出一张命中率报告交人判——不静默丢,也不自动改。 |
| 源表登记的字段不一定对 | 「工程规模」是有国标口径的(按总库容分级),但源表那一列是人填的,填错并不罕见。 做法:入库时按库容重算一遍并与登记值对账,冲突单独列出来给人判定,不自动覆盖。 |
| 没有编码体系可依赖 | 县名有「统计口径名」和「行政全称」两种写法(例如统计表写简称、台账写全称)。 做法:行政区划单独立一张 SSOT 表,其他表一律向它对齐,别名映射写在这一张表里,不在各处各自约定。 |
统一之后,整个数据仓就是几张说得清结构的表。下面的行数是当场数出来的,不是写死的文案。
ALTER TABLE 加列,表会越长越宽,还得回头改所有下游查询。
这里把稳定的部分(县、年份、统计类型)做成三元唯一键建索引,
把易变的指标塞进一个 JSON 列,读的时候再决定有哪些列(schema-on-read)。
代价是不能直接对单个指标建索引——在这个数据量级(几千行)完全可以接受。
| 存储 | 按主题拆成若干个 SQLite 单文件库(工程台账 / 统计年报 / 行政区划 / 调查评价成果 / 空间切片)。单文件意味着免服务、可直接拷给同事、可进版本库看体积变化;跨库查询用 ATTACH DATABASE 在一个连接里 join,不搬数据。 |
| 半结构化 | 统计表 = (县, 年份, 统计类型) 三元唯一键 + 一个 JSON 数据列。年报加字段不用改表结构;查询时按各年字段并集拼列,缺的画「—」。 |
| 幂等导入 | 全部走 INSERT OR REPLACE + 唯一键:同一份源文件重跑多少次,结果都一样。DB 是派生物,源 Excel / PDF / shapefile 才是 SSOT——所以库本身不做备份,坏了重建即可。 |
| 口径纪律 | 子项集合、单位、派生指标公式一律写成带注释的代码常量,不散落在各处口头约定。典型的一条:地表水 = 蓄+引+提+跨流域,所以这四项在算占比时必须排除,否则总和翻倍。 |
| 名称对齐 | 行政区划单独立 SSOT 表(县 + 乡镇/街道,含曾用名与别名字段),其他表向它对齐。匹配失败不静默丢弃,产出命中率报告 + 候选建议,人工确认后再回写。 |
| 查询界面 | Web 端用 Datasette 直接挂库:字段中文别名、默认排序、facet 筛选器全写在一份 metadata 配置里,不写前端代码。同一批数据另配一组 CLI,输出直接就是 Markdown 表格,粘进报告即成稿——这才是设计人员真正每天用的入口。 |
| 空间数据 | shapefile 按县切成 GeoJSON 切片后入库,带 geometry 列,前端直接取切片渲染地图;切片文件比库新就自动重建,不用人记得刷新。 |
这套东西没有任何算法上的新意,它的全部价值在于把「口径」从人的脑子里搬进代码里。 在此之前,「这个县到底有几座中型水库」这种问题的答案取决于谁去数、数的哪一版表; 之后,它是一条可以复现、可以对账、可以被别人挑错的查询。 写报告时数字对不上的返工,比查数本身贵得多。