记录一次SQL优化的过程:从15秒到1秒的飞跃

面对5000万和3000万数据量的两张表,原始查询需要15秒,看看我们如何一步步优化到1秒内完成。

一、问题背景:一个看似简单的统计需求

最近在工作中,我遇到了一个典型的SQL性能问题。业务部门需要统计某个特定日期下,特定类型项目的参与工人数量。
听起来很简单,对吧?但问题是数据量很大:
ads_table_a表:5000万+条记录,存储工人考勤数据
ads_table_b表:3000万+条记录,存储项目信息
需要根据项目表的筛选条件,找到对应项目,再去工人表统计人数。

结果:初始业务查询结果15秒,优化后结果小于1秒。

二、最初的实现:直觉写法的代价

SELECT COUNT(DISTINCT a."PROJECT_WORKER_ID")
FROM SYSDBA."ads_table_a" a
INNER JOIN (
    SELECT DISTINCT "PROJECT_CODE"
    FROM SYSDBA."ads_table_b" t 
    WHERE t."WORK_DATE" = '2025-12-11'
      AND t."PRJ_STATUS" = '在建' 
      AND t."CATEGORY" IN ('房屋建筑工程', '市政公用工程') 
      AND t."AREA_CODE" LIKE '44%'
      AND t."AREA_CODE" NOT LIKE '440S%'
) b ON a."PROJECT_ID" = b."PROJECT_CODE"
WHERE a."ATTENDANCE_DATE" = 20251211;

这个查询在逻辑上完全正确,但执行时间长达15秒!虽然表上已经创建了索引,但性能仍然不理想。

下面对应每个表的数据

select count(*) from SYSDBA."ads_table_a";
select count(*) from SYSDBA."ads_table_b";

问题分析:为什么这么慢?

在开始优化前,我们先要理解问题所在。想象一下这样的场景:

你有两本厚厚的电话簿:

一本记录了5000万人的信息(工人表)

另一本记录了3000万个项目的详细信息(项目表)

你需要先找到所有符合条件的项目(比如在特定地区、特定类型的项目),然后找出这些项目中有哪些工人,最后统计不重复的工人数量。

如果按最初的写法,相当于:

1、在3000万记录的项目表中找出符合条件的项目(几千个)
2、拿着这几千个项目编号,去5000万记录的工人表中找匹配的记录
3、统计找到的工人数量

问题在于,数据库在执行时可能没有按照我们想象的最佳顺序来操作

三、第一次尝试:临时表方案

我首先想到的是”分而治之”的策略:先把中间结果存起来,再继续处理。

DROP TABLE IF EXISTS "TEMP_PROJECTS_FILTERED";
CREATE GLOBAL TEMPORARY TABLE "TEMP_PROJECTS_FILTERED" (
    "PROJECT_CODE" BIGINT PRIMARY KEY
) ON COMMIT PRESERVE ROWS;

-- 为临时表创建索引
CREATE INDEX "IDX_TEMP_PROJECTS" ON "TEMP_PROJECTS_FILTERED"("PROJECT_CODE");

-- 插入符合条件的项目(使用优化索引)
INSERT INTO "TEMP_PROJECTS_FILTERED"
SELECT DISTINCT project_code
FROM SYSDBA."ads_table_b" t 
WHERE t.work_date = '2025-12-11'
  AND t.prj_status = '在建' 
  AND t.category IN ('房屋建筑工程', '市政公用工程') 
  AND t.area_code LIKE '44%'
  AND t.area_code NOT LIKE '440S%';

SELECT COUNT(*) FROM "TEMP_PROJECTS_FILTERED";

-- 执行查询
SELECT COUNT(DISTINCT a.project_worker_id)
FROM SYSDBA."ads_table_a" a
INNER JOIN "TEMP_PROJECTS_FILTERED" b ON a.project_id = b."PROJECT_CODE"
WHERE a."ATTENDANCE_DATE" = 20251211;

行号     COUNT(*)            
---------- --------------------
1          7166

已用时间: 2.196(毫秒). 执行号:336760636.
SQL> SQL> 2   3   4   5   


行号     COUNT(DISTINCTA.PROJECT_WORKER_ID)
---------- ----------------------------------
1          550595

已用时间: 00:00:03.296. 执行号:336760637.

效果: 查询时间从15秒降到了5秒左右!

为什么有效?

把复杂的筛选操作拆分成两步

临时表的结果集很小(只有7166个项目),让后续关联变得简单

但是有个问题: 实际业务代码中,不允许这样拆分成多条SQL执行。我们需要一条SQL搞定所有事情。

四、第二次尝试:CTE(公用表表达式)

既然不能拆成多条SQL,那我就试试用CTE(可以理解为”临时的视图”)来实现同样的逻辑。

尝试一:直接转换(失败)

WITH filtered_projects AS (
    SELECT DISTINCT project_code
    FROM SYSDBA."ads_table_b" t 
    WHERE t.work_date = '2025-12-11'
      AND t.prj_status = '在建' 
      AND t.category IN ('房屋建筑工程', '市政公用工程') 
      AND t.area_code LIKE '44%'
      AND t.area_code NOT LIKE '440S%'
),
temp_counts AS (
    SELECT COUNT(*) as temp_count FROM filtered_projects
),
worker_counts AS (
    SELECT COUNT(DISTINCT a.project_worker_id) as worker_count
    FROM SYSDBA."ads_table_a" a
    INNER JOIN filtered_projects b ON a.project_id = b.project_code
    WHERE a.attendance_date = 20251211
)
SELECT 
    t.temp_count,
    w.worker_count
FROM temp_counts t, worker_counts w;

结果:又回到了15秒!CTE在这里并没有自动优化执行顺序。

三、尝试二:调整结构(部分成功)

WITH filtered_projects AS (
    SELECT DISTINCT project_code
    FROM SYSDBA."ads_table_b" t 
    WHERE t.work_date = '2025-12-11'
      AND t.prj_status = '在建' 
      AND t.category IN ('房屋建筑工程', '市政公用工程') 
      AND t.area_code LIKE '44%'
      AND t.area_code NOT LIKE '440S%'
),
temp_counts AS (
    SELECT COUNT(*) as temp_count FROM filtered_projects
),
filtered_worker AS (
    SELECT DISTINCT a.project_worker_id
    FROM SYSDBA."ads_table_a" a
    INNER JOIN filtered_projects b ON a.project_id = b.project_code
    WHERE a.attendance_date = 20251211
),
worker_counts AS (
    SELECT COUNT(*) as worker_count
    FROM filtered_worker a
)
SELECT 
    t.temp_count,
    w.worker_count
FROM temp_counts t, worker_counts w;

时间减少了,但依然未达到要求,还不够理想。我发现问题在于:数据库还是先关联了两张大表,然后再过滤。不过感觉方式是对的。所以考虑再次进行表拆分,把索引都用上,先过滤出结果再去关联表。

四、最终方案:控制执行顺序

通过前面的尝试,我意识到关键是要告诉数据库先做什么、后做什么。最终的优化方案如下:

WITH filtered_projects AS (
    SELECT DISTINCT project_code
    FROM SYSDBA."ads_table_b" t 
    WHERE t.work_date = '2025-12-11'
      AND t.prj_status = '在建' 
      AND t.category IN ('房屋建筑工程', '市政公用工程') 
      AND t.area_code LIKE '44%'
      AND t.area_code NOT LIKE '440S%'
),
temp_counts AS (
    SELECT COUNT(*) as temp_count FROM filtered_projects
),
filtered_worker_partitioned AS (
    SELECT a.project_worker_id, a.project_id
    FROM SYSDBA."ads_table_a" a
    WHERE a.attendance_date = 20251211
),
filtered_worker AS (
    SELECT DISTINCT a.project_worker_id
    FROM filtered_worker_partitioned a,
    filtered_projects b WHERE a.project_id = b.project_code
),
worker_counts AS (
    SELECT COUNT(*) as worker_count
    FROM filtered_worker a
)
SELECT 
    t.temp_count,
    w.worker_count
FROM temp_counts t, worker_counts w;

结果:查询时间小于1秒!

为什么最终方案这么快?

原始方案的问题:

工人表(5000万) JOIN 项目表(3000万) 
    ↓
在30亿次可能的组合中筛选
    ↓
结果集巨大,统计去重耗时

最终方案的思路:

第一步:项目表(3000万) → 筛选 → 7166个项目
第二步:工人表(5000万) → 按日期筛选 → 假设100万工人
第三步:7166个项目 JOIN 100万工人 → 结果集很小
第四步:统计去重 → 快速完成

关键点在于先过滤、后关联,把大数据集变成小数据集再做关联。

五、最后的话

SQL优化就像解谜游戏,需要耐心和策略。记住这几个原则:

减少数据量:尽可能早地过滤掉不需要的数据

利用索引:让数据库快速找到需要的数据

控制流程:告诉数据库先做什么、后做什么

This entry was posted in 应用. Bookmark the permalink.