SQL公用表表达式(CTE)技术深度解析:考勤与生产数据分析实战

70 阅读22分钟

SQL公用表表达式(CTE)技术深度解析:考勤与生产数据分析实战

公用表表达式(CTE)作为SQL查询中的强大工具,通过模块化设计和临时结果集的高效利用,显著提升了复杂查询的可读性和维护性。在考勤统计与生产数据分析这类多表关联、多层次计算的场景中,CTE技术能够将复杂逻辑分解为清晰的步骤,使查询结构更加直观。本文基于实际生产环境中处理破碎车间考勤与绩效统计的SQL代码案例,全面解析CTE技术的核心概念、语法结构、应用场景及性能优化技巧。

一、CTE基本概念与语法结构

公用表表达式(CTE)是SQL查询中定义的临时命名结果集,通过WITH关键字引入,允许在单个查询中多次引用,且不会实际存储到数据库中。CTE的语法结构简洁明了,通常由三部分组成:表达式名称、列别名(可选)和查询定义。例如,WITH cte_name AS (SELECT column1 FROM table1) SELECT * FROM cte_name;。与传统子查询相比,CTE具有以下优势:

可读性提升:CTE将复杂查询分解为逻辑块,每个CTE都有清晰的名称,使代码结构更清晰。相比之下,多层嵌套的子查询很快会变得难以阅读和调试。Meta面试题的解答案例表明,CTE使复杂查询的可读性显著提升,即使面对需要跨多个表和多次计算的场景,也能保持代码的简洁性。

重用性增强:CTE可以在同一查询中多次引用,避免了重复计算和代码冗余。在需要多次使用相同中间结果的场景中,CTE比子查询更高效。例如,用户代码中ActualWork的考勤统计结果被主查询多次引用,避免了重复计算。

递归查询支持:CTE支持递归查询,这是处理树形结构和层次数据的强大工具。虽然用户代码未涉及递归,但在其他场景如组织架构查询、菜单层级结构等中,递归CTE提供了简洁而高效的解决方案。

代码模块化:CTE使得复杂查询可以按功能拆分为多个模块,每个模块负责特定任务,大大提高了代码的可维护性。当查询需要修改时,只需调整对应的CTE,而无需在整个查询中查找和替换,这在大型企业应用中尤为重要。

二、代码中CTE的逻辑层次与数据流动

用户提供的SQL代码通过三个CTE构建了一个完整的考勤与生产数据分析流程,每个CTE都有明确的功能定位,并形成数据处理的流水线:

StaffDailyHours CTE作为基础层,从考勤表KQ破碎考勤流水视图中提取数据。该CTE使用CAST(kq.day AS DATE)将考勤时间转换为纯日期,按员工和日期分组统计每日工作小时。WHERE子句限制了查询范围为最近30天的考勤数据,确保只处理相关数据。此CTE是整个查询的起点,为后续处理提供了标准化的考勤数据

WITH StaffDailyHours AS (
    SELECT 
        CAST(kq.day AS DATE) AS DDate,
        kq.StaffName,
        SUM(kq.上班小时) AS DailyHours
    FROM KQ破碎考勤流水视图 kq
    WHERE kq.day BETWEEN @startDate AND @endDate
    GROUP BY CAST(kq.day AS DATE), kq.StaffName
)

StaffHoursWithShift CTE作为中间层,通过多表关联补充班次信息。首先与搬运破碎工班次表_明细关联,筛选出非"废带分拣工"的员工;然后与破碎班次表_明细关联,确保考勤日期在班次的有效期内。此CTE的核心是将员工考勤数据与其所属班次进行精确匹配,为后续的绩效计算提供基础。

StaffHoursWithShift AS (
    SELECT 
        s.DDate,
        s.StaffName,
        s.DailyHours,
        c.绩效班次 AS Shift
    FROM StaffDailyHours s
    INNER JOIN 搬运破碎工班次表_明细 b 
        ON s.StaffName = b.员工姓名
        AND b.工作品种 != '废带分拣工'
    INNER JOIN 破碎班次表_明细 c 
        ON b.所属班次 = c.班次
        AND s.DDate BETWEEN c.所属日期 AND c.结束日期
)

ActualWork CTE作为汇总层,按日期和班次汇总人数和总工时。使用COUNT(DISTINCT StaffName)统计实际出勤人数,避免重复计数;使用SUM(DailyHours)计算班次总工时。此CTE完成了考勤数据的最终整理,为后续的绩效计算和工资核算提供了结构化的数据

ActualWork AS (
    SELECT 
        DDate,
        Shift,
        COUNT(DISTINCT StaffName) AS ActualWorkNumber,
        SUM(DailyHours) AS TotalStaffHours
    FROM StaffHoursWithShift
    GROUP BY DDate, Shift
)

这三层CTE构成了一条清晰的数据处理流水线:从原始考勤数据出发,经过班次信息补充,最终形成汇总的考勤统计。这种模块化设计使查询逻辑易于理解,也便于后续的维护和修改。

三、CTE在复杂查询中的应用场景

CTE技术在处理复杂查询时展现出强大的应用价值,尤其适合以下场景:

多表关联与数据清洗:在需要从多个表中提取数据并进行清洗的场景中,CTE提供了分步处理的机制。用户代码中的班次匹配就是一个典型示例,通过CTE分步处理关联和条件筛选,避免了复杂的嵌套子查询。

中间结果复用:当同一个中间结果需要被多次引用时,CTE比子查询更高效。用户代码中ActualWork的考勤统计结果被主查询多次引用,用于计算不同部门的绩效和工资,这种复用避免了重复计算。

分段计算与逻辑拆分:复杂的计算可以分段进行,每个CTE负责一个计算阶段。例如,用户代码中的生产检验单重量统计和计时工作记录分别通过独立的CTE处理,最终在主查询中合并,使整个计算过程更加清晰。

与窗口函数结合:CTE可以与窗口函数结合,实现更复杂的分析计算。虽然用户代码中未使用窗口函数,但在实际应用中,可以使用CTE预处理数据,然后在窗口函数中进行排名、累计等计算。

递归查询:处理树形结构和层级关系数据时,递归CTE提供了简洁的解决方案。例如,查询员工的上下级关系或菜单的层级结构时,递归CTE比传统方法更高效。

在用户代码中,CTE技术被用于构建一个完整的考勤与生产数据分析系统,包括考勤统计、生产检验单重量计算、计时工作记录处理等模块。这种应用方式充分体现了CTE在复杂查询中的核心价值:将复杂逻辑分解为可管理的步骤,提高代码可读性和维护性

四、CTE的执行计划与性能优化

尽管CTE主要是一种结构优化工具,但在某些情况下也可以显著提升查询性能。理解CTE的执行计划是优化查询的关键:

执行顺序:CTE按定义顺序执行,其结果集对后续的CTE和主查询可见。在用户代码中,StaffDailyHours首先执行,然后是StaffHoursWithShift,最后是ActualWork,形成数据处理的流水线。

物化策略:CTE是否会被物化为临时表取决于数据库优化器的决策。SQL Server等数据库系统会对被多次引用的CTE自动物化,以避免重复计算。用户代码中ActualWork的结果被主查询多次使用,因此优化器可能会物化此CTE。

性能优化技巧

  1. 索引优化:为高频使用的关联字段和筛选条件创建索引。例如,为员工姓名班次所属日期等字段创建索引,可以显著提高关联速度。在破碎班次表_明细中,创建复合索引(班次, 所属日期)将有助于快速查找匹配的班次。

  2. JOIN顺序优化:优先连接小表再连接大表,减少中间结果数据量。用户代码中先连接搬运破碎工班次表_明细(假设较小)再连接考勤表(可能较大)的策略是合理的。

  3. 避免全表扫描:确保在WHERE子句中使用索引列,避免对索引列使用函数或计算。用户代码中对day字段使用CAST可能导致索引失效,可以考虑在表中直接存储日期部分或使用覆盖索引。

  4. 物化控制:通过MATERIALIZED提示强制物化CTE,适用于大数据量和需要复用中间结果的场景。例如,在SQL Server中可以使用WITH (MATERIALIZED)强制物化ActualWorkCTE。

  5. 统计信息维护:确保数据库中的统计信息是最新的,以便优化器能够做出正确的执行计划。对于经常更新的表,可以手动更新统计信息:UPDATE STATISTICS KQ破碎考勤流水视图;

注意事项

  1. 数据完整性:用户代码中使用INNER JOIN可能导致未关联到班次表的员工数据丢失。需要根据业务需求判断是否应该使用LEFT JOIN保留所有考勤数据。

  2. 日期范围覆盖:确保破碎班次表_明细中的所属日期结束日期能够正确覆盖考勤日期范围。如果班次表的日期范围设置不当,可能导致考勤数据与班次信息匹配错误。

  3. 字段传递一致性:CTE之间传递的字段(如DDateShift)需要类型匹配,并确保关联条件正确。用户代码中通过CAST统一日期格式的做法是正确的。

  4. 执行计划分析:使用EXPLAIN或图形化工具(如SQL Server Management Studio)分析CTE的执行计划,定位高开销操作。例如,可以查看破碎班次表_明细的关联是否使用了索引,或者是否存在不必要的排序操作。

  5. 避免过深嵌套:虽然用户代码中CTE未嵌套,但需注意CTE的嵌套层级不宜过深。SQL Server等系统对CTE的嵌套层级有限制(如64层),过深嵌套可能导致查询性能下降。

五、CTE在工资计算场景中的实际应用

用户代码中的CTE技术展示了如何处理考勤与绩效数据的关联计算,这种模式在工资计算场景中具有广泛应用价值:

绩效计算模块化:通过BreakingStrapPerformanceBreakingFilmPerformanceOverIronPerformance等字段,将不同部门的绩效计算分解为独立的逻辑块。每个绩效计算都包含基本单价、人数百分比调整后的实际单价,以及与生产重量的乘积,形成了清晰的绩效计算流水线。

COALESCE(pd.单价, 0) * COALESCE(pd_percent.百分比/100, 1) * COALESCE(p.破带重量, 0) AS BreakingStrapPerformance,

计时工作记录整合:通过TimingWorkCTE独立处理计时工作记录,然后在主查询中通过LEFT JOIN与考勤数据合并。这种设计使计时工资计算与考勤绩效计算相互独立,提高了代码的可维护性。

TimingWork AS (
    SELECT 
        CAST(t.开始时间 AS DATE) AS DDate,
        t.班次 AS Shift,
        t.任务类型,
        t.计时人数,
        t.计时时数,
        t.单价
    FROM 破碎计时工作记录表_主表 t
    WHERE t.开始时间 BETWEEN @startDate AND @endDate
)

总工资计算:将不同部门的绩效和计时工资整合为总工资,通过CTE的中间结果复用避免了重复计算。用户代码中将各部门绩效和计时工资相加,然后除以总工时计算"每小时工资",这种设计清晰展示了工资计算的逻辑。

(COALESCE(pd.单价, 0) * COALESCE(pd_percent.百分比/100, 1) * COALESCE(p.破带重量, 0))+
(COALESCE(pm.单价, 0) * COALESCE(pm_percent.百分比/100, 1) * COALESCE(p.破膜重量, 0))+
(COALESCE(pt.单价, 0) * COALESCE(pt_percent.百分比/100, 1) * COALESCE(p.过铁重量, 0))+
COALESCE(tw.单价, 0)
AS TotalSalary,

人数百分比调整:通过BreakingStrapActualUnitPrice等字段,实现了根据实际出勤人数调整单价的计算逻辑。这种动态调整在绩效工资计算中非常常见,CTE使这一复杂逻辑变得清晰易懂。

COALESCE(pd.单价 * COALESCE(pd_percent.百分比/100, 1), 0) AS BreakingStrapActualUnitPrice,

零值防御与默认值:用户代码中大量使用COALESCE函数处理可能的空值,确保计算结果的完整性。例如,COALESCE(tw.计时人数, 0)将空值转换为0,避免了计算错误。

COALESCE(tw.计时人数, 0) AS TimingPeople,

最终结果排序:通过ORDER BY a.DDate, a.Shift对结果进行排序,使输出数据更加有序和易于分析。这种排序在报表生成和数据分析中非常重要。

这种应用模式展示了CTE技术在工资计算中的核心价值:通过模块化设计将复杂计算分解为可管理的步骤,同时利用中间结果复用提高查询效率。在实际应用中,可以进一步扩展这种模式,例如加入税收计算、社保扣除等模块,构建完整的工资核算系统。

六、CTE与其他查询技术的对比与选择

CTE并非适用于所有场景,了解其与其他查询技术的优缺点对比有助于做出合理选择:

与子查询对比

特性CTE子查询
可读性更高(模块化设计)较低(嵌套结构复杂)
重用性可多次引用通常只能使用一次
递归能力支持递归查询不支持递归查询
执行性能与子查询相近,但可减少重复计算可能因重复执行导致性能下降

在用户代码中,CTE的使用明显提升了查询的可读性和维护性。例如,考勤统计和生产检验单重量计算被拆分为独立的CTE,使整个查询结构更加清晰。对于复杂查询,CTE的可读性优势尤为明显,即使查询性能与子查询相近,其结构优势也值得优先考虑

与临时表对比

特性CTE临时表
作用范围仅限当前查询可跨多个查询使用
维护成本无需维护需要创建和清理
性能影响通常与子查询相近可能因临时表操作增加开销
灵活性更灵活(可递归、可复用)较低(结构固定)

CTE在大多数情况下可以替代临时表,特别是在需要模块化设计且中间结果仅在当前查询中使用的情况下。用户代码中的CTE设计避免了临时表的创建和清理开销,同时保持了良好的可读性。然而,对于非常复杂的查询或需要跨多个查询使用中间结果的场景,临时表可能仍是更好的选择。

与视图对比

特性CTE视图
作用范围仅限当前查询可被多个查询引用
参数化支持(可引用变量)不支持参数化
执行效率与子查询相近可能因视图的物化而增加开销
可维护性高(每个查询独立)高(集中维护)

CTE的优势在于其参数化能力和与当前查询的紧密集成,这在用户代码中得到了充分体现——通过变量@startDate@endDate定义日期范围,然后在所有CTE中使用这些变量。这种设计使查询更加灵活,能够轻松调整日期范围而不必修改每个CTE的定义。相比之下,视图无法直接引用变量,需要通过参数化视图或动态SQL实现类似功能,增加了复杂度。

七、CTE技术的最佳实践与安全建议

基于用户代码的分析和CTE技术的特性,以下是使用CTE的最佳实践和安全建议:

命名规范:为CTE赋予清晰、有描述性的名称,反映其功能。用户代码中的StaffDailyHoursStaffHoursWithShift等名称就非常清晰,立即让人明白每个CTE的用途。

关联条件明确:确保JOIN条件和WHERE子句中的条件明确无歧义。用户代码中所属班别 = c.班次s.DDate BETWEEN c.所属日期 AND c.结束日期等条件清晰指定了关联逻辑,避免了潜在的数据匹配错误。

数据完整性保障:根据业务需求选择合适的JOIN类型。用户代码中StaffHoursWithShift使用INNER JOIN确保只有有效关联的班次数据才被保留,这在考勤统计中是合理的。但在某些场景下(如保留所有考勤记录),可能需要使用LEFT JOIN

字段传递一致性:确保CTE之间传递的字段类型和名称一致。用户代码中DDateShift字段在CTE间保持一致,确保了数据对齐的准确性。

避免过深嵌套:虽然用户代码中的CTE未嵌套,但需注意CTE的嵌套层级不宜过深。SQL Server等系统对CTE的嵌套层级有限制(如64层),过深嵌套可能导致查询性能下降。

性能调优策略

  1. 索引优化:为关联字段(如员工姓名班次)和筛选条件(如day字段)创建索引,特别是复合索引。例如,破碎班次表_明细班次所属日期字段可以创建复合索引。

  2. 查询重写:对于性能敏感的查询,可以考虑将CTE转换为临时表或物化视图,特别是当CTE的结果集较大且需要多次引用时。

  3. 统计信息维护:定期更新表的统计信息,确保优化器能够生成高效的执行计划。对于频繁更新的表,可以手动更新统计信息。

  4. 执行计划分析:使用EXPLAIN或图形化工具分析CTE的执行计划,定位高开销操作。例如,可以查看破碎班次表_明细的关联是否使用了索引。

数据安全与权限控制:CTE本身不提供安全机制,但可以通过限制对基础表的访问权限来保护数据。例如,确保只有授权用户才能访问考勤表和班次表。

版本兼容性:CTE在SQL Server 2005及更高版本中支持,但在不同数据库系统中可能存在语法差异。例如,PostgreSQL支持RECURSIVE关键字定义递归CTE,而MySQL 8.0+也支持CTE,但某些功能可能受限。

递归查询限制:如果后续扩展需要递归查询,应设置合理的递归深度限制,避免无限递归。在SQL Server中可以通过OPTION (MAXRECURSION n)设置最大递归次数。

八、总结与扩展应用

公用表表达式(CTE)技术通过模块化设计和中间结果复用,为处理复杂查询提供了强大工具。在考勤统计与生产数据分析这类多表关联、多层次计算的场景中,CTE技术能够显著提升查询的可读性和维护性,同时在某些情况下也能提高查询性能。

用户代码展示了一个完整的CTE应用案例:从考勤数据统计到班次信息关联,再到生产检验单重量计算,最后整合计时工作记录和单价信息,计算总工资和每小时工资。这种模块化设计使每个计算步骤都清晰可见,便于理解和维护。

CTE技术的扩展应用包括:递归查询处理组织架构、菜单层级结构;与窗口函数结合进行排名、累计计算;多步骤数据清洗和转换;以及构建复杂的分析报表。在工资计算场景中,CTE可以进一步扩展为处理税收、社保、奖金等模块,构建完整的工资核算系统。

在性能优化方面,除了索引优化和执行计划分析,还应注意:避免在CTE中进行不必要的复杂计算;根据数据量大小决定是否物化CTE;对于大数据量查询,可以考虑将CTE转换为临时表或分步处理。

最终,CTE技术的价值不仅体现在查询性能上,更体现在代码可读性和维护性上。在处理复杂业务逻辑时,CTE能够帮助开发者构建清晰、可维护的查询,减少代码冗余,提高开发效率。

九、示例代码参考

-- 定义日期范围
DECLARE @endDate DATE = GETDATE();
DECLARE @startDate DATE = DATEADD(DAY, -30, @endDate);

-- CTE 1: 实际工作人数统计
WITH StaffDailyHours AS (
    -- 按员工和日期统计每日上班小时
    SELECT 
        CAST(kq.day AS DATE) AS DDate,
        kq.StaffName,
        SUM(kq.上班小时) AS DailyHours
    FROM KQ破碎考勤流水视图 kq
    WHERE kq.day BETWEEN @startDate AND @endDate
    GROUP BY CAST(kq.day AS DATE), kq.StaffName
),
StaffHoursWithShift AS (
    -- 关联班次信息,确定员工所属班次
    SELECT 
        s.DDate,
        s.StaffName,
        s.DailyHours,
        c.绩效班次 AS Shift
    FROM StaffDailyHours s
    INNER JOIN 搬运破碎工班次表_明细 b 
        ON s.StaffName = b.员工姓名
		AND b.工作品种 != '废带分拣工'
    INNER JOIN 破碎班次表_明细 c 
        ON b.所属班别 = c.班次
        AND s.DDate BETWEEN c.所属日期 AND c.结束日期
),

ActualWork AS (
    -- 按日期和班次汇总人数和总小时数
    SELECT 
        DDate,
        Shift,
        COUNT(DISTINCT StaffName) AS ActualWorkNumber,
        SUM(DailyHours) AS TotalStaffHours
    FROM StaffHoursWithShift
    GROUP BY DDate, Shift
),

-- CTE 2: 生产检验单净重统计(按部门拆分)
ProductionWeight AS (
    SELECT 
        CAST(p.生产日期 AS DATE) AS DDate,
        c.绩效班次 AS Shift,
        SUM(CASE WHEN c.所属部门 = '废带破碎部' THEN m.净重/1000 ELSE 0 END) AS 破带重量,
        SUM(CASE WHEN c.所属部门 = '废膜破碎部' THEN m.净重/1000 ELSE 0 END) AS 破膜重量,
        SUM(CASE WHEN c.所属部门 = '原料过料' THEN m.净重/1000 ELSE 0 END) AS 过铁重量
    FROM 破碎生产检验单_主表 p
    INNER JOIN 破碎生产检验单原料标签_明细 m 
        ON p.ExcelServerRCID = m.ExcelServerRCID
    INNER JOIN 搬运破碎工班次表_明细 b 
        ON m.称重操作人 = b.员工姓名
    INNER JOIN 破碎班次表_明细 c 
        ON p.班次 = c.班次
        AND CAST(p.生产日期 AS DATE) BETWEEN c.所属日期 AND c.结束日期
    WHERE p.生产日期 BETWEEN @startDate AND @endDate
    GROUP BY CAST(p.生产日期 AS DATE), c.绩效班次
),

-- CTE 3: 破碎计时工作记录统计
TimingWork AS (
    SELECT 
        CAST(t.开始时间 AS DATE) AS DDate,
        t.班次 AS Shift,
        t.任务类型,
        t.计时人数,
        t.计时时数,
        t.单价
    FROM 破碎计时工作记录表_主表 t
    WHERE t.开始时间 BETWEEN @startDate AND @endDate
)

-- 最终结果:合并考勤人数与破带/破膜/过铁重量及对应单价,计时信息
SELECT 
    a.DDate AS DDate,
    a.Shift AS Shift,
    a.ActualWorkNumber AS ActualWorkNumber,
    COALESCE(p.破带重量, 0) AS BreakingStrapWeight,
    COALESCE(pd.单价, 0) AS BreakingStrapBasicUnitPrice,
    -- 破带实际单价计算
    COALESCE(pd.单价 * COALESCE(pd_percent.百分比/100, 1), 0) AS BreakingStrapActualUnitPrice,
    -- 破带绩效计算
    COALESCE(pd.单价, 0) * COALESCE(pd_percent.百分比/100, 1) * COALESCE(p.破带重量, 0) AS BreakingStrapPerformance,

    COALESCE(p.破膜重量, 0) AS BreakingFilmWeight,
    COALESCE(pm.单价, 0) AS BreakingFilmBasicUnitPrice,
    -- 破膜实际单价计算
    COALESCE(pm.单价 * COALESCE(pm_percent.百分比/100, 1), 0) AS BreakingFilmActualUnitPrice,
    -- 破膜绩效计算
    COALESCE(pm.单价, 0) * COALESCE(pm_percent.百分比/100, 1) * COALESCE(p.破膜重量, 0) AS BreakingFilmPerformance,

    COALESCE(p.过铁重量, 0) AS OverIronWeight,
    pt.单价 AS OverIronBasicUnitPrice,
    -- 过铁实际单价计算
    pt.单价 * COALESCE(pt_percent.百分比/100, 1) AS OverIronActualUnitPrice,
    -- 过铁绩效计算
    COALESCE(pt.单价, 0) * COALESCE(pt_percent.百分比/100, 1) * COALESCE(p.过铁重量, 0) AS OverIronPerformance,

    -- 计时类型、计时人数、计时时数、计时单价
    COALESCE(tw.任务类型, CAST('无' AS VARCHAR)) AS TaskType,
    COALESCE(tw.计时人数, 0) AS TimingPeople,
    COALESCE(tw.计时时数, 0) AS TimingHours,
    COALESCE(tw.单价, 0) AS TimingSalary,

    -- 计算总工资
    (COALESCE(pd.单价, 0) * COALESCE(pd_percent.百分比/100, 1) * COALESCE(p.破带重量, 0))+
    (COALESCE(pm.单价, 0) * COALESCE(pm_percent.百分比/100, 1) * COALESCE(p.破膜重量, 0))+
    (COALESCE(pt.单价, 0) * COALESCE(pt_percent.百分比/100, 1) * COALESCE(p.过铁重量, 0))+
    COALESCE(tw.单价, 0)
    AS TotalSalary,

    COALESCE(a.TotalStaffHours, 0) + COALESCE(tw.计时时数, 0) AS DutyPersonnelTotalWorkHours,

    -- 最终结果字段修改
    COALESCE(
        (
	        (COALESCE(pd.单价, 0) * COALESCE(pd_percent.百分比/100, 1) * COALESCE(p.破带重量, 0)) +
	        (COALESCE(pm.单价, 0) * COALESCE(pm_percent.百分比/100, 1) * COALESCE(p.破膜重量, 0)) +
	        (COALESCE(pt.单价, 0) * COALESCE(pt_percent.百分比/100, 1) * COALESCE(p.过铁重量, 0)) +
	        COALESCE(tw.单价, 0)
        ) 
        / NULLIF(COALESCE(a.TotalStaffHours, 0), 0),  -- 分母零值防御
        0  -- 默认值
    ) AS DutyPersonnelEveryHourSalary

    FROM ActualWork a
    LEFT JOIN ProductionWeight p 
        ON a.DDate = p.DDate AND a.Shift = p.Shift
    -- 匹配破带单价(类型=废带破碎部)
    LEFT JOIN 破碎类型单价表_明细 pd 
        ON pd.类型 = '废带破碎部'
        AND COALESCE(p.破带重量, 0) >= pd.最小 
        AND COALESCE(p.破带重量, 0) < pd.最大
    -- 匹配破膜单价(类型=废膜破碎部)
    LEFT JOIN 破碎类型单价表_明细 pm 
        ON pm.类型 = '废膜破碎部'
        AND COALESCE(p.破膜重量, 0) >= pm.最小 
        AND COALESCE(p.破膜重量, 0) < pm.最大
    -- 匹配过铁单价(类型=原料过铁)
    LEFT JOIN 破碎类型单价表_明细 pt 
        ON pt.类型 = '原料过铁'
        AND COALESCE(p.过铁重量, 0) >= pt.最小 
        AND COALESCE(p.过铁重量, 0) < pt.最大

    -- 人数百分比匹配
    LEFT JOIN 破碎类型人数百分比 pd_percent 
        ON pd_percent.类型 = '废带破碎部' 
        AND pd_percent.人数 = a.ActualWorkNumber
    LEFT JOIN 破碎类型人数百分比 pm_percent 
        ON pm_percent.类型 = '废膜破碎部' 
        AND pm_percent.人数 = a.ActualWorkNumber
    LEFT JOIN 破碎类型人数百分比 pt_percent 
        ON pt_percent.类型 = '原料过铁' 
        AND pt_percent.人数 = a.ActualWorkNumber

    -- 计时工作记录数据匹配
    LEFT JOIN TimingWork tw 
        ON a.DDate = tw.DDate AND a.Shift = tw.Shift

    ORDER BY a.DDate, a.Shift;