試工程師數(shù)據(jù)庫(kù)操作實(shí)戰(zhàn)指南:從SELECT到事務(wù)與性能排查)
作為測(cè)試工程師你日常的工作流里有一個(gè)繞不開(kāi)的坎不管是功能測(cè)試、接口測(cè)試還是自動(dòng)化腳本跑完用例之后總要回到數(shù)據(jù)庫(kù)里查一筆數(shù)據(jù)、改一個(gè)狀態(tài)、造一條測(cè)試記錄。我見(jiàn)過(guò)太多測(cè)試同行的日常狀態(tài)是——業(yè)務(wù)邏輯和用例設(shè)計(jì)都很熟練但一到數(shù)據(jù)庫(kù)環(huán)節(jié)就開(kāi)始四處求人幫我看下這條數(shù)據(jù)怎么查不出來(lái)這個(gè)SQL寫(xiě)錯(cuò)了哪里測(cè)試環(huán)境的數(shù)據(jù)又被搞臟了誰(shuí)幫忙清一下。說(shuō)實(shí)話數(shù)據(jù)庫(kù)操作命令對(duì)測(cè)試工程師來(lái)說(shuō)不是那種會(huì)用就行的錦上添花而是直接影響你工作效率的基礎(chǔ)功。你不需要達(dá)到DBA那種能調(diào)參優(yōu)化集群的水平但你必須能獨(dú)立完成數(shù)據(jù)準(zhǔn)備、結(jié)果校驗(yàn)、異常數(shù)據(jù)清理這些高頻操作。這篇文章我打算把我自己這些年實(shí)測(cè)下來(lái)最常用的數(shù)據(jù)庫(kù)命令、場(chǎng)景化操作方法和踩坑經(jīng)驗(yàn)做一個(gè)系統(tǒng)性的梳理從最簡(jiǎn)單的SELECT到事務(wù)處理、性能分析都有覆蓋適合各個(gè)階段的測(cè)試工程師對(duì)照著用。1. 測(cè)試工程師為什么必須掌握數(shù)據(jù)庫(kù)操作1.1 測(cè)試工作與數(shù)據(jù)操作的交集在哪里先說(shuō)一個(gè)很現(xiàn)實(shí)的問(wèn)題測(cè)試工程師每天都在跟數(shù)據(jù)庫(kù)打交道但很多人沒(méi)意識(shí)到這一點(diǎn)。功能測(cè)試?yán)锬阋?yàn)證提交訂單后庫(kù)存表數(shù)量是否減一這需要查數(shù)據(jù)庫(kù)接口測(cè)試?yán)镆粋€(gè)創(chuàng)建用戶的接口跑完你要確認(rèn)user表里真的多了一條記錄這也需要查數(shù)據(jù)庫(kù)自動(dòng)化測(cè)試中你要先往數(shù)據(jù)庫(kù)里插入一批符合條件的數(shù)據(jù)作為前置準(zhǔn)備測(cè)試結(jié)束后還要把這些數(shù)據(jù)清理掉避免影響下一輪測(cè)試。我以前帶過(guò)一個(gè)剛?cè)胄械耐滤麥y(cè)一個(gè)用戶注銷(xiāo)的功能界面上提示注銷(xiāo)成功他就把這個(gè)用例標(biāo)記為通過(guò)了。我問(wèn)他你怎么確認(rèn)數(shù)據(jù)庫(kù)里的用戶狀態(tài)真的變成已注銷(xiāo)了他愣住了。后來(lái)我?guī)麑?xiě)了一條簡(jiǎn)單的SELECT語(yǔ)句查用戶表的狀態(tài)字段結(jié)果發(fā)現(xiàn)實(shí)際狀態(tài)還是正?!瓉?lái)那個(gè)功能的前端校驗(yàn)有Bug接口壓根沒(méi)調(diào)成功。這個(gè)例子很典型不做數(shù)據(jù)庫(kù)校驗(yàn)很多看起來(lái)通過(guò)的用例實(shí)際上是在掩耳盜鈴。1.2 測(cè)試場(chǎng)景中高頻率使用的數(shù)據(jù)庫(kù)操作類(lèi)型從我個(gè)人的實(shí)踐經(jīng)驗(yàn)看測(cè)試工程師使用數(shù)據(jù)庫(kù)的場(chǎng)景高度集中在四類(lèi)操作上。第一類(lèi)是查詢驗(yàn)證占比最高幾乎所有測(cè)試用例都離不開(kāi)用SELECT語(yǔ)句核對(duì)接口或頁(yè)面操作后的數(shù)據(jù)結(jié)果。第二類(lèi)是數(shù)據(jù)準(zhǔn)備比如測(cè)試下單流程你得先準(zhǔn)備一個(gè)庫(kù)存充足的商品、一個(gè)余額足夠的賬號(hào)這些前置數(shù)據(jù)往往需要直接用INSERT語(yǔ)句造出來(lái)。第三類(lèi)是數(shù)據(jù)清理一輪測(cè)試跑完臟數(shù)據(jù)、關(guān)聯(lián)數(shù)據(jù)、中間表殘留都需要你清理還原避免污染下一輪測(cè)試DELETE和TRUNCATE在這里高頻出現(xiàn)。第四類(lèi)是數(shù)據(jù)修改某些場(chǎng)景下你需要繞過(guò)業(yè)務(wù)邏輯、直接改一條記錄的字段來(lái)模擬特定狀態(tài)比如把訂單狀態(tài)從待支付改為已支付以便測(cè)試后續(xù)流程這種操作需要UPDATE語(yǔ)句來(lái)實(shí)現(xiàn)。了解這些場(chǎng)景之后你會(huì)發(fā)現(xiàn)測(cè)試工程師對(duì)數(shù)據(jù)庫(kù)命令的掌握程度直接決定了你的測(cè)試效率和質(zhì)量。如果只會(huì)百度一個(gè)SQL然后復(fù)制粘貼你永遠(yuǎn)要比別人多花幾倍的時(shí)間在處理數(shù)據(jù)和驗(yàn)證結(jié)果上。2. 最核心的查詢命令SELECT的實(shí)戰(zhàn)級(jí)用法2.1 SELECT基礎(chǔ)語(yǔ)法與經(jīng)驗(yàn)習(xí)慣查詢是測(cè)試工程師使用頻率最高的數(shù)據(jù)庫(kù)操作沒(méi)有之一。SELECT語(yǔ)句的基本語(yǔ)法很簡(jiǎn)單SELECT 字段列表 FROM 表名 WHERE 條件。但我見(jiàn)過(guò)很多人在這一步就養(yǎng)成了壞習(xí)慣——?jiǎng)硬粍?dòng)就SELECT *。我在實(shí)際工作中強(qiáng)烈建議你養(yǎng)成顯式列出所需字段的習(xí)慣原因有兩個(gè)第一生產(chǎn)環(huán)境的表字段可能非常多全字段查詢會(huì)拉取大量不必要的數(shù)據(jù)影響查詢效率第二字段過(guò)多會(huì)把結(jié)果集刷屏你反而看不清自己關(guān)心的字段值。比如你要查訂單表里某條記錄的支付狀態(tài)就寫(xiě)SELECT order_id, pay_status, amount FROM orders WHERE order_id 12345既清晰又高效。新手最容易忽略的一個(gè)細(xì)節(jié)是MySQL默認(rèn)對(duì)字符串比較不區(qū)分大小寫(xiě)所以WHERE user_name admin和WHERE user_name ADMIN查出的結(jié)果是一樣的。但如果某個(gè)表的字段設(shè)置了BINARY屬性或使用utf8mb4_bin排序規(guī)則大小寫(xiě)就是敏感的。你在測(cè)試排查問(wèn)題時(shí)如果發(fā)現(xiàn)明明有這條數(shù)據(jù)但查不出來(lái)先看一眼排序規(guī)則這類(lèi)問(wèn)題很坑人。2.2 WHERE條件過(guò)濾從基礎(chǔ)到進(jìn)階WHERE子句是查詢的過(guò)濾核心。常見(jiàn)操作符包括、!、、、、、BETWEEN、LIKE、IN和IS NULL。測(cè)試場(chǎng)景中有一個(gè)高頻需求按時(shí)間范圍查數(shù)據(jù)。比如我要驗(yàn)證過(guò)去24小時(shí)內(nèi)的所有登錄日志SQL可以寫(xiě)成SELECT user_id, login_time FROM login_log WHERE login_time DATE_SUB(NOW(), INTERVAL 1 DAY) ORDER BY login_time DESC;這里有個(gè)實(shí)操經(jīng)驗(yàn)值得說(shuō)一下判斷字段是否為NULL必須用IS NULL或IS NOT NULL不能寫(xiě) NULL。 NULL永遠(yuǎn)返回空結(jié)果這是SQL的規(guī)范設(shè)計(jì)不是Bug。我每次帶新人都會(huì)專(zhuān)門(mén)強(qiáng)調(diào)這個(gè)點(diǎn)因?yàn)樵谂挪闉槭裁磾?shù)據(jù)查不出來(lái)時(shí)WHERE status NULL這種寫(xiě)法出現(xiàn)的頻率實(shí)在太高了。另外測(cè)試場(chǎng)景中還經(jīng)常會(huì)用到LIKE做模糊匹配。需要注意LIKE %關(guān)鍵字%這種寫(xiě)法無(wú)法利用索引數(shù)據(jù)量大時(shí)查詢會(huì)很慢。如果只是驗(yàn)證某條數(shù)據(jù)是否存在我更建議用精確匹配加LIMIT 1或者用EXISTS子查詢效率會(huì)高很多。2.3 分組統(tǒng)計(jì)與結(jié)果排序數(shù)據(jù)校驗(yàn)的關(guān)鍵步驟測(cè)試中經(jīng)常要驗(yàn)證數(shù)量對(duì)不對(duì)。比如注冊(cè)功能測(cè)試完你要確認(rèn)今天新增了多少用戶下單功能測(cè)試完你要確認(rèn)訂單總數(shù)和總金額是否正確。這類(lèi)需求離不開(kāi)GROUP BY和聚合函數(shù)。我舉一個(gè)實(shí)際場(chǎng)景測(cè)試一個(gè)促銷(xiāo)活動(dòng)活動(dòng)規(guī)則是每個(gè)用戶最多只能參與3次你要驗(yàn)證系統(tǒng)是否真的限制了同一用戶參與次數(shù)。可以用一條分組統(tǒng)計(jì)SQL快速核對(duì)SELECT user_id, COUNT(*) AS participate_count FROM activity_participation GROUP BY user_id HAVING COUNT(*) 3;這條SQL會(huì)把參與次數(shù)超過(guò)3條的用戶全查出來(lái)如果有結(jié)果說(shuō)明系統(tǒng)限制邏輯存在問(wèn)題。這里的HAVING是在分組后做條件過(guò)濾而WHERE是在分組前過(guò)濾兩者區(qū)別必須搞清楚。WHERE無(wú)法使用聚合函數(shù)作為條件這就是為什么需要HAVING存在的原因。排序方面ORDER BY支持多字段排序比如ORDER BY create_time DESC, id ASC的意思是先按創(chuàng)建時(shí)間倒序時(shí)間相同再按ID正序。測(cè)試中你要確認(rèn)最新一條數(shù)據(jù)時(shí)最普遍的做法是ORDER BY id DESC LIMIT 1配合自增主鍵非常穩(wěn)健。2.4 分頁(yè)查詢LIMIT的正確打開(kāi)方式分頁(yè)查詢?cè)诮涌跍y(cè)試和頁(yè)面測(cè)試?yán)锍霈F(xiàn)頻率很高。MySQL中用LIMIT實(shí)現(xiàn)分頁(yè)有兩種常見(jiàn)寫(xiě)法LIMIT 10表示只返回前10條LIMIT 10, 20表示從第11條開(kāi)始返回20條。另一個(gè)容易混淆的寫(xiě)法是LIMIT 20 OFFSET 10它和LIMIT 10, 20的含義是一樣的。這個(gè)順序問(wèn)題我建議你記住一個(gè)口訣寫(xiě)在前面的是偏移量寫(xiě)在后面的是返回條數(shù)。分頁(yè)查詢有一個(gè)典型的測(cè)試場(chǎng)景驗(yàn)證列表接口的分頁(yè)參數(shù)是否正確。比如前端傳page2size10你要去數(shù)據(jù)庫(kù)里數(shù)一下總共多少條數(shù)據(jù)然后手動(dòng)算出第二頁(yè)應(yīng)該展示哪些數(shù)據(jù)再和接口返回對(duì)比。這種校驗(yàn)光靠肉眼一條條看是低效的直接用SELECT ... LIMIT 10 OFFSET 10查出第二頁(yè)數(shù)據(jù)然后和接口返回做比對(duì)效率會(huì)高很多。3. 數(shù)據(jù)準(zhǔn)備與清理INSERT、UPDATE、DELETE的測(cè)試應(yīng)用3.1 INSERT造測(cè)試數(shù)據(jù)的正確姿勢(shì)測(cè)試執(zhí)行前準(zhǔn)備數(shù)據(jù)是高頻場(chǎng)景INSERT命令幾乎每天都要用?;菊Z(yǔ)法是INSERT INTO 表名 (字段1, 字段2) VALUES (值1, 值2)。但實(shí)際測(cè)試中造數(shù)據(jù)的方式遠(yuǎn)不止這一種。最基礎(chǔ)的表結(jié)構(gòu)是用戶表、訂單表、商品表之間存在外鍵關(guān)聯(lián)。你不可能只往一張表里插入數(shù)據(jù)就能完事需要同時(shí)維護(hù)多張關(guān)聯(lián)表的數(shù)據(jù)一致性。比如測(cè)試下單流程你需要先有用戶、有商品、有庫(kù)存記錄如果直接INSERT一張空訂單表業(yè)務(wù)流程走不通。這時(shí)候我通常會(huì)寫(xiě)一個(gè)SQL腳本按依賴順序依次插入多張表的數(shù)據(jù)并且在一個(gè)事務(wù)里執(zhí)行保證要么全部成功、要么全部回滾。還有一種非常實(shí)用的寫(xiě)法是復(fù)制表數(shù)據(jù)來(lái)造數(shù)據(jù)INSERT INTO target_table (col1, col2) SELECT col1, col2 FROM source_table WHERE ...。比如我想給某個(gè)用戶多造10個(gè)已完成訂單可以直接從已有的相似訂單里復(fù)制這樣數(shù)據(jù)更接近真實(shí)比手工一個(gè)個(gè)INSERT效率高得多。3.2 UPDATE場(chǎng)景模擬和狀態(tài)切換的利器UPDATE語(yǔ)句在測(cè)試中的應(yīng)用非常巧妙。比如你要測(cè)試訂單列表頁(yè)在訂單處于不同狀態(tài)下的展示是否符合預(yù)期你沒(méi)有耐心一個(gè)個(gè)通過(guò)業(yè)務(wù)操作去觸發(fā)狀態(tài)流轉(zhuǎn)那就可以直接改數(shù)據(jù)庫(kù)。這屬于典型的測(cè)試后門(mén)操作合法且高效。UPDATE orders SET order_status PAID, pay_time NOW() WHERE order_id 20240101001;這種操作的性價(jià)比非常高幾分鐘內(nèi)你就能把一條訂單從待支付改到已支付再改到已發(fā)貨完整覆蓋各種狀態(tài)下的頁(yè)面展示和邏輯分支。但需要注意直接改數(shù)據(jù)庫(kù)繞過(guò)了業(yè)務(wù)邏輯校驗(yàn)改出來(lái)的數(shù)據(jù)可能與正常業(yè)務(wù)流轉(zhuǎn)產(chǎn)生的數(shù)據(jù)存在細(xì)微差異。所以這種操作適合功能驗(yàn)證不適合做嚴(yán)格的接口數(shù)據(jù)一致性測(cè)試。我踩過(guò)的一個(gè)大坑是UPDATE語(yǔ)句忘了加WHERE條件結(jié)果把整張表的數(shù)據(jù)全部更新了。這事在測(cè)試環(huán)境還好說(shuō)恢復(fù)一下就行但如果在生產(chǎn)環(huán)境后果不堪設(shè)想。所以我給自己定了一個(gè)鐵律寫(xiě)UPDATE和DELETE語(yǔ)句時(shí)先寫(xiě)WHERE條件再回來(lái)寫(xiě)UPDATE或DELETE后面的部分。同時(shí)養(yǎng)成寫(xiě)完SQL先執(zhí)行SELECT驗(yàn)證影響范圍的習(xí)慣。3.3 DELETE與TRUNCATE數(shù)據(jù)清理的學(xué)問(wèn)測(cè)試結(jié)束后的數(shù)據(jù)清理是很多人忽略但必須重視的環(huán)節(jié)。DELETE和TRUNCATE都能刪除數(shù)據(jù)但區(qū)別很大。DELETE是逐條刪除支持WHERE條件刪除過(guò)程記錄日志可以配合事務(wù)回滾TRUNCATE是直接清空整張表速度快但不可回滾也不能加WHERE條件。我建議的清理策略是只清理你這次測(cè)試造出來(lái)的數(shù)據(jù)用DELETE加精確的篩選條件比如按測(cè)試標(biāo)識(shí)、特定時(shí)間段或特定的測(cè)試賬號(hào)前綴來(lái)刪。TRUNCATE這種全表清空操作只適合用在完全獨(dú)立的臨時(shí)表上。我曾經(jīng)見(jiàn)過(guò)一個(gè)測(cè)試為了省事直接TRUNCATE了一張基礎(chǔ)數(shù)據(jù)表結(jié)果整個(gè)測(cè)試環(huán)境所有依賴這張表的模塊全部報(bào)錯(cuò)修復(fù)花了大半天得不償失。一個(gè)更穩(wěn)妥的做法是造數(shù)據(jù)的時(shí)候帶上明顯的測(cè)試標(biāo)識(shí)。比如插入測(cè)試用戶時(shí)用戶名統(tǒng)一加test_前綴或者加一個(gè)固定的batch_no字段這樣清理時(shí)一條DELETE就能精準(zhǔn)搞定不會(huì)誤刪別人的數(shù)據(jù)。4. 聯(lián)表查詢驗(yàn)證跨表數(shù)據(jù)的核心技能4.1 INNER JOIN、LEFT JOIN的場(chǎng)景差異測(cè)試中經(jīng)常要驗(yàn)證多張表的數(shù)據(jù)聯(lián)動(dòng)。比如用戶下單后用戶表要扣余額、訂單表要生成訂單、庫(kù)存表要減庫(kù)存、流水表要記一筆明細(xì)。你要驗(yàn)證這些數(shù)據(jù)的一致性不可能一張表一張表分開(kāi)查了再自己比對(duì)直接聯(lián)表查詢更高效。INNER JOIN只返回兩張表中匹配上的記錄適合驗(yàn)證應(yīng)該有對(duì)應(yīng)關(guān)系的數(shù)據(jù)。LEFT JOIN返回左表所有記錄右表沒(méi)有匹配的字段顯示為NULL適合驗(yàn)證左表存在的記錄在右表是否有對(duì)應(yīng)記錄。我舉一個(gè)典型的測(cè)試場(chǎng)景驗(yàn)證所有已支付訂單是否都有支付流水。這個(gè)需求用LEFT JOIN最容易查出不匹配的數(shù)據(jù)SELECT o.order_id, o.pay_status, p.pay_id FROM orders o LEFT JOIN payment_records p ON o.order_id p.order_id WHERE o.pay_status PAID AND p.pay_id IS NULL;這條SQL查出的是已支付但沒(méi)有支付流水的訂單如果有結(jié)果說(shuō)明系統(tǒng)存在數(shù)據(jù)不一致問(wèn)題這就是一個(gè)真正的Bug。測(cè)試中這類(lèi)校驗(yàn)特別管用。4.2 聯(lián)表查詢中的別名使用與常見(jiàn)錯(cuò)誤寫(xiě)聯(lián)表查詢時(shí)給表起別名的習(xí)慣非常重要。多個(gè)表有同名字段比如每張表都可能有create_time或status如果不加別名區(qū)分SQL會(huì)報(bào)模糊錯(cuò)誤。實(shí)踐中的正確寫(xiě)法是SELECT o.order_id, p.pay_time FROM orders o INNER JOIN payment_records p ON o.order_id p.order_id。我還想提醒一個(gè)高頻錯(cuò)誤JOIN條件寫(xiě)錯(cuò)會(huì)導(dǎo)致結(jié)果集爆炸。比如兩張表通過(guò)兩個(gè)字段才能唯一關(guān)聯(lián)你只寫(xiě)了一個(gè)關(guān)聯(lián)條件結(jié)果就會(huì)產(chǎn)生笛卡爾積式的重復(fù)數(shù)據(jù)查詢結(jié)果比實(shí)際數(shù)據(jù)多出無(wú)數(shù)倍。我排查過(guò)一次訂單數(shù)據(jù)翻倍的問(wèn)題最后發(fā)現(xiàn)是JOIN條件漏了o.user_id p.user_id這第二個(gè)關(guān)聯(lián)字段。4.3 子查詢?cè)跍y(cè)試中的應(yīng)用子查詢也是測(cè)試中的常用手段。比如你要查購(gòu)買(mǎi)過(guò)商品A的所有用戶可以先查出購(gòu)買(mǎi)記錄里包含商品A的user_id集合再查這些用戶的詳細(xì)信息。寫(xiě)法有兩種用IN子查詢或用JOIN測(cè)試中的數(shù)據(jù)量通常不大兩者都可以但I(xiàn)N子查詢語(yǔ)義更直觀SELECT user_id, user_name FROM users WHERE user_id IN ( SELECT user_id FROM order_items WHERE product_id P001 );如果你的測(cè)試數(shù)據(jù)量特別大建議用JOIN代替IN因?yàn)镴OIN在多數(shù)數(shù)據(jù)庫(kù)優(yōu)化器里能走更好的執(zhí)行計(jì)劃。5. 索引與性能觀測(cè)定位測(cè)試中的慢SQL5.1 索引是什么測(cè)試工程師也需要了解很多測(cè)試工程師覺(jué)得索引是開(kāi)發(fā)或DBA才需要管的東西。其實(shí)不然。你寫(xiě)的SQL如果沒(méi)走對(duì)索引一條本來(lái)幾毫秒的查詢會(huì)變成幾秒鐘直接影響測(cè)試執(zhí)行效率。尤其是接口性能測(cè)試和全量回歸測(cè)試階段慢SQL一多會(huì)導(dǎo)致整個(gè)測(cè)試進(jìn)度被拖累。索引的核心原理可以類(lèi)比成一本書(shū)的目錄沒(méi)有目錄你要一頁(yè)頁(yè)翻有目錄你直接翻到對(duì)應(yīng)頁(yè)碼。數(shù)據(jù)庫(kù)里的索引就是給表的某個(gè)或某幾個(gè)字段建立快速查找路徑。測(cè)試中最常見(jiàn)的是普通索引和唯一索引。查詢條件經(jīng)常用的字段比如訂單號(hào)order_id建議建索引而唯一索引可以保證字段值不重復(fù)在造數(shù)據(jù)的時(shí)候還能幫你防止插入重復(fù)數(shù)據(jù)。5.2 用EXPLAIN快速分析SQL執(zhí)行計(jì)劃排查SQL效率問(wèn)題時(shí)EXPLAIN是必備工具。用法超級(jí)簡(jiǎn)單在SQL前面加EXPLAIN關(guān)鍵字執(zhí)行后數(shù)據(jù)庫(kù)會(huì)告訴你這個(gè)SQL打算怎么執(zhí)行。關(guān)鍵要看幾個(gè)字段type表示訪問(wèn)類(lèi)型從好到差依次是const、eq_ref、ref、range、index、ALLALL代表全表掃描說(shuō)明你的SQL沒(méi)有走索引需要重點(diǎn)排查key表示實(shí)際使用的索引名rows是一個(gè)估算值表示預(yù)計(jì)要掃描多少行這個(gè)數(shù)字越大說(shuō)明SQL越低效。我之前排查過(guò)一個(gè)典型的案例查詢一條訂單記錄耗時(shí)2秒多用EXPLAIN一看type是ALL全表掃了兩百多萬(wàn)行。后來(lái)發(fā)現(xiàn)order_id字段沒(méi)建索引建議開(kāi)發(fā)加了索引之后同樣一條SQL幾十毫秒就返回了。整個(gè)排查過(guò)程不到十分鐘但如果沒(méi)有EXPLAIN這個(gè)工具你可能還在那里懷疑是不是數(shù)據(jù)庫(kù)服務(wù)器性能問(wèn)題。5.3 測(cè)試環(huán)境與生產(chǎn)環(huán)境的性能差異提醒這里有一條重要的經(jīng)驗(yàn)測(cè)試環(huán)境的SQL執(zhí)行耗時(shí)和生產(chǎn)環(huán)境往往差異巨大因?yàn)閿?shù)據(jù)量完全不在一個(gè)數(shù)量級(jí)。你在測(cè)試環(huán)境看一條SQL跑200毫秒覺(jué)得沒(méi)問(wèn)題但在生產(chǎn)環(huán)境可能因?yàn)閿?shù)據(jù)量增長(zhǎng)變成1秒、甚至5秒這就可能在真實(shí)的流量場(chǎng)景下拖垮接口。所以我的建議是測(cè)試工程師不要光看SQL能不能查出正確結(jié)果還要養(yǎng)成跑EXPLAIN的習(xí)慣看關(guān)鍵查詢是否走索引、預(yù)計(jì)掃描行數(shù)是否合理。如果發(fā)現(xiàn)明顯是全表掃描要提缺陷或反饋給開(kāi)發(fā)這是性能隱患不只是測(cè)試效率問(wèn)題。6. 事務(wù)與鎖保證測(cè)試數(shù)據(jù)一致性的基礎(chǔ)概念6.1 手動(dòng)控制事務(wù)測(cè)試聯(lián)動(dòng)的硬保障造數(shù)據(jù)時(shí)如果涉及多張表關(guān)聯(lián)插入最穩(wěn)妥的做法是把多步操作包在一個(gè)事務(wù)里。MySQL中默認(rèn)是自動(dòng)提交每一條SQL執(zhí)行完就立即生效無(wú)法回滾。手動(dòng)控制事務(wù)的語(yǔ)法是START TRANSACTION; INSERT INTO users (user_id, user_name) VALUES (U001, test_user); INSERT INTO user_account (user_id, balance) VALUES (U001, 1000); COMMIT;這個(gè)事務(wù)保證了users表和user_account表的插入要么同時(shí)成功要么同時(shí)失敗。如果中途某條SQL執(zhí)行失敗你可以執(zhí)行ROLLBACK把整個(gè)事務(wù)回滾不會(huì)留下只有用戶沒(méi)有賬戶這種臟數(shù)據(jù)。事務(wù)的經(jīng)典特性ACID我簡(jiǎn)單解釋一下原子性指事務(wù)中所有操作要么全部成功、要么全部失敗就像轉(zhuǎn)賬要么扣款和入賬都完成要么都不發(fā)生一致性指事務(wù)前后數(shù)據(jù)狀態(tài)符合業(yè)務(wù)規(guī)則隔離性指多個(gè)事務(wù)并發(fā)執(zhí)行時(shí)互相不干擾持久性指事務(wù)一旦提交數(shù)據(jù)永久生效。對(duì)測(cè)試工程師來(lái)說(shuō)理解原子性和持久性已經(jīng)能覆蓋日常90%以上的事務(wù)應(yīng)用場(chǎng)景。6.2 隔離級(jí)別與鎖測(cè)試并發(fā)場(chǎng)景時(shí)要注意什么并發(fā)測(cè)試中會(huì)涉及隔離級(jí)別和鎖的概念。MySQL默認(rèn)隔離級(jí)別是REPEATABLE READ可重復(fù)讀意味著同一個(gè)事務(wù)內(nèi)多次查詢結(jié)果一致。如果你在并發(fā)測(cè)試中發(fā)現(xiàn)數(shù)據(jù)出現(xiàn)了明明事務(wù)提交了另一個(gè)連接卻查不到的情況多半跟隔離級(jí)別或事務(wù)未提交有關(guān)。這里有一個(gè)我反復(fù)用到的小技巧測(cè)試并發(fā)接口時(shí)為了確認(rèn)數(shù)據(jù)庫(kù)層面的真實(shí)數(shù)據(jù)變化建議新開(kāi)一個(gè)數(shù)據(jù)庫(kù)連接去查詢不要復(fù)用已經(jīng)開(kāi)啟事務(wù)的連接。因?yàn)橥粋€(gè)連接里未提交的事務(wù)修改可能可以被自己查到容易造成數(shù)據(jù)已生效的誤判。在實(shí)際工作中我把這個(gè)坑叫同連接事務(wù)幻覺(jué)不少測(cè)試都栽在上面。鎖方面最直觀的場(chǎng)景是一個(gè)測(cè)試人員開(kāi)啟事務(wù)執(zhí)行了UPDATE但沒(méi)提交另一個(gè)測(cè)試人員再去UPDATE同一行數(shù)據(jù)時(shí)會(huì)被阻塞等待。這不是數(shù)據(jù)庫(kù)卡死了是行鎖被第一個(gè)事務(wù)持有。排查這種問(wèn)題時(shí)可以用SHOW PROCESSLIST查看當(dāng)前連接和執(zhí)行的SQL找到那個(gè)一直掛著沒(méi)結(jié)束的事務(wù)確認(rèn)后要么讓其提交要么殺掉對(duì)應(yīng)連接。這類(lèi)情況在共享測(cè)試環(huán)境里特別常見(jiàn)掌握查看命令能省下很多溝通成本。6.3 隔離級(jí)別設(shè)置查看與修改命令查看當(dāng)前會(huì)話的隔離級(jí)別用SELECT transaction_isolation臨時(shí)修改用SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED。測(cè)試工程師不太需要深入理解每種隔離級(jí)別的實(shí)現(xiàn)細(xì)節(jié)但要清楚不同隔離級(jí)別下相同的并發(fā)操作可能產(chǎn)生不同的數(shù)據(jù)表現(xiàn)。如果你測(cè)的是一個(gè)高并發(fā)扣庫(kù)存的接口發(fā)現(xiàn)庫(kù)存出現(xiàn)負(fù)數(shù)先別急著報(bào)Bug去確認(rèn)一下數(shù)據(jù)庫(kù)隔離級(jí)別和接口的事務(wù)實(shí)現(xiàn)方式這個(gè)排查思路會(huì)幫你少走很多彎路。7. 測(cè)試場(chǎng)景中的高階技巧與實(shí)戰(zhàn)案例7.1 造數(shù)據(jù)腳本的模板化管理我工作中積累的一個(gè)經(jīng)驗(yàn)把常用的造數(shù)據(jù)SQL整理成模板腳本按業(yè)務(wù)模塊分門(mén)別類(lèi)存放。每次接到新需求需要造數(shù)據(jù)直接復(fù)制模板修改參數(shù)比從頭寫(xiě)SQL快得多。一個(gè)典型的造數(shù)據(jù)腳本大概長(zhǎng)這樣-- 造測(cè)試用戶 INSERT INTO users (user_id, user_name, phone, reg_time) VALUES (U_20240101_001, test_user_001, 13800000001, NOW()); -- 造用戶賬戶并初始化余額 INSERT INTO user_account (user_id, balance, version) VALUES (U_20240101_001, 1000.00, 0); -- 造一張已支付訂單 INSERT INTO orders (order_id, user_id, amount, pay_status, create_time) VALUES (O_20240101_001, U_20240101_001, 99.50, PAID, NOW());腳本化之后你的數(shù)據(jù)準(zhǔn)備時(shí)間能從十幾分鐘壓縮到幾秒鐘。另一個(gè)細(xì)節(jié)凡是腳本造出來(lái)的數(shù)據(jù)我都會(huì)在關(guān)鍵字段上標(biāo)注測(cè)試特征比如ID前綴加TEST_或時(shí)間戳這樣清理時(shí)一條SQL就能精準(zhǔn)搞定而且不會(huì)誤傷手工測(cè)試同學(xué)的正常數(shù)據(jù)。7.2 清理數(shù)據(jù)的正確思路與操作順序清理數(shù)據(jù)是有順序講究的。先刪子表、再刪父表因?yàn)橛型怄I約束的表刪除順序反了會(huì)報(bào)錯(cuò)。舉例來(lái)說(shuō)訂單明細(xì)表(order_items)引用了訂單表(orders)的主鍵清理時(shí)必須先刪order_items再刪orders。但現(xiàn)實(shí)測(cè)試環(huán)境中很多表壓根沒(méi)建外鍵這時(shí)候刪除順序雖然不會(huì)報(bào)錯(cuò)但如果你亂刪可能會(huì)出現(xiàn)明細(xì)還在、主單已刪的空殼記錄影響下一輪測(cè)試。一個(gè)更穩(wěn)的清數(shù)思路是不直接刪除原表數(shù)據(jù)而是把測(cè)試數(shù)據(jù)所在行標(biāo)記成廢棄狀態(tài)或者整體遷移到一張歸檔表。不過(guò)這種做法需要和開(kāi)發(fā)約定如果測(cè)試環(huán)境的數(shù)據(jù)口徑完全由你控制直接DELETE最省事。還有一點(diǎn)必須提醒清理前先備份。哪怕是在測(cè)試環(huán)境養(yǎng)成SELECT * FROM orders WHERE ...先確認(rèn)影響行數(shù)的好習(xí)慣能避免很多意外。7.3 數(shù)據(jù)校驗(yàn)的黃金組合界面、接口、數(shù)據(jù)庫(kù)三查合一真正嚴(yán)格的測(cè)試需要把界面展示、接口返回、數(shù)據(jù)庫(kù)記錄三者對(duì)齊校驗(yàn)。我把它叫三層一致性驗(yàn)證。舉個(gè)例子測(cè)一個(gè)訂單詳情頁(yè)界面顯示訂單狀態(tài)為已發(fā)貨接口返回的JSON里order_status是SHIPPED這兩個(gè)一致還不能立刻斷言通過(guò)。你還得去數(shù)據(jù)庫(kù)里查orders表的實(shí)際狀態(tài)字段如果表里是PAID而接口返回SHIPPED說(shuō)明接口層存在數(shù)據(jù)轉(zhuǎn)換錯(cuò)誤或緩存未更新的問(wèn)題。實(shí)際執(zhí)行中先跑接口拿返回結(jié)果再用接口返回的關(guān)鍵字段比如order_id去數(shù)據(jù)庫(kù)里比對(duì)最后對(duì)照界面展示。這套組合拳打下來(lái)很多隱蔽問(wèn)題才會(huì)現(xiàn)出原形。我在之前的項(xiàng)目里靠這種方式查出過(guò)好幾個(gè)接口返回正常但數(shù)據(jù)庫(kù)實(shí)際數(shù)據(jù)錯(cuò)誤的深層Bug如果只看界面基本發(fā)現(xiàn)不了。8. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄8.1 SQL執(zhí)行很慢應(yīng)該從哪里入手排查測(cè)試中最讓人抓狂的問(wèn)題之一就是同樣一條SQL不知道為什么突然跑不動(dòng)了。我的排查套路一般是三步走。第一步用EXPLAIN看執(zhí)行計(jì)劃確認(rèn)有沒(méi)有走索引、掃描了多少行第二步看是不是有鎖等待用SHOW PROCESSLIST查看當(dāng)前數(shù)據(jù)庫(kù)連接狀態(tài)如果發(fā)現(xiàn)大量Waiting for table metadata lock這類(lèi)狀態(tài)說(shuō)明有其他事務(wù)占著資源第三步看是不是查詢條件寫(xiě)得太寬泛比如LIKE %關(guān)鍵字%導(dǎo)致無(wú)法走索引。給一個(gè)具體的經(jīng)驗(yàn)數(shù)字一張百萬(wàn)行級(jí)別的表走索引的精確查詢應(yīng)該在幾十毫秒內(nèi)完成如果超過(guò)500毫秒基本就能判定SQL或索引存在優(yōu)化空間。你把這個(gè)基準(zhǔn)記在心里排查時(shí)就知道問(wèn)題大概出在哪個(gè)環(huán)節(jié)。8.2 數(shù)據(jù)查不到的幾類(lèi)隱蔽原因我整理了一個(gè)高頻問(wèn)題速查表覆蓋測(cè)試中最常遇到的數(shù)據(jù)查不到場(chǎng)景現(xiàn)象可能原因排查方向明明有數(shù)據(jù)但SELECT查不到查詢條件字段類(lèi)型不匹配比如字段是VARCHAR但條件用了數(shù)值檢查字段類(lèi)型和條件寫(xiě)法查出來(lái)的結(jié)果和想象中不同數(shù)據(jù)庫(kù)連接到了錯(cuò)誤的環(huán)境比如測(cè)試連到預(yù)發(fā)庫(kù)確認(rèn)連接配置和當(dāng)前環(huán)境WHERE條件看起來(lái)正確但返回空字段是NULL用條件永遠(yuǎn)匹配不上改用IS NULL中文條件查不到字符集或排序規(guī)則不一致查看表字符集和連接字符集時(shí)間范圍查詢結(jié)果不對(duì)時(shí)區(qū)設(shè)置差異導(dǎo)致時(shí)間偏移用NOW()和字段值對(duì)比檢查這類(lèi)問(wèn)題大多數(shù)時(shí)候不是SQL寫(xiě)錯(cuò)了而是對(duì)數(shù)據(jù)本身和環(huán)境的理解不夠。排查時(shí)先確認(rèn)我在哪個(gè)環(huán)境這張表長(zhǎng)什么樣數(shù)據(jù)到底存了什么比反復(fù)改SQL更有意義。8.3 測(cè)試環(huán)境數(shù)據(jù)被污染后的自救方法共享測(cè)試環(huán)境最怕的就是數(shù)據(jù)被污染你造的數(shù)據(jù)被別人改掉了或者別人跑測(cè)試留下了一堆半成品數(shù)據(jù)導(dǎo)致你的用例執(zhí)行總是失敗。我的自救方案是造數(shù)據(jù)時(shí)盡量帶上自己的測(cè)試標(biāo)識(shí)同時(shí)把關(guān)鍵數(shù)據(jù)的查詢條件設(shè)計(jì)成能精準(zhǔn)鎖定自己數(shù)據(jù)的形式。比如用戶名加前綴、訂單號(hào)加批次號(hào)、備注字段寫(xiě)入自己的標(biāo)記這樣不管別人怎么污染你都能一條SQL精準(zhǔn)撈回自己需要的數(shù)據(jù)。如果污染已經(jīng)發(fā)生且無(wú)法定位備份恢復(fù)是最后的底牌。我會(huì)在造完關(guān)鍵數(shù)據(jù)后用mysqldump導(dǎo)出單表數(shù)據(jù)備份測(cè)試過(guò)程中如果發(fā)現(xiàn)數(shù)據(jù)異常直接用備份文件恢復(fù)。這個(gè)操作在MySQL里相當(dāng)實(shí)用準(zhǔn)備大版本數(shù)據(jù)時(shí)一定要做。8.4 日常工作中的數(shù)據(jù)庫(kù)操作安全底線講幾個(gè)我給自己立的規(guī)矩希望對(duì)你有參考價(jià)值。第一條任何UPDATE和DELETE操作先寫(xiě)WHERE、再寫(xiě)語(yǔ)句主體這是保命級(jí)的習(xí)慣。第二條操作前先SELECT確認(rèn)影響范圍哪怕影響一條數(shù)據(jù)也要確認(rèn)。第三條嚴(yán)禁在測(cè)試環(huán)境執(zhí)行沒(méi)經(jīng)過(guò)驗(yàn)證的腳本更不用說(shuō)生產(chǎn)環(huán)境。第四條共享環(huán)境操作完主動(dòng)清理現(xiàn)場(chǎng)不給別人留坑。這些底線聽(tīng)起來(lái)像老生常談但我在真實(shí)項(xiàng)目中見(jiàn)過(guò)太多因?yàn)楹鲆曔@些規(guī)矩導(dǎo)致的事故比如一條未加WHERE的UPDATE把整張訂單表狀態(tài)全部刷成已支付開(kāi)發(fā)排查了一整天。做測(cè)試的人對(duì)自己操作的數(shù)據(jù)要有敬畏心。數(shù)據(jù)庫(kù)命令的熟練度不是一蹴而就的它的提升路徑和測(cè)試用例設(shè)計(jì)很像——你不需要背會(huì)所有命令而是要在真實(shí)場(chǎng)景里反復(fù)用用多了自然就內(nèi)化成自己的經(jīng)驗(yàn)。我希望這篇整理能幫你在日常測(cè)試工作中少走一些彎路也歡迎你把自己的實(shí)戰(zhàn)心得補(bǔ)充進(jìn)來(lái)數(shù)據(jù)庫(kù)這個(gè)領(lǐng)域越交流越漲經(jīng)驗(yàn)。