轻松聘AI开发
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

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(占位符形式):

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