多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

全球城市经纬度SQL数据:中英文与层级关系导入查询指南

全球城市经纬度SQL数据:中英文与层级关系导入查询指南 简介这份SQL文件面向地理信息系统开发者、C#应用工程师及需要位置服务的数据分析人员提供全球主要城市的经纬度数据解决地图服务、位置追踪与地理统计中缺乏统一城市坐标基础表的问题。压缩包共1个文件为单个sql脚本体积约146KB内含建表DDL与数据插入DML语句可直接导入数据库使用。数据精确到城市级别包含中英文对照的城市名称并保留国家、地区、州/省等层级关系便于区域分析与多语言应用开发。已有151人学习下载。读者可借此快速构建城市坐标表用于两点距离计算、导航定位、地理统计分析并结合C#等语言开发具备位置服务能力的应用或集成到现有系统中扩展地理信息功能省去自行采集与整理全球城市经纬度的工作量。1. 全球主要城市经纬度数据一份 SQL 文件怎么把中英文和层级关系一次说清做地理相关的业务时最容易被低估的往往不是地图渲染而是底层那份城市清单。你拿到一份「全球主要城市_经纬度数据_中英文_层级关系_精确到城市_SQL文件.zip」第一反应可能是不就是一张表字段有城市名、经纬度、国家、省份吗真导入进去用起来问题才一个个冒出来——同一个城市中文名有「旧金山」和「圣弗朗西斯科」两种写法英文名带不带州后缀层级关系里「市」和「区」混在一列经纬度精度在小数点后四位还是六位之间反复横跳。这份数据的价值不在于它有多少行而在于它把中英文对照、行政层级、坐标精度这三件事同时压进了一个可被 SQL 直接消费的结构里。适合谁用做跨境物流地址校验的、做多语言站点地区选择器的、做数据看板里城市维度下钻的以及任何需要「按城市聚合但不想自己维护一份脏清单」的团队。下面按导入、理解结构、查询、避坑、进阶的顺序把这份 SQL 文件从解压到跑通讲透。2. 先看懂 SQL 文件里的表结构和层级设计2.1 为什么层级关系不能只靠一张平表很多人拿到城市数据的第一反应是建一张宽表城市名、省份名、国家名、经纬度全塞一行。这种设计在查询「某个国家下所有城市」时很快但一旦要做「从洲到国家到省到市」的四级下钻或者处理「一个城市属于多个上级行政区」的边界情况平表就会暴露冗余和更新异常。这份 SQL 文件通常采用自引用或分层编码的方式来表达层级每一行有一个唯一 ID同时有一个 parent_id 指向上一级或者用类似level字段标记当前是洲、国家、省还是市。精确到城市意味着最细粒度那一层的 level 值固定往上聚合时只需要按 parent_id 递归或按层级编码前缀匹配。常见做法是两种邻接表parent_id和路径枚举path 字段存/洲/国家/省/市。邻接表写入简单、更新方便但递归查询依赖数据库的 CTE 支持路径枚举查询快但移动节点时要批量改路径。这份数据既然强调「层级关系」大概率在表里同时保留了 parent_id 和一个可读的层级路径或层级码方便你按前缀 LIKE 查询。导入前先确认你的数据库版本支持递归 CTEMySQL 8.0 以上、PostgreSQL 全系、SQL Server 2005 以上都没问题老版本 MySQL 5.7 就得靠自连接硬撑。2.2 中英文字段的命名习惯与编码陷阱中英文对照不是简单加两列name_cn和name_en就完事。实际数据里常见的坑是英文列里混着本地语言转写中文列里夹着繁体或异体字还有的用name_zh、name_en、local_name三列并存。导入前先用文本编辑器或head命令看前几十行 INSERT 语句确认列名和字符集。如果 SQL 文件里建表语句写了CHARSETutf8mb4那中文和 emoji 都能存如果只写了utf8MySQL 里其实是 utf8mb3遇到生僻字或某些符号会截断或报错。另一个高频问题是排序规则。utf8mb4_general_ci和utf8mb4_unicode_ci对中英文混合排序结果不同做「按城市名排序」的列表时前者可能把中文排在英文后面且顺序不符合拼音习惯。如果业务需要按拼音或笔画排序光靠数据库默认排序不够得额外加一列name_pinyin或sort_key。这份数据如果没带拼音列你可以在导入后用脚本补但别指望 SQL 文件本身帮你解决。2.3 导入前的三步检查编码、分隔符、批量大小拿到.sql文件先别急着source。第一步用file -i看文件编码常见是 UTF-8 带 BOM 或不带 BOM带 BOM 时第一行建表语句可能报语法错误。第二步看 INSERT 语句是单行一条还是批量多值批量多值时注意max_allowed_packet是否够大默认 4MB 或 16MB全球城市数据加上中英文和层级字段批量 INSERT 很容易超。第三步确认 SQL 文件里有没有CREATE DATABASE和USE语句如果没有你得先建库再导入。# 检查文件编码和大小 file -i global_cities.sql ls -lh global_cities.sql # 查看前 50 行确认建表语句和字符集 head -n 50 global_cities.sql # 查看是否有 CREATE DATABASE / USE 语句 grep -n -E CREATE DATABASE|USE global_cities.sql | head上面命令的逻辑说明file -i输出 MIME 类型和字符集如果显示charsetbinary或unknown-8bit说明编码不是标准 UTF-8需要转码后再导入。head -n 50让你在不打开大文件的情况下看清表结构定义重点看CHARSET和COLLATE。grep用来确认库名避免导入到错误的数据库。参数上如果文件超过 100MB建议用mysql命令行客户端而不是图形工具图形工具在批量 INSERT 时容易超时或内存溢出。3. 把 SQL 文件跑起来建库、导入、验证的最小闭环3.1 建库与调整导入参数导入前先根据数据量调整几个关键参数。max_allowed_packet决定单条 SQL 语句的最大长度批量 INSERT 多行时容易撞上限innodb_buffer_pool_size影响导入速度但生产环境别为了导入临时调太大autocommit在导入时建议关闭用事务包住批量插入能快很多。下面是一套可抄的导入流程以 MySQL 为例。-- 创建数据库字符集必须 utf8mb4 CREATE DATABASE IF NOT EXISTS geo_city DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE geo_city; -- 导入前临时调整会话参数只影响当前连接 SET SESSION sql_mode NO_AUTO_VALUE_ON_ZERO; SET SESSION autocommit 0; SET SESSION unique_checks 0; SET SESSION foreign_key_checks 0;逻辑说明utf8mb4是必须的否则中文城市名里的生僻字和某些符号会丢。sql_mode设为NO_AUTO_VALUE_ON_ZERO是为了防止自增列遇到 0 值时报错有些数据文件会用 0 表示未知父级。关闭autocommit、unique_checks、foreign_key_checks是导入加速的常规操作导入完成后记得改回来。参数上unique_checks0只在确定数据无重复时用如果数据本身有重复主键导入后会留下隐患建议导入后跑一次去重检查。3.2 用命令行导入并观察错误# 在 shell 中执行导入把错误输出到单独文件 mysql -u root -p --default-character-setutf8mb4 geo_city global_cities.sql 2 import_error.log # 查看错误日志里有没有报错 grep -i -E error|warning import_error.log | head -n 20逻辑说明--default-character-setutf8mb4确保客户端和服务端握手时用对字符集少了这个参数中文可能变问号。2 import_error.log把标准错误重定向到文件避免刷屏导入后集中看。如果错误日志里出现Packet too large就去调大max_allowed_packet出现Incorrect string value说明文件编码和数据库字符集不匹配需要转码或改列字符集。3.3 导入后的三张验证查询导入完成不等于数据可用。至少跑三类验证行数是否符合预期、层级关系是否闭环、经纬度是否在合法范围。-- 验证 1总行数和各层级行数 SELECT level, COUNT(*) AS cnt FROM cities GROUP BY level ORDER BY level; -- 验证 2找出没有父级的非顶级节点层级断裂 SELECT id, name_cn, level, parent_id FROM cities WHERE parent_id IS NULL AND level continent LIMIT 20; -- 验证 3经纬度合法性检查 SELECT id, name_cn, latitude, longitude FROM cities WHERE latitude NOT BETWEEN -90 AND 90 OR longitude NOT BETWEEN -180 AND 180 LIMIT 20;逻辑说明第一条按层级分组计数能快速看出数据覆盖是否完整比如城市层行数远少于预期可能是导入中断。第二条找层级断裂parent_id为空但 level 不是顶级说明数据有缺失或字段映射错了。第三条检查坐标范围纬度超出 ±90 或经度超出 ±180 的记录要么是数据错误要么是字段顺序颠倒把经度当纬度存了。参数上LIMIT 20只是抽样看真要全量检查就去掉 LIMIT 看总数。4. 按城市查经纬度和中英文名的常用 SQL 写法4.1 精确匹配与模糊匹配的取舍业务里查城市经纬度最常用的是按名称精确匹配。但中英文混合场景下「精确」本身就有歧义用户输入「北京」要能命中输入「Beijing」也要能命中输入「北京市」最好也能命中。如果表里只有name_cn和name_en两列精确匹配就得写两个条件用 OR 连接或者用 UNION。更稳的做法是建一个统一的搜索列把中英文和别名拼在一起加全文索引或前缀索引。-- 精确匹配中英文名含常见后缀变体 SELECT id, name_cn, name_en, latitude, longitude, level FROM cities WHERE level city AND (name_cn 北京 OR name_en Beijing OR name_cn 北京市) LIMIT 10; -- 模糊匹配适合搜索框联想 SELECT id, name_cn, name_en, latitude, longitude FROM cities WHERE level city AND (name_cn LIKE 北% OR name_en LIKE Bei%) ORDER BY name_en LIMIT 20;逻辑说明第一条用 OR 枚举常见变体适合已知输入规范的场景。第二条用 LIKE 前缀匹配适合搜索框实时联想但注意LIKE %北京%这种前后都带通配符的写法无法走索引数据量大时会全表扫描。参数上level city是为了排除省和国家层同名干扰比如「吉林」既是省也是市不加层级过滤会返回多条。4.2 按层级下钻从国家拿到所有城市层级关系的核心价值是下钻。假设你要查「某个国家下所有城市及其经纬度」用邻接表递归 CTE 是最标准的写法。下面以 PostgreSQL 和 MySQL 8.0 通用的递归 CTE 为例。-- 从国家节点递归拿到其下所有城市 WITH RECURSIVE city_tree AS ( -- 锚点先找到目标国家节点 SELECT id, name_cn, name_en, level, parent_id, latitude, longitude FROM cities WHERE name_en France AND level country UNION ALL -- 递归逐层向下找子节点 SELECT c.id, c.name_cn, c.name_en, c.level, c.parent_id, c.latitude, c.longitude FROM cities c INNER JOIN city_tree ct ON c.parent_id ct.id ) SELECT id, name_cn, name_en, latitude, longitude FROM city_tree WHERE level city ORDER BY name_en;逻辑说明锚点部分定位到国家节点递归部分通过c.parent_id ct.id逐层展开直到没有子节点为止。最后过滤level city只取城市层。参数上如果数据里层级深度超过默认递归限制MySQL 默认cte_max_recursion_depth是 1000一般够用如果数据用路径枚举而不是 parent_id就把递归部分换成WHERE path LIKE /France/%这种前缀匹配速度更快但依赖路径字段的格式统一。4.3 中英文对照输出的排序与去重做多语言站点时经常需要「按当前语言排序但输出中英文两列」。如果直接ORDER BY name_cn中文排序结果取决于数据库的 collation可能不是拼音顺序。一个折中方案是加一列name_en作为次级排序因为英文排序稳定且可预期。-- 按英文名排序输出中英文对照去重同名城市 SELECT DISTINCT name_cn, name_en, latitude, longitude FROM cities WHERE level city AND name_en IS NOT NULL AND name_cn IS NOT NULL ORDER BY name_en ASC LIMIT 100;逻辑说明DISTINCT用来去掉中英文名和坐标完全相同的重复行有些数据文件在合并多个来源时会留下重复。name_en IS NOT NULL和name_cn IS NOT NULL过滤掉缺失翻译的记录保证输出对照完整。参数上LIMIT 100只是示例实际分页要用LIMIT offset, size或游标分页避免大偏移量性能问题。5. 避坑导入和查询这份城市数据时最容易翻车的 5 个点5.1 现象中文城市名导入后变成问号或乱码原因SQL 文件本身是 GBK 或 Latin1 编码但导入时客户端用了 utf8mb4或者建表时列字符集没跟上。解决先用file -i确认文件编码如果是 GBK用iconv -f GBK -t UTF-8转码后再导入建表语句里所有文本列显式写CHARACTER SET utf8mb4别依赖库默认。5.2 现象递归查询报错「Recursive query aborted after 1001 iterations」原因数据里存在循环引用A 的 parent 是 BB 的 parent 又是 A递归 CTE 陷入死循环。解决导入后跑一次循环检测找出 parent_id 链上回到自身的记录。MySQL 里可以临时调大cte_max_recursion_depth看看到底循环在哪但根治方法是修数据把循环的 parent_id 置空或指向正确上级。-- 检测简单循环自己指向自己 SELECT id, name_cn, parent_id FROM cities WHERE id parent_id; -- 检测两级循环A-B-A SELECT a.id, a.name_cn, b.id AS parent_id, b.name_cn AS parent_name FROM cities a JOIN cities b ON a.parent_id b.id WHERE b.parent_id a.id;5.3 现象经纬度查出来是 0,0 或者明显偏移原因数据里用 0,0 表示未知坐标或者经纬度列顺序颠倒纬度存了经度值。解决导入后跑范围检查把 0,0 的记录单独标记如果发现大量纬度绝对值大于 90基本可以确定列顺序反了用 UPDATE 交换两列。5.4 现象按城市名查询返回多条分不清哪个是目标原因不同国家有同名城市比如「圣何塞」在美国和哥斯达黎加都有「剑桥」在英国和美国都有。解决查询时必须带上国家或省份过滤或者用层级路径字段做前缀限定。如果业务允许用户只输城市名返回结果里要带上国家名和经纬度让用户自己选。5.5 现象导入速度极慢几万行跑了半小时原因autocommit 开着每条 INSERT 都刷盘或者批量 INSERT 的包太大反复重试。解决导入前关 autocommit用START TRANSACTION包住把大文件拆成多个小文件分批导入每批 5000 到 10000 行如果用的是 InnoDB导入期间临时把innodb_flush_log_at_trx_commit设为 2导入后改回 1。6. 进阶用法用这份数据做城市距离计算和区域聚合6.1 用经纬度算球面距离的 SQL 写法拿到城市经纬度后一个高频需求是「查附近城市」或「算两个城市直线距离」。MySQL 里可以用 ST_Distance_Sphere 函数PostgreSQL 用 earthdistance 扩展或 PostGIS。下面是不依赖扩展的纯 SQL 近似算法Haversine 公式适合快速验证。-- 计算北京到上海的大圆距离单位公里 SELECT ROUND( 6371 * ACOS( COS(RADIANS(39.9042)) * COS(RADIANS(31.2304)) * COS(RADIANS(121.4737) - RADIANS(116.4074)) SIN(RADIANS(39.9042)) * SIN(RADIANS(31.2304)) ), 2 ) AS distance_km;逻辑说明6371 是地球平均半径公里RADIANS把角度转弧度ACOS里的表达式是球面余弦定理。参数上纬度用latitude经度用longitude注意顺序别写反。这个公式在短距离几百公里内精度够用跨半球或极地附近误差会变大生产环境建议用 PostGIS 的ST_DistanceSphere或ST_Distance带 geography 类型。6.2 按国家或洲聚合城市数量与中心点层级关系让聚合变得简单按 parent_id 分组就能统计每个国家有多少城市再往上按洲分组。如果要算某个区域的城市中心点可以对经纬度取平均值但注意经度在 ±180 附近跨线时平均值会出错比如斐济和新西兰的城市经度平均后可能跑到 0 度附近。更稳的做法是用向量平均或直接取区域边界框中心。-- 统计每个国家的城市数量按数量降序 SELECT p.name_cn AS country_name, COUNT(*) AS city_count FROM cities c JOIN cities p ON c.parent_id p.id WHERE c.level city AND p.level country GROUP BY p.id, p.name_cn ORDER BY city_count DESC LIMIT 20; -- 计算某国家所有城市的经纬度平均值仅作粗略中心点 SELECT p.name_cn AS country_name, AVG(c.latitude) AS avg_lat, AVG(c.longitude) AS avg_lon FROM cities c JOIN cities p ON c.parent_id p.id WHERE c.level city AND p.level country AND p.name_en France GROUP BY p.id, p.name_cn;逻辑说明第一条自连接把城市和它的父级国家关联起来按国家分组计数。第二条用 AVG 算平均坐标只适合经度范围不跨 ±180 的国家。参数上JOIN条件用c.parent_id p.id依赖层级数据完整如果 parent_id 有缺失这些城市会被排除在统计外跑之前先用第 3 章的验证查询确认层级闭环。6.3 把城市数据做成可缓存的查询接口如果业务里频繁按城市名查经纬度每次走数据库不是最优解。可以在导入后把level city的记录导出成一份轻量缓存比如 Redis 的 Hash 结构key 用city:name_cn或city:name_envalue 存经纬度和层级路径。更新频率低的话这份缓存可以长期有效数据有更新时重新导入 SQL 后刷新缓存即可。注意中英文 key 要分开存避免大小写和空格差异导致命中失败。我自己的习惯是拿到任何一份带层级的城市数据先跑一遍第 3 章的验证查询确认行数、层级、坐标范围三项都正常再开始写业务查询。这一步花五分钟能省掉后面几小时排查脏数据的后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表