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场景:提前提取并存储年/月/日为独立字段

相关推荐
安当加密2 小时前
TDE透明加密:精准管控数据库文件的读取、复制与权限,实现全方位版权保护
数据库·oracle
Irene19912 小时前
Oracle 数据库表操作总结(新建表,删除表,表的增删改查等)
数据库·sql·oracle
Gauss松鼠会3 小时前
GaussDB for DWS 数据融合:Oracle数据迁移到GaussDB(DWS)
数据库·oracle·数据库开发·gaussdb
大江东去浪淘尽千古风流人物3 小时前
【UV-SLAM 】彻底吃透UV-SLAM:创新原理、工程实现与直线几何核心代码详解
数据库·人工智能·python·机器学习·oracle·uv
还是阿落呀3 小时前
数据库和表的基本操作
数据库·oracle
百锦再4 小时前
时序数据库选型指南:大数据时代的“数据基建”与 IoTDB 的工业原生之路
大数据·数据库·mysql·oracle·sqlserver·时序数据库·iotdb
河阿里13 小时前
SQL数据库:五大范式(NF)
数据库·sql·oracle
wytraining19 小时前
快速入门 FastAPI 项目
jvm·oracle·fastapi
安当加密1 天前
SQL Server 数据库安全新范式:TDE 透明加密+ DBG数据库安全网关 双重装甲
数据库·oracle