ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SQL表设计与优化实战:从建表、去重到跨表合并与锁表排查

SQL表设计与优化实战:从建表、去重到跨表合并与锁表排查 “表”大概是SQL世界里出镜率最高的那个词了。查数据第一件事是搜表建库第一件事是建表不管是MySQL、SQL Server、PostgreSQL还是时序数据库TDengine表都是数据库最小粒度的逻辑容器。我见过不少写SQL写了两三年的研发SELECT和JOIN玩得很溜但真让他从零设计一张表建完之后字段类型不对、忘了加注释、主键选错后面全链条跟着改真的很伤人。这篇东西就围绕“SQL表相关”这件事把从建表、改表、数据清洗、跨表合并到大表优化、特殊场景表模型、常见故障排查串一遍偏向实操能直接抄作业的那种。1. 建表把业务翻译成表结构1.1 建表语句拆解字段类型先想明白一张表最根本的就是字段集合字段类型选择决定了存储效率、查询性能和后续扩展空间。实战里最常出现的问题就是随便用VARCHAR(255)装一切或者金额用FLOAT日期用字符串等到数据量上来才发现索引用不上、统计口径对不齐。拿最常见的MySQL建表为例CREATE TABLE order ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键ID, order_no VARCHAR(64) NOT NULL COMMENT 订单编号, user_id BIGINT UNSIGNED NOT NULL COMMENT 用户ID, total_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态0待支付1已支付2已发货3已完成4已取消, pay_time DATETIME DEFAULT NULL COMMENT 支付时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;字段类型的选择逻辑我一般这么判断整数状态、数量、年龄类的用TINYINT/SMALLINT/INT/BIGINT不要一上来就是BIGINT省空间就是省IO。小数金额、价格、汇率固定精度一律用DECIMAL不要用FLOAT和DOUBLE浮点数的精度问题是老生常谈但对账差一分钱真的够你查一晚上。字符串定长且短的用CHAR其余用VARCHAR但VARCHAR长度不要拍脑袋64和255在索引长度、内存排序上的开销差不少。时间业务时间用DATETIME或TIMESTAMP。TIMESTAMP有2038年问题且带时区转换常规业务我更喜欢DATETIME。文本超过几千字符的用TEXT/MEDIUMTEXT但这类字段尽量拆到附属表避免拖慢主表查询。这里要特别强调字段注释comment一定要写。你写的每一行comment都是在给三个月后的自己、给接手项目的同事留活路。没有注释的字段过段时间谁看谁头疼尤其status这种数字状态位0代表什么1代表什么全靠猜出一回线上事故就知道注释多值钱了。1.2 主键设计自增还是业务键主键是表的灵魂。我见过一种很常见的烂设计用订单号、手机号这类业务字段直接当主键。表面上看逻辑没毛病可一旦业务规则调整比如订单号规则重定义、手机号可以修改整张表的主键也跟着崩关联表的外键全部要改属于牵一发动全身。正确做法通常是两个流派自增主键BIGINT UNSIGNED AUTO_INCREMENT插入效率高、索引紧凑适合绝大多数OLTP系统。雪花ID/分布式ID分布式场景、需要全局唯一、不想暴露行数规律时用雪花算法生成的BIGINT配合应用层生成。还有一点容易被忽略明明设置了自增主键却因为表引擎是MyISAM或者复制模式下主键冲突导致性能异常。InnoDB表是聚集索引组织主键定了物理存储顺序就定了主键越短越好、越单调越好这也是为什么强烈不建议用超长随机字符串当主键会造成插入时页分裂严重随机IO暴涨。1.3 表结构ALTER用什么姿势改表才不炸上线后发现字段不够要加列、加索引这是常态。但直接执行ALTER TABLE在大表上可能锁表很久尤其是MySQL 5.6之前的版本或者操作里隐含表重建。一个千万级大表直接ALTER TABLE ADD COLUMN如果是COPY算法会把整张表复制一遍期间写操作被阻塞线上业务分分钟雪崩。现在MySQL 8.0的在线DDL支持ALGORITHMINSTANT但也不是所有操作都能瞬间完成。实操心得-- 加字段8.0里如果满足条件可以INSTANT ALTER TABLE order ADD COLUMN remark VARCHAR(255) NOT NULL DEFAULT COMMENT 备注, ALGORITHMINSTANT; -- 加索引注意不要在大表上反复尝试 ALTER TABLE order ADD KEY idx_status_paytime (status, pay_time);经验上大表加字段这种操作放业务低峰期或者用pt-online-schema-change这类工具在后台跑先建新表、同步数据、切换表名把锁表影响降到可控范围。这属于运维基本功等出一次事再学就晚了。2. 表里数据的日常操作去重、空值与批量处理2.1 表数据去重三种常见姿势热词榜里“sql语句去重”排得很靠前说明这是个真实高频需求。去重逻辑分两种一种是查询时不想看到重复行一种是表里物理存在重复数据要清掉。查询去重最直观的是DISTINCTSELECT DISTINCT user_id FROM order WHERE status 1;如果要去重多个字段DISTINCT后面跟多列就行。但注意DISTINCT本质是全字段比较字段多、数据量大的时候性能一般更常见的是按某个维度取最新/最老记录这时DISTINCT就不够用了得上窗口函数SELECT t.* FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY create_time DESC) AS rn FROM order ) t WHERE t.rn 1;这个写法堪称“取每个用户最新订单”的标准答案PARTITION BY指定分组维度ORDER BY决定同一组内的排序rn 1取每组第一条。实际优化时给user_id create_time建联合索引窗口函数跑起来会快很多。物理删除重复数据保留一条这是清洗动作。MySQL里惯用法是借助临时表或者直接DELETE关联DELETE o1 FROM order o1 JOIN ( SELECT MIN(id) AS keep_id FROM order GROUP BY order_no ) o2 ON o1.id ! o2.keep_id WHERE o1.order_no NOT IN (SELECT order_no FROM (SELECT MIN(id) keep_id FROM order GROUP BY order_no) x);这个SQL要仔细说先把每个order_no里保留的MIN(id)找出来然后删除不在保留集合里的行。实际操作时建议先SELECT COUNT(*)评估影响行数再导出备份最后在执行前再用事务包一层出问题还能回滚。2.2 空值处理NULL是个容易踩爆的雷“sql去除空值”背后是无数个被NULL坑惨的夜晚。NULL代表“未知”不是空字符串不是0也不是FALSE在SQL里它和任何值比较都返回NULLWHERE name NULL永远查不到数据正确写法是WHERE name IS NULL。常见的空值处理套路-- 查询时将NULL替换成默认值 SELECT COALESCE(remark, 暂无备注) FROM order; SELECT IFNULL(remark, 暂无备注) FROM order; -- MySQL写法 -- 过滤掉NULL SELECT * FROM order WHERE pay_time IS NOT NULL; -- 统计时注意COUNT(NULL)不会计入 SELECT COUNT(1), COUNT(pay_time) FROM order; -- 两者不同后者自动忽略NULLCOUNT(1)和COUNT(字段)的差别特别容易疏忽COUNT(字段)会跳过NULL行如果支付时间为空统计已支付订单数就漏了。多个表关联时NULL会导致JOIN匹配不上外连接时也会出现大量NULL填充行处理这类结果集时先把NULL的语义搞清楚再下手过滤。2.3 批量导入导出与表变动的影响开发中经常要把Excel、CSV的数据导进表或者从在线库拉数据到本地分析。小数据量直接在DataGrip、Navicat里操作就行但上了十万行工具里复制粘贴就频繁卡死最靠谱的还是走文件导入流程或写脚本走接口。用MySQL的话LOAD DATA INFILE效率最猛LOAD DATA INFILE /tmp/order.csv INTO TABLE order FIELDS TERMINATED BY , OPTIONALLY ENCLOSED BY LINES TERMINATED BY \n IGNORE 1 LINES;Python连接公司系统自动拉表也是热门需求本质上就是用pymysql、sqlalchemy这类库建好连接执行查询后把结果转成DataFrame。以pymysql为例import pymysql import pandas as pd conn pymysql.connect(host10.0.0.10, userread_user, passwordyour_pass, databasebusiness_db, charsetutf8mb4) df pd.read_sql(SELECT * FROM order WHERE create_time 2025-01-01, conn) conn.close()顺带提醒一个安全细节公司系统的账号密码不要明文写死在脚本里用环境变量或者配置中心管理脚本提交到仓库前先扫一遍敏感信息这习惯能帮你避免很多麻烦。C#里用SqlBulkCopy批量写SQL Server时如果目标表正在变动比如有人ALTER TABLE加了列运行时容易报字段映射错位原因是SqlBulkCopy依赖列顺序和元数据快照。稳妥做法是先DataTable的列名跟表列名一一对应并在批量导入前确认表结构变更已完成必要时加锁或错峰执行。3. 跨表合并与多表关联的实战解法3.1 JOIN行级关联一张表放不下业务时的必然产物现代业务几乎没有单表单打独斗的订单表、用户表、商品表天然就是关联关系。跨表关联最核心的JOIN就四种选错会影响结果正确性和性能。INNER JOIN两张表都匹配上的行才返回用得最多。LEFT JOIN左表全部返回右表不匹配的补NULL。RIGHT JOIN反过来右表全部返回实际用得少多数人更习惯把表顺序调换后用LEFT JOIN。FULL OUTER JOIN两边都保留不匹配的行MySQL原生不支持要UNION两个方向模拟。实际写业务查询时最容易翻车的是关联条件不充分导致的笛卡尔积。比如统计订单信息FROM order o LEFT JOIN user u ON o.user_id u.id如果两个表都有重复记录结果行数会成倍放大查出来的数据明明是错的表面看还不容易发现。写完后先SELECT COUNT(*)对比一下两个表的基数再决定是否先对子查询做去重。3.2 UNION把多张结构相似的表堆成一个结果集热词“跨表合并”除了JOIN另一个高频操作就是UNION。两张表结构相同比如月度订单表order_202501、order_202502要合在一起查用UNION直接堆叠SELECT order_no, user_id, total_amount, 2025-01 AS month FROM order_202501 UNION ALL SELECT order_no, user_id, total_amount, 2025-02 AS month FROM order_202502;UNION会自动去重UNION ALL不去重。这个区别在性能上天差地别因为UNION需要对结果集做排序去重数据量大时消耗非常大而UNION ALL就是物理拼接最快。直觉上应该用UNION“更严谨”但实际业务中如果你能确定两个子集本身不重复或者重复也无所谓一律用UNION ALL必要的时候在外面套一层去重。3.3 Excel里的VLOOKUP和SQL跨表本质是同一件事提到跨表Excel里的“vlookup跨表两个表格匹配”也是高频搜索其实它和SQL里的JOIN是同一个思维模型。Excel里写VLOOKUP(查找值, 表范围, 返回列, FALSE)本质上就是拿当前表的某个键去另一张表里找对应关系返回目标列的值。但VLOOKUP有个大坑它默认要求查找值在表范围的第一列而且只能正向查找。SQL里就没这个限制ON条件可以任意指定字段还能搞多键关联。两者的定位不同Excel适合本地快速看数SQL适合规模化、可复现的数据加工。很多运营同学VLOOKUP用得飞起但一旦数据量从几千行变成几百万行Excel会卡到怀疑人生迁移到SQL是迟早的路。这里分享一个迁移经验把Excel的匹配表导成CSV后灌进MySQL建临时表然后一条JOIN就能完成跨表匹配速度秒回效率完全不是一个量级。4. 大表优化索引、回表与慢SQL攻坚4.1 几千万行大表的真相先优化表结构再谈SQL技巧“几千万行大表”听起来吓人但在一线实践中只要索引设计合理、查询走对索引千万级在关系型数据库里并不算压力太大真正的问题往往不是行数多而是写SQL时不带过滤条件或者过滤条件用不上索引导致全表扫描。说穿了几千万行大表的性能瓶颈大多数时候在索引设计而不是数据库本身。慢SQL优化有一个标准排查顺序EXPLAIN看执行计划找到typeALL的行这就是在扫描全表。看possible_keys是否为空空说明没有可用索引。看key是否真正用到了索引用ref、range还是index。检查rows估算扫描行数这个数字越接近返回结果集越理想。专门提一个关键索引不是越多越好。每增加一个索引插入和更新时都要同步维护索引B树写放大明显。热词“辅助索引如何避免回表”这个问题值得拆开说。4.2 辅助索引与回表机制InnoDB表是聚集索引组织主键索引的叶子节点存的是整行数据辅助索引二级索引的叶子节点存的是主键值。当你用非主键字段做条件查询时MySQL会先去辅助索引B树找到主键再用主键去主键索引B树查整行数据这个动作就是“回表”。回表需要两次B树查找性能比只走主键索引一次查找差不少。如果查询的字段恰好都包含在辅助索引里优化器可以直接用辅助索引覆盖查询不用回表这就是覆盖索引。-- 表里有联合索引 idx_user_status(user_id, status) -- 这条查询只用到 user_id 和 status可以直接走覆盖索引不回表 SELECT user_id, status FROM order WHERE user_id 10086; -- 这条查询要取 total_amount索引里没有就要回表 SELECT user_id, status, total_amount FROM order WHERE user_id 10086;覆盖索引是优化高频查询的高性价比手段。把SELECT的字段有意识地设计进联合索引里等于用索引空间换查询速度。代价是索引变宽、占空间增加所以只对最热的查询路径做覆盖索引不要一上来给所有查询都建宽索引。4.3 大表分页与并行SQL优化大表分页是另一个经典痛点。LIMIT 1000000, 10这种写法数据库要把前面100万行全扫描掉再丢弃耗时非常可观。业界常见的优化方式是把分页条件改成基于上一页最大ID的游标方式-- 传统写法深分页慢 SELECT * FROM order ORDER BY id LIMIT 1000000, 10; -- 游标写法用上一次拿到的最大id继续往下翻 SELECT * FROM order WHERE id 1000000 ORDER BY id LIMIT 10;后者的执行计划能直接用主键索引范围扫描效率高得多。如果业务分页有跳页需求那就用延迟关联先查出主键范围再回原表取整行数据也能大幅降低扫描成本。“并行sql优化”这个词的意思是数据库优化器在无法更优串行执行时通过并行扫描把一个大查询拆成多个子任务比如Oracle、PG的并行扫描SQL Server的并行度控制。MySQL 8.0也有parallel_read相关的能力但大多数场景中靠索引到位就不需要并行。真正的实践建议是先确保SQL本身不傻再谈数据库层面的并行参数。4.4 锁表问题排查“mysql锁表”也是高频搜索业务上表现就是某条SQL一直卡着不动其他写入全部堵住。排查顺序-- 查当前正在执行的线程 SHOW PROCESSLIST; -- 查锁等待与阻塞关系 SELECT * FROM sys.innodb_lock_waits;常见原因是长事务持有行锁不提交或者ALTER TABLE拿不到表锁。处理锁表不能一上来就KILL先看事务开启时间和涉及的表如果是程序里事务忘了提交KILL掉对应连接并让开发修复代码才是根治。我见过最典型的案例一个后台任务开启了事务循环里有一半数据没有提交成功就异常退出连接没关闭结果整张业务表的行锁被长期占用线上大量INSERT卡死。经验总结是事务代码务必用try/finally或者with上下文管理器确保提交和关闭数据库里也要监控长事务。5. 特殊场景TDengine、Hive 与嵌入式库中的表5.1 TDengine 超级表子表时序表独有的建模方式时序数据库TDengine的表模型和传统关系库差异很大很多从MySQL转过来的同学一开始很不适应。TDengine里有两层表结构超级表STABLE和子表TABLE。超级表定义的是数据的公共schema时间戳字段加一组业务字段同时指定标签字段标签相当于设备的静态属性。子表则是超级表中的一条具体设备/具体对象实例继承超级表schema但有自己的标签值。举例理解智能电表采集场景建设一个超级表meters字段有ts、voltage、current、power标签是device_id和location。每块电表就是一个子表比如meters_d_001。查询某块电表历史数据查子表查询所有华北电表的平均功率查超级表并带标签过滤。这套模型天然支持海量设备的海量时序点按设备做数据隔离查询效率极高。关键问题“TDengine如何做到多个表时序一致”如果业务要拿多个子表做对齐查询、比如同时比较10台设备的电压曲线TDengine提供了INTERP线性插值等时间对齐函数配合PARTITION BY按子表分组可以在同一时间轴上把多个表的数据对齐。SELECT _wstart, device_id, AVG(voltage) FROM meters WHERE ts 2025-01-01 00:00:00 AND ts 2025-01-02 00:00:00 PARTITION BY device_id INTERVAL(1m);这条SQL按分钟粒度聚合并按设备分区解决多表时序一致性的核心思路是时间间隔统一、分区维度明确、再用插值填充缺失点。5.2 MySQL表结构自动转TDengine超级表子表从MySQL迁移到TDengine热词里“mysql表结构自动转tdengine超级表子表”是很多人找方案的点。常规做法可以分为三步第一步分析MySQL原表把时间列识别出来作为TDengine的主时间戳列把普通业务列映射成字段。第二步识别标签维度通常是把原来查询里高频出现在WHERE条件中的设备ID、站点ID、类型ID当作标签。第三步建超级表后用子表创建语句把每个实体注册成子表。CREATE STABLE meters (ts TIMESTAMP, voltage FLOAT, current FLOAT, power FLOAT) TAGS (device_id NCHAR(32), location NCHAR(128)); CREATE TABLE meters_d_001 USING meters TAGS (D001, Beijing-01);这个转换不能纯自动化必须有业务判断哪些列做标签、哪些列做字段直接决定查询性能和存储效率。标签列过多会膨胀元数据标签列过少则查询过滤不充分。5.3 Hive 表 DDL数仓里的表更讲究分区Hive里的表DDL和MySQL风格差异很大。典型离线数仓建表会声明PARTITIONED BY、存储格式、压缩算法CREATE TABLE dwd_order ( order_no STRING, user_id BIGINT, total_amount DECIMAL(12,2), status TINYINT ) PARTITIONED BY (dt STRING) STORED AS PARQUET TBLPROPERTIES (parquet.compressionsnappy);Hive分区表的核心价值是按dt等维度做物理文件裁剪查询时只要WHERE dt2025-01-01就能只扫当天数据。实际工作中容易踩的坑包括动态分区数量过大导致小文件过多以及分区字段类型不一致导致ALTER TABLE ADD PARTITION失败。Hive表DDL的调整通常不是直接ALTER重建而是先改元数据再做数据重写整个流程和在线业务库不是一个思路要先转变观念再动手。5.4 SQLite 报错no such column 背后的表结构缓存“sqliteexception(1): while preparing statement, no such column: test_url”这其实是SQLite开发中特别常见的低级错误。原因基本就三种表里根本没有这个字段你代码写错了列名。你改了表结构加了列但代码连接的是旧数据库文件。连接对象有缓存或者数据库文件路径不对打开了旧的副本。排查方式很简单用工具打开实际使用的db文件执行PRAGMA table_info(表名)看字段列表再检查连接串指定的数据库路径是否和你以为的是同一个文件。SQLite还有个特点对字段类型不强制但你给一个不存在的列写值它只会报错或者忽略所以字段名拼写必须绝对正确大小写也尽量保持一致。6. 工具与常见故障SQL Server、DataGrip 实战记录6.1 SQL Server 常见连接故障排查“solidworks electrical 无法连接到 sql server”、“sql server 2012密码到期”这些都是SQL Server日常维护里的经典问题。先理清一个铁律SQL Server连接类故障第一步永远是区分网络层还是认证层。网络层的症状是连接超时、找不到服务器排查顺序是能否Ping通目标IP是否开启了TCP 1433端口SQL Server是否启用了TCP/IP协议。认证层的症状是登录失败或者密码过期需要确认是否启用了混合验证模式用户名是否存在密码是否过期或已锁定。SQL Server密码到期是本地账户策略连带引起的如果服务账号或SQL登录账号受Windows组策略密码过期策略影响运行一段时间后就会出现“用户登录失败密码已过期”。常规解决方案是关闭密码过期策略或用ALTER LOGIN重置ALTER LOGIN [sa] WITH PASSWORD NewStrongPass123!; ALTER LOGIN [sa] WITH CHECK_POLICY OFF, CHECK_EXPIRATION OFF;这条SQL配合使用CHECK_EXPIRATION OFF是为了避免再次过期CHECK_POLICY OFF是关闭密码复杂度策略。注意这类操作要评估公司安全要求不是所有环境都能随便关。另外一个让不少人头疼的坑是安装时“无法连接到SQL Server”大部分情况是服务根本没启动。Windows服务管理器里找到SQL Server (MSSQLSERVER)确认状态是“正在运行”再检查连接字符串中的实例名。命名实例和默认实例的连接写法不同默认实例是localhost命名实例是localhost\SQLEXPRESS写错一个斜杠就是连不上。6.2 DataGrip 表数据复制与导出 ER 图DataGrip是这几年JetBrains系里口碑不错的数据库工具。表数据复制的体验比Navicat更顺滑打开表后选中目标行直接复制成INSERT语句或者CSV方便做测试数据准备。但它也有个坑在表编辑界面直接修改数据时DataGrip默认会开启事务必须手动提交才能真正落库如果忘了提交看着数据变了实际库里没变很容易造成误判。MySQL表导出ER关系图也是热门需求。DataGrip里右键数据库选择Diagrams就能自动生成表关系图反向查看依赖关系非常方便。如果表数量多建议只选择核心几张表生成ER图全库导出来又绕又卡反而失去了关系图的意义。6.3 常见问题速查表问题场景典型症状最快处理路径MySQL锁表写入SQL卡住不返回SHOW PROCESSLIST找到锁源提交或回滚事务慢SQL查询延迟高CPU暴涨EXPLAIN分析有无全表扫描优化索引大表深分页翻页越往后越慢改成游标分页或延迟关联去重后数据异常行数对不上先确认DISTINCT与GROUP BY语义差异再查NULL影响SQL Server连不上超时或登录失败区分网络层和认证层分段排查SQLite字段报错no such column检查实际db文件路径和字段拼写跨表合并结果膨胀结果行数远大于预期检查关联键是否唯一必要时先对子查询去重热词里还有像“navicat for sql server激活码”、“sql server 2022企业版密钥”这类这个我不展开说了商业软件请走正规授权渠道。开发调试用SQL Server Express免费版企业部署按需购买授权这点上不值得省省下来都是隐患。6.4 关于SQL注入说点正经的安全实践搜索热词里出现了多轮SQL注入相关内容作为一个数据库方向的老兵我有必要明确提醒SQL注入是Web应用中最危险的漏洞之一它本身不是“绕过技巧”而是因为程序把外部输入直接拼进SQL导致用户输入被当成SQL代码执行。杜绝SQL注入的实践很简单也很成熟就是参数化查询和预编译语句。Python里用pymysql参数化sql SELECT * FROM order WHERE user_id %s cursor.execute(sql, (user_id,))Java里用JDBC预编译PreparedStatement ps conn.prepareStatement(SELECT * FROM order WHERE user_id ?); ps.setLong(1, userId);永远不要用字符串拼接去拼SQL里的用户输入这是底线。所谓“万能密码绕过”本质是拼接SQL被注入后的现象属于错误写法的后果不是要学的东西。真正该学的是给数据库账号最小权限、使用参数化、对敏感操作做审计。这个意识能从源头上掐掉绝大多数注入风险。7. 收尾前最后分享一点体会做了这么多年SQL相关的工作最大的感悟是表结构设计得好后面能省十倍的事表结构设计得烂后面每天都是在填坑。很多优化问题表面上是慢SQL、是锁表、是数据对不上深挖到底都跟表最初的设计有关。字段注释写没写、主键选没选对、索引建得克不克制、NULL语义有没有统一这些细节决定了一张表的长期健康。再分享一个实用习惯凡是改动表结构不管是加字段还是加索引尽量先在一个独立环境把脚本跑一遍看执行时间和影响行数评估清楚再上生产。线上大表的一切操作都要先设计后执行把回滚方案想好了再动手这是数据库操作里最重要的保命守则。表的学问很基础但恰恰是这些基础的东西决定了一个工程师能走多远。
返回列表