
最近在論壇和社群里看到不少人在搜“sql語句去重”“sql去除空值”“sql server 時間函數”“慢sql優化 explain主要看哪些信息”這類問題說實話挺感慨的。很多同學寫SQL已經能跑通業務了但遇到去重、NULL判斷、時間比較這種基礎操作還是會卡住或者寫出看似正確實則埋雷的語句。這篇SQL常用語句基礎大全我不打算給你羅列一堆官方文檔式的語法而是站在實際開發的角度把日常最高頻、面試最容易問、踩坑最多的一批語句掰開揉碎講清楚。適合剛學數據庫的入門者也適合寫了半年一年仍然對某些細節模棱兩可的人。看完這篇你會發現很多“奇怪問題”其實都是基礎沒夯實。1. 先懂執行順序再談SQL常用語句1.1 為什么很多人把SQL寫成“黑盒”我見過不少開發同學寫SQL全靠肌肉記憶SELECT后面跟一堆列FROM表WHERE過濾GROUP BY分組ORDER BY排序看起來都有。但一旦出錯只能瞎試一會兒把條件放WHERE一會兒放HAVING一會兒在SELECT里用別名做篩選發現報錯就懵了。根源在于他從沒理解SQL的執行順序把SQL當成了“從上往下翻譯”的普通編程語言。SQL是一門聲明式語言核心邏輯是“你告訴我想要什么結果數據庫自己決定怎么取”。這個思維和命令式語言完全不同。你去問MySQL優化器“這條語句該怎么跑”它內部會基于統計信息、索引情況生成一個執行計劃。但優化器再怎么變邏輯上的執行順序是相對固定的。如果你不知道這個順序寫出來的SQL可能慢得離譜甚至結果都是錯的。1.2 邏輯執行順序FROM先于SELECTWHERE先于GROUP BY我直接給結論SQL邏輯執行順序大致如下FROM確定數據源如果是多表先做笛卡爾積或聯表WHERE對FROM的結果做行級篩選GROUP BY把篩選后的數據分組HAVING對分組后的結果做過濾SELECT投影出你要的列此時可以計算別名等表達式ORDER BY對最終結果排序LIMIT / OFFSET截取部分行很多人不理解為什么WHERE里面不能用SELECT的別名因為SELECT的別名是在第5步才生成的WHERE在第2步執行那時候別名根本不存在。同理很多人問為什么WHERE和HAVING看著都能過濾差別在哪差別就在于執行時機WHERE先于GROUP BY所以它過濾的是“分組前的原始行”HAVING后于GROUP BY過濾的是“分組后的聚合結果”。你可以在WHERE里寫amount 100但如果想過濾“總金額大于100的客戶”就必須用HAVING因為SUM(amount)這種聚合值是在GROUP BY之后才出來的。理解了這個順序還能解釋一個常見現象為什么某些SQL在數據量小的時候跑得飛快數據量一大就卡死。比如你在WHERE里用了UPPER(name) ABC或者YEAR(create_time) 2024這類對列做函數運算的寫法會破壞索引優化器只能全表掃描。基礎語句好寫但寫好需要你對“索引對查詢的影響”有意識。1.3 掌握執行順序后很多“怪問題”會自然消失我舉個例子。有次幫同事排查一個報表SQL他寫的是SELECT department, COUNT(*) AS cnt FROM employee WHERE cnt 10 GROUP BY department;這條語句執行直接報錯他一度懷疑是數據庫版本問題。實際上就是執行順序問題cnt這個別名在SELECT階段才生成WHERE階段根本訪問不到。正確寫法是SELECT department, COUNT(*) AS cnt FROM employee GROUP BY department HAVING COUNT(*) 10;另一個常見坑是ORDER BY和LIMIT的順序。有人寫LIMIT 10 ORDER BY score DESC在大多數數據庫里這種語法不會報錯但含義完全不對優化器會先取前10行再排序結果亂七八糟。正確順序一定是先ORDER BY再LIMIT。類似這類看似小到不能再小的問題恰恰是基礎不牢的表現。我的建議是每寫一條SQL都在心里過一遍它的邏輯執行順序這對后續調優、排查都會有質的幫助。2. 查詢三件套條件篩選、排序與去重的細節2.1 WHERE條件運算符、優先級與LIKE細節條件篩選是SQL里最常用的部分但細節決定成敗。先看一個完整示例SELECT emp_id, emp_name, salary, dept_id, hire_date FROM employee WHERE dept_id 10 AND salary 5000 AND hire_date 2023-01-01 ORDER BY salary DESC;這里需要注意幾個運算符的優先級問題。AND的優先級高于OR所以WHERE a 1 OR a 2 AND b 3實際是a 1 OR (a 2 AND b 3)不是你以為的(a 1 OR a 2) AND b 3。這種隱晦的優先級很容易讓結果出乎意料。我的習慣是只要條件組合超過兩層一律加括號。括號不會讓SQL變慢但能讓閱讀的人和你自己少掉很多頭發。IN和NOT IN也很常用但有個細節當IN列表里含NULL時行為會變得詭異。比如WHERE dept_id NOT IN (10, 20, NULL)這條語句不會返回任何行。原因是NOT IN遇到NULL時比較結果不是TRUE而是UNKNOWNUNKNOWN會被當作不滿足條件過濾掉。很多人第一次遇到這個現象都以為是bug其實是三值邏輯的必然結果。如果你確實要排除某些值且列表可能含NULL要么加AND dept_id IS NOT NULL要么用NOT EXISTS。再說LIKE。基礎用法是%表示任意多個字符_表示一個字符。比如WHERE name LIKE 張%是姓張的人WHERE name LIKE _張%是第二個字為“張”的人。但LIKE的坑在于通配符放在開頭%abc會導致索引失效全表掃描數據量大時性能堪憂。如果你要匹配的文本本身包含%或_需要轉義LIKE 50\% ESCAPE \。我在實際開發里見過太多因為LIKE性能問題導致的慢SQL“慢sql優化”搜索詞常年上榜不是沒原因的。判斷一個篩選條件能不能走索引可以簡單看一點條件左側是不是對列做了運算或類型轉換。WHERE name LIKE abc%可以走索引WHERE name LIKE %abc%走不了這句話背下來能解決你30%的SQL慢問題。2.2 ORDER BY排序多列排序與NULL位置排序看起來簡單ORDER BY column DESC誰都會寫但多列排序和NULL值的排序位置往往被忽略。看這個例子SELECT emp_id, emp_name, salary, dept_id FROM employee ORDER BY dept_id ASC, salary DESC;這種寫法表示先按dept_id升序部門相同再按salary降序。你要理解的是排序優先級是從左到右不是讓你先單獨按dept_id排一次再按salary排一次。如果想按“每個部門里工資最高的人”這個語義篩選那需要窗口函數或GROUP BY不是簡單ORDER BY能解決的后面第5章會展開。關于NULL的排序位置不同數據庫行為不一樣MySQLNULL默認排在最小值之前也就是ASC時NULL排最前。SQL ServerNULL默認排最前。OracleNULL默認排最后ASC時排最后。PostgreSQLNULL默認排最后。這就是為什么如果你依賴默認行為寫報表換數據庫后結果對不上。建議明確指定自己的意圖。比如MySQL中想強行把NULL放最后可以寫SELECT emp_id, emp_name, salary FROM employee ORDER BY ISNULL(salary) ASC, salary DESC;這里ISNULL(salary)為1的NULL行排在最后。排序這東西寫清楚比寫花哨重要因為你永遠不知道看這條SQL的下一個人是什么基礎。2.3 DISTINCT去重的本質與三個容易踩的坑去重是搜索高頻詞“sql語句去重查詢”里的核心操作。先明確一點SELECT DISTINCT col1, col2去重的單位是“整行組合”不是單個列。很多人疑惑為什么SELECT DISTINCT department FROM employee明明有重復結果還是出現了重復。如果你寫的是SELECT DISTINCT department FROM employee那確實會去掉完全相同的行但如果后面多跟了一個id列DISTINCT會基于id, department的組合判斷這時department重復就正常了。我遇到過三個比較典型的坑第一DISTINCT和COUNT的搭配。SELECT COUNT(DISTINCT department) FROM employee是統計不重復的部門數這個沒問題。但SELECT COUNT(department) FROM employee統計的是非NULL的行數不是不重復數。如果你是先SELECT DISTINCT department出來再數有幾個那等價于前者。別再犯“先查出來再數行數”這種低級錯誤了。第二DISTINCT和ORDER BY的沖突。比如SELECT DISTINCT department FROM employee ORDER BY emp_name這經常直接報錯因為emp_name沒有出現在SELECT列表中排序無法進行。邏輯上也能理解去重后的結果集里根本沒有emp_name這個列。解決方法是把排序列也放入SELECT中或改用GROUP BY。第三DISTINCT和GROUP BY的選擇。很多人不知道SELECT DISTINCT a, b FROM t其實等價于SELECT a, b FROM t GROUP BY a, b。理解到這一層之后遇到“去重后還要和其他表聯表”的場景你就會更傾向于用GROUP BY的寫法因為它能順帶帶上聚合或子查詢結果擴展性更強。DISTINCT適合簡單去重要實現“保留每組最新一條記錄”這類需求得靠窗口函數單純DISTINCT搞不定。3. 空值與時間兩類極易翻車的數據處理場景3.1 NULL不是空字符串IS NULL與COALESCE的正確用法搜索熱詞里常年有“sql去除空值”說明NULL處理是很多人的痛點。我反復跟團隊強調一句話NULL不是值它表示“未知”。它既不是0也不是空字符串更不等于NULL本身。所以在SQL里寫WHERE name NULL永遠是查不到數據的必須用WHERE name IS NULL。處理NULL的常用手段有IS NULL/IS NOT NULL判斷是否為空。COALESCE(col1, col2, 0)返回第一個非NULL的值適合給NULL填默認值。IFNULL(col, 0)MySQL專用兩個參數等價于COALESCE(col, 0)。NULLIF(a, b)如果a等于b返回NULL否則返回a。這個在除法運算里防除零特別好用。舉個實際例子統計員工工資但獎金字段可能為NULL想算“總收入”SELECT emp_name, salary COALESCE(bonus, 0) AS total_income FROM employee;如果直接寫salary bonus只要bonus為NULL整個表達式結果就變成NULL因為“某個數加上一個未知數結果仍是未知”。我見過有人的報表里收入合計莫名少了幾行排查半天就是這里出的問題。聚合函數對NULL也有講究COUNT(*)統計行數包括NULLCOUNT(col)統計非NULL值的個數SUM、AVG會忽略NULL但有個坑——如果所有值都是NULLSUM返回NULL而不是0前端展示時可能顯示空白建議包一層COALESCE(SUM(col), 0)。3.2 時間函數格式化、日期差與區間判斷時間處理是SQL里另一大痛點。搜索詞“sql server 時間函數”印證了這一點。不同數據庫的時間函數有差異我以MySQL為例講核心套路再補充說明其他數據庫的差異點。MySQL常用時間函數函數作用示例NOW()當前日期時間2024-03-18 14:30:00CURDATE()當前日期2024-03-18DATE_ADD(date, INTERVAL 1 DAY)日期加法明天DATE_SUB(date, INTERVAL 1 MONTH)日期減法上個月DATEDIFF(d1, d2)兩個日期差天天數DATE_FORMAT(date, %Y-%m-%d)格式化2024-03-18YEAR(date) / MONTH(date)提取年/月2024 / 3比較實用的寫法場景按天分組統計時create_time是DATETIME類型你如果直接GROUP BY create_time會把同一個“天”里的不同時間點分成多組。正確做法是格式化到天再分組SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, COUNT(*) AS cnt FROM orders GROUP BY DATE_FORMAT(create_time, %Y-%m-%d);查詢“最近7天”的數據不能用WHERE create_time 2024-03-11這種寫死的日期正確姿勢是SELECT * FROM orders WHERE create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY);這里要特別提醒一個性能點不要在條件左側對時間列做函數運算。比如WHERE YEAR(create_time) 2024這會破壞索引全表掃描。更好的寫法是WHERE create_time 2024-01-01 AND create_time 2025-01-01既走索引語義也更清晰。這是個很容易被忽視、卻對慢SQL優化至關重要的細節。在SQL Server里函數名不同對應關系大概是GETDATE()對應NOW()DATEADD(DAY, 1, date)對應DATE_ADDDATEDIFF(DAY, d1, d2)對應DATEDIFF但參數順序相反。我建議寫跨庫兼容代碼時把時間函數封裝在視圖或數據層里避免后面換庫時到處改。3.3 類型轉換CAST的顯式用法與隱式轉換陷阱類型轉換是個“用到時才想起用完就忘”的知識點。顯式轉換用CAST(expr AS type)比如SELECT CAST(123 AS SIGNED INTEGER);如果你面對的是“字符串形式的數字參與比較或排序”建議顯式轉換。比如用戶表里存了手機號字段是VARCHAR你要按手機號排序時ORDER BY phone是按字典序排的結果是“139...”排在“150...”后面符合預期但如果你要按某個數字字符串字段排序字典序就會出問題比如“10”會排在“9”前面。這時候ORDER BY CAST(phone_number AS UNSIGNED)才能得到數值序。隱式轉換的坑更隱蔽。比如你有一個VARCHAR字段里面存的是001、002但你和數字1比較時數據庫會嘗試把字符串轉成數字結果001變成1然后匹配成功。這有時候是好事有時候會誤傷。更危險的是如果一邊是字符串一邊是日期不同數據庫的隱式轉換規則不同同樣的SQL在MySQL和Oracle里結果可能不一樣。我的習慣是跨類型比較時永遠寫顯式轉換別讓優化器替你做決定。有同事問我排查線上問題時最怕什么我排第一的就是隱式轉換導致的索引失效——你明明建了索引WHERE varchar_col 123這種寫法照樣全表掃描因為數據庫要先把每行的字符串轉成數字才能比較。4. 從單表到多表聯表查詢、子查詢與聚合統計4.1 JOIN的本質與INNER/LEFT/RIGHT的選擇邏輯聯表查詢是SQL進階的第一道坎。很多人靠背“LEFT JOIN就是左表全部保留”去寫SQL能對付簡單場景但一旦遇到一對多、多對多就亂了。先搞清楚JOIN的本質它是把兩張表按某個關聯條件做“行的配對”配對不上怎么辦取決于JOIN類型。JOIN類型匹配行左表不匹配行右表不匹配行INNER JOIN保留丟棄丟棄LEFT JOIN保留保留右表列填NULL丟棄RIGHT JOIN保留丟棄保留左表列填NULLFULL OUTER JOIN保留保留右表列填NULL保留左表列填NULL記憶方法INNER JOIN是“只要兩邊都對得上”LEFT JOIN是“左邊是老大無論如何都要留在結果里右邊有就對上是緣分沒有就補NULL”。寫JOIN時最容易踩的坑是關聯條件寫在WHERE里還是ON里。看下面兩種寫法-- 寫法A SELECT e.emp_name, d.dept_name FROM employee e LEFT JOIN department d ON e.dept_id d.dept_id AND d.status 1; -- 寫法B SELECT e.emp_name, d.dept_name FROM employee e LEFT JOIN department d ON e.dept_id d.dept_id WHERE d.status 1;這兩種寫法的結果可能完全不同。寫法A中d.status 1是JOIN配對條件的一部分配對不上時仍保留左表行d.dept_name為NULL。寫法B中WHERE d.status 1是在JOIN完成之后做的過濾干凈利落地把d.status為NULL或非1的行全刪掉了相當于把LEFT JOIN“降級”成了INNER JOIN。這個坑非常隱蔽我建議寫LEFT JOIN時凡是跟右表相關的過濾條件默認先放ON里想清楚語義再決定要不要挪到WHERE。4.2 子查詢WHERE內子查詢與FROM派生表的差異子查詢分兩種常見形態一是放在WHERE里作為篩選條件二是放在FROM里作為一張臨時表派生表。兩者思路不同。WHERE內子查詢典型例子是“找工資高于部門平均工資的員工”SELECT emp_id, emp_name, salary, dept_id FROM employee e WHERE salary ( SELECT AVG(salary) FROM employee WHERE dept_id e.dept_id );這種寫法叫關聯子查詢外層每處理一行內層子查詢就執行一次。數據量大了性能很差。優化思路是改成JOINSELECT e.emp_id, e.emp_name, e.salary, e.dept_id FROM employee e JOIN ( SELECT dept_id, AVG(salary) AS avg_salary FROM employee GROUP BY dept_id ) d ON e.dept_id d.dept_id AND e.salary d.avg_salary;這里用GROUP BY先算每個部門的平均工資再和外層JOIN。子查詢只執行一次性能大幅提升。這個思路值得重點記能先聚合縮小數據集的就先把數據集縮小再聯表。FROM派生表的另一種常見場景是“取每個部門工資最高的員工”。很多人會想用GROUP BY MAX但這樣只能得到部門和最高工資拿不到員工ID和姓名。正確做法是在FROM里放一個“取最近一條”的子查詢配合窗口函數。我記得這是一個非常經典的面試題后面第5章會給出完整寫法。關于EXISTS和IN的選擇子查詢結果集很大時EXISTS通常效率更高因為它只需要判斷是否存在不必構造完整結果集。反過來子查詢結果集很小時IN可讀性更好。但要注意NOT IN和NOT EXISTS不是等價的NOT IN遇到NULL值會出問題前面講過NOT EXISTS則不會。寫代碼時我基本傾向于用NOT EXISTS。4.3 GROUP BY聚合與HAVING過濾時機決定結果GROUP BY是SQL里最貼近“統計報表”思維的語句。它的執行時機在WHERE之后作用是把行按某幾列分成若干組然后對每組做聚合計算。理解這個時機你就能明白WHERE不能訪問聚合函數但是GROUP BY之前可以先縮小要參與分組的數據范圍。常見的聚合函數SELECT dept_id, COUNT(*) AS emp_cnt, COUNT(salary) AS has_salary_cnt, AVG(salary) AS avg_salary, MAX(salary) AS max_salary, MIN(salary) AS min_salary, SUM(salary) AS total_salary FROM employee WHERE status active GROUP BY dept_id;上面這條SQL的語義是先篩出在職員工再按部門分組統計各部門的人數、有薪水的人數、平均薪水等。注意COUNT(*)和COUNT(salary)的區別前者統計組內行數后者統計組內salary非NULL的行數。HAVING的典型場景是“篩選分組后的結果”。比如找出平均工資超過8000的部門SELECT dept_id, AVG(salary) AS avg_salary FROM employee GROUP BY dept_id HAVING AVG(salary) 8000;注意HAVING后面也可以使用別名部分數據庫支持MySQL支持但推薦直接寫聚合表達式兼容性更好。GROUP BY的一個重要易錯點SELECT中出現的非聚合列必須出現在GROUP BY中。這是SQL標準的要求否則結果不確定。雖然MySQL默認配置下對這種寫法不會報錯但從邏輯和規范角度都不對。比如SELECT dept_id, emp_name, MAX(salary) FROM employee GROUP BY dept_id;這條語句在MySQL里能跑但emp_name到底取哪一行是不確定的——在同一部門里取的是工資最高那個人的名字還是隨機的某個人完全取決于執行計劃和數據分布。很多人拿這個去依賴MySQL的“隱式邏輯”結果上線后某天數據順序一變結果就錯了。要“每組里某個字段最大對應的整行記錄”老老實實用窗口函數不要依賴這種非標準行為。5. 進階但常用的窗口函數與增刪改操作5.1 窗口函數入門ROW_NUMBER、RANK與SUM OVER窗口函數是近幾年面試高頻詞也是“sql窗口函數”搜索量居高不下的原因。它和GROUP BY的核心區別是GROUP BY會把多行合并成一行窗口函數不會——它保留每一行同時給你一個窗口范圍內的計算結果。你可以把窗口函數理解為“在每一行旁邊開了一扇窗戶透過窗戶能看到自己所在分組的信息”。最常見的幾個窗口函數SELECT emp_id, emp_name, dept_id, salary, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn, RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk, DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS drk FROM employee;ROW_NUMBER是連續編號1、2、3RANK在遇到并列時會跳過編號比如1、1、3DENSE_RANK不跳過1、1、2。三者用哪個取決于業務語義只要唯一序號用ROW_NUMBER比賽排名那種“并列后留空位”用RANK并列后連續排名用DENSE_RANK。經典的“取每個部門工資最高員工”用窗口函數實現SELECT emp_id, emp_name, dept_id, salary FROM ( SELECT emp_id, emp_name, dept_id, salary, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM employee ) t WHERE t.rn 1;這個寫法的邏輯是先在窗口內給每個部門按工資從高到低編號再在外部篩出編號為1的行。注意子查詢別名t必須有不然會報語法錯誤。窗口函數還可以做累計統計。比如統計每個部門截至當月的累計銷售額SELECT dept_id, month, sales_amount, SUM(sales_amount) OVER (PARTITION BY dept_id ORDER BY month) AS cumulative_sales FROM sales_table;這個寫法的執行機制是以部門為窗口按月份排序從窗口起點到當前行做累加。理解窗口函數的關鍵是抓住“PARTITION BY窗口怎么分 ORDER BY窗口內怎么走 聚合函數對走到哪算哪的窗口做什么計算”。5.2 INSERT、UPDATE、DELETE寫操作的風險控制寫操作雖然基礎但風險比查詢大得多。一次手滑的UPDATE沒有WHERE可能就把整張表數據改沒了。我見過不止一個事故開發同學在測試環境寫UPDATE employee SET salary salary * 1.1忘加WHERE然后連到了生產庫結果全公司工資都漲了10%。INSERT基本語法INSERT INTO employee (emp_id, emp_name, dept_id, salary) VALUES (1001, 張三, 10, 8000);批量插入時可以多組VALUESINSERT INTO employee (emp_id, emp_name, dept_id, salary) VALUES (1002, 李四, 10, 9000), (1003, 王五, 20, 7500);UPDATE的正確姿勢是先用SELECT驗證范圍再改寫成UPDATE。這是我給所有新人的鐵律-- 先查 SELECT * FROM employee WHERE dept_id 10; -- 再改 UPDATE employee SET salary salary * 1.05 WHERE dept_id 10;DELETE同理DELETE FROM employee WHERE emp_id 1001之前先SELECT確認這條記錄沒刪錯。如果表數據量很大刪除時最好加上LIMIT分批刪避免一次鎖太多行、產生巨大的事務日志。還有一個容易被忽略的點UPDATE和DELETE的WHERE條件里的ONLY_FULL_GROUP_BY之類的SQL模式會影響行為嗎不會但會影響安全。在MySQL里如果忘了WHERE它會“禮貌地”更新所有行——沒有任何提示。想在MySQL里防止這種事故可以在啟動參數里加--safe-updates它會強制要求UPDATE和DELETE帶WHERE或LIMIT否則拒絕執行。SQL Server和Oracle也有類似的事務保護機制關鍵是你要養成習慣寫操作之前想清楚影響行數有條件就放到事務里執行確認無誤后再提交。關于SQL注入多說一句。寫動態SQL時千萬不要直接拼接用戶輸入。比如WHERE name userInput 這種寫法一旦用戶輸入 OR 11你的查詢就會變成WHERE name OR 11返回全部數據這就是經典注入。正確做法是用參數化查詢或者預處理語句讓數據庫把用戶輸入當數據而不是SQL代碼。這個基礎意識能幫你省掉很多安全賬單。6. 建表、索引與慢SQL優化基礎語句之上的性能意識6.1 CREATE TABLE與字段類型選擇很多人學SQL只關注查詢語句忽略建表但表結構設計不合理后面怎么調SQL都救不回來。建表基礎語句很簡單CREATE TABLE employee ( emp_id INT PRIMARY KEY, emp_name VARCHAR(50) NOT NULL, dept_id INT NOT NULL, salary DECIMAL(10, 2), hire_date DATE, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_dept_id (dept_id), INDEX idx_create_time (create_time) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4;字段類型選擇有幾個基本講究能用INT就不用BIGINT能用BIGINT就不用VARCHAR存儲空間直接決定查詢性能。手機號、身份證號這類看起來像數字的建議用VARCHAR因為不會參與數值計算而且可能含有前置0或X。金額使用DECIMAL(10, 2)不要用FLOAT/DOUBLE浮點數是近似值做財務計算會有精度問題。時間字段用DATE或DATETIME不要用VARCHAR存字符串否則沒法高效比較大小。索引的選擇也是建表時就要想的。索引的核心價值是“減少掃描行數”但索引不是越多越好因為每次INSERT/UPDATE都要維護所有索引寫性能和存儲空間都會受影響。基本原則高頻查詢的WHERE條件和JOIN關聯字段建索引區分度低的字段比如性別建索引意義不大聯合索引要考慮最左前綴原則比如(dept_id, salary)聯合索引能支持dept_id ? ORDER BY salary但單獨WHERE salary ?用不上。6.2 索引為什么加索引后查詢還是慢“為什么加了索引查詢還是慢”是慢SQL優化里最經典的問題。我列幾個高頻原因索引失效條件左側做了函數運算、隱式類型轉換、LIKE前置通配符、OR連接的條件含非索引列這些都可能導致索引失效。回表開銷大InnoDB中非主鍵索引存的是主鍵值回表查詢需要二次定位。如果索引的區分度不高查詢優化器覺得“回表太多還不如全表掃描”就會放棄索引。數據量的量級變了索引能不能用優化器會基于基數估算。百萬行和十億行優化器的選擇可能完全不同。小數據量測試時SQL快不代表大數據量也一樣。聯合索引順序不對(a, b, c)聯合索引你只查b ?用不上索引查a ? AND c ?能用到a但c那部分用不上。排序列沒進索引WHERE dept_id 10 ORDER BY salary DESC如果聯合索引是(dept_id, salary)那排序可以直接用索引順序如果只有(dept_id)索引還需要文件排序。排查這類問題不要靠猜直接看執行計劃。6.3 EXPLAIN的核心指標type、rows、Extra怎么讀搜索熱詞“慢sql優化 explain主要看哪些信息”指向的正是這個環節。EXPLAIN是SQL優化最重要的工具它展示一條SQL的執行計劃。基本用法是EXPLAIN SELECT ...MySQL還會額外顯示一些計算成本信息。EXPLAIN SELECT e.emp_id, e.emp_name, d.dept_name FROM employee e LEFT JOIN department d ON e.dept_id d.dept_id WHERE e.salary 5000;輸出結果里我主要看四列type訪問類型從好到差大致是consteq_refrefrangeindexALL。ALL就是全表掃描通常需要警惕range代表索引范圍掃描是良好狀態。key實際用到的索引。如果為NULL說明沒走索引。rows預估掃描行數。行數越大查詢越慢。不同SQL之間的rows差距能直觀反映優化效果。Extra附加信息重點看有沒有Using filesort文件排序意味著排序沒走索引和Using temporary使用了臨時表常見于GROUP BY或DISTINCT配合不當性能很差。舉個例子有個慢SQLSELECT * FROM orders WHERE YEAR(create_time) 2024 ORDER BY amount DESC;EXPLAIN很可能顯示typeALL和Using filesort因為YEAR(create_time)破壞了索引排序也沒走索引。優化后SELECT * FROM orders WHERE create_time 2024-01-01 AND create_time 2025-01-01 ORDER BY amount DESC;再看執行計劃type會變成range如果amount也在聯合索引里Using filesort也可能消失。這就是“從執行計劃出發做優化”和“盲猜優化”的區別。最后想分享一個我自己的排查經驗拿到一條慢SQL我的固定動作是先看EXPLAIN再看rows和Extra如果出現全表掃描就去查這列的索引情況如果索引存在但沒用上基本就是函數運算或類型轉換的問題如果索引用上了但rows還是很大就要考慮加條件縮小范圍或重新設計聯合索引。這套流程說起來不復雜但能解決大部分線上慢SQL問題。SQL常用語句基礎大全的意義就在這里不僅是語法更是幫你建立從“寫得出”到“寫得好”的思維鏈路。