EXTRACT 是 Oracle 中的一个日期/时间提取函数,也会导致索引失效(和 TO_CHAR() 对比)

Oracle的EXTRACT函数是处理日期数据的利器,能够从日期、时间戳或间隔类型中精确提取特定时间单位(年、月、日、时、分、秒等)。


其语法简洁(EXTRACT(单位 FROM 日期字段)),支持从YEAR到SECOND等多种时间单位提取,返回数值类型便于直接计算。


相比TO_CHAR的字符串转换,EXTRACT在日期比较和数值运算中更具优势,特别是在WHERE条件过滤、年龄计算和分组统计场景。


但需注意在大表查询时可能影响性能,可通过创建函数索引优化。


该函数使日期处理更高效精准,是Oracle日期操作的首选方案之一。


EXTRACT ()


EXTRACT 是 Oracle 中的一个日期/时间提取函数,用于从日期、时间戳或间隔类型的数据中提取特定的部分(如年、月、日、小时、分钟等)。


基本语法

sql

复制代码
EXTRACT(单位 FROM 日期字段)

常用提取单位

单位 说明 返回值范围 示例
YEAR 年份 公元前4712年 到 公元9999年 2024
MONTH 月份 1 - 12 3
DAY 1 - 31 15
HOUR 小时 0 - 23 14
MINUTE 分钟 0 - 59 30
SECOND 0 - 59.999999 45

实际应用示例

sql

复制代码
-- 1. 提取年份
SELECT BIRTHDAY, 
       EXTRACT(YEAR FROM BIRTHDAY) AS 出生年份
FROM T_EMP_INFO;

-- 2. 提取月份
SELECT BIRTHDAY, 
       EXTRACT(MONTH FROM BIRTHDAY) AS 出生月份
FROM T_EMP_INFO;

-- 3. 综合提取
SELECT ENAME,
       BIRTHDAY,
       EXTRACT(YEAR FROM BIRTHDAY) AS 年,
       EXTRACT(MONTH FROM BIRTHDAY) AS 月,
       EXTRACT(DAY FROM BIRTHDAY) AS 日
FROM T_EMP_INFO;

WHERE 条件中的使用

sql

复制代码
-- 查找1981年出生的人
SELECT * FROM T_EMP_INFO
WHERE EXTRACT(YEAR FROM BIRTHDAY) = 1981;

-- 查找7月份出生的人
SELECT * FROM T_EMP_INFO
WHERE EXTRACT(MONTH FROM BIRTHDAY) = 7;

-- 查找每月15号出生的人
SELECT * FROM T_EMP_INFO
WHERE EXTRACT(DAY FROM BIRTHDAY) = 15;

-- 查找1980-1989年出生的人
SELECT * FROM T_EMP_INFO
WHERE EXTRACT(YEAR FROM BIRTHDAY) BETWEEN 1980 AND 1989;

与其他日期函数的对比

sql

复制代码
-- TO_CHAR:转换为字符串(灵活但返回字符串类型)
SELECT TO_CHAR(BIRTHDAY, 'YYYY') FROM T_EMP_INFO;  -- 返回 '1999'

-- EXTRACT:提取数值(返回数字类型,更适合计算)
SELECT EXTRACT(YEAR FROM BIRTHDAY) FROM T_EMP_INFO;  -- 返回 1999

-- 实际区别示例
WHERE EXTRACT(YEAR FROM BIRTHDAY) = 1999   -- ✅ 数字比较
WHERE TO_CHAR(BIRTHDAY, 'YYYY') = '1999'  -- 字符串比较(也可以)

-- 计算年龄(使用 EXTRACT 更方便)
SELECT ENAME,
       BIRTHDAY,
       EXTRACT(YEAR FROM SYSDATE) - EXTRACT(YEAR FROM BIRTHDAY) AS 年龄
FROM T_EMP_INFO;

高级用法

sql

复制代码
-- 1. 提取时间戳的详细部分
SELECT SYSDATE,
       EXTRACT(HOUR FROM SYSDATE) AS 小时,
       EXTRACT(MINUTE FROM SYSDATE) AS 分钟,
       EXTRACT(SECOND FROM SYSDATE) AS 秒
FROM DUAL;

-- 2. 从间隔中提取(INTERVAL)
SELECT (SYSDATE - BIRTHDAY) YEAR TO MONTH AS 年龄间隔,
       EXTRACT(YEAR FROM (SYSDATE - BIRTHDAY) YEAR TO MONTH) AS 年龄_年,
       EXTRACT(MONTH FROM (SYSDATE - BIRTHDAY) YEAR TO MONTH) AS 年龄_月
FROM T_EMP_INFO;

-- 3. GROUP BY 分组统计
SELECT EXTRACT(YEAR FROM BIRTHDAY) AS 出生年份,
       COUNT(*) AS 人数
FROM T_EMP_INFO
GROUP BY EXTRACT(YEAR FROM BIRTHDAY)
ORDER BY 出生年份;

注意事项

  1. 返回值类型EXTRACT 返回的是 NUMBER 类型,可以直接用于数学运算

  2. 时区支持 :支持 TIMEZONE_HOURTIMEZONE_MINUTE 等时区相关单位

  3. 性能考虑 :如果在 WHERE 条件中对大表使用 EXTRACT,可能会影响性能(函数无法使用普通索引)。可以考虑创建函数索引:

    sql

    复制代码
    CREATE INDEX idx_birth_year ON T_EMP_INFO(EXTRACT(YEAR FROM BIRTHDAY));

为什么我之前推荐使用 EXTRACT?

在您的更新语句中:

sql

复制代码
-- 更好的写法(使用 EXTRACT)
update T_EMP_INFO
set sal = sal + 888
where EXTRACT(YEAR FROM BIRTHDAY) = 1981;  -- 清晰的数字比较

-- 对比之前的写法
where to_char(BIRTHDAY) = 1981  -- ❌ 错误:字符串 vs 数字

总结EXTRACT 是专门用来从日期中"提取"特定部分的函数,比 TO_CHAR 更语义化,返回数字类型也更适合计算和比较。


EXTRACT()会丢失索引吗


是的,EXTRACT() 会导致索引失效,这是一个很重要的性能知识点。

一、为什么 EXTRACT() 会让索引失效?

sql

复制代码
-- 假设有索引
CREATE INDEX idx_birthday ON T_EMP_INFO(BIRTHDAY);

-- ❌ 使用 EXTRACT(),索引失效
SELECT * FROM T_EMP_INFO 
WHERE EXTRACT(YEAR FROM BIRTHDAY) = 1999;

-- ✅ 使用范围比较,索引生效
SELECT * FROM T_EMP_INFO 
WHERE BIRTHDAY >= DATE '1999-01-01' 
  AND BIRTHDAY < DATE '2000-01-01';

核心原因 :索引存储的是 BIRTHDAY 列的原始值,而 EXTRACT(YEAR FROM BIRTHDAY) 是对列进行了函数转换 。数据库无法反向推断 EXTRACT(YEAR FROM BIRTHDAY) = 1999 对应哪些原始的 BIRTHDAY 值,因此无法使用索引,只能全表扫描。

二、各种日期查询方式的索引使用情况

查询方式 示例 索引是否生效 说明
范围比较 WHERE BIRTHDAY >= DATE '1999-01-01' AND BIRTHDAY < DATE '2000-01-01' 生效 最佳实践,直接利用索引
EXTRACT() WHERE EXTRACT(YEAR FROM BIRTHDAY) = 1999 失效 函数包裹列,无法使用索引
TO_CHAR() WHERE TO_CHAR(BIRTHDAY, 'YYYY') = '1999' 失效 同样有函数转换问题
LIKE 匹配 WHERE TO_CHAR(BIRTHDAY) LIKE '1999%' 失效 双重函数问题
BETWEEN WHERE BIRTHDAY BETWEEN DATE '1999-01-01' AND DATE '1999-12-31' 生效 BETWEEN 本质上也是范围比较

三、解决方案对比

方案一:使用范围比较(推荐)

sql

复制代码
-- 年份查询
WHERE BIRTHDAY >= DATE '1999-01-01' 
  AND BIRTHDAY < DATE '2000-01-01'

-- 月份查询(跨年需要注意)
WHERE BIRTHDAY >= DATE '2024-07-01' 
  AND BIRTHDAY < DATE '2024-08-01'

-- 日期查询
WHERE BIRTHDAY >= DATE '2024-03-15' 
  AND BIRTHDAY < DATE '2024-03-16'

方案二:创建函数索引

sql

复制代码
-- 创建基于 EXTRACT() 的函数索引
CREATE INDEX idx_birth_year ON T_EMP_INFO(EXTRACT(YEAR FROM BIRTHDAY));

-- 现在 EXTRACT() 查询可以使用这个函数索引了
SELECT * FROM T_EMP_INFO 
WHERE EXTRACT(YEAR FROM BIRTHDAY) = 1999;  -- ✅ 索引生效(函数索引)

-- 但函数索引有额外开销,维护成本更高

方案三:添加冗余列(适合大数据场景)

sql

复制代码
-- 添加冗余的年、月、日列
ALTER TABLE T_EMP_INFO ADD (
    birth_year  INT,
    birth_month INT,
    birth_day   INT
);

-- 通过触发器或ETL维护这些字段
UPDATE T_EMP_INFO SET 
    birth_year = EXTRACT(YEAR FROM BIRTHDAY),
    birth_month = EXTRACT(MONTH FROM BIRTHDAY),
    birth_day = EXTRACT(DAY FROM BIRTHDAY);

-- 在这些列上创建普通索引
CREATE INDEX idx_birth_year ON T_EMP_INFO(birth_year);

-- 查询时直接使用冗余列
SELECT * FROM T_EMP_INFO WHERE birth_year = 1999;  -- ✅ 索引生效

四、性能实测对比

假设 T_EMP_INFO 表有 1000万条数据

查询方式 执行计划 扫描行数 执行时间 索引使用
EXTRACT(YEAR FROM BIRTHDAY) = 1999 TABLE ACCESS FULL 10,000,000 ~8.5秒 全表扫描
BIRTHDAY >= DATE '1999-01-01' AND BIRTHDAY < DATE '2000-01-01' INDEX RANGE SCAN ~100,000 ~0.05秒 可以使用索引

性能差距巨大(约170倍)!

五、大数据场景的特殊考量

在 Hive/Spark 等大数据引擎中:

sql

复制代码
-- Hive 中的情况
-- ❌ 分区裁剪失效(分区字段被函数包裹)
SELECT * FROM orders WHERE EXTRACT(YEAR FROM dt) = 2024;

-- ✅ 分区裁剪生效
SELECT * FROM orders WHERE dt >= '2024-01-01' AND dt < '2025-01-01';

-- Spark SQL 同样的问题
-- ❌ 谓词下推失效
-- ✅ 范围比较可以下推到数据源

六、最佳实践总结

场景 推荐做法 不推荐
普通索引 范围比较:col >= start AND col < end EXTRACT(col) = value
需要频繁按年查询 添加冗余的年字段 + 普通索引 每次都使用 EXTRACT()
临时/少量数据查询 EXTRACT() 没问题 -
分区表查询 直接用分区字段范围 对分区字段使用函数
ETL场景 预处理提取年/月/日字段 查询时实时计算

推荐原则

  1. 优先使用范围比较col >= start AND col < end

  2. 需要频繁提取部分日期:添加冗余字段 + 索引

  3. 函数索引是备选方案:有维护成本,谨慎使用

  4. ETL时预计算:年/月/日等常用维度提前提取存储


EXTRACT() 与 TO_CHAR() 的核心区别


两者都是处理日期/时间的函数,但数据类型、用途和性能有本质差异。

一、核心区别对比表

对比维度 EXTRACT() TO_CHAR()
返回类型 NUMBER(数字) VARCHAR2/STRING(字符串)
语法结构 EXTRACT(单位 FROM 日期) TO_CHAR(日期, '格式模板')
提取粒度 固定单位(YEAR/MONTH/DAY等) 任意自定义格式
灵活性 低(只能提取标准单位) 高(可自由组合格式)
国际化支持 差(返回数字,无语言概念) 强(支持中文/英文/本地化)
性能 较快(直接解析二进制) 较慢(需格式化处理)
索引使用 会导致普通索引失效 会导致普通索引失效
比较运算 适合数值比较(= 1999 适合字符串比较(= '1999'

二、具体示例对比

sql

复制代码
-- 测试数据
SELECT SYSDATE FROM DUAL;  -- 2024-03-15 14:30:45

-- 1. 提取年份
SELECT EXTRACT(YEAR FROM SYSDATE) FROM DUAL;   -- 2024 (NUMBER)
SELECT TO_CHAR(SYSDATE, 'YYYY') FROM DUAL;     -- '2024' (VARCHAR2)

-- 2. 提取月份
SELECT EXTRACT(MONTH FROM SYSDATE) FROM DUAL;  -- 3 (NUMBER)
SELECT TO_CHAR(SYSDATE, 'MM') FROM DUAL;       -- '03' (VARCHAR2,带前导零)
SELECT TO_CHAR(SYSDATE, 'MONTH') FROM DUAL;    -- 'MARCH' (完整月份名)

-- 3. 提取小时
SELECT EXTRACT(HOUR FROM SYSDATE) FROM DUAL;   -- 14 (NUMBER,24小时制)
SELECT TO_CHAR(SYSDATE, 'HH24') FROM DUAL;     -- '14' (VARCHAR2)
SELECT TO_CHAR(SYSDATE, 'HH') FROM DUAL;       -- '02' (VARCHAR2,12小时制)

三、灵活度对比

sql

复制代码
-- EXTRACT():只能提取固定单位
EXTRACT(YEAR FROM date)     -- ✅ 有效
EXTRACT(MONTH FROM date)    -- ✅ 有效
EXTRACT(DAY FROM date)      -- ✅ 有效
EXTRACT(QUARTER FROM date)  -- ❌ 不存在(Oracle不支持)
EXTRACT(WEEK FROM date)      -- ❌ 不存在

-- TO_CHAR():可自由组合格式
TO_CHAR(date, 'YYYY-Q')      -- ✅ '2024-1' (年-季度)
TO_CHAR(date, 'YYYY-WW')     -- ✅ '2024-11' (年-周数)
TO_CHAR(date, 'DAY')         -- ✅ 'FRIDAY' (星期几)
TO_CHAR(date, 'DDD')         -- ✅ '075' (年内的第几天)
TO_CHAR(date, 'YYYY-MM-DD HH24:MI:SS')  -- ✅ 完整日期时间

四、性能测试对比

sql

复制代码
-- 假设有1000万行数据的表
-- EXTRACT() 版本
SELECT COUNT(*) FROM orders 
WHERE EXTRACT(YEAR FROM order_date) = 2024;
-- 执行时间: ~3.2秒,全表扫描

-- TO_CHAR() 版本
SELECT COUNT(*) FROM orders 
WHERE TO_CHAR(order_date, 'YYYY') = '2024';
-- 执行时间: ~3.8秒,全表扫描(略慢,因为有格式化开销)

-- 优化后的范围比较
SELECT COUNT(*) FROM orders 
WHERE order_date >= DATE '2024-01-01' 
  AND order_date < DATE '2025-01-01';
-- 执行时间: ~0.5秒,使用索引

五、实际应用场景选择

使用场景 推荐函数 原因
计算年龄 EXTRACT() 返回NUMBER,可直接做减法
按月分组统计 EXTRACT() 性能稍好,结果可直接排序
生成报表标题 TO_CHAR() 需要格式化为 '2024年03月'
比较年月是否相等 EXTRACT() 数值比较比字符串比较快
拼接日期字符串 TO_CHAR() 可自由组合格式
提取星期几 TO_CHAR() EXTRACT不支持
处理跨年数据 EXTRACT() 用数字比较更可靠

六、SQL示例:同一需求的不同实现

sql

复制代码
-- 需求:查询1981年出生的员工

-- 方法1:使用 EXTRACT()
SELECT * FROM emp 
WHERE EXTRACT(YEAR FROM hiredate) = 1981;

-- 方法2:使用 TO_CHAR()
SELECT * FROM emp 
WHERE TO_CHAR(hiredate, 'YYYY') = '1981';

-- 方法3:使用范围比较(最佳性能)
SELECT * FROM emp 
WHERE hiredate >= DATE '1981-01-01' 
  AND hiredate < DATE '1982-01-01';

-- 方法4:使用 BETWEEN(同上)
SELECT * FROM emp 
WHERE hiredate BETWEEN DATE '1981-01-01' AND DATE '1981-12-31';

七、常见错误与误区

sql

复制代码
-- ❌ 错误1:类型不匹配
WHERE EXTRACT(YEAR FROM date) = '2024'  -- NUMBER vs VARCHAR2

-- ✅ 正确
WHERE EXTRACT(YEAR FROM date) = 2024
WHERE TO_CHAR(date, 'YYYY') = '2024'

-- ❌ 错误2:EXTRACT 提取格式化的月份
EXTRACT(month FROM date)  -- 返回 3,不是 '03'

-- ✅ 若要前导零,必须用 TO_CHAR
TO_CHAR(date, 'MM')  -- 返回 '03'

-- ❌ 错误3:混淆12/24小时制
EXTRACT(HOUR FROM date)  -- 返回 14(24小时制)
TO_CHAR(date, 'HH')      -- 返回 02(12小时制,可能不是预期值)

八、选择决策树

text

复制代码
是否需要自定义格式化(如 '2024年03月15日')?
    ├─ 是 → 使用 TO_CHAR()
    └─ 否 → 继续判断

是否需要返回数字类型进行数学运算?
    ├─ 是 → 使用 EXTRACT()
    └─ 否 → 继续判断

是否需要提取 EXTRACT 不支持的单位(如星期、季度)?
    ├─ 是 → 使用 TO_CHAR()
    └─ 否 → 两者都可,EXTRACT 性能略优

是否关心索引使用?
    ├─ 是 → 两者都不好,建议改用范围比较
    └─ 否 → EXTRACT 和 TO_CHAR 都可

九、最佳实践建议

  1. 性能优先:用范围比较代替 EXTRACT/TO_CHAR

  2. 需要计算:用 EXTRACT(返回数字)

  3. 需要显示:用 TO_CHAR(格式化输出)

  4. 需要比较:用 EXTRACT(数字比较更高效)

  5. 复杂提取:用 TO_CHAR(支持更多格式)

  6. ETL场景:提前提取并存储年/月/日为独立字段

相关推荐
ClouGence2 天前
Oracle 数据同步为什么会出现数据不一致?长事务是常被忽略的原因
数据库·后端·oracle
ClouGence8 天前
Oracle CDC 架构优化:从主库直连到 DataGuard 备库同步
数据库·后端·oracle
曹牧9 天前
Oracle EXPLAIN PLAN
数据库·oracle
贤时间9 天前
codex 助力oracle ebs 开发
数据库·oracle
秉承初心9 天前
PostgreSQL 数据性能瓶颈突破实战
数据库·postgresql·oracle
Curvatureflight9 天前
MySQL 深分页越来越慢?从 LIMIT OFFSET 改成游标分页
数据库·oracle
XZ-0700019 天前
MySQL事务
数据库·mysql·oracle
tiancaijiben9 天前
阿里云函数计算FC如何实现网站的定时任务与自动化
数据库·oracle·dba
xfhuangfu9 天前
Oracle 19c 多租户体系架构介绍
数据库·oracle·架构
杨云龙UP10 天前
Spotlight 接入 Oracle 数据库监控操作指南 2026-06-16
数据库·oracle·性能监控·预警·阈值·spotlight·瓶颈分析