You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
10 KiB
10 KiB
Q01 MySQL 执行计划证据(由 JobSearchQueryExplainMySqlIntegrationTest 生成)
- 数据库:
mysql:8.4Testcontainers 独立容器,正式 Flyway 迁移,ddl-auto=validate - EXPLAIN 形式:
EXPLAIN FORMAT=JSON,与 Criteria 路径绑定同一组参数 - 夹具规模:每个城市分别 1 / 20 / 50 条 PUBLISHED 岗位(刻意做小,用于验证语义而非压测)
- Q01 不新增索引;执行计划问题记录给 R01
1. 组合过滤 + latest 分页
Hibernate 实际生成并执行的 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]):
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 原文
{
"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]):
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 原文
{
"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]):
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 原文
{
"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]):
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 原文
{
"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 |