[{"data":1,"prerenderedAt":1024},["ShallowReactive",2],{"doc-sql-sql-join-and-index":3,"same-cat-sql-sql-join-and-index":1023},{"id":4,"title":5,"author":6,"body":7,"category":26,"date":1004,"description":1005,"draft":1006,"extension":1007,"meta":1008,"navigation":86,"path":1009,"seo":1010,"slug":1011,"stem":1012,"summary":1013,"tags":1017,"__hash__":1022},"docs\u002Fdocs\u002Fsql\u002Fsql-join-and-index.md","SQL JOIN 图解 + MySQL 索引优化:EXPLAIN 看懂执行计划","元析技术团队",{"type":8,"value":9,"toc":974},"minimark",[10,14,18,21,147,150,154,157,162,182,185,189,218,226,240,245,312,315,319,330,334,341,369,371,375,379,382,396,403,407,413,517,543,549,551,555,576,580,654,660,664,687,694,698,701,705,776,779,787,790,796,799,801,805,812,874,879,896,898,902,910,913,917,927,939,945,953,960,962,965,970],[11,12,13],"h2",{"id":13},"校验环境",[15,16,17],"p",{},"MySQL 8.0.39,InnoDB 引擎,字符集 utf8mb4。所有 SQL 在 10 万行的订单表 + 1 万行的用户表上实跑,EXPLAIN 输出真实。",[15,19,20],{},"准备两张表(后面所有 JOIN 和索引示例用):",[22,23,28],"pre",{"className":24,"code":25,"language":26,"meta":27,"style":27},"language-sql shiki shiki-themes github-light github-dark","CREATE TABLE users (\n  id          BIGINT PRIMARY KEY AUTO_INCREMENT,\n  phone       VARCHAR(20)  NOT NULL UNIQUE,\n  nickname    VARCHAR(64)  NOT NULL,\n  level       TINYINT      NOT NULL DEFAULT 1,\n  created_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  INDEX idx_level_created (level, created_at)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\nCREATE TABLE orders (\n  id            BIGINT PRIMARY KEY AUTO_INCREMENT,\n  user_id       BIGINT       NOT NULL,\n  amount        INT          NOT NULL,           -- 分,避免浮点数\n  status        TINYINT      NOT NULL,          -- 1=待支付 2=已支付 3=取消 4=退款\n  paid_at       DATETIME     NULL,\n  created_at    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,\n  INDEX idx_user_created (user_id, created_at),\n  INDEX idx_status_paid    (status, paid_at)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n","sql","",[29,30,31,39,45,51,57,63,69,75,81,88,94,100,106,112,118,124,130,136,142],"code",{"__ignoreMap":27},[32,33,36],"span",{"class":34,"line":35},"line",1,[32,37,38],{},"CREATE TABLE users (\n",[32,40,42],{"class":34,"line":41},2,[32,43,44],{},"  id          BIGINT PRIMARY KEY AUTO_INCREMENT,\n",[32,46,48],{"class":34,"line":47},3,[32,49,50],{},"  phone       VARCHAR(20)  NOT NULL UNIQUE,\n",[32,52,54],{"class":34,"line":53},4,[32,55,56],{},"  nickname    VARCHAR(64)  NOT NULL,\n",[32,58,60],{"class":34,"line":59},5,[32,61,62],{},"  level       TINYINT      NOT NULL DEFAULT 1,\n",[32,64,66],{"class":34,"line":65},6,[32,67,68],{},"  created_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,\n",[32,70,72],{"class":34,"line":71},7,[32,73,74],{},"  INDEX idx_level_created (level, created_at)\n",[32,76,78],{"class":34,"line":77},8,[32,79,80],{},") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n",[32,82,84],{"class":34,"line":83},9,[32,85,87],{"emptyLinePlaceholder":86},true,"\n",[32,89,91],{"class":34,"line":90},10,[32,92,93],{},"CREATE TABLE orders (\n",[32,95,97],{"class":34,"line":96},11,[32,98,99],{},"  id            BIGINT PRIMARY KEY AUTO_INCREMENT,\n",[32,101,103],{"class":34,"line":102},12,[32,104,105],{},"  user_id       BIGINT       NOT NULL,\n",[32,107,109],{"class":34,"line":108},13,[32,110,111],{},"  amount        INT          NOT NULL,           -- 分,避免浮点数\n",[32,113,115],{"class":34,"line":114},14,[32,116,117],{},"  status        TINYINT      NOT NULL,          -- 1=待支付 2=已支付 3=取消 4=退款\n",[32,119,121],{"class":34,"line":120},15,[32,122,123],{},"  paid_at       DATETIME     NULL,\n",[32,125,127],{"class":34,"line":126},16,[32,128,129],{},"  created_at    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,\n",[32,131,133],{"class":34,"line":132},17,[32,134,135],{},"  INDEX idx_user_created (user_id, created_at),\n",[32,137,139],{"class":34,"line":138},18,[32,140,141],{},"  INDEX idx_status_paid    (status, paid_at)\n",[32,143,145],{"class":34,"line":144},19,[32,146,80],{},[148,149],"hr",{},[11,151,153],{"id":152},"第一部分4-种-join-用-venn-图文字版讲清","第一部分:4 种 JOIN 用 Venn 图文字版讲清",[15,155,156],{},"下面 4 条 SQL 的结果差异,用「用户表(左) 和 订单表(右) ON users.id = orders.user_id」说明。",[158,159,161],"h3",{"id":160},"_1-inner-join-交集两边都有才保留","1. INNER JOIN = 交集(两边都有才保留)",[22,163,165],{"className":24,"code":164,"language":26,"meta":27,"style":27},"SELECT u.nickname, o.amount, o.status\nFROM users u\nINNER JOIN orders o ON o.user_id = u.id;\n",[29,166,167,172,177],{"__ignoreMap":27},[32,168,169],{"class":34,"line":35},[32,170,171],{},"SELECT u.nickname, o.amount, o.status\n",[32,173,174],{"class":34,"line":41},[32,175,176],{},"FROM users u\n",[32,178,179],{"class":34,"line":47},[32,180,181],{},"INNER JOIN orders o ON o.user_id = u.id;\n",[15,183,184],{},"结果:只保留「有下单记录的用户」×「该用户的订单」。一个用户如果没有任何订单,不会出现在结果里;\n一条订单如果 user_id 在 users 表里找不到(脏数据),也不会出现。",[158,186,188],{"id":187},"_2-left-join-左表全保留右表能匹配就填匹配不到填-null","2. LEFT JOIN = 左表全保留,右表能匹配就填,匹配不到填 NULL",[22,190,192],{"className":24,"code":191,"language":26,"meta":27,"style":27},"SELECT u.nickname, SUM(o.amount) AS pay_total\nFROM users u\nLEFT JOIN orders o\n  ON o.user_id = u.id AND o.status = 2   -- ← 注意:status 条件写 ON 里\nGROUP BY u.id;\n",[29,193,194,199,203,208,213],{"__ignoreMap":27},[32,195,196],{"class":34,"line":35},[32,197,198],{},"SELECT u.nickname, SUM(o.amount) AS pay_total\n",[32,200,201],{"class":34,"line":41},[32,202,176],{},[32,204,205],{"class":34,"line":47},[32,206,207],{},"LEFT JOIN orders o\n",[32,209,210],{"class":34,"line":53},[32,211,212],{},"  ON o.user_id = u.id AND o.status = 2   -- ← 注意:status 条件写 ON 里\n",[32,214,215],{"class":34,"line":59},[32,216,217],{},"GROUP BY u.id;\n",[15,219,220,221,225],{},"结果:",[222,223,224],"strong",{},"所有用户","都出现,无论他有没有订单。",[227,228,229,233],"ul",{},[230,231,232],"li",{},"有已支付订单的用户:pay_total = 对应金额之和",[230,234,235,236,239],{},"没有已支付订单的用户:pay_total = NULL(你想让他显示 0?套 ",[29,237,238],{},"COALESCE(SUM(o.amount), 0)",")",[15,241,242],{},[222,243,244],{},"🔥 LEFT JOIN 最常见坑:条件写 ON 还是 WHERE 完全两码事",[22,246,248],{"className":24,"code":247,"language":26,"meta":27,"style":27},"-- A 写法:status = 2 写 ON\nSELECT u.nickname, o.amount\nFROM users u LEFT JOIN orders o\n  ON o.user_id = u.id AND o.status = 2;\n-- 结果:所有用户都保留,没支付2订单的用户 amount=NULL\n\n-- B 写法:status = 2 写 WHERE\nSELECT u.nickname, o.amount\nFROM users u LEFT JOIN orders o\n  ON o.user_id = u.id\nWHERE o.status = 2;\n-- 结果:只有「支付成功的订单」存在的那几行出现,和 INNER JOIN 结果一样!\n-- 因为 WHERE 在 JOIN 之后执行,NULL = 2 的判断是 NULL(非真),那些行被排除了。\n",[29,249,250,255,260,265,270,275,279,284,288,292,297,302,307],{"__ignoreMap":27},[32,251,252],{"class":34,"line":35},[32,253,254],{},"-- A 写法:status = 2 写 ON\n",[32,256,257],{"class":34,"line":41},[32,258,259],{},"SELECT u.nickname, o.amount\n",[32,261,262],{"class":34,"line":47},[32,263,264],{},"FROM users u LEFT JOIN orders o\n",[32,266,267],{"class":34,"line":53},[32,268,269],{},"  ON o.user_id = u.id AND o.status = 2;\n",[32,271,272],{"class":34,"line":59},[32,273,274],{},"-- 结果:所有用户都保留,没支付2订单的用户 amount=NULL\n",[32,276,277],{"class":34,"line":65},[32,278,87],{"emptyLinePlaceholder":86},[32,280,281],{"class":34,"line":71},[32,282,283],{},"-- B 写法:status = 2 写 WHERE\n",[32,285,286],{"class":34,"line":77},[32,287,259],{},[32,289,290],{"class":34,"line":83},[32,291,264],{},[32,293,294],{"class":34,"line":90},[32,295,296],{},"  ON o.user_id = u.id\n",[32,298,299],{"class":34,"line":96},[32,300,301],{},"WHERE o.status = 2;\n",[32,303,304],{"class":34,"line":102},[32,305,306],{},"-- 结果:只有「支付成功的订单」存在的那几行出现,和 INNER JOIN 结果一样!\n",[32,308,309],{"class":34,"line":108},[32,310,311],{},"-- 因为 WHERE 在 JOIN 之后执行,NULL = 2 的判断是 NULL(非真),那些行被排除了。\n",[15,313,314],{},"原则:**想过滤右表的条件、且想让左表没匹配的行保留为 NULL,就必须写在 ON 里。**写 WHERE 会把 LEFT JOIN 变成 INNER JOIN 效果。",[158,316,318],{"id":317},"_3-right-join-右表全保留左表匹配不到填-null","3. RIGHT JOIN = 右表全保留,左表匹配不到填 NULL",[15,320,321,322,325,326,329],{},"和 LEFT JOIN 对称,",[29,323,324],{},"A RIGHT JOIN B"," 等价 ",[29,327,328],{},"B LEFT JOIN A","。实际项目中很少有人写 RIGHT JOIN(读起来绕),全改成 LEFT JOIN 就行。",[158,331,333],{"id":332},"_4-full-outer-join-并集两边都保留不匹配那侧-null","4. FULL OUTER JOIN = 并集(两边都保留,不匹配那侧 NULL)",[15,335,336,337,340],{},"MySQL 没有原生 FULL JOIN 语法,用 LEFT + RIGHT 的结果 ",[29,338,339],{},"UNION"," 模拟(UNION 会去重):",[22,342,344],{"className":24,"code":343,"language":26,"meta":27,"style":27},"SELECT u.nickname, o.amount\nFROM users u LEFT JOIN orders o ON o.user_id = u.id\nUNION\nSELECT u.nickname, o.amount\nFROM users u RIGHT JOIN orders o ON o.user_id = u.id;\n",[29,345,346,350,355,360,364],{"__ignoreMap":27},[32,347,348],{"class":34,"line":35},[32,349,259],{},[32,351,352],{"class":34,"line":41},[32,353,354],{},"FROM users u LEFT JOIN orders o ON o.user_id = u.id\n",[32,356,357],{"class":34,"line":47},[32,358,359],{},"UNION\n",[32,361,362],{"class":34,"line":53},[32,363,259],{},[32,365,366],{"class":34,"line":59},[32,367,368],{},"FROM users u RIGHT JOIN orders o ON o.user_id = u.id;\n",[148,370],{},[11,372,374],{"id":373},"第二部分innodb-btree-索引-最左前缀原则","第二部分:InnoDB B+Tree 索引 + 最左前缀原则",[158,376,378],{"id":377},"主键索引primary-key和二级索引secondary-index的叶子节点存什么","主键索引(Primary Key)和二级索引(Secondary Index)的叶子节点存什么",[15,380,381],{},"InnoDB 是「索引组织表」:",[227,383,384,390],{},[230,385,386,389],{},[222,387,388],{},"主键索引(聚簇索引)"," 叶子页 = 完整一整行数据。按主键物理排序。",[230,391,392,395],{},[222,393,394],{},"二级索引(普通\u002F唯一\u002F联合索引)"," 叶子页 =「索引列的值 + 主键值」。想拿到其他列必须回表(再查一次主键索引)。",[15,397,398,399,402],{},"这就是为什么「覆盖索引」能快:如果 ",[29,400,401],{},"SELECT"," 的列全在二级索引里,就不用回表了。",[158,404,406],{"id":405},"联合索引最左前缀原则一个索引多种用法","联合索引最左前缀原则:一个索引多种用法",[15,408,409,412],{},[29,410,411],{},"INDEX(a, b, c)"," 建了这 1 个联合索引,实际上下面 3 种 WHERE 都能用得上:",[414,415,416,434],"table",{},[417,418,419],"thead",{},[420,421,422,427,431],"tr",{},[423,424,426],"th",{"align":425},"left","WHERE 条件",[423,428,430],{"align":429},"center","能不能用 (a,b,c) 索引?",[423,432,433],{"align":425},"实际用到了哪些列?",[435,436,437,451,463,475,488,501],"tbody",{},[420,438,439,445,448],{},[440,441,442],"td",{"align":425},[29,443,444],{},"a = 1",[440,446,447],{"align":429},"✅",[440,449,450],{"align":425},"a 列",[420,452,453,458,460],{},[440,454,455],{"align":425},[29,456,457],{},"a = 1 AND b = 2",[440,459,447],{"align":429},[440,461,462],{"align":425},"a, b 列",[420,464,465,470,472],{},[440,466,467],{"align":425},[29,468,469],{},"a = 1 AND b = 2 AND c = 3",[440,471,447],{"align":429},[440,473,474],{"align":425},"a, b, c 列",[420,476,477,482,485],{},[440,478,479],{"align":425},[29,480,481],{},"b = 2 AND c = 3",[440,483,484],{"align":429},"❌",[440,486,487],{"align":425},"一根毛都用不到(跳过了最左的 a,索引树找不到入口)",[420,489,490,495,498],{},[440,491,492],{"align":425},[29,493,494],{},"a = 1 AND c = 3",[440,496,497],{"align":429},"⚠️",[440,499,500],{"align":425},"只用 a 列,扫到 a=1 的一堆记录后逐个过滤 c=3(不能跳到 c)",[420,502,503,508,510],{},[440,504,505],{"align":425},[29,506,507],{},"a = 1 AND b > 2 AND c = 3",[440,509,497],{"align":429},[440,511,512,513,516],{"align":425},"用 a 列 + b 的范围,",[222,514,515],{},"c 列用不到","(b 是范围查询,之后的列跳过)",[15,518,519,522,523,526,527,526,530,526,533,526,536,526,539,542],{},[222,520,521],{},"让中间列索引用不上的坏兄弟",":",[29,524,525],{},">"," ",[29,528,529],{},"\u003C",[29,531,532],{},">=",[29,534,535],{},"\u003C=",[29,537,538],{},"BETWEEN",[29,540,541],{},"LIKE 'xxx%'","(前缀匹配) —— 只要中间某列用了这些,后面的列就退出索引。",[15,544,545,548],{},[222,546,547],{},"列顺序是建联合索引时唯一要关心的事",",等值过滤列放前面,范围放后面,高选择性(区分度大)的放前面。",[148,550],{},[11,552,554],{"id":553},"第三部分explain-只看-4-列就够","第三部分:EXPLAIN 只看 4 列就够",[15,556,557,558,561,562,565,566,565,569,565,572,575],{},"执行:",[29,559,560],{},"EXPLAIN SELECT ..."," 会出一张 10+ 列的表。90% 的慢查询定位只看 4 列:",[29,563,564],{},"type","、",[29,567,568],{},"key",[29,570,571],{},"rows",[29,573,574],{},"Extra","。",[158,577,579],{"id":578},"type联接类型从好到坏","type:联接类型(从好到坏)",[414,581,582,594],{},[417,583,584],{},[420,585,586,588,591],{},[423,587,564],{"align":425},[423,589,590],{"align":425},"含义",[423,592,593],{"align":429},"合格线",[435,595,596,607,617,630,641],{},[420,597,598,601,604],{},[440,599,600],{"align":425},"const \u002F eq_ref",[440,602,603],{"align":425},"主键\u002F唯一键精准查一行",[440,605,606],{"align":429},"🌟",[420,608,609,612,615],{},[440,610,611],{"align":425},"ref",[440,613,614],{"align":425},"二级索引等值匹配",[440,616,447],{"align":429},[420,618,619,622,628],{},[440,620,621],{"align":425},"range",[440,623,624,625,239],{"align":425},"索引范围扫描(",[29,626,627],{},"IN \u002F BETWEEN \u002F > \u002F \u003C",[440,629,447],{"align":429},[420,631,632,635,638],{},[440,633,634],{"align":425},"index",[440,636,637],{"align":425},"扫描整棵二级索引树(比 ALL 好一点,但还是全扫)",[440,639,640],{"align":429},"⚠️ 看数据量",[420,642,643,648,651],{},[440,644,645],{"align":425},[222,646,647],{},"ALL",[440,649,650],{"align":425},"全表扫(主键树整棵扫)",[440,652,653],{"align":429},"❌ 必须优化",[15,655,656,657,659],{},"你的慢查询 type 是 ",[29,658,647],{},"?先看是不是没加索引,或者 WHERE 写法让索引失效了。",[158,661,663],{"id":662},"keymysql-最终选中的索引","key:MySQL 最终选中的索引",[15,665,666,669,670,672,673,675,676,678,679,682,683,686],{},[29,667,668],{},"possible_keys"," = 候选有哪些;",[29,671,568],{}," = 优化器最后选的那条。\n如果 ",[29,674,668],{}," 里有你建的索引,但 ",[29,677,568],{}," 却是 ",[29,680,681],{},"NULL"," → 要么数据量太少 MySQL 觉得全扫更快,要么统计信息不准(",[29,684,685],{},"ANALYZE TABLE t;"," 刷新一下)。",[15,688,689,690,693],{},"如果你觉得该走 A 索引但走了 B,可以 ",[29,691,692],{},"FORCE INDEX(idx_xxx)"," 强行走(只建议临时排查用,不要留到生产代码)。",[158,695,697],{"id":696},"rows预估要扫多少行","rows:预估要扫多少行",[15,699,700],{},"是个估算值(不是真实值),但量级很有参考。\nrows = 100000,实际只返回 10 行?说明索引没选对,扫了 10 万行才过滤出 10 条,典型「Using where」。",[158,702,704],{"id":703},"extra一些非常关键的附加信息","Extra:一些非常关键的附加信息",[414,706,707,719],{},[417,708,709],{},[420,710,711,714,716],{},[423,712,713],{"align":425},"Extra 内容",[423,715,590],{"align":425},[423,717,718],{"align":429},"是否危险",[435,720,721,732,745,755,766],{},[420,722,723,726,729],{},[440,724,725],{"align":425},"Using where",[440,727,728],{"align":425},"Server 层在收到存储引擎返回的行后再做 WHERE 过滤",[440,730,731],{"align":429},"⚠️ 配合 rows 大就是危险",[420,733,734,737,743],{},[440,735,736],{"align":425},"Using index",[440,738,739,742],{"align":425},[222,740,741],{},"覆盖索引","!不需要回表,这是好东西",[440,744,447],{"align":429},[420,746,747,750,753],{},[440,748,749],{"align":425},"Using index condition",[440,751,752],{"align":425},"ICP(索引下推),MySQL 5.6+ 自动用",[440,754,447],{"align":429},[420,756,757,760,763],{},[440,758,759],{"align":425},"Using filesort",[440,761,762],{"align":425},"ORDER BY 没用到索引,在内存或磁盘排一遍序",[440,764,765],{"align":429},"❌ 一定要看",[420,767,768,771,774],{},[440,769,770],{"align":425},"Using temporary",[440,772,773],{"align":425},"GROUP BY \u002F DISTINCT 用了临时表",[440,775,765],{"align":429},[15,777,778],{},"例子:",[22,780,785],{"className":781,"code":783,"language":784},[782],"language-text","Extra: Using where; Using index;\n","text",[29,786,783],{"__ignoreMap":27},[15,788,789],{},"→ Using where 说明 WHERE 有一部分不能用索引(Server 层又过滤了一遍),但同时 Using index 说明用了覆盖索引,总体还 OK。",[22,791,794],{"className":792,"code":793,"language":784},[782],"Extra: Using where; Using filesort;\n",[29,795,793],{"__ignoreMap":27},[15,797,798],{},"→ WHERE 没完全走索引,ORDER BY 也没走(额外排序了),典型需要优化的慢查询。",[148,800],{},[11,802,804],{"id":803},"第四部分覆盖索引-不要-select","第四部分:覆盖索引 + 不要 SELECT *",[15,806,807,808,811],{},"有联合索引 ",[29,809,810],{},"INDEX(user_id, created_at)","。下面这条 SQL 能走覆盖索引吗?",[22,813,815],{"className":24,"code":814,"language":26,"meta":27,"style":27},"-- 语句 1:不回表 ✅\nSELECT user_id, created_at\nFROM orders\nWHERE user_id = 123;\n-- 叶子节点直接就有 (user_id, created_at, id),SELECT 的两列都在里面,Extra = Using index\n\n-- 语句 2:需要回表 ⚠️\nSELECT *\nFROM orders\nWHERE user_id = 123;\n-- SELECT * 要 amount \u002F status \u002F paid_at,这些不在二级索引里,\n-- 每条都得拿叶子里的主键 id 再查一次聚簇索引 = 回表\n",[29,816,817,822,827,832,837,842,846,851,856,860,864,869],{"__ignoreMap":27},[32,818,819],{"class":34,"line":35},[32,820,821],{},"-- 语句 1:不回表 ✅\n",[32,823,824],{"class":34,"line":41},[32,825,826],{},"SELECT user_id, created_at\n",[32,828,829],{"class":34,"line":47},[32,830,831],{},"FROM orders\n",[32,833,834],{"class":34,"line":53},[32,835,836],{},"WHERE user_id = 123;\n",[32,838,839],{"class":34,"line":59},[32,840,841],{},"-- 叶子节点直接就有 (user_id, created_at, id),SELECT 的两列都在里面,Extra = Using index\n",[32,843,844],{"class":34,"line":65},[32,845,87],{"emptyLinePlaceholder":86},[32,847,848],{"class":34,"line":71},[32,849,850],{},"-- 语句 2:需要回表 ⚠️\n",[32,852,853],{"class":34,"line":77},[32,854,855],{},"SELECT *\n",[32,857,858],{"class":34,"line":83},[32,859,831],{},[32,861,862],{"class":34,"line":90},[32,863,836],{},[32,865,866],{"class":34,"line":96},[32,867,868],{},"-- SELECT * 要 amount \u002F status \u002F paid_at,这些不在二级索引里,\n",[32,870,871],{"class":34,"line":102},[32,872,873],{},"-- 每条都得拿叶子里的主键 id 再查一次聚簇索引 = 回表\n",[15,875,876,522],{},[222,877,878],{},"结论",[880,881,882,889],"ol",{},[230,883,884,885,888],{},"尽量写具体的列名,不写 ",[29,886,887],{},"SELECT *","。真要所有列也不是不能写,但你要知道它破坏了覆盖索引的可能。",[230,890,891,892,895],{},"如果某条高频查询就是要「user_id + amount」,可以直接建 ",[29,893,894],{},"INDEX(user_id, amount)"," 让它变成覆盖索引。",[148,897],{},[11,899,901],{"id":900},"常见问题-faq","常见问题 FAQ",[158,903,905,906,909],{"id":904},"q1-like-xxx前缀有通配符能走索引吗","Q1. ",[29,907,908],{},"LIKE '%xxx'","(前缀有通配符)能走索引吗?",[15,911,912],{},"不能。B+Tree 是按前缀有序的,你第一位就是 %,不知道从哪个节点下钻,只能全扫。\n业务如果需要「后缀匹配」「模糊搜任意位置」,请走专用搜索引擎(Elasticsearch \u002F MeiliSearch),不要在 MySQL 里硬扛。",[158,914,916],{"id":915},"q2-or-两边的列都有索引为什么-explain-显示没用到","Q2. OR 两边的列都有索引,为什么 EXPLAIN 显示没用到?",[15,918,919,920,923,924,926],{},"MySQL 5.7+ 有 Index Merge 优化,部分情况会同时用两个索引然后合并。但大多数场景 OR 会让优化器直接选全扫。\n改法:把 ",[29,921,922],{},"WHERE a = 1 OR b = 2"," 改写成两个 SELECT ",[29,925,339],{}," 各走各的索引。",[158,928,930,931,934,935,938],{"id":929},"q3-建了索引-status-paid_at为什么-where-paid_at-2026-08-01-不走它","Q3. 建了索引 ",[29,932,933],{},"(status, paid_at)",",为什么 ",[29,936,937],{},"WHERE paid_at > '2026-08-01'"," 不走它?",[15,940,941,942,575],{},"最左前缀原则:你没给最左列 status 限定,status 从 1 到 4 所有子树都要扫描,MySQL 觉得不如全扫快。\n如果你这条查询就是经常要按 paid_at 查且不带 status,单独建 ",[29,943,944],{},"INDEX(paid_at)",[158,946,948,949,952],{"id":947},"q4-字符串列存手机号用-where-phone-13800000000数字为什么不走-phone-的-unique-索引","Q4. 字符串列存手机号,用 ",[29,950,951],{},"WHERE phone = 13800000000","(数字)为什么不走 phone 的 UNIQUE 索引?",[15,954,955,956,959],{},"类型隐式转换:phone 是 VARCHAR,右边常量是数字。MySQL 把 phone 转成数字再比较,索引上的值就不是原来的顺序了,没法用。\n改:",[29,957,958],{},"WHERE phone = '13800000000'","(字符串常量)。",[148,961],{},[11,963,964],{"id":964},"更正记录",[227,966,967],{},[230,968,969],{},"2026-08-09:首版发布,MySQL 8.0.39 实跑。",[971,972,973],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":27,"searchDepth":41,"depth":41,"links":975},[976,977,983,987,993,994,1003],{"id":13,"depth":41,"text":13},{"id":152,"depth":41,"text":153,"children":978},[979,980,981,982],{"id":160,"depth":47,"text":161},{"id":187,"depth":47,"text":188},{"id":317,"depth":47,"text":318},{"id":332,"depth":47,"text":333},{"id":373,"depth":41,"text":374,"children":984},[985,986],{"id":377,"depth":47,"text":378},{"id":405,"depth":47,"text":406},{"id":553,"depth":41,"text":554,"children":988},[989,990,991,992],{"id":578,"depth":47,"text":579},{"id":662,"depth":47,"text":663},{"id":696,"depth":47,"text":697},{"id":703,"depth":47,"text":704},{"id":803,"depth":41,"text":804},{"id":900,"depth":41,"text":901,"children":995},[996,998,999,1001],{"id":904,"depth":47,"text":997},"Q1. LIKE '%xxx'(前缀有通配符)能走索引吗?",{"id":915,"depth":47,"text":916},{"id":929,"depth":47,"text":1000},"Q3. 建了索引 (status, paid_at),为什么 WHERE paid_at > '2026-08-01' 不走它?",{"id":947,"depth":47,"text":1002},"Q4. 字符串列存手机号,用 WHERE phone = 13800000000(数字)为什么不走 phone 的 UNIQUE 索引?",{"id":964,"depth":41,"text":964},"2026-08-09","后端写 SQL 一半的慢查都出在 JOIN 和索引上。本文用 Venn 图文字版讲 INNER\u002FLEFT\u002FRIGHT\u002FFULL JOIN 区别、联合索引最左前缀原则、EXPLAIN 的 type\u002Fkey\u002Frows\u002FExtra 4 列怎么看、覆盖索引、为什么不能 SELECT *。",false,"md",{},"\u002Fdocs\u002Fsql\u002Fsql-join-and-index",{"title":5,"description":1005},"sql-join-and-index","docs\u002Fsql\u002Fsql-join-and-index",[1014,1015,1016],"JOIN 4 种记一句话:INNER=交集、LEFT=左表全保留+右表匹配为 NULL、RIGHT=右表全保留、FULL=并集(MySQL 不原生支持,用 UNION 模拟)。LEFT JOIN 过滤条件写 ON 还是 WHERE 结果完全不同。","B+Tree 联合索引遵循「最左前缀」:索引 (a,b,c) 能当 (a) \u002F (a,b) \u002F (a,b,c) 用,不能当 (b,c) 用。中间出现范围查询(> \u003C BETWEEN LIKE 前缀)后面的列用不到索引。","EXPLAIN 只看 4 列就够定位 90% 慢查:type 是否 ≥ range、key 有没有选到对的索引、rows 预估扫描行数是不是太大、Extra 里有没有 Using filesort \u002F Using temporary \u002F Using where 不该出现的。",[1018,1019,1020,1021],"SQL JOIN","MySQL 索引","EXPLAIN","SQL 性能调优","A7THpcYh18hx-SsosvqpIFM3NXXtjLJuBgqgxqzCnKg",[],1794088893185]