简介:这份资源面向金融系统开发、支付清算与数据建模方向的工程师,提供一套完整的银行基础数据建表与初始化脚本,解决省份、城市、银行总行与支行、超网号及银行卡Bin信息分散、难以统一维护的问题。压缩包共7个文件,全部为sql脚本,整体约2.19MB,按业务维度拆分为省份、城市、总行、支行、超网、卡Bin等独立脚本,并附常用查询SQL,便于按需导入与二次扩展。目前已有2395人学习下载,适合需要快速搭建银行主数据环境或进行联调测试的开发者参考。读者可直接获得可直接执行的关系型表结构与数据插入语句,覆盖银行—分行—地区—超网—卡Bin的关联链路,省去自行整理基础数据的成本,也能借助常用查询脚本快速验证数据完整性与关联逻辑,为支付路由、卡识别、机构归属等场景提供可靠的数据底座。
1. 一套能跑通的银行网点与卡BIN基础数据模型,到底解决什么问题
做过支付、清结算或者风控系统的同行大概都有过这种体验:业务方甩过来一个需求,要按省份、城市、银行、分行、超网号、卡BIN做多维路由和校验,结果发现手头的数据要么缺字段,要么对不上号,要么根本没人说得清超网号和卡BIN之间是什么关系。这套「2021最完整省份、城市、银行、分行、超网号、卡Bin信息,表结构及数据创建脚本」要解决的,就是把这几层数据用一套自洽的表结构串起来,让你在本地或者测试环境里几分钟就能把基础库建好,而不是花两周去拼凑Excel。
它适合三类人:一是做支付路由、清算对账的后端,需要一张能按卡BIN反查总行/分行的映射表;二是做风控和反欺诈的,需要知道某个卡BIN属于哪家银行、哪个省份,用来做地域规则;三是做数据仓库建模的,需要一套规范的行政区划加金融机构层级模型作为维度表。核心难点不在数据量,而在于层级关系怎么设计、超网号到底挂在哪一层、卡BIN和分行是不是一对一。下面按建表、灌数据、查关系、避坑、进阶的顺序拆开讲。
2. 表结构怎么设计:五张表把行政区划和银行层级拆干净
2.1 为什么拆成省份、城市、银行、分行、卡BIN五张表
很多人第一反应是搞一张大宽表,把省、市、银行、分行、超网号、卡BIN全塞进去。我一开始也这么干过,结果数据一更新就翻车:同一个卡BIN可能对应多个分行,同一个分行又可能属于不同银行的历史沿革,宽表里全是重复行,改一个字段要动几千条记录。正确做法是按实体拆表,用外键或者业务键关联。
拆表的依据是数据本身的层级:省份和城市是行政区划,属于地理维度;银行是法人机构;分行是银行的下级机构,带超网号;卡BIN是卡号前几位,关联到发卡机构。这五张表的关系是:省份一对多城市,银行一对多分行,分行一对多卡BIN(实际业务里卡BIN通常挂到法人,但分行级路由需要更细粒度)。超网号是分行的一个属性,不是独立实体,所以放在分行表里。
提示:如果你的业务只需要总行级路由,卡BIN表可以只关联银行表,不必关联分行。但做本地清算或区域风控时,分行级粒度是刚需。
2.2 建表脚本:五张表的DDL与字段说明
下面这套DDL用的是通用SQL语法,MySQL和PostgreSQL都能跑,字段类型按需微调。注意字符集统一用utf8mb4,银行名称里偶尔有生僻字。
-- 省份表:行政区划第一级 CREATE TABLE province ( province_code VARCHAR(6) PRIMARY KEY COMMENT '省级行政区划代码,如110000', province_name VARCHAR(50) NOT NULL COMMENT '省份名称', region_code VARCHAR(2) COMMENT '大区代码,如01华北', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='省份信息表'; -- 城市表:行政区划第二级,关联省份 CREATE TABLE city ( city_code VARCHAR(6) PRIMARY KEY COMMENT '城市行政区划代码,如110100', city_name VARCHAR(50) NOT NULL COMMENT '城市名称', province_code VARCHAR(6) NOT NULL COMMENT '所属省份代码', city_level VARCHAR(10) COMMENT '城市级别:一线/二线/三线', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_province (province_code), CONSTRAINT fk_city_province FOREIGN KEY (province_code) REFERENCES province(province_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='城市信息表'; -- 银行表:法人机构 CREATE TABLE bank ( bank_code VARCHAR(20) PRIMARY KEY COMMENT '银行机构代码,如ICBC', bank_name VARCHAR(100) NOT NULL COMMENT '银行全称', bank_short_name VARCHAR(50) COMMENT '银行简称', bank_type VARCHAR(20) COMMENT '类型:国有/股份制/城商/农商', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='银行法人信息表'; -- 分行表:带超网号,关联银行和城市 CREATE TABLE branch ( branch_code VARCHAR(30) PRIMARY KEY COMMENT '分行机构号', branch_name VARCHAR(100) NOT NULL COMMENT '分行名称', bank_code VARCHAR(20) NOT NULL COMMENT '所属银行代码', city_code VARCHAR(6) COMMENT '所在城市代码', supernet_no VARCHAR(20) COMMENT '超网号,用于跨行清算路由', branch_level VARCHAR(10) COMMENT '层级:总行/一级分行/二级分行', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_bank (bank_code), INDEX idx_city (city_code), INDEX idx_supernet (supernet_no), CONSTRAINT fk_branch_bank FOREIGN KEY (bank_code) REFERENCES bank(bank_code), CONSTRAINT fk_branch_city FOREIGN KEY (city_code) REFERENCES city(city_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='银行分行信息表'; -- 卡BIN表:卡号前6-8位,关联发卡分行 CREATE TABLE card_bin ( bin_code VARCHAR(8) PRIMARY KEY COMMENT '卡BIN,6-8位数字', bank_code VARCHAR(20) NOT NULL COMMENT '发卡银行代码', branch_code VARCHAR(30) COMMENT '发卡分行代码,可为空表示总行级', card_type VARCHAR(20) COMMENT '卡种:借记/贷记/预付', card_brand VARCHAR(20) COMMENT '卡组织:银联/visa等', bin_length TINYINT DEFAULT 6 COMMENT 'BIN实际长度', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_bank (bank_code), INDEX idx_branch (branch_code), CONSTRAINT fk_bin_bank FOREIGN KEY (bank_code) REFERENCES bank(bank_code), CONSTRAINT fk_bin_branch FOREIGN KEY (branch_code) REFERENCES branch(branch_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='卡BIN信息表';逻辑说明:province和city用行政区划代码做主键,这是国标编码,天然唯一。bank用自定义机构代码,因为不同系统对银行的编码不统一,用简称做业务键更稳。branch表里supernet_no加了索引,因为清算路由查询基本都走这个字段。card_bin的bin_code做主键,注意实际卡号前6位可能重复,但加上卡种和卡组织后基本唯一,如果遇到冲突需要把bin_length纳入主键。
参数说明:province_code用6位字符串而不是整数,因为行政区划代码有前导零,比如北京110000,用整数会丢零。supernet_no用VARCHAR(20),实际超网号长度不固定,有的系统用数字有的带字母。bin_length字段用来标记这个BIN是6位还是8位,查询时用LEFT(card_no, bin_length)匹配。
2.3 索引和外键的取舍
外键在测试环境能帮你发现数据不一致,但生产环境高并发写入时外键检查会拖慢速度。我的习惯是:建表脚本里保留外键,方便初始化时校验数据完整性;上线前用脚本把外键去掉,改成应用层校验。索引方面,branch表的supernet_no和card_bin表的bin_code是查询热点,必须建。city表的province_code建索引是为了按省查城市列表。
注意:如果用的是MySQL 5.7以下版本,utf8mb4索引长度有限制,VARCHAR(100)的bank_name建索引会报错,需要改前缀索引或者升级版本。
3. 数据怎么灌:从行政区划到卡BIN的初始化脚本
3.1 省份和城市数据的批量插入写法
省份数据量小,直接INSERT就行。城市数据有三百多条,用批量INSERT能快很多。下面给的是示例数据,实际使用时替换成完整的行政区划数据。
-- 省份示例数据 INSERT INTO province (province_code, province_name, region_code) VALUES ('110000', '北京市', '01'), ('310000', '上海市', '02'), ('440000', '广东省', '03'), ('320000', '江苏省', '04'); -- 城市示例数据,批量插入 INSERT INTO city (city_code, city_name, province_code, city_level) VALUES ('110100', '北京市', '110000', '一线'), ('310100', '上海市', '310000', '一线'), ('440100', '广州市', '440000', '一线'), ('440300', '深圳市', '440000', '一线'), ('320100', '南京市', '320000', '二线'), ('320500', '苏州市', '320000', '二线');逻辑说明:批量INSERT比逐条快一个数量级,但要注意单条SQL长度限制,MySQL默认max_allowed_packet是4MB,城市数据分两批插比较稳。region_code是我自己加的大区编码,方便做区域统计,如果你的业务不需要可以去掉。
参数说明:city_level字段不是国标,是业务标签,用来做差异化路由。实际数据里一线城市和二三线城市的清算时效要求可能不同,这个字段在风控规则里能直接用。
3.2 银行和分行数据的关联插入顺序
因为有外键约束,插入顺序必须是province → city → bank → branch → card_bin。分行数据里supernet_no是关键,下面示例里给了几种典型情况。
-- 银行数据 INSERT INTO bank (bank_code, bank_name, bank_short_name, bank_type) VALUES ('BANK_A', '某大型国有银行', '某国有行', '国有'), ('BANK_B', '某股份制商业银行', '某股份行', '股份制'), ('BANK_C', '某城市商业银行', '某城商行', '城商'); -- 分行数据,注意supernet_no的填写 INSERT INTO branch (branch_code, branch_name, bank_code, city_code, supernet_no, branch_level) VALUES ('BR_A_001', '某国有行北京市分行', 'BANK_A', '110100', '10000001', '一级分行'), ('BR_A_002', '某国有行上海市分行', 'BANK_A', '310100', '10000002', '一级分行'), ('BR_B_001', '某股份行深圳市分行', 'BANK_B', '440300', '20000001', '一级分行'), ('BR_C_001', '某城商行南京市分行', 'BANK_C', '320100', '30000001', '一级分行');逻辑说明:branch_code用「银行代码_序号」的格式,保证全局唯一且可读。supernet_no在真实清算系统里是参与行号的一种,长度和编码规则各清算机构不同,这里用8位数字示例。branch_level区分总行和分行,总行的supernet_no可能为空或者用特殊值。
参数说明:如果某个分行没有超网号,supernet_no留空,查询时用IS NULL过滤。一级分行下面可能还有二级分行,branch_level字段用来做层级查询,比如查某银行所有一级分行。
3.3 卡BIN数据的导入与校验
卡BIN数据量最大,通常从卡组织或者银行获取,格式可能是CSV。下面给的是插入示例和校验查询。
-- 卡BIN数据插入 INSERT INTO card_bin (bin_code, bank_code, branch_code, card_type, card_brand, bin_length) VALUES ('622202', 'BANK_A', 'BR_A_001', '借记', '银联', 6), ('622203', 'BANK_A', 'BR_A_001', '借记', '银联', 6), ('622588', 'BANK_B', 'BR_B_001', '贷记', '银联', 6), ('622609', 'BANK_C', 'BR_C_001', '借记', '银联', 6), ('621234', 'BANK_A', NULL, '贷记', '银联', 6); -- 校验:查每个银行有多少个卡BIN SELECT b.bank_name, COUNT(c.bin_code) AS bin_count FROM bank b LEFT JOIN card_bin c ON b.bank_code = c.bank_code GROUP BY b.bank_name; -- 校验:查没有关联分行的卡BIN SELECT bin_code, bank_code FROM card_bin WHERE branch_code IS NULL;逻辑说明:卡BIN插入时branch_code可以为空,表示这个BIN是总行级发卡,不区分分行。校验查询用来发现数据质量问题,比如某个银行一个BIN都没有,或者大量BIN没有关联分行。
参数说明:bin_length默认6,如果卡号前8位才能唯一识别,就填8。查询时用LEFT(card_no, bin_length)去匹配bin_code,注意card_no是字符串类型,不要用数字类型存卡号,会丢前导零。
4. 查询与关联:按卡BIN反查分行和超网号的三种写法
4.1 单表查询和两表关联的适用场景
最简单的需求是给一个卡BIN,查它属于哪家银行。这个两表关联就够了。
-- 按卡BIN查银行和卡种 SELECT c.bin_code, c.card_type, b.bank_name, b.bank_short_name FROM card_bin c JOIN bank b ON c.bank_code = b.bank_code WHERE c.bin_code = '622202';逻辑说明:card_bin和bank通过bank_code关联,这是最基础的查询。如果card_bin表在bin_code上有索引,这个查询是毫秒级。
参数说明:WHERE条件里的bin_code要用字符串,不要写成数字,否则可能匹配不到带字母的BIN。
4.2 三表关联查分行和超网号
需要知道发卡分行和超网号时,要关联到branch表。
-- 按卡BIN查分行、城市、超网号 SELECT c.bin_code, b.bank_name, br.branch_name, p.province_name, ci.city_name, br.supernet_no FROM card_bin c JOIN bank b ON c.bank_code = b.bank_code LEFT JOIN branch br ON c.branch_code = br.branch_code LEFT JOIN city ci ON br.city_code = ci.city_code LEFT JOIN province p ON ci.province_code = p.province_code WHERE c.bin_code = '622202';逻辑说明:用LEFT JOIN是因为branch_code可能为空,如果卡BIN是总行级发卡,分行信息就查不到,但银行信息还是要返回。city和province通过branch的city_code关联,如果branch为空,这两级也返回空。
参数说明:如果业务要求必须返回分行信息,把LEFT JOIN改成INNER JOIN,但这样总行级卡BIN就查不出来了。我的习惯是保留LEFT JOIN,在应用层判断branch_name是否为空。
4.3 按超网号反查所有卡BIN
清算场景里经常需要根据超网号列出该分行下所有卡BIN。
-- 按超网号查该分行下所有卡BIN SELECT br.supernet_no, br.branch_name, c.bin_code, c.card_type FROM branch br JOIN card_bin c ON br.branch_code = c.branch_code WHERE br.supernet_no = '10000001' ORDER BY c.bin_code;逻辑说明:branch和card_bin通过branch_code关联,supernet_no在branch表上有索引,查询效率高。注意如果一个卡BIN关联了多个分行(理论上不应该,但数据脏的时候会发生),这里会返回多行。
参数说明:ORDER BY bin_code让结果稳定,方便对比。如果数据量大,可以加LIMIT分页。
5. 避坑与排查:数据初始化时最容易翻车的五个点
5.1 行政区划代码前导零丢失
现象:插入北京市省份数据后,查出来province_code是110000,但用程序读取时变成11000或者110000.0。原因是数据库字段用了INT类型,或者程序里把字符串转成了数字。
解决:province_code和city_code必须用CHAR或VARCHAR,长度固定6位。程序里读取时保持字符串类型,不要做数值转换。如果已经用了INT,用LPAD(province_code, 6, '0')补零。
5.2 超网号重复导致路由错误
现象:两个不同分行填了同一个supernet_no,清算时资金路由到了错误的分行。原因是数据导入时没有做唯一性校验。
解决:在branch表的supernet_no上建唯一索引(如果业务允许空值,用部分索引)。导入前用GROUP BY supernet_no HAVING COUNT(*) > 1查出重复项。如果业务上确实允许多个分行共用一个超网号,那查询时要用branch_code做二次筛选。
5.3 卡BIN长度不一致导致匹配失败
现象:卡号前6位是622202,但card_bin表里存的是62220200(8位),用LEFT(card_no, 6)匹配不到。原因是bin_length字段没有正确使用。
解决:查询时用LEFT(card_no, c.bin_length)动态匹配,而不是固定6位。导入数据时确保bin_length和bin_code的实际长度一致。如果卡组织升级了BIN长度,要批量更新bin_length字段。
5.4 外键约束导致批量导入失败
现象:导入分行数据时报外键约束错误,因为bank表里还没有对应的bank_code。原因是插入顺序错了,或者银行数据漏了。
解决:严格按照province → city → bank → branch → card_bin的顺序导入。如果数据源里银行和分行是混在一起的,先抽出银行去重插入,再插分行。临时关闭外键检查可以用SET FOREIGN_KEY_CHECKS=0,但导入后要记得打开并跑一致性校验。
5.5 字符集不统一导致银行名称乱码
现象:银行名称里的生僻字显示成问号或者乱码。原因是建表时用了utf8而不是utf8mb4,或者连接字符集没设置。
解决:建表统一用utf8mb4,连接串里加characterEncoding=utf8mb4。如果已经建了表,用ALTER TABLE bank CONVERT TO CHARACTER SET utf8mb4; 迁移。导入CSV时确认文件编码是UTF-8,不要用GBK。
6. 进阶用法:用视图和存储过程把常用查询固化下来
6.1 建一个卡BIN全链路视图
每次写五表关联太麻烦,建个视图把常用字段拼好。
CREATE VIEW v_card_bin_full AS SELECT c.bin_code, c.card_type, c.card_brand, c.bin_length, b.bank_code, b.bank_name, b.bank_short_name, br.branch_code, br.branch_name, br.supernet_no, ci.city_code, ci.city_name, p.province_code, p.province_name FROM card_bin c JOIN bank b ON c.bank_code = b.bank_code LEFT JOIN branch br ON c.branch_code = br.branch_code LEFT JOIN city ci ON br.city_code = ci.city_code LEFT JOIN province p ON ci.province_code = p.province_code; -- 用视图查就简单了 SELECT * FROM v_card_bin_full WHERE bin_code = '622202';逻辑说明:视图把关联逻辑封装起来,应用层直接查视图,字段名统一。注意视图不存储数据,每次查询还是走底层表,性能取决于索引。
参数说明:如果视图查询慢,可以在视图上再建物化视图(PostgreSQL支持,MySQL需要用定时任务刷中间表)。
6.2 用存储过程做卡BIN批量校验
导入新卡BIN数据前,用存储过程跑一遍校验,把有问题的行标出来。
DELIMITER // CREATE PROCEDURE validate_card_bin() BEGIN -- 检查卡BIN是否关联了不存在的银行 SELECT c.bin_code, c.bank_code, '银行不存在' AS error_msg FROM card_bin c LEFT JOIN bank b ON c.bank_code = b.bank_code WHERE b.bank_code IS NULL; -- 检查卡BIN是否关联了不存在的分行 SELECT c.bin_code, c.branch_code, '分行不存在' AS error_msg FROM card_bin c LEFT JOIN branch br ON c.branch_code = br.branch_code WHERE c.branch_code IS NOT NULL AND br.branch_code IS NULL; -- 检查bin_length和实际长度是否一致 SELECT bin_code, bin_length, LENGTH(bin_code) AS actual_len, '长度不一致' AS error_msg FROM card_bin WHERE bin_length != LENGTH(bin_code); END // DELIMITER ; -- 调用校验 CALL validate_card_bin();逻辑说明:存储过程把三类常见数据问题一次性查出来,返回结果集直接看。DELIMITER改分隔符是因为存储过程体里有分号,不改的话MySQL会提前截断。
参数说明:如果数据量大,这些查询可能慢,建议在业务低峰期跑。可以把结果插入到临时表里,方便后续处理。
6.3 一个我踩过的坑:视图里的LEFT JOIN顺序
有次我把视图里的LEFT JOIN写成了JOIN,结果总行级卡BIN(branch_code为空)全查不出来,业务方反馈说少了一批卡。排查了半天才发现是JOIN类型的问题。后来我的习惯是:只要字段可能为空,一律用LEFT JOIN,然后在应用层判断空值。这个习惯帮我省了很多后悔药。
另外,视图定义里如果用了SELECT *,底层表加字段后视图不会自动更新,需要重建。我一般显式列出字段名,虽然写的时候麻烦点,但后面省事。希望帮到你。
本文还有配套的精品资源,点击获取