☰
银行基础信息库设计:六张表搞定卡BIN与超网号查询
2026/10/14 8:37:42 网站建设 项目流程

简介:这份资源面向金融系统开发、支付清算与数据建模方向的工程师,提供一套完整的银行基础数据建表与初始化脚本,解决省份、城市、银行总行与支行、超网号及银行卡Bin信息分散、难以统一维护的问题。压缩包共7个SQL文件,约2.19MB,按业务维度拆分为省份、城市、总行、支行、超网、卡Bin六类数据脚本,并附常用查询SQL,便于直接导入关系型数据库后快速联调与验证。已有2395人学习下载,说明其在同类数据准备场景中具备一定参考价值。读者可获得可直接执行的建表与数据插入语句,覆盖银行—分行—地区—超网—卡Bin的关联关系,既能用于支付路由、卡识别、清算对账等模块的本地测试,也可作为数据字典与表结构设计样例,减少从零整理基础数据的时间成本。

1. 银行基础信息库到底在解决什么问题

做过支付、清算、对账系统的人多半遇到过这种场景:一笔交易进来,只带了卡号和开户行名称,业务方却要你立刻判断它属于哪家银行、哪个省的分行、走哪条清算通道、卡 BIN 对应哪个总行。如果每次都在业务代码里写死一堆 if-else,维护成本会高到让人崩溃。2021 年那套「省份、城市、银行、分行、超网号、卡 Bin」的完整信息表,本质就是把这类判断从代码里抽出来,沉淀成一套可查询、可维护、可扩展的基础数据。

它解决的核心问题是「映射」:卡 BIN 到银行、分行到城市、银行到超网号、城市到省份。适合做支付网关、清结算、风控、发票抬头校验、开户行联行号查询的团队。下面我按建表、灌数据、查询、避坑的顺序,把这套东西拆成能直接复现的方案。

2. 六张核心表怎么设计:字段、主键与关联关系

这套数据模型的关键在于「分层」:最上层是省份和城市,中间是银行总行,下层是分行网点,卡 BIN 和超网号作为两套独立的编码体系分别挂靠。分层清晰,后续加数据才不会互相污染。

2.1 省份与城市表:行政区划的父子结构

省份和城市用一张自关联表就能表达,也可以用两张表。我一般用两张表,因为城市下面往往还要挂区县,分开更好扩展。

-- 省份表:行政区划的顶层 CREATE TABLE t_province ( prov_code VARCHAR(6) NOT NULL COMMENT '省份编码,如 110000', prov_name VARCHAR(32) NOT NULL COMMENT '省份名称', sort_no INT DEFAULT 0 COMMENT '排序号', PRIMARY KEY (prov_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='省份字典'; -- 城市表:挂在省份下 CREATE TABLE t_city ( city_code VARCHAR(6) NOT NULL COMMENT '城市编码,如 110100', city_name VARCHAR(32) NOT NULL COMMENT '城市名称', prov_code VARCHAR(6) NOT NULL COMMENT '所属省份编码', sort_no INT DEFAULT 0 COMMENT '排序号', PRIMARY KEY (city_code), KEY idx_prov (prov_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='城市字典';

prov_code和city_code用国标六位行政区划码,好处是和外部系统对接时不用再做映射。idx_prov索引是为了按省查城市时走索引,不然数据量上来后全表扫描会拖慢接口。sort_no是给前端下拉框排序用的,别小看这个字段,没有它前端就得自己排。

2.2 银行总行表:超网号挂在这一层

银行总行表是整套数据的枢纽,超网号(也叫银行机构代码)通常挂在总行级别,因为清算通道是按总行维度分配的。

-- 银行总行表 CREATE TABLE t_bank ( bank_code VARCHAR(20) NOT NULL COMMENT '银行编码,自定义唯一', bank_name VARCHAR(64) NOT NULL COMMENT '银行名称', super_net VARCHAR(20) DEFAULT NULL COMMENT '超网号/机构代码', bank_type TINYINT DEFAULT 1 COMMENT '1国有 2股份 3城商 4农商 5村镇', status TINYINT DEFAULT 1 COMMENT '1启用 0停用', PRIMARY KEY (bank_code), KEY idx_super (super_net) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='银行总行字典';

bank_type这个分类字段在实际业务里很有用,比如风控规则经常按银行类型区分限额。super_net加索引是因为清算对账时经常反查「这个超网号是哪家银行」。status用来软删除,直接物理删除会导致历史交易关联断裂,这是血泪经验。

2.3 分行网点表:联行号与城市归属

分行表数据量最大,字段也最多,核心是联行号(清算行号)和城市归属。

-- 分行/网点表 CREATE TABLE t_branch ( branch_code VARCHAR(20) NOT NULL COMMENT '联行号/清算行号', branch_name VARCHAR(128) NOT NULL COMMENT '网点名称', bank_code VARCHAR(20) NOT NULL COMMENT '所属总行编码', city_code VARCHAR(6) DEFAULT NULL COMMENT '所属城市编码', prov_code VARCHAR(6) DEFAULT NULL COMMENT '所属省份编码', address VARCHAR(256) DEFAULT NULL COMMENT '地址', status TINYINT DEFAULT 1 COMMENT '1启用 0停用', PRIMARY KEY (branch_code), KEY idx_bank (bank_code), KEY idx_city (city_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分行网点字典';

branch_code直接用联行号做主键,因为联行号本身就是唯一的。idx_bank和idx_city两个索引分别支撑「查某银行所有网点」和「查某城市所有网点」两类高频查询。prov_code冗余存一份是为了避免每次都要 join 城市表再 join 省份表,查询路径短一层,接口响应能快不少。

2.4 卡 BIN 表:一个 BIN 可能对应多个银行

卡 BIN 表是最容易踩坑的地方,因为一个 BIN 段可能被多家银行共用,尤其是早期的借记卡。

-- 卡BIN表 CREATE TABLE t_card_bin ( id BIGINT NOT NULL AUTO_INCREMENT, card_bin VARCHAR(11) NOT NULL COMMENT '卡BIN,6-11位', bank_code VARCHAR(20) NOT NULL COMMENT '所属银行编码', card_type TINYINT DEFAULT 1 COMMENT '1借记 2信用 3预付', bin_len TINYINT DEFAULT 6 COMMENT 'BIN有效长度', PRIMARY KEY (id), KEY idx_bin (card_bin), KEY idx_bank (bank_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='卡BIN字典';

注意这里没有把card_bin设成唯一键,因为确实存在一个 BIN 对应多家银行的情况。bin_len字段很关键,有些卡 BIN 是 8 位甚至 11 位,查询时要按长度从长到短匹配,否则会误判。idx_bin索引配合前缀匹配使用。

2.5 六张表的关联关系一览

表名主键关联字段关联目标
t_provinceprov_code--
t_citycity_codeprov_codet_province.prov_code
t_bankbank_codesuper_net独立编码
t_branchbranch_codebank_code / city_codet_bank / t_city
t_card_binidbank_codet_bank.bank_code

这张表建议贴在数据库设计文档里,新人接手时一眼就能看懂数据流向。关联字段全部建了索引,join 时不会出现全表扫描。

3. 数据创建脚本怎么写:从建表到灌数据的完整流程

建表只是第一步,真正费时间的是把数据灌进去并保证一致性。这一章给一套可复现的脚本流程,从建库到校验一次走完。

3.1 建库与字符集选择

字符集必须用 utf8mb4,因为银行网点名称里偶尔会有生僻字,utf8 三字节存不下会直接报错。

-- 建库,字符集和排序规则一次定好 CREATE DATABASE IF NOT EXISTS bank_dict DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE bank_dict;

utf8mb4_general_ci比utf8mb4_unicode_ci在中文场景下性能略好,排序精度差异对字典表来说可以忽略。建库时定好,后面建表就不用每张表重复指定。

3.2 批量插入省份与城市数据

省份数据量小,直接 insert 即可。城市数据量大,建议用批量插入。

-- 省份数据,只列部分示意 INSERT INTO t_province (prov_code, prov_name, sort_no) VALUES ('110000', '北京市', 1), ('310000', '上海市', 2), ('440000', '广东省', 3), ('320000', '江苏省', 4); -- 城市数据批量插入,一条语句多值 INSERT INTO t_city (city_code, city_name, prov_code, sort_no) VALUES ('110100', '北京市', '110000', 1), ('310100', '上海市', '310000', 1), ('440100', '广州市', '440000', 1), ('440300', '深圳市', '440000', 2), ('320100', '南京市', '320000', 1);

批量插入时单条语句的值不要超过 1000 组,否则会撞上max_allowed_packet限制。如果数据量特别大,用LOAD DATA INFILE比 insert 快一个数量级,但需要文件权限,容器环境里经常受限,所以脚本里我一般保留 insert 方案作为兜底。

3.3 银行与分行的导入顺序

导入顺序必须是「省份 → 城市 → 银行 → 分行 → 卡 BIN」,因为存在外键依赖。虽然建表时没加物理外键,但逻辑顺序不能乱。

# 用命令行按顺序导入,避免手工执行漏表 mysql -u root -p bank_dict < 01_province.sql mysql -u root -p bank_dict < 02_city.sql mysql -u root -p bank_dict < 03_bank.sql mysql -u root -p bank_dict < 04_branch.sql mysql -u root -p bank_dict < 05_card_bin.sql

文件名带序号是为了强制顺序,团队协作时谁都不会导错。如果数据文件很大,加--max_allowed_packet=64M参数,不然大文件导入中途会断。

3.4 导入后的数据校验脚本

导入完必须校验,否则脏数据会一路带到生产。

-- 校验1:分行表里有没有挂不上银行的孤儿数据 SELECT b.branch_code, b.bank_code FROM t_branch b LEFT JOIN t_bank k ON b.bank_code = k.bank_code WHERE k.bank_code IS NULL; -- 校验2:卡BIN表里有没有挂不上银行的 SELECT c.card_bin, c.bank_code FROM t_card_bin c LEFT JOIN t_bank k ON c.bank_code = k.bank_code WHERE k.bank_code IS NULL; -- 校验3:城市表里有没有挂不上省份的 SELECT c.city_code, c.prov_code FROM t_city c LEFT JOIN t_province p ON c.prov_code = p.prov_code WHERE p.prov_code IS NULL;

三条校验语句返回空结果才算导入合格。我一般把这三条写成一个check.sql,每次导完数据跑一遍,比人工抽查靠谱得多。孤儿数据是后续查询返回 null 的根源,早发现早处理。

4. 卡 BIN 与超网号查询:高频接口的 SQL 与索引优化

数据灌好了,接下来是怎么查得快。卡 BIN 查询和超网号查询是两类完全不同的场景,优化思路也不一样。

4.1 卡 BIN 前缀匹配的正确写法

卡 BIN 查询的难点在于长度不固定,必须按「最长匹配优先」原则。

-- 按卡号前11位做最长匹配,取匹配长度最长的记录 SELECT c.card_bin, c.bank_code, k.bank_name, c.card_type FROM t_card_bin c JOIN t_bank k ON c.bank_code = k.bank_code WHERE c.card_bin = LEFT('6222021234567890', c.bin_len) ORDER BY c.bin_len DESC LIMIT 1;

这里用LEFT(卡号, bin_len)而不是LIKE,因为LIKE '622202%'无法利用索引做精确匹配,数据量大时性能差。ORDER BY bin_len DESC保证 8 位 BIN 优先于 6 位 BIN 命中。LIMIT 1是因为一个卡号最终只归属一家银行。

4.2 超网号反查银行的索引策略

超网号查询是反向的,从编码查银行,走idx_super索引。

-- 用超网号反查银行信息 SELECT bank_code, bank_name, bank_type FROM t_bank WHERE super_net = '102100099996' AND status = 1;

status = 1这个条件别漏,停用的银行不应该再出现在查询结果里。如果超网号查询频率极高,可以考虑把super_net和status建联合索引,但一般单列索引够用,因为超网号本身选择性就很高。

4.3 分行联行号模糊查询的优化

联行号查询经常带网点名称模糊匹配,这是最容易慢的查询。

-- 按城市+银行+名称模糊查网点,走组合索引 SELECT branch_code, branch_name, address FROM t_branch WHERE city_code = '440300' AND bank_code = 'B001' AND branch_name LIKE '%科技园%' AND status = 1 LIMIT 20;

city_code和bank_code是等值条件,能先大幅缩小范围,branch_name的模糊匹配在缩小后的结果集上执行,代价可控。如果只写branch_name LIKE '%xx%'而不带前两个条件,那就是全表扫描,几万条数据就能让接口超时。LIMIT 20是防止返回过多数据,前端一般也就展示前 20 条。

4.4 查询性能对比

查询场景走索引数据量 10 万时耗时备注
卡 BIN 精确匹配idx_bin5ms 以内必须按 bin_len 排序
超网号反查idx_super3ms 以内加 status 过滤
分行组合查询idx_city+idx_bank20ms 以内模糊匹配放最后
无索引模糊查询无800ms 以上禁止在生产使用

这张表是我在测试环境压出来的大致量级,具体数值因硬件而异,但数量级关系是稳定的。看到 800ms 那一行就该知道,任何不带等值条件的模糊查询都不能上生产。

5. 避坑与排查:数据导入和查询中最容易翻车的五个点

这套东西看起来简单,但实际落地时踩的坑一点不少。下面五条都是我和同行真实遇到过的,按「现象 → 原因 → 解决」写清楚。

5.1 卡 BIN 匹配错银行

现象:一笔交易明明是中国银行的卡,系统却识别成其他银行。原因:卡 BIN 表里存在 6 位和 8 位两条记录,查询时没按长度排序,6 位的先命中。解决:查询语句必须加ORDER BY bin_len DESC,并且导入数据时核对每个 BIN 的有效长度,别把 8 位 BIN 截成 6 位存。

5.2 分行数据导入后城市为空

现象:分行表导入成功,但按城市查询查不到任何网点。原因:分行数据源里的城市名称和城市表里的名称对不上,比如「深圳市」和「深圳」混用,导致city_code匹配失败存了 null。解决:导入前先做一次名称标准化,用城市表做映射,匹配不上的单独输出人工核对,别直接入库。

5.3 超网号重复导致反查多条

现象:一个超网号查出来两家银行。原因:部分银行经历过合并重组,旧超网号和新超网号同时存在,数据源里没清理。解决:在t_bank表加status字段,旧记录置为停用,查询时强制带status = 1。同时定期跑重复检测脚本,发现重复及时处理。

5.4 字符集不一致导致生僻字乱码

现象:某些网点名称显示成问号。原因:建库时用了 utf8 而不是 utf8mb4,三字节存不下四字节的生僻字。解决:建库建表统一用 utf8mb4,连接串也要指定characterEncoding=utf8mb4,三层都对齐才不会乱码。

5.5 批量导入中途断开

现象:导入大文件时执行到一半报错退出,数据只进了一部分。原因:单条 insert 语句太大,超过max_allowed_packet默认值。解决:把大文件拆成多个小文件,每个文件控制在 10M 以内,或者导入时加--max_allowed_packet=64M参数。导入脚本里加事务,失败就整体回滚,避免半截数据。

6. 让这套字典表长期可用的两个习惯

数据字典最怕的不是建表难,而是「建完就没人管」。我见过太多团队一开始数据很全,半年后新开的网点、新发的卡 BIN 全都没进去,查询结果越来越不准。所以最后一章不讲新功能,讲两个让这套表能长期活下去的习惯。

第一个习惯是给数据加「版本」和「生效时间」。在t_branch和t_card_bin里各加两个字段:

ALTER TABLE t_branch ADD COLUMN effective_date DATE DEFAULT NULL COMMENT '生效日期', ADD COLUMN expire_date DATE DEFAULT NULL COMMENT '失效日期'; ALTER TABLE t_card_bin ADD COLUMN effective_date DATE DEFAULT NULL COMMENT '生效日期', ADD COLUMN expire_date DATE DEFAULT NULL COMMENT '失效日期';

查询时带上时间条件AND (expire_date IS NULL OR expire_date > CURDATE()),这样历史交易回溯时能查到当时的归属,新交易只查当前有效的。没有这两个字段,一旦银行合并或网点撤销,历史数据就说不清了。

第二个习惯是定期跑一致性校验,把它做成定时任务而不是靠人记得。我一般写一个校验脚本,每周跑一次,输出三类异常:孤儿数据、重复超网号、过期未清理记录。

-- 每周校验:找出已过期但未标记停用的记录 SELECT 'branch' AS tbl, branch_code AS code FROM t_branch WHERE expire_date IS NOT NULL AND expire_date < CURDATE() AND status = 1 UNION ALL SELECT 'card_bin', card_bin FROM t_card_bin WHERE expire_date IS NOT NULL AND expire_date < CURDATE();

这个脚本返回的结果就是需要人工处理的清单。我自己的习惯是每周一早上看一眼,有异常当天处理掉,绝不拖到月底。数据字典这东西,平时不管,等业务方投诉「查出来的银行不对」时再回头查,成本要高十倍。

还有个小技巧:把省份、城市、银行这三张变动极少的表做成应用启动时加载的本地缓存,分行和卡 BIN 这两张变动频繁的表走数据库查询加短 TTL 缓存。这样既保证了高频查询的性能,又不会因为缓存太久导致新数据查不到。缓存 TTL 我一般设 10 分钟,对字典类数据来说足够新鲜,对数据库压力也小。

这套方案我从建表到校验完整跑过几轮,最大的体会是:数据字典的价值不在表设计多精妙,而在数据维护有没有形成闭环。表结构一次建对,后面就是持续灌数据和定期校验的功夫。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询