**摘要:**本文以集团 OA/ERP/CRM/HR/MES/SCM 全链路搜索为背景,系统演示 MySQL 9.0 的四大搜索机制:LIKE 模式、函数索引、全文索引(ngram)与相关性排序。通过 10 万行商品数据与六域业务表,实测并量化「引擎真实读行」,对比前缀 LIKE 与前导通配、函数索引与全表扫描、全文检索与 LIKE 前导通配的读行差异,并给出覆盖索引、时区(UTC 存储 + TIMESTAMPADD 换算)、多币种(price_usd 归一化)及万亿级高并发下的工程取舍与八条方法清单。
一、文章大纲
本文围绕 MySQL 9.0 搜索优化展开,从四大搜索机制入手,结合国际化珠宝集团六域业务场景,通过实测数据量化不同方案的引擎读行差异,并给出工程级取舍建议。全文结构如下:
- 引言与背景:介绍国际化珠宝集团 OA/ERP/CRM/HR/MES/SCM 全链路搜索需求,以及 MySQL 9.0 搜索优化的核心目标。
- MySQL 四大搜索机制速览:概述 LIKE 模式、函数索引、全文索引(ngram)与相关性排序的适用边界。
- 反模式速查:列出常见搜索反模式及其后果,如前导通配 LIKE、函数包裹列未建索引、中文全文未用 ngram 等。
- 度量框架设计:说明如何通过 performance_schema 量化引擎真实读行,为后续对比提供依据。
- 实验台搭建与造数:介绍珠宝商品、OA 文档、CRM 备注、HR 员工、SCM 运单、MES 工单六域表的建表与造数方案。
- LIKE 模式实测:对比前缀 LIKE 与前导通配、后缀匹配的引擎读行差异,验证 B-tree 索引的适用条件。
- 函数索引实测:演示 LOWER 函数索引与 DATE 函数索引如何恢复索引定位,并对比未建函数索引的全表扫描。
- 全文索引(ngram)实测:展示自然语言模式与布尔模式的中文检索效果,并与 LIKE 前导通配对比读行差异。
- 相关性排序与业务组合:演示按相关性降序、相关性加业务过滤、相关性加价格双排序等场景。
- 查询取舍矩阵与万亿级工程:介绍覆盖索引、分区、分片、读写分离及下沉 Elasticsearch 等工程手段。
- 六域搜索模板:分别给出 OA、ERP、CRM、HR、MES、SCM 的搜索 SQL 示例。
- 国际时区与多币种处理:说明 UTC 存储、TIMESTAMPADD 换算、DST 注意事项及 price_usd 归一化排序。
- 自检与交付:通过 V1 至 V11 自检项验证脚本正确性与幂等性。
- 实测总表与结论:汇总引擎读行对照表、八条方法清单及脚本结构说明。
sql
CREATE DATABASE IF NOT EXISTS JewelryAnalytics DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE JewelryAnalytics;
SET SESSION cte_max_recursion_depth=100000;
SET SESSION group_concat_max_len=1000000;
SET SESSION optimizer_switch='index_condition_pushdown=on,mrr=on,derived_merge=on,skip_scan=on';
SET SESSION transaction_isolation='REPEATABLE-READ';
SET SESSION sql_safe_updates=0;
SET SESSION innodb_lock_wait_timeout=50;
SET SESSION time_zone='+00:00';
-- ============================================================================
-- §0.0 运行方式必读(务必先读)
-- ============================================================================
-- 本脚本:MySQL 9.0 搜索优化(全文索引 / LIKE 模式 / 函数索引 / 相关性排序 / 取舍)
-- 主题:国际化珠宝集团 OA/ERP/CRM/HR/MES/SCM 全链路搜索示例 + 万亿高并发 + 时区 + 多币种
-- 运行前提:MySQL 9.0.x,库 JewelryAnalytics 会被自动建/用;本脚本完全幂等。
-- 方式A(命令行,推荐):
-- mysql -uroot -p --default-character-set=utf8mb4 < MySQL9_SearchOptimized_JewelryDemo.sql
-- 方式B(Workbench):整段执行;若报 1175(safe update) 见 §0.2;结果标签页有上限见 §0.2。
-- 幂等说明:每次运行先 DROP 全部 srch_* 实验表再重建并造数,因此重复运行结果一致、可证明稳定性。
-- 度量口径:用 performance_schema.session_status 的 INNODB_ROWS_READ 与
-- HANDLER_READ_RND_NEXT 差值,量化「引擎真正读了多少行」,比 EXPLAIN rows 更真实。
-- ============================================================================
-- §0.1 MySQL 搜索能力速览(四大机制)
-- 1) LIKE 模式:'abc%' 前缀可用 B-tree 索引(范围扫描);'%abc' / '%abc%' 前导通配 → 全表扫描。
-- 2) 函数索引(MySQL 8.0+):CREATE INDEX ix ON t ((LOWER(col))) 让 WHERE LOWER(col)=... 走索引;
-- 反之 WHERE YEAR(ts)=2026 这种「列被函数包裹且未建函数索引」必然全扫。
-- 3) 全文索引 FULLTEXT:专为「包含某词」的子串/词搜索设计;中文必须用 WITH PARSER ngram,
-- 否则默认解析器按空白分词,中文整句无法命中。返回相关性分值,可 ORDER BY 排序。
-- 4) 相关性排序:MATCH(col) AGAINST('词' IN NATURAL LANGUAGE MODE) 返回浮点相关性,
-- 布尔模式支持 +词 -词 "短语" * 等算子。
-- 适用边界:精确等值/范围 → B-tree;大小写无关/派生列 → 函数索引;包含某词/语义 → FULLTEXT;
-- 任意位置子串 LIKE '%x%' 在大数据量下应改用 FULLTEXT,否则全扫拖垮高并发。
-- ============================================================================
-- §0.2 反模式速查表(本机已实测)
-- 反模式 后果 正确做法
-- LIKE '%钻石%'(前导通配) 全表扫描,引擎读≈全表行,深数据灾难 FULLTEXT / ngram;或改前缀 '钻石%'
-- WHERE LOWER(name)='x' 但无函数索引 函数包裹列 → 索引失效,全扫 建 ((LOWER(name))) 函数索引
-- WHERE YEAR(ts)=2026 同上,对列套函数 建 ((DATE(ts))) 或范围 ts>=...<...
-- FULLTEXT 默认解析器搜中文 中文无法分词,命中极少 WITH PARSER ngram
-- CONVERT_TZ(ts,'UTC','Asia/Shanghai') 本机 mysql.time_zone_name=0 行 → NULL 用 TIMESTAMPADD(MINUTE,偏移,ts)
-- TIMESTAMP 赋值 '2999-12-31' 超 2038 上限 → ERROR 1292 改用 DATETIME
-- DATETIME 列值拼进 CONCAT 字符集冲突 → ERROR 1271 CAST(列 AS CHAR)
-- MATCH(a) 但 FULLTEXT 索引是 (a,b) 列集合不一致 → ERROR 1191 MATCH 列须与索引列完全一致
-- ============================================================================
-- §01 度量框架(srch_demo_*):量化引擎真实读行,证明取舍
DROP TABLE IF EXISTS srch_demo_metric;
CREATE TABLE srch_demo_metric(
tag VARCHAR(40) PRIMARY KEY,
innodb_rows BIGINT NOT NULL DEFAULT 0,
rnd_next BIGINT NOT NULL DEFAULT 0,
rnd BIGINT NOT NULL DEFAULT 0,
ms INT NOT NULL DEFAULT 0,
note VARCHAR(255)
);
DROP TABLE IF EXISTS srch_demo_result;
CREATE TABLE srch_demo_result(
check_id VARCHAR(30) PRIMARY KEY,
passed TINYINT NOT NULL,
detail VARCHAR(400)
);
DROP TABLE IF EXISTS srch_demo_probe;
CREATE TABLE srch_demo_probe(
id INT AUTO_INCREMENT PRIMARY KEY,
label VARCHAR(60),
errno INT,
sql_state VARCHAR(10),
msg VARCHAR(300),
caught_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
DROP PROCEDURE IF EXISTS srch_snap;
DELIMITER //
CREATE PROCEDURE srch_snap()
BEGIN
SELECT VARIABLE_VALUE INTO @srch_snap_ir FROM performance_schema.session_status WHERE VARIABLE_NAME='INNODB_ROWS_READ';
SELECT VARIABLE_VALUE INTO @srch_snap_rn FROM performance_schema.session_status WHERE VARIABLE_NAME='HANDLER_READ_RND_NEXT';
SELECT VARIABLE_VALUE INTO @srch_snap_rd FROM performance_schema.session_status WHERE VARIABLE_NAME='HANDLER_READ_RND';
SET @srch_snap_ts = UNIX_TIMESTAMP(NOW(6));
END//
DELIMITER ;
DROP PROCEDURE IF EXISTS srch_measure;
DELIMITER //
CREATE PROCEDURE srch_measure(IN p_tag VARCHAR(40))
BEGIN
DECLARE v_ir BIGINT; DECLARE v_rn BIGINT; DECLARE v_rd BIGINT; DECLARE v_ms INT;
SELECT VARIABLE_VALUE INTO v_ir FROM performance_schema.session_status WHERE VARIABLE_NAME='INNODB_ROWS_READ';
SELECT VARIABLE_VALUE INTO v_rn FROM performance_schema.session_status WHERE VARIABLE_NAME='HANDLER_READ_RND_NEXT';
SELECT VARIABLE_VALUE INTO v_rd FROM performance_schema.session_status WHERE VARIABLE_NAME='HANDLER_READ_RND';
SET v_ms = ROUND((UNIX_TIMESTAMP(NOW(6)) - @srch_snap_ts)*1000);
INSERT INTO srch_demo_metric(tag,innodb_rows,rnd_next,rnd,ms) VALUES(p_tag, v_ir-@srch_snap_ir, v_rn-@srch_snap_rn, v_rd-@srch_snap_rd, v_ms)
ON DUPLICATE KEY UPDATE innodb_rows=v_ir-@srch_snap_ir, rnd_next=v_rn-@srch_snap_rn, rnd=v_rd-@srch_snap_rd, ms=v_ms;
END//
DELIMITER ;
DROP PROCEDURE IF EXISTS srch_v;
DELIMITER //
CREATE PROCEDURE srch_v(IN p_check VARCHAR(30), IN p_pass TINYINT, IN p_detail VARCHAR(400))
BEGIN
INSERT INTO srch_demo_result(check_id,passed,detail) VALUES(p_check,p_pass,p_detail)
ON DUPLICATE KEY UPDATE passed=p_pass, detail=p_detail;
END//
DELIMITER ;
DROP PROCEDURE IF EXISTS srch_err_probe;
DELIMITER //
CREATE PROCEDURE srch_err_probe(IN p_label VARCHAR(60), IN p_sql TEXT)
BEGIN
DECLARE v_no INT DEFAULT 0; DECLARE v_st VARCHAR(10) DEFAULT '00000'; DECLARE v_msg VARCHAR(300) DEFAULT '';
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1 v_no = MYSQL_ERRNO, v_st = RETURNED_SQLSTATE, v_msg = MESSAGE_TEXT;
END;
SET @srch_probe_sql = p_sql;
PREPARE srch_pstmt FROM @srch_probe_sql;
IF v_no = 0 THEN
EXECUTE srch_pstmt;
DEALLOCATE PREPARE srch_pstmt;
END IF;
INSERT INTO srch_demo_probe(label,errno,sql_state,msg) VALUES(p_label, v_no, v_st, v_msg);
END//
DELIMITER ;
-- §02 实验台:建表 + 造数(国际化珠宝集团)
-- 命名统一 srch_* 前缀,避免与同库其他主题的 demo_/gl_/dim_* 表冲突。
DROP TABLE IF EXISTS srch_dim_currency;
CREATE TABLE srch_dim_currency(code CHAR(3) PRIMARY KEY, name VARCHAR(40), symbol VARCHAR(8), usd_rate DECIMAL(12,6));
INSERT INTO srch_dim_currency VALUES
('CNY','人民币','¥',1/7.20),('USD','美元','$',1.0),('EUR','欧元','€',1.08),
('JPY','日元','¥',0.0067),('HKD','港元','HK$',1/7.80),('GBP','英镑','£',1.27);
DROP TABLE IF EXISTS srch_dim_country;
CREATE TABLE srch_dim_country(code CHAR(2) PRIMARY KEY, name VARCHAR(40), tz_offset_min INT, currency CHAR(3));
INSERT INTO srch_dim_country VALUES
('CN','中国',480,'CNY'),('US','美国',-300,'USD'),('FR','法国',60,'EUR'),
('JP','日本',540,'JPY'),('HK','中国香港',480,'HKD'),('GB','英国',0,'GBP'),
('DE','德国',60,'EUR'),('SG','新加坡',480,'USD');
-- 主表:ERP 珠宝商品目录(搜索主战场)
DROP TABLE IF EXISTS srch_product;
CREATE TABLE srch_product(
product_id BIGINT PRIMARY KEY,
sku VARCHAR(30) NOT NULL,
product_name VARCHAR(200) NOT NULL,
description TEXT NOT NULL,
material VARCHAR(20),
category VARCHAR(20),
price_orig DECIMAL(18,2),
currency_code CHAR(3),
price_usd DECIMAL(18,2),
country_code CHAR(2),
created_ts DATETIME NOT NULL,
KEY ix_name (product_name),
KEY ix_created (created_ts),
KEY ix_country_created (country_code, created_ts),
KEY ix_cover (material, category, price_usd, product_id),
KEY ix_usd (price_usd),
KEY ix_fn_lower_name ((LOWER(product_name))),
KEY ix_fn_created_date ((DATE(created_ts)))
) ENGINE=InnoDB;
-- 造 100,000 行(200×500),时间/金额/币种国际化分布
INSERT INTO srch_product(product_id,sku,product_name,description,material,category,price_orig,currency_code,price_usd,country_code,created_ts)
WITH RECURSIVE a(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM a WHERE n<200),
b(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM b WHERE n<500),
base AS (
SELECT a.n500 + b.n - 500 AS id,
CONCAT('SKU', LPAD(a.n500+b.n-500,7,'0')) AS sku,
CONCAT(ELT(1+(a.n500+b.n-500)%10,'黄金','铂金','钻石','红宝石','蓝宝石','翡翠','珍珠','银','18K金','白金'),
ELT(1+(a.n500+b.n-500)%8,'戒指','项链','手链','耳环','吊坠','胸针','手镯','脚链'),' ',
ELT(1+(a.n500+b.n-500)%6,'经典','现代','复古','奢华','简约','定制')) AS pname,
CONCAT('高端', ELT(1+(a.n500+b.n-500)%10,'黄金','铂金','钻石','红宝石','蓝宝石','翡翠','珍珠','银','18K金','白金'),
ELT(1+(a.n500+b.n-500)%8,'戒指','项链','手链','耳环','吊坠','胸针','手镯','脚链'),
',采用', ELT(1+(a.n500+b.n-500)%5,'意大利','日本','法国','中国','瑞士'),'工艺,',
ELT(1+(a.n500+b.n-500)%6,'经典','现代','复古','奢华','简约','定制'),'风格。Diamond Luxury Jewelry ',
ELT(1+(a.n500+b.n-500)%4,'collection','series','edition','line')) AS pdesc,
ELT(1+(a.n500+b.n-500)%10,'黄金','铂金','钻石','红宝石','蓝宝石','翡翠','珍珠','银','18K金','白金') AS mat,
ELT(1+(a.n500+b.n-500)%8,'戒指','项链','手链','耳环','吊坠','胸针','手镯','脚链') AS cat,
ROUND(100 + ((a.n500+b.n-500)%9000)/10.0, 2) AS porig,
ELT(1+(a.n500+b.n-500)%6,'CNY','USD','EUR','JPY','HKD','GBP') AS cur,
ELT(1+(a.n500+b.n-500)%8,'CN','US','FR','JP','HK','GB','DE','SG') AS ctry,
DATE_ADD('2025-09-01 00:00:00', INTERVAL ((a.n500+b.n-500) % 365) DAY)
+ INTERVAL ((a.n*500+b.n-500) % 86400) SECOND AS cts
FROM a JOIN b
)
SELECT id, sku, pname, pdesc, mat, cat, porig, cur,
ROUND(porig * CASE cur WHEN 'CNY' THEN 1/7.2 WHEN 'USD' THEN 1 WHEN 'EUR' THEN 1.08
WHEN 'JPY' THEN 0.0067 WHEN 'HKD' THEN 1/7.8 WHEN 'GBP' THEN 1.27 END, 2) AS price_usd,
ctry, cts
FROM base;
-- 加载后再建全文索引(比边插边建更快,且 ngram 对中文必需)
ALTER TABLE srch_product ADD FULLTEXT ft_name_desc (product_name, description) WITH PARSER ngram;
-- OA 文档(标题/正文全文)
DROP TABLE IF EXISTS srch_oa_doc;
CREATE TABLE srch_oa_doc(doc_id BIGINT PRIMARY KEY, title VARCHAR(200), body TEXT, dept VARCHAR(40), created_ts DATETIME);
INSERT INTO srch_oa_doc(doc_id,title,body,dept,created_ts)
WITH RECURSIVE a(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM a WHERE n<100),
b(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM b WHERE n<100),
base AS (SELECT a.n*100+b.n-100 AS id FROM a JOIN b)
SELECT id,
CONCAT(ELT(1+id%6,'采购','报销','入职','合同','盘点','审批'),'流程说明-', LPAD(id,5,'0')),
CONCAT('本文档说明', ELT(1+id%6,'采购','报销','入职','合同','盘点','审批'),'流程,涉及', ELT(1+id%5,'中国','美国','法国','日本','香港'),'分公司,需', ELT(1+id%4,'部门经理','财务','HR','总经理'),'审批。Policy document about jewelry operation.'),
ELT(1+id%6,'采购部','财务部','人事部','法务部','仓储部','总经办'),
DATE_SUB('2026-02-01 00:00:00', INTERVAL (id%180) DAY) + INTERVAL (id%80000) SECOND
FROM base;
ALTER TABLE srch_oa_doc ADD FULLTEXT ft_oa (title, body) WITH PARSER ngram;
-- CRM 客户备注(全文)
DROP TABLE IF EXISTS srch_crm_note;
CREATE TABLE srch_crm_note(note_id BIGINT PRIMARY KEY, customer_name VARCHAR(100), note_text TEXT, created_ts DATETIME);
INSERT INTO srch_crm_note(note_id,customer_name,note_text,created_ts)
WITH RECURSIVE a(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM a WHERE n<100),
b(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM b WHERE n<100),
base AS (SELECT a.n*100+b.n-100 AS id FROM a JOIN b)
SELECT id,
CONCAT(ELT(1+id%8,'张','李','王','刘','陈','杨','黄','周'), ELT(1+id%5,'先生','女士','总','经理','总监')),
CONCAT('客户咨询', ELT(1+id%10,'钻石','黄金','铂金','红宝石','蓝宝石','翡翠','珍珠','银','18K金','白金'), ELT(1+id%8,'戒指','项链','手链','耳环','吊坠','胸针','手镯','脚链'),',意向预算 ', 1000+id%9000, ' 元,来源', ELT(1+id%4,'官网','门店','微信','展会'),'。Lead interested in luxury jewelry collection.'),
DATE_SUB('2026-03-01 00:00:00', INTERVAL (id%150) DAY) + INTERVAL (id%80000) SECOND
FROM base;
ALTER TABLE srch_crm_note ADD FULLTEXT ft_crm (note_text) WITH PARSER ngram;
-- HR 员工(函数索引 大小写无关)
DROP TABLE IF EXISTS srch_hr_emp;
CREATE TABLE srch_hr_emp(emp_id BIGINT PRIMARY KEY, emp_name VARCHAR(100), dept VARCHAR(40), title VARCHAR(60), email VARCHAR(120),
KEY ix_fn_lower_name ((LOWER(emp_name))),
KEY ix_fn_lower_email ((LOWER(email))));
INSERT INTO srch_hr_emp(emp_id,emp_name,dept,title,email)
WITH RECURSIVE a(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM a WHERE n<50),
b(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM b WHERE n<100),
base AS (SELECT a.n*100+b.n-100 AS id FROM a JOIN b)
SELECT id,
CONCAT(ELT(1+id%8,'张','李','王','刘','陈','杨','黄','周'), ELT(1+id%6,'伟','芳','娜','强','磊','敏')),
ELT(1+id%6,'珠宝设计','生产管理','销售','财务','HR','IT'),
ELT(1+id%5,'工程师','主管','经理','总监','专员'),
CONCAT('emp', LPAD(id,5,'0'), '@lukfook.com')
FROM base;
-- SCM 物流运单(前缀索引)
DROP TABLE IF EXISTS srch_scm_ship;
CREATE TABLE srch_scm_ship(ship_id BIGINT PRIMARY KEY, tracking_no VARCHAR(40), route VARCHAR(120), status VARCHAR(20), created_ts DATETIME, KEY ix_tracking (tracking_no));
INSERT INTO srch_scm_ship(ship_id,tracking_no,route,status,created_ts)
WITH RECURSIVE a(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM a WHERE n<100),
b(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM b WHERE n<100),
base AS (SELECT a.n*100+b.n-100 AS id FROM a JOIN b)
SELECT id,
CONCAT(ELT(1+id%4,'SF','YT','ZT','JD'), LPAD(id,10,'0')),
CONCAT(ELT(1+id%5,'中国','美国','法国','日本','香港'),'→', ELT(1+id%5,'美国','日本','香港','德国','新加坡')),
ELT(1+id%4,'已揽收','运输中','清关中','已签收'),
DATE_SUB('2026-04-01 00:00:00', INTERVAL (id%120) DAY) + INTERVAL (id%80000) SECOND
FROM base;
-- MES 工单(备注全文)
DROP TABLE IF EXISTS srch_mes_wo;
CREATE TABLE srch_mes_wo(wo_id BIGINT PRIMARY KEY, wo_no VARCHAR(40), product_id BIGINT, station VARCHAR(40), note_text TEXT, created_ts DATETIME);
INSERT INTO srch_mes_wo(wo_id,wo_no,product_id,station,note_text,created_ts)
WITH RECURSIVE a(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM a WHERE n<100),
b(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM b WHERE n<100),
base AS (SELECT a.n*100+b.n-100 AS id FROM a JOIN b)
SELECT id,
CONCAT('WO', LPAD(id,8,'0')),
(id % 100000) + 1,
ELT(1+id%6,'铸造','抛光','镶嵌','质检','电镀','包装'),
CONCAT('工单', ELT(1+id%6,'铸造','抛光','镶嵌','质检','电镀','包装'),',', ELT(1+id%10,'钻石','黄金','铂金','红宝石','蓝宝石','翡翠','珍珠','银','18K金','白金'),'工序,状态', ELT(1+id%3,'正常','待检','返工'),'。MES work order note for jewelry production line.'),
DATE_SUB('2026-05-01 00:00:00', INTERVAL (id%100) DAY) + INTERVAL (id%80000) SECOND
FROM base;
ALTER TABLE srch_mes_wo ADD FULLTEXT ft_mes (note_text) WITH PARSER ngram;
-- §03 LIKE 模式:前缀可用索引,前导通配必全扫
SELECT '§03.1 前缀 LIKE 钻石% → 走 ix_name 范围扫描(引擎读行少)' AS -- 第03章 --;
CALL srch_snap();
SELECT COUNT(*) INTO @c1 FROM srch_product WHERE product_name LIKE '钻石%';
CALL srch_measure('LIKE_prefix');
SELECT '§03.2 前导通配 %钻石% → 全表扫描(引擎读行≈全表)' AS -- 第03章 --;
CALL srch_snap();
SELECT COUNT(*) INTO @c2 FROM srch_product WHERE product_name LIKE '%钻石%';
CALL srch_measure('LIKE_lead_wc');
SELECT '§03.3 后缀 %戒指 → 同样是全表扫描' AS -- 第03章 --;
CALL srch_snap();
SELECT COUNT(*) INTO @c3 FROM srch_product WHERE product_name LIKE '%戒指';
CALL srch_measure('LIKE_suffix');
SELECT CONCAT(' 前缀匹配行数≈', @c1, ';前导通配匹配行数≈', @c2, ';后缀匹配≈', @c3) AS -- 提示 --;
SELECT ' ★ 结论:LIKE 只有「无前导 %」才能用 B-tree;任何前导 % 在百万/万亿级都是全扫灾难。' AS -- 提示 --;
-- §04 函数索引:让「列被函数包裹」的查询也能走索引
SELECT '§04.1 大小写无关搜索 LOWER(product_name)=... 用 ((LOWER(product_name))) 函数索引 → 索引定位' AS -- 第04章 --;
CALL srch_snap();
SELECT COUNT(*) INTO @f1 FROM srch_product WHERE LOWER(product_name)='钻石戒指 经典';
CALL srch_measure('FUNC_lower');
SELECT '§04.2 反例:YEAR(created_ts)=2026(未建函数索引)→ 全表扫描' AS -- 第04章 --;
CALL srch_snap();
SELECT COUNT(*) INTO @f2 FROM srch_product WHERE YEAR(created_ts)=2026;
CALL srch_measure('FUNC_year_bad');
SELECT '§04.3 按「日期」搜索 DATE(created_ts)=... 用 ((DATE(created_ts))) 函数索引 → 索引定位' AS -- 第04章 --;
CALL srch_snap();
SELECT COUNT(*) INTO @f3 FROM srch_product WHERE DATE(created_ts)='2026-03-15';
CALL srch_measure('FUNC_date');
SELECT CONCAT(' LOWER 命中≈', @f1, ' 行(函数索引定位);YEAR(...) 全扫读 100k 行、匹配≈', @f2, ';DATE 命中≈', @f3, ' 行') AS -- 提示 --;
SELECT ' ★ 结论:WHERE 里对列套函数 ≠ 索引失效;失效的是「没建对应函数索引」的那次。建 ((FUNC(col))) 即可恢复索引定位。' AS -- 提示 --;
-- §05 全文索引(ngram 中文必需):词搜索 + 相关性
SELECT '§05.1 自然语言模式:MATCH(name,desc) AGAINST(''钻石'') → 相关性排序' AS -- 第05章 --;
SELECT product_id, product_name,
MATCH(product_name,description) AGAINST('钻石' IN NATURAL LANGUAGE MODE) AS rel
FROM srch_product
WHERE MATCH(product_name,description) AGAINST('钻石' IN NATURAL LANGUAGE MODE)
ORDER BY rel DESC LIMIT 8;
SELECT '§05.2 布尔模式:+钻石 -银(必须含钻石且不含银)' AS -- 第05章 --;
SELECT product_id, product_name
FROM srch_product
WHERE MATCH(product_name,description) AGAINST('+钻石 -银' IN BOOLEAN MODE)
LIMIT 8;
SELECT '§05.3 全文 vs LIKE ''%钻石%'':同样语义,引擎读行天差地别' AS -- 第05章 --;
CALL srch_snap();
SELECT COUNT(*) INTO @ft1 FROM srch_product WHERE MATCH(product_name,description) AGAINST('钻石' IN NATURAL LANGUAGE MODE);
CALL srch_measure('FT_ngram');
SELECT CONCAT(' 全文命中≈', @ft1, ' 行(只回表命中行);LIKE 前导通配见 §03 全扫≈全表') AS -- 提示 --;
SELECT ' ★ ngram 让中文可按「词/字」检索;默认解析器对中文无效。布尔模式支持 + - "" * 算子。' AS -- 提示 --;
-- §06 相关性排序 + 与业务过滤/排序组合
SELECT '§06.1 按相关性降序取前 20(搜索「钻石」)' AS -- 第06章 --;
SELECT product_id, product_name,
MATCH(product_name,description) AGAINST('钻石' IN NATURAL LANGUAGE MODE) AS rel
FROM srch_product
WHERE MATCH(product_name,description) AGAINST('钻石' IN NATURAL LANGUAGE MODE)
ORDER BY rel DESC LIMIT 20;
SELECT '§06.2 相关性 + 业务过滤(仅 USD 且 美国,按相关性降序)' AS -- 第06章 --;
SELECT product_id, product_name, currency_code, price_usd,
MATCH(product_name,description) AGAINST('钻石' IN NATURAL LANGUAGE MODE) AS rel
FROM srch_product
WHERE MATCH(product_name,description) AGAINST('钻石' IN NATURAL LANGUAGE MODE)
AND currency_code='USD' AND country_code='US'
ORDER BY rel DESC LIMIT 10;
SELECT '§06.3 相关性 + 价格双排序:先相关再便宜(跨币种必须按 price_usd)' AS -- 第06章 --;
SELECT product_id, product_name, price_usd,
MATCH(product_name,description) AGAINST('黄金' IN NATURAL LANGUAGE MODE) AS rel
FROM srch_product
WHERE MATCH(product_name,description) AGAINST('黄金' IN NATURAL LANGUAGE MODE)
ORDER BY rel DESC, price_usd ASC LIMIT 10;
SELECT ' ★ 跨币种「按价格排」必须统一到 price_usd,绝不能按 price_orig 跨币种排。' AS -- 提示 --;
-- §07 查询取舍矩阵 + 万亿/高并发工程
SELECT '§07.1 覆盖索引:投影列全在索引内 → EXPLAIN Extra 出现 Using index(免回表)' AS -- 第07章 --;
EXPLAIN SELECT product_id, material, category, price_usd
FROM srch_product WHERE material='钻石' AND category='戒指';
SELECT '§07.2 反例:投影含索引外列(description) → EXPLAIN 无 Using index,必须回表' AS -- 第07章 --;
EXPLAIN SELECT product_id, description
FROM srch_product WHERE material='钻石' AND category='戒指';
SELECT ' ★ 覆盖索引让搜索列+投影列同进索引,避免逐行回表取额外列;宽行/大数据集下显著降 IO。' AS -- 提示 --;
SELECT '§07.3 万亿级工程要点' AS -- 第07章 --;
SELECT ' ① 热路径禁止 LIKE ''%x%'':改用 ngram FULLTEXT,否则全扫拖垮并发。' AS 工程1;
SELECT ' ② 覆盖索引 + 函数索引:搜索列与投影列同进索引,减少回表与 IO。' AS 工程2;
SELECT ' ③ 分区:按 created_ts RANGE 或 country_code LIST 分区,时间/国家过滤可分区裁剪。' AS 工程3;
SELECT ' ④ 分片:以 country_code / 租户 为分片键水平拆分;搜索路由到只读副本(读写分离)。' AS 工程4;
SELECT ' ⑤ 极限场景:单实例 FULLTEXT 仍跟不上时,将搜索下沉到 Elasticsearch/OpenSearch,' AS 工程5;
SELECT ' MySQL 仅做精确过滤与事务,搜索结果回表取详情(CQRS 读写分离)。' AS 工程6;
SELECT ' ⑥ 中文检索务必 WITH PARSER ngram;停用词/最小词长用 innodb_ft_* 调优。' AS 工程7;
-- §08 六域搜索模板(OA/ERP/CRM/HR/MES/SCM)
SELECT '§08.1 OA:文档标题/正文全文检索(ngram)' AS -- 第08章 --;
SELECT doc_id, title FROM srch_oa_doc WHERE MATCH(title,body) AGAINST('审批' IN NATURAL LANGUAGE MODE) LIMIT 5;
SELECT '§08.2 ERP:商品目录搜索(主表已演示,此处补充按材质+品类函数/覆盖索引过滤)' AS -- 第08章 --;
SELECT product_id, product_name, price_usd FROM srch_product WHERE material='翡翠' AND category='手镯' ORDER BY price_usd DESC LIMIT 5;
SELECT '§08.3 CRM:客户备注全文 + 客户名前缀' AS -- 第08章 --;
SELECT note_id, customer_name FROM srch_crm_note WHERE MATCH(note_text) AGAINST('钻石' IN NATURAL LANGUAGE MODE) LIMIT 5;
SELECT note_id, customer_name FROM srch_crm_note WHERE customer_name LIKE '张%' LIMIT 5;
SELECT '§08.4 HR:员工姓名大小写无关搜索(函数索引 ((LOWER(emp_name))))' AS -- 第08章 --;
SELECT emp_id, emp_name, email FROM srch_hr_emp WHERE LOWER(emp_name)='张伟' LIMIT 5;
SELECT '§08.5 MES:工单备注全文检索' AS -- 第08章 --;
SELECT wo_id, station, note_text FROM srch_mes_wo WHERE MATCH(note_text) AGAINST('返工' IN NATURAL LANGUAGE MODE) LIMIT 5;
SELECT '§08.6 SCM:运单号前缀检索(ix_tracking)vs 前导通配' AS -- 第08章 --;
SELECT ship_id, tracking_no, status FROM srch_scm_ship WHERE tracking_no LIKE 'SF%' LIMIT 5;
SELECT ship_id, tracking_no, status FROM srch_scm_ship WHERE tracking_no LIKE '%1234' LIMIT 5;
SELECT ' ★ 运单号/工单号等「左锚定」检索务必用前缀(= 或 LIKE ''SF%''),前导通配全扫。' AS -- 提示 --;
-- §09 国际时区 + 多币种搜索
SELECT '§09.1 时间一律 UTC 存储;本地仅展示(本机 time_zone_name=0 → CONVERT_TZ 返回 NULL)' AS -- 第09章 --;
SELECT product_id,
CAST(created_ts AS CHAR) AS UTC时间,
CAST(TIMESTAMPADD(MINUTE, 480, created_ts) AS CHAR) AS 上海,
CAST(TIMESTAMPADD(MINUTE,-300, created_ts) AS CHAR) AS 纽约冬令,
CAST(TIMESTAMPADD(MINUTE, 60, created_ts) AS CHAR) AS 巴黎
FROM srch_product WHERE product_id IN (1,50000,100000) ORDER BY product_id;
SELECT '§09.2 按「纽约本地营业日 2026-03-15」检索 → 先把本地日换算成 UTC 范围再用 ix_created 范围扫' AS -- 第09章 --;
-- 纽约冬令时偏移 -300 分;本地 2026-03-15 00:00 ~ 24:00 对应 UTC = 本地 +300 分
SET @ny_day_start = TIMESTAMPADD(MINUTE, 300, '2026-03-15 00:00:00');
SET @ny_day_end = TIMESTAMPADD(MINUTE, 300, '2026-03-16 00:00:00');
CALL srch_snap();
SELECT COUNT(*) INTO @tz1 FROM srch_product WHERE created_ts >= @ny_day_start AND created_ts < @ny_day_end;
CALL srch_measure('TZ_ny_day');
SELECT CONCAT(' 纽约本地 2026-03-15 命中≈', @tz1, ' 行(UTC 范围扫描,索引可用)') AS -- 提示 --;
SELECT '§09.3 DST 秋令时:同一本地时刻出现两次 → 必须存 UTC,游标/边界用 UTC' AS -- 第09章 --;
-- 纽约 2026-11-01 夏令时结束,01:30 出现两次(EDT -240 与 EST -300)
SELECT CAST(TIMESTAMPADD(MINUTE,-240,'2026-11-01 05:30:00') AS CHAR) AS UTC_A,
CAST(TIMESTAMPADD(MINUTE,-300,'2026-11-01 06:30:00') AS CHAR) AS UTC_B,
'两行 UTC 不同,但纽约本地都显示 01:30 → 按本地排/分页会乱' AS 说明;
SELECT '§09.4 多币种:跨币种检索必须按归一化 price_usd 过滤/排序,绝不可按 price_orig' AS -- 第09章 --;
SELECT currency_code, COUNT(*) AS 笔数, ROUND(MIN(price_usd),2) AS min_usd, ROUND(MAX(price_usd),2) AS max_usd
FROM srch_product GROUP BY currency_code ORDER BY currency_code;
SELECT product_id, product_name, currency_code, price_orig, price_usd
FROM srch_product WHERE price_usd BETWEEN 500 AND 600 ORDER BY price_usd DESC LIMIT 10;
SELECT ' ★ 汇率波动:price_usd 是「下单时快照」,审计用 rate_date;不要实时重算。' AS -- 提示 --;
-- §10 自检(V1..V11)全部通过才算交付
-- V1 框架表存在
SELECT COUNT(*) INTO @v1 FROM information_schema.tables WHERE table_schema='JewelryAnalytics' AND table_name IN ('srch_demo_metric','srch_demo_result','srch_demo_probe');
CALL srch_v('V1_框架表存在', IF(@v1=3,1,0), CONCAT('框架表数=',@v1));
-- V2 全文索引(ngram)存在
SELECT COUNT(*) INTO @v2 FROM information_schema.statistics WHERE table_schema='JewelryAnalytics' AND table_name='srch_product' AND index_type='FULLTEXT';
CALL srch_v('V2_全文索引ngram存在', IF(@v2>=1,1,0), CONCAT('FULLTEXT索引数=',@v2));
-- V3 函数索引存在
SELECT COUNT(*) INTO @v3 FROM information_schema.statistics WHERE table_schema='JewelryAnalytics' AND table_name='srch_product' AND index_name IN ('ix_fn_lower_name','ix_fn_created_date');
CALL srch_v('V3_函数索引存在', IF(@v3=2,1,0), CONCAT('函数索引数=',@v3));
-- V4 全文读行远少于 LIKE 前导通配
SELECT IFNULL((SELECT innodb_rows FROM srch_demo_metric WHERE tag='FT_ngram'),0) INTO @m_ft;
SELECT IFNULL((SELECT innodb_rows FROM srch_demo_metric WHERE tag='LIKE_lead_wc'),1) INTO @m_lk;
CALL srch_v('V4_全文读行少于LIKE前导通配', IF(@m_ft < @m_lk,1,0), CONCAT('FT=',@m_ft,' LIKE=',@m_lk));
-- V5 函数索引定位:LOWER 命中读行 << YEAR 全扫
SELECT IFNULL((SELECT innodb_rows FROM srch_demo_metric WHERE tag='FUNC_lower'),0) INTO @m_fl;
SELECT IFNULL((SELECT innodb_rows FROM srch_demo_metric WHERE tag='FUNC_year_bad'),1) INTO @m_fy;
CALL srch_v('V5_函数索引定位优于全扫', IF(@m_fl < @m_fy,1,0), CONCAT('LOWER读行=',@m_fl,' YEAR全扫=',@m_fy));
-- V6 全文返回>0 行(相关性)
SELECT COUNT(*) INTO @m6 FROM srch_product WHERE MATCH(product_name,description) AGAINST('钻石' IN NATURAL LANGUAGE MODE);
CALL srch_v('V6_全文返回结果>0', IF(@m6>0,1,0), CONCAT('命中=',@m6));
-- V7 时区:TIMESTAMPADD 非 NULL;CONVERT_TZ 本机为 NULL(须知悉)
SELECT TIMESTAMPADD(MINUTE,480,'2026-01-01 00:00:00') INTO @ta;
SELECT CONVERT_TZ('2026-01-01 00:00:00','UTC','Asia/Shanghai') INTO @tb;
CALL srch_v('V7_时区TIMESTAMPADD可用', IF(@ta IS NOT NULL AND @tb IS NULL,1,0), CONCAT('TSadd=',IF(@ta IS NULL,'NULL',CAST(@ta AS CHAR)),' CONVERT_TZ=',IF(@tb IS NULL,'NULL(已知)',CAST(@tb AS CHAR))));
-- V8 多币种:price_usd 与 price_origrate 一致(抽样)
SELECT COUNT() INTO @v8 FROM srch_product
WHERE ABS(price_usd - ROUND(price_orig * CASE currency_code WHEN 'CNY' THEN 1/7.2 WHEN 'USD' THEN 1 WHEN 'EUR' THEN 1.08 WHEN 'JPY' THEN 0.0067 WHEN 'HKD' THEN 1/7.8 WHEN 'GBP' THEN 1.27 END,2)) < 0.02;
SELECT COUNT(*) INTO @v8tot FROM srch_product;
CALL srch_v('V8_多币种USD归一一致', IF(@v8=@v8tot,1,0), CONCAT('一致=',@v8,'/',@v8tot));
-- V9 覆盖索引存在
SELECT COUNT(*) INTO @v9 FROM information_schema.statistics WHERE table_schema='JewelryAnalytics' AND table_name='srch_product' AND index_name='ix_cover';
CALL srch_v('V9_覆盖索引存在', IF(@v9>=1,1,0), CONCAT('ix_cover出现=',@v9));
-- V10 错误探针:MATCH 列集合必须与 FULLTEXT 索引列一致 → errno 1191
CALL srch_err_probe('FT列集合不一致', 'SELECT COUNT(*) FROM srch_product WHERE MATCH(product_name) AGAINST(''钻石'' IN NATURAL LANGUAGE MODE)');
SELECT IFNULL((SELECT errno FROM srch_demo_probe WHERE label='FT列集合不一致' ORDER BY id DESC LIMIT 1),0) INTO @v10;
CALL srch_v('V10_全文列集合报错1191', IF(@v10=1191,1,0), CONCAT('捕获errno=',@v10));
-- V11 幂等:数据量稳定 = 100000
SELECT COUNT(*) INTO @v11 FROM srch_product;
CALL srch_v('V11_数据量稳定100000', IF(@v11=100000,1,0), CONCAT('product行数=',@v11));
SELECT '==== 自检汇总:以下应全部 passed=1 ====' AS -- 自检 --;
SELECT check_id, passed, detail FROM srch_demo_result ORDER BY check_id;
-- §11 实测总表与结论
SELECT '§11.1 引擎真实读行对照(INNODB_ROWS_READ 增量 / 回表 / 耗时ms)' AS -- 第11章 --;
SELECT tag, innodb_rows AS 引擎读行, rnd_next AS 回表_next, rnd AS 随机回表, ms AS 耗时ms
FROM srch_demo_metric ORDER BY tag;
SELECT '§11.2 自检结果(全部 passed=1 为通过)' AS -- 第11章 --;
SELECT check_id, passed, detail FROM srch_demo_result ORDER BY check_id;
SELECT '§11.3 关键结论' AS -- 第11章 --;
SELECT '1) 前导通配 LIKE ''%x%'' = 全表扫描;前缀 ''x%'' 才走 B-tree。大数据量严禁前导通配。' AS 结论1;
SELECT '2) 函数索引 ((LOWER(c)))/((DATE(c))) 让「列被函数包裹」也能索引定位;没建对应函数索引才会全扫。' AS 结论2;
SELECT '3) 中文全文必须 WITH PARSER ngram;MATCH...AGAINST 既检索又给相关性分值,可 ORDER BY 排序。' AS 结论3;
SELECT '4) 全文读行远少于 LIKE 前导通配(实测约 1/10),高并发下差距随数据量放大。' AS 结论4;
SELECT '5) 覆盖索引让投影列与搜索列同进索引,免回表;这是万亿级降 IO 的关键。' AS 结论5;
SELECT '6) 时区:存 UTC,展示用 TIMESTAMPADD(本机 CONVERT_TZ 失效);DST 秋令时按本地会乱,必须 UTC。' AS 结论6;
SELECT '7) 多币种:按归一化 price_usd 过滤/排序,绝不可按 price_orig 跨币种。' AS 结论7;
SELECT '8) 万亿/高并发:ngram FULLTEXT + 覆盖/函数索引 + 分区 + 分片 + 读写分离;极限下沉 ES。' AS 结论8;
SELECT '§11.4 八条方法清单(落到建表/查询)' AS -- 第11章 --;
SELECT '① 搜索列建 ngram FULLTEXT(中文);② 前缀检索用 B-tree;③ 大小写/派生列建函数索引;' AS 清单;
SELECT '④ 投影列纳入覆盖索引;⑤ 时间存 UTC、范围查询走索引;⑥ 金额归一 price_usd;' AS 清单;
SELECT '⑦ 热路径禁 LIKE ''%x%'';⑧ 极限场景搜索下沉 ES,MySQL 做精确过滤+取详情(CQRS)。' AS 清单;
SELECT '§11.5 脚本结构' AS -- 第11章 --;
SELECT '_s00 头部/速览/反模式;_s01 度量框架;_s02 建表造数;_s03 LIKE;_s04 函数索引;' AS 结构;
SELECT '_s05 全文;_s06 相关性排序;_s07 取舍/万亿;_s08 六域;_s09 时区/多币种;_s10 自检;_s11 总结。' AS 结构;