当前位置:首页 > 娱乐

sql实战

线上事故复盘会上,一张截图被投到屏幕上:订单表里出现了两条完全相同的记录,除了自增ID不同。开发同事说代码里加了“先查再插”的逻辑,理论上不可能重复。DBA没说话,直接连上从库跑了一条SQL,把最近十分钟的插入记录拉了出来,指着其中一条说:“你这个‘查’走的是从库,‘插’走的是主库,主从延迟200毫秒,并发一上来,两条请求都查不到对方,就都插进去了。”

这就是SQL实战里最常见的一类问题——不是语法不会写,而是不知道语句在什么环境下执行、会踩到什么坑。很多教程教的是SELECT * FROM table WHERE id = 1,但真实生产环境里,一条SQL背后牵扯的是隔离级别、索引结构、锁的粒度、执行计划,甚至主从架构。

先看一个索引失效的现场

有张用户表,phone字段建了索引,类型是varchar(20)。某天运营反馈查询特别慢,SQL长这样:

SELECT * FROM user WHERE phone = 13800138000;

看起来没问题。但用EXPLAIN一看,type=ALL,全表扫描。原因在于参数传的是数字,MySQL做了隐式类型转换,把phone列转成了数字再比较,索引直接废掉。改成phone = '13800138000',type立刻变成ref。这类问题在ORM框架里更隐蔽,因为参数类型由代码层决定,写代码的人根本看不到最终拼出来的SQL长什么样。

UPDATE语句的锁范围比你想的大

另一回,一个简单的UPDATE orders SET status = 2 WHERE order_no = 'A123'把整张表锁了将近十秒。排查发现order_no没有索引,InnoDB在RR隔离级别下,对没有索引的更新会锁住所有扫描过的行,实际效果接近锁表。加上索引之后,锁范围缩小到那一行。很多人背过“InnoDB支持行锁”,但行锁是建在索引上的,没索引就没有行锁。

分页查询的深水区

LIMIT 1000000, 20这种写法,在数据量大的表上就是灾难。MySQL会先读取前1000020条记录,再扔掉前1000000条。优化思路通常是用游标或者子查询先定位ID:

SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;

前提是ID连续且有序。如果业务上ID有空洞,还得结合WHERE created_at > '某时间点'来辅助定位。没有银弹,得看具体数据分布。

写SQL的人得懂执行计划

EXPLAIN是必须会看的。type列从system到ALL,性能依次下降;key列显示实际用了哪个索引;rows列是预估扫描行数,和实际差太多说明统计信息过期,需要ANALYZE TABLE。还有Extra列里的Using filesort、Using temporary,看到这两个词就得想想能不能通过调整索引或改写语句来消除。

我自己的习惯是,任何要上线的SQL,先在测试库用真实数据量跑一遍EXPLAIN,再开profiling看实际耗时。别信“数据量小的时候没问题”,生产环境的数据增长往往超出预期。

事务里别掺无关操作

见过一个案例:一个事务里先更新订单状态,然后调用外部HTTP接口通知物流,最后再写一条日志。HTTP接口超时了30秒,整个事务持有了30秒的锁,后面所有请求全堵住。事务要短,外部调用要挪到事务提交之后。这个道理说起来简单,但代码写起来很容易顺手就把RPC调用塞进@Transactional方法里。

SQL实战拼的不是谁背的函数多,而是对数据行为、并发场景、执行路径的理解。一条语句写下去,得清楚它会在什么隔离级别下跑、走哪个索引、锁哪些行、主从延迟会不会影响结果。这些细节,才是真正区分“会写SQL”和“能扛生产”的地方。

分享到: