
简介一套覆盖小车、客车、货车、摩托车四类车型的驾考科目一科目四题库资源题目以SQL表结构和JSON格式打包方便直接导入题库系统或做二次开发。压缩包共2000个文件内含2个SQL、2个JSON数据文件以及1995张与题目对应的WebP图片素材和1个GIF示意图整体约103MBSQL适合结构化查询JSON方便程序读取图片可用于练题界面还原真实场景。题库体量充足客车科目一2154题、科目四2126题小车科目一1600题、科目四1300题摩托车科目一446题、科目四383题货车科目一2162题、科目四1206题合计超过一万一千道覆盖高频考点与重点题型。已有995人浏览学习适合驾校教学、模拟考试系统搭建或个人离线复习入手后即可直接使用免去逐题录入整理的繁琐。1. 科目一科目四题库的SQL与JSON先把车型、题目、图片拆对接手驾考类App的题库需求时最常见的现状是拿到一个几百KB的SQL文件科目一科目四的题混在一张表里选项用逗号字符串塞在字段中答案按“A|B|D”格式存图片直接写死成“C:\images\1001.jpg”。这套数据在数据库客户端里能查能改一旦要导出成JSON给小程序端加载问题就会同时爆发车型过滤查不动、多选题答案拼不出来、图片路径在服务器上找不到文件。科目一科目四题库的数据规模通常只有几千到一万多道题算很小的数据集但它同时踩了SQL建模、JSON序列化、静态资源映射三个领域。车型分小车、客车、货车、摩托车一道题可能四类共用科目一偏交规理论科目四偏安全文明驾驶章节维度还不一样。真正高效的落地方式不是把题“导成JSON”就完事而是先把表结构拆好让SQL能查、JSON能交、图片能对得上。下面这套方案我实际搭过能直接复制改字段用。2. 题目数据建模科目一科目四题库的车型维度、题型与图片引用2.1 车型不要做成字段做成关联关系先看一个几乎必然会踩的坑把适用车型直接写到题目表的vehicle_type列里值存成1,2,3或者CAR,TRUCK。从写代码角度这最快但从查询角度一旦客户端要按“当前用户准驾车型”筛选题库SQL就会变成WHERE vehicle_type LIKE %CAR%命中不了索引几千条数据也能跑出几十毫秒。更麻烦的是驾考规则里科目一、科目四都有“小车”“客车”“货车”“摩托车”四个维度一道题可能是“小车和摩托车共用”也可能是“四类全用”逗号列的语义既表达不了“精确归属”也表达不了“部分共用”。正确的模型是车型和科目拆成独立的维度表题目与维度之间用关联表表达多对多。车型本身只有四类没必要做成树形科目也只有两个。把“(科目,车型)”作为一个组合维度去管理例如“小车-科目一”是一个category“货车-科目四”是另一个category。题目不直接挂科目和车型两个字段而是通过关联表挂“维度ID”这样增删一种适用场景时不需要改题目表。2.2 科目一与科目四共表还是分表科目一和科目四的题面结构完全一致题干、选项、答案、解析、图片区分只在“归属的分组”和“考核侧重点”。很多早期项目会拆成subject1_question和subject4_question两张表理由是“查询方便、互不影响”。但后续每次加字段要改两张表做统计要union维护成本是双倍的。常见做法是共表用subject字段区分。我这里更推荐在维度表里带subject字段而不在题目表里冗余subject。原因在于题目表如果存了subject又要存vehicle_type两个字段一起才能表达完整维度仍然绕回多对多问题。与其这样不如让“科目车型”作为维度表的唯一键题目只和维度ID关联。查询单科单车型的题库时先查维度表拿到ID再走关联表过滤题目。2.3 题型决定答案字段怎么存科目一科目四的题型只有三种判断题、单选题、多选题。判断题答案非对即错单选题答案是A/B/C/D之一多选题答案是一组选项。用一个answer字符串存的话会出现“1”“B”“ABD”三种无法统一解析的值。建议在题表上用type字段区分题型选项独立成表答案语义落到选项表。多选题的正确答案就是选项表里is_answer1的多行判断题可以视觉化成两个选项“对/错”也可以特殊处理。导出JSON时answer统一变成数组判断题是[0]或[1]单选是[2]多选是[1,3]前端解析逻辑只要一套。示例如下-- 多选题在选项表里的数据形态 INSERT INTO question_option (question_id, opt_key, content, is_answer, sort) VALUES (1001, A, 尽快撤离到安全地带, 1, 1), (1001, B, 在行车道内等待救援, 0, 2), (1001, C, 开启危险报警闪光灯, 1, 3), (1001, D, 站在车后等待交警, 0, 4);这段SQL点出了两个关键点选项表必须用(question_id, opt_key)做复合主键或唯一键防止同一题插入重复选项答案不是独立字段而是通过is_answer表达这是整个题库建模最核心的取舍。2.4 图片素材存相对路径不存二进制图片素材主要是交通标志牌、交警手势图、道路场景图特征是文件不大但数量多且同一张图可能被多道题引用。比如“直行标志”这张图判断题和单选题里可能各出现一次。如果image_url直接作为题目表字段冗余存储图片更新时就要update多行如果单独抽图片表题目表存image_id查询时多一次关联成本可接受。我个人倾向折中题目表保持image_url字段但值只存相对路径如/img/tiku/a20301.jpg图片文件按题目编号或哈希名存放。题库数据量小冗余更新代价不大但导出JSON时客户端拿到的必须是完整的绝对URL拼接CDN域名的工作放在导出脚本里做不要存进数据库否则换域名要全表update。3. 科目一科目四题库的SQL表设计与批导入3.1 题目主表和选项表的DDL下面这套DDL以MySQL 8.0为准字段命名和类型选择对其他关系型数据库也适用。题目表的主键是自增ID但对外暴露的是question_no业务题号这样即使数据库迁移、ID变化客户端引用的题号也不会变。CREATE TABLE question ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, question_no VARCHAR(32) NOT NULL COMMENT 业务题号如 CAR_001, type TINYINT NOT NULL COMMENT 1判断 2单选 3多选, stem TEXT NOT NULL COMMENT 题干, image_url VARCHAR(255) NULL COMMENT 图片相对路径如 /img/tiku/car_001.jpg, analysis TEXT NULL COMMENT 解析, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_question_no (question_no), KEY idx_type_status (type, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目主表;选项表的要点是复合主键和排序字段。sort决定选项展示顺序is_answer标记正确项。不要用is_answer作普通索引以外的特殊处理因为多选时一行表里就有多行is_answer1。CREATE TABLE question_option ( question_id BIGINT UNSIGNED NOT NULL, opt_key VARCHAR(4) NOT NULL COMMENT A/B/C/D, content TEXT NOT NULL, is_answer TINYINT NOT NULL DEFAULT 0, sort INT NOT NULL DEFAULT 0, PRIMARY KEY (question_id, opt_key), KEY idx_question_sort (question_id, sort) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选项表;字段类型上有几个容易纠结的点题干用TEXT而不是VARCHAR因为部分题干接近500字且带换行opt_key用VARCHAR(4)而不是固定长度的CHAR兼容可能出现的“对/错”中文键名is_answer用TINYINT不碰BIT类型ORM驱动兼容性更好。字段选择的依据整理如下表字段类型理由question_noVARCHAR(32)业务编号跨库稳定不随自增ID漂移typeTINYINT三类题型数值枚举足够stemTEXT题干长度不固定且可能有图片描述文本image_urlVARCHAR(255)只存相对路径长度可控is_answerTINYINT避免BIT类型在JDBC等驱动里需要特殊映射sortINT选项顺序可调整不依赖插入顺序3.2 车型和科目维度的关联表维度表可以叫vehicle_category唯一键是(subject, vehicle_type)。科目一和科目四各四类车型初始数据就是8行。CREATE TABLE vehicle_category ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, subject TINYINT NOT NULL COMMENT 1科目一 4科目四, vehicle_type VARCHAR(10) NOT NULL COMMENT CAR小车 BUS客车 TRUCK货车 MOTO摩托车, category_name VARCHAR(32) NOT NULL, UNIQUE KEY uk_subject_vehicle (subject, vehicle_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT科目车型维度表;题目与维度的关系落到question_vehicleCREATE TABLE question_vehicle ( question_id BIGINT UNSIGNED NOT NULL, category_id BIGINT UNSIGNED NOT NULL, PRIMARY KEY (question_id, category_id), KEY idx_category_question (category_id, question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题目-维度关联表;这步拆完查询“小车科目一全部题目”就是一个标准JOIN。万一后续增加“公交车”这类新车型只需要在维度表加一行再补关联不动三张核心表的任何结构。这也是很多人把题目表直接ALTER TABLE ADD COLUMN vehicle_type后追悔莫及的原因。3.3 批量导入事务、去重与SQL Server差异题库导入通常是给一个Excel或CSV逐条插几十次既不安全也慢。我一般先用临时表接原始数据再通过INSERT INTO ... SELECT从临时表过滤后写入主表。选项和关联表的数据在同一个事务里提交避免做到一半报错留下脏数据。Python里用pymysql时这样控制import pymysql conn pymysql.connect(host127.0.0.1, userroot, password, databasedrive_license, charsetutf8mb4) cur conn.cursor() try: cur.execute(START TRANSACTION) cur.execute( INSERT INTO question (question_no, type, stem, image_url, analysis, status) SELECT question_no, type, stem, image_url, analysis, 1 FROM tmp_question_import WHERE question_no NOT IN (SELECT question_no FROM question) ) # 再根据 question_no 回表关联补选项数据 conn.commit() except Exception as e: conn.rollback() print(import failed:, e)这段代码的要点是导入前先按question_no去重重复数据直接跳过START TRANSACTION和commit/rollback保证原子性选项和图片文件的写入在同一事务里完成防止题目与选项错位。如果团队里有人用SQL Server 2008 R2注意它没有IF NOT EXISTS这种轻量写法去重逻辑要在INSERT前显式IF NOT EXISTS (SELECT 1 FROM question WHERE question_no...)并且事务隔离级别默认是READ COMMITTED批量导入时改成READ UNCOMMITTED能明显减少锁等待——对几千行的题库数据集来说这个差异不那么致命但能省不少排错时间。3.4 索引检查慢SQL优化explain主要看哪些信息题库表数据量不大但“按车型分页查询”这条SQL如果写得不带索引慢查询日志依然会被刷出来。排查时做EXPLAIN主要看type、key、rows三个字段。目标结果是关联表扫描的type达到refrows控制在几十行以内最终回表取题目用主键。EXPLAIN SELECT q.question_no, q.stem, q.type, q.image_url FROM question_vehicle qv JOIN question q ON q.id qv.question_id WHERE qv.category_id 1 ORDER BY q.id LIMIT 20;这条SQL里question_vehicle表上的idx_category_question就是为这个查询建的category_id过滤后能直接用索引匹配。常见误用是给question表的status建单列索引后以为万事大吉实际慢在JOIN的驱动顺序而不是过滤字段本身。EXPLAIN里如果出现typeALL且rows上万就先检查关联表索引如果key为NULL说明查询条件字段没被索引覆盖回表次数会放大两三倍。4. 把SQL表数据导成JSON嵌套结构、图片URL与格式化校验4.1 JSON结构答案用数组图片用相对路径导出JSON前先把结构定好。题库JSON给客户端用最忌一个对象里套无数层别名。我常用的结构是顶层放version和syncquestions是数组而不是对象因为数组天然支持排序和分页客户端可以直接map渲染。{ version: 20240101, sync: full, category: { id: 1, subject: 1, vehicle_type: CAR, name: 小车-科目一 }, questions: [ { id: CAR_001, type: 2, stem: 驾驶机动车在高速公路发生故障时应开启什么灯, image: /img/tiku/car_001.jpg, options: [ {key: A, content: 危险报警闪光灯}, {key: B, content: 远光灯}, {key: C, content: 近光灯}, {key: D, content: 雾灯} ], answer: [A], analysis: 应当开启危险报警闪光灯并在来车方向设置警告标志。 } ] }关键是options和answer的结构。选项带key是为了前端能渲染出A/B/C/D的序号答案用字符串数组多选时天然支持多个值。千万别把答案存成A|B|D或ABD客户端每次解析都要split还容易出歧义。image字段存相对路径导出时再拼CDN前缀这样同一份JSON文件在测试环境和生产环境能直接复用。4.2 纯SQL导出JSONMySQL的JSON_ARRAYAGG用法MySQL 5.7以上可以用JSON_OBJECT配合JSON_ARRAYAGG直接拼嵌套JSON适合一次性迁移。以查询“小车科目一全部题目”为例SELECT q.question_no, q.type, q.stem, q.image_url, q.analysis, JSON_ARRAYAGG( JSON_OBJECT( key, o.opt_key, content, o.content, isAnswer, o.is_answer ) ORDER BY o.sort ) AS options FROM question q JOIN question_vehicle qv ON qv.question_id q.id JOIN vehicle_category vc ON vc.id qv.category_id LEFT JOIN question_option o ON o.question_id q.id WHERE vc.subject 1 AND vc.vehicle_type CAR AND q.status 1 GROUP BY q.id, q.question_no, q.type, q.stem, q.image_url, q.analysis;这段SQL有几个坑要注意。第一GROUP BY必须把SELECT里的非聚合列全部写全否则ONLY_FULL_GROUP_BY模式直接报错第二LEFT JOIN question_option会放大行数一个多选三选项的题会重复三行靠外层聚合去重第三答案仍然需要靠is_answer1的选项筛纯SQL可以在JSON_OBJECT里加CASE WHEN但嵌套数组里保留isAnswer字段更通用。这套写法性能不算好但胜在无脚本依赖适合临时导出。4.3 用Python脚本导出可维护、可校验数据量到几千道题时我更倾向用脚本导出。原因很简单脚本里能同时做图片检查、答案格式校验、题号去重纯SQL做不到。用pymysql直接从库查逐行拼接字典最后统一json.dumpimport json import pymysql conn pymysql.connect(host127.0.0.1, userroot, password, databasedrive_license, charsetutf8mb4) cur conn.cursor(pymysql.cursors.DictCursor) cur.execute( SELECT q.question_no, q.type, q.stem, q.image_url, q.analysis, o.opt_key, o.content, o.is_answer FROM question q JOIN question_vehicle qv ON qv.question_id q.id JOIN vehicle_category vc ON vc.id qv.category_id LEFT JOIN question_option o ON o.question_id q.id WHERE vc.subject %s AND vc.vehicle_type %s AND q.status 1 ORDER BY q.id, o.sort , (1, CAR)) questions {} for row in cur.fetchall(): qid row[question_no] if qid not in questions: questions[qid] { id: qid, type: row[type], stem: row[stem], image: row[image_url] or , analysis: row[analysis] or , options: [], answer: [] } if row[opt_key] is not None: opt {key: row[opt_key], content: row[content]} questions[qid][options].append(opt) if row[is_answer] 1: questions[qid][answer].append(row[opt_key]) payload { version: 20240101, sync: full, category: {id: 1, subject: 1, vehicle_type: CAR, name: 小车-科目一}, questions: list(questions.values()) } with open(tiku_CAR_subject1_v20240101.json, w, encodingutf-8) as f: json.dump(payload, f, ensure_asciiFalse, indent2)脚本里ensure_asciiFalse是必须的否则中文会变成\uXXXX文件体积膨胀且难读。导出过程顺带做了两件校验LEFT JOIN带来的opt_key is None说明这道题没有选项属于异常数据需要排查answer只收集is_answer1的项如果多选题一个答案都没收集到说明数据源有问题。这些检查写在导出脚本里比事后写独立校验脚本高效。4.4 JSON格式化的两个验证命令导出完成后先跑一次JSON解析验证语法不过关的文件是废文件。常见做法是python -m json.tool tiku_CAR_subject1_v20240101.json /dev/nullpython -m json.tool是标准库自带工具输出到/dev/null只做语法检查。如果文件里有NaN或Infinity这类非标准JSON值它会直接报错。线上环境还可以用jq继续验证结构和统计jq .questions | length tiku_CAR_subject1_v20240101.json jq [.questions[] | select((.answer | length) 1)] | length tiku_CAR_subject1_v20240101.json第一条输出题目总数第二条统计多选题数量。这两个数字如果和源库里的统计对不上就能立刻发现导出丢数据。格式化工具的用法很简单但很多项目把这一步省了最终客户端解析报错时才回头排查。5. 答案合法性校验、图片素材检查与增量更新5.1 写一个可复用的答案校验规则题库文件给到客户端前我会在CI或者发布脚本里挂一道校验关卡。判断题答案数组长度必须为1且值只能是A或B单选题答案长度必须为1且对应key要真实存在于options多选题答案长度必须大于1去重后长度不变且所有key都存在。直接写成断言式脚本with open(tiku_CAR_subject1_v20240101.json, encodingutf-8) as f: data json.load(f) errors [] for q in data[questions]: keys {o[key] for o in q[options]} ans q[answer] if q[type] 1 and ans not in ([A], [B]): errors.append((q[id], 判断题答案需为A或B)) if q[type] 2 and (len(ans) ! 1 or ans[0] not in keys): errors.append((q[id], 单选题答案非法)) if q[type] 3 and (len(ans) 2 or len(set(ans)) ! len(ans) or not set(ans).issubset(keys)): errors.append((q[id], 多选题答案非法)) print(errors:, errors)5.2 图片素材检查不能只看路径JSON里image字段存在不代表图片文件真的在服务器上。发布前按路径批量探测文件存在性比等到客户端真机渲染时出现裂图再返工实在得多。脚本遍历questions里的image字段拼上预设的静态资源根目录用os.path.exists检查顺带核对扩展名白名单和文件大小阈值防止空文件或错误资源混进素材包。import os IMAGE_ROOT /data/tiku/assets missing [] for q in data[questions]: path q[image] if not path: continue full os.path.join(IMAGE_ROOT, path.lstrip(/)) if not os.path.exists(full): missing.append((q[id], path)) elif os.path.getsize(full) 0: missing.append((q[id], empty file)) print(missing:, missing)注意path.lstrip(/)这步。如果JSON里存的是/img/tiku/car_001.jpg直接交给os.path.join(IMAGE_ROOT, path)后者会认为第二段是绝对路径把IMAGE_ROOT整个丢掉检查结果永远是“文件不存在”。这个坑在跨平台环境尤其隐蔽Windows路径分隔符和Linux的差异会导致同样代码在不同服务器上行为不一致。5.3 增量更新按update_time差量导出按题号合并题库不会永远不变题目修订和图片替换都触达增量发布。全量JSON越到后期越大客户端下载体验差增量协议要提前定。同步周期内变更的题目靠updated_at字段筛选SELECT question_no, updated_at FROM question WHERE updated_at 2025-11-01 00:00:00 AND updated_at 2025-12-01 00:00:00;增量JSON里带上baseVersion和currentVersion客户端或服务端合并时以question_no为唯一键做upsert禁用删除后重建的方式。question_no同时是图片文件的命名建议前缀如CAR_001.jpg这样增量包只打包变更题目引用的图片不需要每次都同步整个素材目录。CDN域名变更时更新完数据库里的相对路径重新生成一份全量JSON即可客户端请求图片时在URL后拼接版本参数?v20241201强刷缓存避免新资源被旧缓存卡住。这套“SQL表数据 车型维度关联 JSON导出 图片素材校验”的组合能覆盖科目一科目四题库从入库到上线的完整链路。图片素材的命名规范和question_no严格绑定后上面这套脚本可以直接接进定时任务或发布流程后续新增题目只需要走同一套导入事务。本文还有配套的精品资源点击获取