公司动态

Oracle Job调度从入门到精通:DBMS_JOB与DBMS_SCHEDULER实战指南

📅 2026/8/17 8:11:09
Oracle Job调度从入门到精通:DBMS_JOB与DBMS_SCHEDULER实战指南
1. 从一次深夜告警说起为什么我们需要关注Oracle Job凌晨两点手机突然震动一条数据库告警信息弹了出来“核心报表数据未按时生成业务方已投诉”。睡眼惺忪地连上服务器检查了一圈发现本该在凌晨1点自动运行的聚合计算脚本这次竟然悄无声息地“罢工”了。问题根源很快定位负责调度这个脚本的那个DBMS_JOB不知道什么原因状态变成了BROKEN。这已经不是第一次了手工创建的作业缺乏有效的监控和管理就像一颗定时炸弹不知道什么时候会给你来个“惊喜”。这次经历让我下定决心必须把Oracle数据库中的定时任务——也就是JOB——这套机制彻底搞明白、用规范。无论是传统的DBMS_JOB还是功能更强大的DBMS_SCHEDULER它们都是DBA和开发人员手中自动化运维的利器。从简单的数据清理、统计信息收集到复杂的ETL流程、业务报表生成JOB的身影无处不在。但利器用不好反而容易伤到自己。很多团队对JOB的使用停留在“能跑起来就行”的层面忽略了其状态监控、异常处理、依赖管理和资源控制最终导致像我一样在深夜被告警叫醒。所以这篇内容不是简单的语法罗列而是结合我多年踩坑、填坑的经验带你深入理解Oracle Job的创建、管理、监控以及排错的全过程。我们会从最基础的DBMS_JOB入手再深入到更现代的DBMS_SCHEDULER并重点探讨如何在生产环境中安全、可靠地使用它们。无论你是刚开始接触数据库运维的新手还是希望优化现有任务调度体系的老手相信这些实战细节都能给你带来直接的帮助。2. 基石篇深入理解传统的DBMS_JOB虽然Oracle早已推出了更先进的DBMS_SCHEDULER但大量遗留系统仍在广泛使用DBMS_JOB理解它是读懂历史、排查旧问题的关键。更重要的是它的核心概念——作业、提交、运行——是理解所有任务调度的基础。2.1 DBMS_JOB的核心机制与关键参数DBMS_JOB的本质是一个存储在数据库中的任务队列。当你提交一个作业(SUBMIT)时并不是立即执行而是由后台的CJQ0进程协调作业队列进程和JNNN进程作业队列从属进程来协同调度。SNP进程在较老版本中负责此功能这是一个重要的版本差异点。创建一个最基本的DBMS_JOB核心是调用DBMS_JOB.SUBMIT过程。这个过程有四个关键参数每一个都值得细细琢磨DECLARE v_jobno NUMBER; -- 用于接收系统生成的作业编号 BEGIN DBMS_JOB.SUBMIT( job v_jobno, -- OUT参数作业唯一编号 what pkg_report.generate_daily;, -- 要执行的任务 next_date SYSDATE, -- 下一次运行时间 interval SYSDATE 1 -- 执行间隔 ); COMMIT; -- 必须提交作业才会真正进入队列 DBMS_OUTPUT.PUT_LINE(成功创建作业编号 || v_jobno); END;参数深度解析what参数任务本体这是作业的核心定义了“做什么”。它必须是一个合法的PL/SQL调用语句。存储过程调用‘my_proc;’或‘my_proc(参数);’。这是最推荐的方式逻辑封装性好易于管理。匿名块‘BEGIN ... END;’。适合简单逻辑但不便于复用和修改。一个关键细节语句末尾的分号是必须的。缺少分号是新手最常见的错误之一会导致作业提交成功但运行时报错。interval参数调度的心脏这是DBMS_JOB最灵活也最容易出错的地方。它是一个返回DATE类型的字符串表达式。绝对时间‘TRUNC(SYSDATE) 1 2/24’表示每天凌晨2点。相对间隔‘SYSDATE 1/24/60’表示1分钟后运行常用于测试。复杂周期‘NEXT_DAY(TRUNC(SYSDATE), ‘‘MONDAY’’) 9/24’表示每周一上午9点。重要陷阱interval是在每次作业成功运行完成后才计算的。这意味着如果你的作业在next_date时间点因为某种原因如实例关闭没有运行那么它会在实例恢复后以后台进程检查到它的时刻作为“上一次运行时间”来计算下一次的next_date。这可能导致作业的调度时间发生“漂移”。而DBMS_SCHEDULER的repeat_interval则没有这个问题它基于固定的时间表。next_date参数启动的扳机它指定了作业首次或下一次尝试运行的时间。设置为SYSDATE意味着作业提交后一旦后台进程轮询到通常有几秒到几分钟的延迟就会立即执行。job参数身份的标识这是一个OUT参数系统会自动分配一个唯一的作业编号。务必记录下这个编号它是后续所有管理操作查看、修改、删除的钥匙。注意DBMS_JOB.SUBMIT是一个需要显式提交(COMMIT)的DDL操作。如果你在PL/SQL块中提交了作业但没有COMMIT那么这个作业只存在于你当前的事务中其他会话和后台进程都看不到它自然也不会执行。这是另一个常见坑点。2.2 作业状态管理与监控实战作业提交后我们不能放任不管。它的生老病死、健康状态都存储在USER_JOBS、DBA_JOBS、DBA_JOBS_RUNNING等数据字典视图中。1. 查看作业基本信息SELECT job, log_user, what, last_date, last_sec, this_date, this_sec, next_date, next_sec, broken, failures, interval FROM user_jobs ORDER BY job;broken: 值为Y或N。这是最关键的字段之一。Y表示作业已损坏调度器将不再尝试运行它。作业运行连续失败16次后会自动标记为BROKEN。failures: 连续失败的次数。成功运行一次后会清零。last_date/last_sec: 上一次成功运行的开始日期和时间。this_date/this_sec:当前正在运行的作业的开始日期和时间。如果为NULL表示作业当前未运行。next_date/next_sec: 计划下一次运行的日期和时间。2. 管理作业状态运行一次作业DBMS_JOB.RUN(job_number);。这会强制作业立即运行一次且不影响其原有的next_date调度计划。常用于测试或手动补数据。修改作业属性使用DBMS_JOB.CHANGE。你可以单独修改what、next_date或interval而不影响其他属性。-- 只修改执行间隔为每天凌晨3点 DBMS_JOB.CHANGE(job 1234, interval TRUNC(SYSDATE1) 3/24); COMMIT;启动作业DBMS_JOB.BROKEN(job_number, broken FALSE);将broken状态从Y改为N作业恢复调度。停止作业DBMS_JOB.BROKEN(job_number, broken TRUE);将作业标记为损坏停止调度。同时next_date会被设置为4000-01-01这样一个遥远的日期。删除作业DBMS_JOB.REMOVE(job_number); COMMIT;3. 一个真实的排错案例作业静默失败曾经遇到一个作业next_date一直停留在过去某个时间点broken‘N‘但就是不运行。检查DBA_JOBS_RUNNING也没有它的记录。排查过程如下检查后台进程ps -ef | grep ora_j查看JNNN进程是否存在且正常。检查作业队列进程参数SHOW PARAMETER job_queue_processes。这个参数定义了最多可以同时运行多少个作业。如果它为0所有作业都不会被调度这是Oracle安装后有时被忽略的一个参数。ALTER SYSTEM SET job_queue_processes 20; -- 根据系统负载调整检查作业定义what字段中的过程名或包名是否存在拼写错误是否有同义词指向了错误的对象检查依赖对象状态如果作业调用的存储过程依赖的某个表或视图失效作业运行时可能会因编译错误而立即失败但错误信息不易捕捉。可以尝试手动RUN一次作业同时在另一个会话用SELECT * FROM DBA_JOBS_RUNNING和SELECT sid, serial#, sql_id FROM v$session WHERE program LIKE ‘%J0%‘找到对应的会话然后去v$session和v$sql中查找错误信息。这个案例的核心教训是作业不运行首先检查job_queue_processes其次手动RUN并捕获实时错误。3. 进化篇掌握强大的DBMS_SCHEDULER如果说DBMS_JOB是一把可靠但功能单一的螺丝刀那么DBMS_SCHEDULER就是一个完整的、现代化的机械工具箱。它提供了作业(JOB)、调度(SCHEDULE)、程序(PROGRAM)、链(CHAIN)、窗口(WINDOW)、资源管理器(RESOURCE MANAGER)集成等一整套特性更适合管理复杂的企业级任务调度。3.1 核心概念与创建你的第一个调度作业DBMS_SCHEDULER采用了更清晰的对象模型PROGRAM定义“做什么”。可以是一个存储过程、一个PL/SQL块甚至是一个操作系统可执行程序。SCHEDULE定义“何时做”。一个独立的时间计划对象可以被多个作业复用。JOB将PROGRAM和SCHEDULE关联起来并定义执行环境如凭证、目标等。它是最常用的入口。让我们创建一个最简单的、功能等同于之前DBMS_JOB例子的调度作业BEGIN DBMS_SCHEDULER.CREATE_JOB ( job_name ‘MY_DAILY_REPORT_JOB‘, job_type ‘STORED_PROCEDURE‘, job_action ‘pkg_report.generate_daily‘, start_date SYSTIMESTAMP, repeat_interval ‘FREQDAILY; BYHOUR2; BYMINUTE0‘, -- 每天2点 enabled TRUE, comments ‘生成每日业务报表‘ ); END;执行这条语句后作业立即生效无需COMMIT。你会发现repeat_interval参数使用了更直观的日历表达式这比DBMS_JOB的日期运算字符串要强大和规范得多。3.2 日历表达式详解与高级调度策略日历表达式是DBMS_SCHEDULER的调度灵魂它遵循FREQ、INTERVAL、BY系列关键字的结构。基础频率FREQSECONDLY; INTERVAL30每30秒FREQMINUTELY; INTERVAL15每15分钟FREQHOURLY; INTERVAL2每2小时FREQDAILY每天FREQWEEKLY每周FREQMONTHLY每月FREQYEARLY每年复杂组合示例工作日早上9点FREQDAILY; BYDAYMON,TUE,WED,THU,FRI; BYHOUR9; BYMINUTE0每月最后一天下午5点FREQMONTHLY; BYMONTHDAY-1; BYHOUR17每季度第一个周一FREQMONTHLY; BYMONTH1,4,7,10; BYDAY1MON(这里BYDAY1MON表示每月的第一个周一)每隔3天运行FREQDAILY; INTERVAL3高级调度特性开始与结束时间start_date和end_date可以精确控制作业的生命周期。持续时间与最大运行时间可以设置作业每次运行的最长时间(max_run_duration)超时后会被强制停止。依赖调度作业可以依赖于另一个作业的成功完成。这通过创建CHAIN链来实现是构建复杂ETL工作流的基础。3.3 作业类、资源管理与执行环境控制这是DBMS_SCHEDULER超越DBMS_JOB的另一个维度——对任务执行资源的精细化管理。1. 作业类作业类(JOB_CLASS)是一组共享资源分配规则的作业的集合。创建作业时可以指定其所属的类。BEGIN DBMS_SCHEDULER.CREATE_JOB_CLASS( job_class_name ‘LOW_PRIORITY_BATCH‘, resource_consumer_group ‘LOW_GROUP‘, -- 关联到资源消费者组 logging_level DBMS_SCHEDULER.LOGGING_FULL, comments ‘用于低优先级批处理作业‘ ); END;然后创建作业时指定job_class ‘LOW_PRIORITY_BATCH‘。这样所有属于此类的作业都会在指定的资源消费者组下运行避免影响高优先级的在线业务。2. 凭证对于需要访问操作系统资源如执行Shell脚本、读写文件的作业需要定义凭证(CREDENTIAL)。BEGIN DBMS_SCHEDULER.CREATE_CREDENTIAL( credential_name ‘OS_USER_CRED‘, username ‘oracle‘, password ‘your_password‘ ); END;创建外部作业时使用credential_name ‘OS_USER_CRED‘。这比传统上用job_queue_processes跑外部脚本更安全、更可控。3. 目标在RAC环境中你可以指定作业在哪个实例上运行或者允许它在任何可用实例上运行(‘any‘)这提供了高可用性。通过作业类、凭证、目标的组合你可以构建一个隔离的、资源受控的、高可用的自动化任务执行环境。4. 运维实战监控、排错与性能优化创建作业只是开始让作业集群健康、稳定、高效地运行才是真正的挑战。4.1 全方位的监控体系1. 数据字典视图你的监控仪表盘*_SCHEDULER_JOBS所有作业的定义和当前状态ENABLED,DISABLED,RUNNING,FAILED等。*_SCHEDULER_JOB_LOG作业每次运行的详细日志。这是排查故障的第一现场。*_SCHEDULER_JOB_RUN_DETAILS比LOG更详细的运行信息包括运行时长、CPU使用等。*_SCHEDULER_RUNNING_JOBS当前正在运行的作业信息。一个实用的监控查询用于查看最近失败的任务及其错误信息SELECT log_id, job_name, status, error#, error_message, req_start_date, actual_start_date, run_duration FROM dba_scheduler_job_run_details WHERE status ‘FAILED‘ AND actual_start_date SYSDATE - 1 -- 查看最近一天内的失败 ORDER BY actual_start_date DESC;2. 自定义告警与事件DBMS_SCHEDULER支持基于事件的通知。你可以设置当作业失败、超过最大运行时长等事件发生时自动发送邮件或调用一个处理程序。BEGIN DBMS_SCHEDULER.ADD_EVENT_QUEUE_SUBSCRIBER( subscriber_name ‘MY_ALERT_QUEUE_SUB‘ ); -- 然后可以配置规则将特定作业的失败事件放入队列再由高级队列(AQ)触发后续动作 END;对于大多数场景定期查询JOB_LOG和RUN_DETAILS视图并集成到现有的监控平台如Zabbix, Prometheus是更通用的做法。4.2 常见问题排查手册问题1作业状态为“SCHEDULED”但迟迟不运行。检查调度器开关SELECT * FROM DBA_SCHEDULER_GLOBAL_ATTRIBUTE WHERE ATTRIBUTE_NAME ‘SCHEDULER_DISABLED‘;如果值为TRUE整个调度器都被禁用了。检查作业是否被禁用SELECT enabled FROM USER_SCHEDULER_JOBS WHERE job_name ‘...‘;检查资源限制作业所属的作业类(JOB_CLASS)关联的资源消费者组(RESOURCE_CONSUMER_GROUP)可能没有足够的资源份额或者资源计划(RESOURCE_PLAN)未激活。检查依赖如果作业是链(CHAIN)的一部分检查其前置步骤是否成功。问题2作业运行失败错误信息为“ORA-27486: insufficient privileges”。这是权限问题。执行作业的用户通常是作业的owner需要CREATE JOB权限。对于外部作业还需要CREATE EXTERNAL JOB权限以及对应凭证(CREDENTIAL)的执行权限。解决方案GRANT CREATE JOB TO your_user; -- 对于外部作业 GRANT CREATE EXTERNAL JOB TO your_user; EXEC DBMS_SCHEDULER.GRANT_EXECUTE_ON_CREDENTIAL(‘OS_USER_CRED‘, ‘your_user‘);问题3作业运行时间过长甚至挂起。查看当前运行详情SELECT sj.job_name, sjr.session_id, s.sid, s.serial#, s.sql_id, s.event, s.seconds_in_wait FROM dba_scheduler_running_jobs sjr JOIN dba_scheduler_jobs sj ON sjr.job_name sj.job_name LEFT JOIN v$session s ON sjr.session_id s.sid WHERE sjr.job_name ‘YOUR_JOB_NAME‘;通过sql_id可以进一步查看正在执行的SQL通过event可以查看会话在等待什么资源如锁、IO等。这可能是由于作业内部的SQL效率低下或者与其它会话存在资源争用。问题4DBMS_JOB作业在RAC环境中只在某个实例运行。这是DBMS_JOB的固有缺陷作业与创建它的实例绑定。解决方案是迁移到DBMS_SCHEDULER或者使用DBMS_JOB的instance参数不推荐管理复杂。4.3 性能优化与最佳实践避免高频短作业如果业务需要每秒执行一次检查考虑使用DBMS_PIPE、Advanced Queueing或应用层的定时器而不是创建每秒运行一次的数据库作业。频繁的作业启停会消耗可观的CPU和内存资源。合理设置job_queue_processes对于DBMS_JOB此参数不宜设置过大通常10-20足够。过大会增加进程管理开销。对于DBMS_SCHEDULER其并发由资源管理器控制更精细。使用作业类进行资源隔离务必为批处理作业创建独立的作业类并将其绑定到专用的资源消费者组如BATCH_GROUP。在资源管理计划中给这个组分配固定的CPU份额和并行度限制确保批处理不会挤占联机事务处理OLTP的资源。详尽的日志记录创建作业时设置logging_level DBMS_SCHEDULER.LOGGING_FULL。虽然会占用更多空间但在排查复杂问题时完整的日志是无价之宝。定期清理历史日志即可。为作业设置超时使用max_run_duration参数。例如INTERVAL ‘PT2H‘表示最长运行2小时。防止失控的作业永远运行下去。设计幂等性作业作业逻辑应设计成可重复执行而不会产生副作用或重复数据。这样当作业失败重跑时就不会引入新的问题。例如使用MERGE语句代替INSERT或者在处理前先删除目标时间段的数据。5. 从DBMS_JOB迁移到DBMS_SCHEDULER对于历史系统将关键的DBMS_JOB迁移到DBMS_SCHEDULER是提升可维护性和可靠性的重要步骤。迁移不是简单的语法转换而是涉及调度语义、异常处理和资源管理的重构。迁移步骤与注意事项清单梳理首先从DBA_JOBS中导出所有作业的详细信息what, interval, broken状态等。语义分析重点分析interval字符串。DBMS_JOB的interval计算基于“上一次成功完成的时间”而DBMS_SCHEDULER的repeat_interval基于固定的时间表。如果原作业对执行时间的“漂移”有依赖需要重新设计调度策略。大多数情况下你需要的是基于日历的固定调度。创建等价的SCHEDULER JOB使用DBMS_SCHEDULER.CREATE_JOB创建新作业。对于存储过程调用直接转换。对于复杂的匿名块考虑将其封装成存储过程。并行运行与验证不要立即删除旧作业。让新旧作业并行运行一段时间可以先将旧作业的interval改为一个很大的值使其暂时不调度但保留定义。对比新旧作业的输出结果和执行时间确保功能一致。切换与清理验证无误后将旧作业标记为BROKEN或直接REMOVE并确保新作业的enabled状态为TRUE。更新依赖检查是否有其他脚本、程序或监控系统通过作业编号(job number)引用旧作业需要更新这些引用。一个迁移工具示例思路你可以编写一个PL/SQL脚本读取DBA_JOBS并尝试自动生成对应的CREATE_JOB语句。但对于复杂的interval逻辑和作业依赖人工审核和调整是必不可少的。迁移的核心价值在于你将获得更稳定的调度无时间漂移、更强大的监控详尽的日志、更精细的控制资源管理以及更好的可维护性对象化的调度和程序。虽然需要一些前期投入但从长期的运维成本来看这笔投资是值得的。6. 复杂场景应用作业链与依赖管理当单个作业无法满足需求需要将多个任务按特定顺序和逻辑组织起来时DBMS_SCHEDULER的链功能就派上了用场。链允许你定义一组有依赖关系的程序步骤并控制它们的执行流程成功、失败、超时后的动作。6.1 创建与运行一个简单的作业链假设我们有一个简单的数据加载流程1) 清空临时表 2) 从源系统加载数据到临时表 3) 验证数据 4) 将有效数据合并到目标表。第一步创建链对象BEGIN DBMS_SCHEDULER.CREATE_CHAIN ( chain_name ‘ETL_DAILY_LOAD_CHAIN‘, rule_set_name NULL, evaluation_interval NULL, comments ‘每日ETL数据加载流程‘ ); END;第二步定义链中的步骤每个步骤指向一个PROGRAM或内联的PL/SQL代码块。BEGIN -- 步骤1清空临时表 DBMS_SCHEDULER.DEFINE_CHAIN_STEP ( chain_name ‘ETL_DAILY_LOAD_CHAIN‘, step_name ‘CLEAN_STAGE‘, program_name ‘CLEAN_STAGING_TABLE_PROC‘ -- 这是一个预先创建好的PROGRAM ); -- 步骤2加载数据 DBMS_SCHEDULER.DEFINE_CHAIN_STEP ( chain_name ‘ETL_DAILY_LOAD_CHAIN‘, step_name ‘LOAD_DATA‘, program_name ‘LOAD_DATA_TO_STAGE_PROC‘ ); -- 步骤3验证数据 DBMS_SCHEDULER.DEFINE_CHAIN_STEP ( chain_name ‘ETL_DAILY_LOAD_CHAIN‘, step_name ‘VALIDATE_DATA‘, program_name ‘VALIDATE_STAGE_DATA_PROC‘ ); -- 步骤4合并到目标表 DBMS_SCHEDULER.DEFINE_CHAIN_STEP ( chain_name ‘ETL_DAILY_LOAD_CHAIN‘, step_name ‘MERGE_TO_TARGET‘, program_name ‘MERGE_DATA_PROC‘ ); END;第三步定义步骤间的依赖规则规则决定了步骤的执行顺序和条件。BEGIN -- 链开始时首先运行 CLEAN_STAGE DBMS_SCHEDULER.DEFINE_CHAIN_RULE ( chain_name ‘ETL_DAILY_LOAD_CHAIN‘, condition ‘TRUE‘, action ‘START CLEAN_STAGE‘, rule_name ‘ETL_RULE_START‘ ); -- CLEAN_STAGE 成功后运行 LOAD_DATA DBMS_SCHEDULER.DEFINE_CHAIN_RULE ( chain_name ‘ETL_DAILY_LOAD_CHAIN‘, condition ‘CLEAN_STAGE COMPLETED‘, action ‘START LOAD_DATA‘, rule_name ‘ETL_RULE_1‘ ); -- LOAD_DATA 成功后运行 VALIDATE_DATA DBMS_SCHEDULER.DEFINE_CHAIN_RULE ( chain_name ‘ETL_DAILY_LOAD_CHAIN‘, condition ‘LOAD_DATA COMPLETED‘, action ‘START VALIDATE_DATA‘, rule_name ‘ETL_RULE_2‘ ); -- VALIDATE_DATA 成功后运行 MERGE_TO_TARGET DBMS_SCHEDULER.DEFINE_CHAIN_RULE ( chain_name ‘ETL_DAILY_LOAD_CHAIN‘, condition ‘VALIDATE_DATA COMPLETED‘, action ‘START MERGE_TO_TARGET‘, rule_name ‘ETL_RULE_3‘ ); -- MERGE_TO_TARGET 完成后整个链结束 DBMS_SCHEDULER.DEFINE_CHAIN_RULE ( chain_name ‘ETL_DAILY_LOAD_CHAIN‘, condition ‘MERGE_TO_TARGET COMPLETED‘, action ‘END‘, rule_name ‘ETL_RULE_END‘ ); END;第四步启用链并创建按计划运行的链作业BEGIN DBMS_SCHEDULER.ENABLE(‘ETL_DAILY_LOAD_CHAIN‘); DBMS_SCHEDULER.CREATE_JOB ( job_name ‘RUN_ETL_CHAIN_JOB‘, job_type ‘CHAIN‘, job_action ‘ETL_DAILY_LOAD_CHAIN‘, repeat_interval ‘FREQDAILY; BYHOUR1‘, enabled TRUE ); END;6.2 链的复杂逻辑与错误处理链的强大之处在于其基于规则的条件判断。处理失败你可以定义当某个步骤失败时是跳转到另一个清理步骤还是直接结束整个链并标记为失败。-- 如果 VALIDATE_DATA 步骤失败则运行一个清理和告警步骤 DBMS_SCHEDULER.DEFINE_CHAIN_RULE ( chain_name ‘ETL_DAILY_LOAD_CHAIN‘, condition ‘VALIDATE_DATA FAILED‘, action ‘START CLEANUP_AND_ALERT_STEP‘, rule_name ‘ETL_RULE_ON_VALIDATE_FAIL‘ );超时处理可以在定义步骤时指定step_timeout并在规则中处理step_name TIMED_OUT事件。并行执行规则条件可以支持AND和OR。例如‘STEP_A COMPLETED AND STEP_B COMPLETED‘ 这样STEP_C就可以在A和B都完成后才开始实现了并行分支的同步。查看链运行状态使用*_SCHEDULER_CHAINS、*_SCHEDULER_CHAIN_STEPS、*_SCHEDULER_CHAIN_RULES等视图来监控链的定义和运行状态。通过链你可以将分散的、有依赖关系的作业组织成一个可视化的、逻辑清晰的工作流大大提升了复杂批处理任务的可靠性和可维护性。这对于数据仓库的ETL流程、定期的系统维护任务串联等场景是必不可少的工具。