# Q01 MySQL 执行计划证据(由 JobSearchQueryExplainMySqlIntegrationTest 生成) - 数据库:`mysql:8.4` Testcontainers 独立容器,正式 Flyway 迁移,`ddl-auto=validate` - EXPLAIN 形式:`EXPLAIN FORMAT=JSON`,与 Criteria 路径绑定同一组参数 - 夹具规模:每个城市分别 1 / 20 / 50 条 PUBLISHED 岗位(刻意做小,用于验证语义而非压测) - Q01 不新增索引;执行计划问题记录给 R01 ## 1. 组合过滤 + latest 分页 Hibernate 实际生成并执行的 SQL(占位符形式): ```sql select j1_0.id,j1_0.city,j1_0.company_id,j1_0.created_at,j1_0.description,j1_0.employment_type,j1_0.published_at,j1_0.requirements,j1_0.salary_max,j1_0.salary_min,j1_0.status,j1_0.title,j1_0.updated_at from jobs j1_0 where j1_0.status=? and (lower(j1_0.title) like ? escape '' or lower(j1_0.description) like ? escape '' or lower(j1_0.requirements) like ? escape '') and lower(j1_0.city)=? and lower(j1_0.employment_type)=? and j1_0.salary_max>=? and j1_0.salary_min<=? and j1_0.company_id=? order by j1_0.created_at desc,j1_0.id desc limit ?,? select count(j1_0.id) from jobs j1_0 where j1_0.status=? and (lower(j1_0.title) like ? escape '' or lower(j1_0.description) like ? escape '' or lower(j1_0.requirements) like ? escape '') and lower(j1_0.city)=? and lower(j1_0.employment_type)=? and j1_0.salary_max>=? and j1_0.salary_min<=? and j1_0.company_id=? ``` 等价 SQL(绑定参数 [PUBLISHED, %q01e75001岗位%, %q01e75001岗位%, %q01e75001岗位%, Q01E75001CITY20, FULL_TIME, 5000, 30000, 1, 20, 0]): ```sql SELECT j.id FROM jobs j WHERE j.status = ? AND (lower(j.title) LIKE ? OR lower(j.description) LIKE ? OR lower(j.requirements) LIKE ?) AND lower(j.city) = ? AND lower(j.employment_type) = ? AND j.salary_max >= ? AND j.salary_min <= ? AND j.company_id = ? ORDER BY j.created_at DESC, j.id DESC LIMIT ? OFFSET ? ``` 计划摘要: ``` query_cost = 1.35 using_filesort = true using_temporary_table = false - 表 query_block/ORDER BY: table_name="j", access_type="ALL", possible_keys=["idx_jobs_company_status","idx_jobs_status_created","idx_jobs_city_status","idx_jobs_status_employment_type","idx_jobs_status_salary_max"], rows_examined_per_scan=1, rows_produced_per_join=1, filtered="100.00" ```
EXPLAIN FORMAT=JSON 原文 ```json { "query_block": { "select_id": 1, "cost_info": { "query_cost": "1.35" }, "ordering_operation": { "using_filesort": true, "cost_info": { "sort_cost": "1.00" }, "table": { "table_name": "j", "access_type": "ALL", "possible_keys": [ "idx_jobs_company_status", "idx_jobs_status_created", "idx_jobs_city_status", "idx_jobs_status_employment_type", "idx_jobs_status_salary_max" ], "rows_examined_per_scan": 1, "rows_produced_per_join": 1, "filtered": "100.00", "cost_info": { "read_cost": "0.25", "eval_cost": "0.10", "prefix_cost": "0.35", "data_read_per_join": "1K" }, "used_columns": [ "id", "company_id", "title", "description", "requirements", "city", "employment_type", "salary_min", "salary_max", "status", "created_at" ], "attached_condition": "((`mydreamjob_q01`.`j`.`company_id` = 1) and (`mydreamjob_q01`.`j`.`status` = 'PUBLISHED') and ((lower(`mydreamjob_q01`.`j`.`title`) like '%q01e75001岗位%') or (lower(`mydreamjob_q01`.`j`.`description`) like '%q01e75001岗位%') or (lower(`mydreamjob_q01`.`j`.`requirements`) like '%q01e75001岗位%')) and (lower(`mydreamjob_q01`.`j`.`city`) = 'q01e75001city20') and (lower(`mydreamjob_q01`.`j`.`employment_type`) = 'full_time') and (`mydreamjob_q01`.`j`.`salary_max` >= 5000) and (`mydreamjob_q01`.`j`.`salary_min` <= 30000))" } } } } ```
## 2. latest 分页 等价 SQL(绑定参数 [PUBLISHED, Q01E75001CITY20, 20, 0]): ```sql SELECT j.id FROM jobs j WHERE j.status = ? AND lower(j.city) = ? ORDER BY j.created_at DESC, j.id DESC LIMIT ? OFFSET ? ``` 计划摘要: ``` query_cost = 1.35 using_filesort = true using_temporary_table = false - 表 query_block/ORDER BY: table_name="j", access_type="ALL", possible_keys=["idx_jobs_status_created","idx_jobs_city_status","idx_jobs_status_employment_type","idx_jobs_status_salary_max"], rows_examined_per_scan=1, rows_produced_per_join=1, filtered="100.00" ```
EXPLAIN FORMAT=JSON 原文 ```json { "query_block": { "select_id": 1, "cost_info": { "query_cost": "1.35" }, "ordering_operation": { "using_filesort": true, "cost_info": { "sort_cost": "1.00" }, "table": { "table_name": "j", "access_type": "ALL", "possible_keys": [ "idx_jobs_status_created", "idx_jobs_city_status", "idx_jobs_status_employment_type", "idx_jobs_status_salary_max" ], "rows_examined_per_scan": 1, "rows_produced_per_join": 1, "filtered": "100.00", "cost_info": { "read_cost": "0.25", "eval_cost": "0.10", "prefix_cost": "0.35", "data_read_per_join": "1K" }, "used_columns": [ "id", "city", "status", "created_at" ], "attached_condition": "((`mydreamjob_q01`.`j`.`status` = 'PUBLISHED') and (lower(`mydreamjob_q01`.`j`.`city`) = 'q01e75001city20'))" } } } } ```
## 3. salary 排序(salaryMax DESC NULLS LAST) 等价 SQL(绑定参数 [PUBLISHED, Q01E75001CITY20, 20, 0]): ```sql SELECT j.id FROM jobs j WHERE j.status = ? AND lower(j.city) = ? ORDER BY CASE WHEN j.salary_max IS NULL THEN 1 ELSE 0 END ASC, j.salary_max DESC, j.id DESC LIMIT ? OFFSET ? ``` 计划摘要: ``` query_cost = 1.35 using_filesort = true using_temporary_table = false - 表 query_block/ORDER BY: table_name="j", access_type="ALL", possible_keys=["idx_jobs_status_created","idx_jobs_city_status","idx_jobs_status_employment_type","idx_jobs_status_salary_max"], rows_examined_per_scan=1, rows_produced_per_join=1, filtered="100.00" ```
EXPLAIN FORMAT=JSON 原文 ```json { "query_block": { "select_id": 1, "cost_info": { "query_cost": "1.35" }, "ordering_operation": { "using_filesort": true, "cost_info": { "sort_cost": "1.00" }, "table": { "table_name": "j", "access_type": "ALL", "possible_keys": [ "idx_jobs_status_created", "idx_jobs_city_status", "idx_jobs_status_employment_type", "idx_jobs_status_salary_max" ], "rows_examined_per_scan": 1, "rows_produced_per_join": 1, "filtered": "100.00", "cost_info": { "read_cost": "0.25", "eval_cost": "0.10", "prefix_cost": "0.35", "data_read_per_join": "1K" }, "used_columns": [ "id", "city", "salary_max", "status" ], "attached_condition": "((`mydreamjob_q01`.`j`.`status` = 'PUBLISHED') and (lower(`mydreamjob_q01`.`j`.`city`) = 'q01e75001city20'))" } } } } ```
## 4. count 查询 等价 SQL(绑定参数 [PUBLISHED, %q01e75001岗位%, %q01e75001岗位%, %q01e75001岗位%, Q01E75001CITY20, FULL_TIME, 5000, 30000, 1]): ```sql SELECT count(j.id) FROM jobs j WHERE j.status = ? AND (lower(j.title) LIKE ? OR lower(j.description) LIKE ? OR lower(j.requirements) LIKE ?) AND lower(j.city) = ? AND lower(j.employment_type) = ? AND j.salary_max >= ? AND j.salary_min <= ? AND j.company_id = ? ``` 计划摘要: ``` query_cost = 0.35 using_filesort = false using_temporary_table = false - 表 query_block: table_name="j", access_type="ALL", possible_keys=["idx_jobs_company_status","idx_jobs_status_created","idx_jobs_city_status","idx_jobs_status_employment_type","idx_jobs_status_salary_max"], rows_examined_per_scan=1, rows_produced_per_join=1, filtered="100.00" ```
EXPLAIN FORMAT=JSON 原文 ```json { "query_block": { "select_id": 1, "cost_info": { "query_cost": "0.35" }, "table": { "table_name": "j", "access_type": "ALL", "possible_keys": [ "idx_jobs_company_status", "idx_jobs_status_created", "idx_jobs_city_status", "idx_jobs_status_employment_type", "idx_jobs_status_salary_max" ], "rows_examined_per_scan": 1, "rows_produced_per_join": 1, "filtered": "100.00", "cost_info": { "read_cost": "0.25", "eval_cost": "0.10", "prefix_cost": "0.35", "data_read_per_join": "1K" }, "used_columns": [ "id", "company_id", "title", "description", "requirements", "city", "employment_type", "salary_min", "salary_max", "status" ], "attached_condition": "((`mydreamjob_q01`.`j`.`company_id` = 1) and (`mydreamjob_q01`.`j`.`status` = 'PUBLISHED') and ((lower(`mydreamjob_q01`.`j`.`title`) like '%q01e75001岗位%') or (lower(`mydreamjob_q01`.`j`.`description`) like '%q01e75001岗位%') or (lower(`mydreamjob_q01`.`j`.`requirements`) like '%q01e75001岗位%')) and (lower(`mydreamjob_q01`.`j`.`city`) = 'q01e75001city20') and (lower(`mydreamjob_q01`.`j`.`employment_type`) = 'full_time') and (`mydreamjob_q01`.`j`.`salary_max` >= 5000) and (`mydreamjob_q01`.`j`.`salary_min` <= 30000))" } } } ```
## 5. 结果规模 1 / 20 / 50 下的 SQL 语句条数 Spring Data 对首页有优化:首页返回行数小于 `pageSize` 时不再发 count 查询, 因此结果数 < pageSize 时为 1 条(仅数据查询),否则为 2 条(count + 数据查询)。 两种情况下语句条数都**不随结果规模增长**,即不存在 N+1。 | 匹配结果数 | 返回行数 | totalElements | Repository SQL数 | 完整Service SQL数 | |---|---|---|---|---| | 1 | 1 | 1 | 1 | 2 | | 20 | 20 | 20 | 2 | 3 | | 50 | 20 | 50 | 2 | 3 |