前言
你有没有遇到过这种情况------建表的时候随便选了个VARCHAR,结果存金额的时候精度丢了;或者存IP地址用了字符串,查询的时候怎么都筛不准?
说白了,这些问题都跟数据类型的选择有关。选对类型,数据存得准、查得快;选错了,小则浪费空间,大则数据失真。
这篇文章我把数据库里所有内置数据类型都梳理了一遍,从最常用的字符、数值,到JSON、XML、几何类型,每个都配上了实际能跑的代码。不管你是刚入门还是已经写过不少SQL,都能用得上。
文章目录
- 前言
-
- 一、字符类型:存文本,这几个就够了
-
- [1.1 CHAR------固定长度,不够就补空格](#1.1 CHAR——固定长度,不够就补空格)
- [1.2 VARCHAR2------用多少占多少,最灵活](#1.2 VARCHAR2——用多少占多少,最灵活)
- [1.3 NCHAR与NVARCHAR2------多语言场景的标配](#1.3 NCHAR与NVARCHAR2——多语言场景的标配)
- 二、数值类型:钱用NUMBER,计数用INT
-
- [2.1 NUMBER------万能数值,精度你说了算](#2.1 NUMBER——万能数值,精度你说了算)
- [2.2 INT与SMALLINT------存整数,轻量又高效](#2.2 INT与SMALLINT——存整数,轻量又高效)
- [2.3 浮点类型------科学计算用,别拿来存钱](#2.3 浮点类型——科学计算用,别拿来存钱)
- 三、日期时间类型:不只是"存个日期"那么简单
-
- [3.1 DATE------基本的日期+时间](#3.1 DATE——基本的日期+时间)
- [3.2 TIMESTAMP------精确到毫秒甚至微秒](#3.2 TIMESTAMP——精确到毫秒甚至微秒)
- [3.3 带时区的时间戳------跨时区应用的必备](#3.3 带时区的时间戳——跨时区应用的必备)
- [3.4 INTERVAL------表示"一段时间"](#3.4 INTERVAL——表示"一段时间")
- 四、大对象类型:图片、长文、视频往里塞
-
- [4.1 BLOB------二进制文件专用](#4.1 BLOB——二进制文件专用)
- [4.2 CLOB与NCLOB------超长文本的归宿](#4.2 CLOB与NCLOB——超长文本的归宿)
- [4.3 BFILE------不存文件,只存"文件在哪"](#4.3 BFILE——不存文件,只存"文件在哪")
- 五、RAW类型:二进制数据"原样搬运"
- 六、JSON类型:灵活数据的存储利器
-
- [6.1 JSON和JSONB到底选哪个?](#6.1 JSON和JSONB到底选哪个?)
- [6.2 JSON基本操作](#6.2 JSON基本操作)
- [6.3 JSONB查询:包含、存在、嵌套](#6.3 JSONB查询:包含、存在、嵌套)
- [6.4 给JSONB建索引,查询飞起来](#6.4 给JSONB建索引,查询飞起来)
- [6.5 JSONPATH------更灵活的路径查询](#6.5 JSONPATH——更灵活的路径查询)
- 七、XML类型:结构化文档怎么存
-
- [7.1 XML基础用法](#7.1 XML基础用法)
- [7.2 XMLType------更强大的XML操作](#7.2 XMLType——更强大的XML操作)
- 八、几何类型:坐标、区域、距离计算
-
- [8.1 几何计算示例](#8.1 几何计算示例)
- 九、网络地址类型:别再用字符串存IP了
-
- [9.1 inet与cidr](#9.1 inet与cidr)
- [9.2 MAC地址](#9.2 MAC地址)
- 十、全文搜索:让数据库帮你"找文章"
-
- [10.1 tsvector------把文本变成可搜索的格式](#10.1 tsvector——把文本变成可搜索的格式)
- [10.2 tsquery------构建搜索条件](#10.2 tsquery——构建搜索条件)
- [10.3 实战:创建全文搜索系统](#10.3 实战:创建全文搜索系统)
- 十一、范围类型:优雅地表达"从A到B"
-
- [11.1 内置的6种范围类型](#11.1 内置的6种范围类型)
- [11.2 范围操作实战](#11.2 范围操作实战)
- [11.3 自定义范围类型](#11.3 自定义范围类型)
- 十二、其他实用类型
-
- [12.1 ROWID------每一行的"身份证号"](#12.1 ROWID——每一行的"身份证号")
- [12.2 用户自定义类型](#12.2 用户自定义类型)
- 总结:一张表选对数据类型
一、字符类型:存文本,这几个就够了
字符类型是打交道最多的,存名字、存地址、存备注都离不开它。说简单点,就是数据库里专门用来"装文字"的容器。
数据库同时支持单字节和多字节字符集,中英文混合存储完全没问题。
1.1 CHAR------固定长度,不够就补空格
CHAR是定长字符串。什么意思呢?你定义一个CHAR(10),不管往里塞"abc"还是"一二三四五",数据库都会把内容补齐到10个字符长度------不够的用空格填。
语法很简单:
sql
CHAR [(size [BYTE | CHAR])]
size最大能到2000。这里有个容易踩坑的地方:长度单位到底是"字节"还是"字符"?
sql
-- 按字符算:一个中文算1个字符,一个英文也算1个字符
CREATE TABLE test_char (col CHAR(4 CHAR));
INSERT INTO test_char VALUES ('一二三四'); -- 没问题,刚好4个字符
INSERT INTO test_char VALUES ('一二三四五'); -- 报错!超了1个字符
-- 按字节算:一个中文通常占3个字节,一个英文占1个字节
CREATE TABLE test_byte (col CHAR(4 BYTE));
INSERT INTO test_byte VALUES ('1234'); -- 没问题,4个ASCII字符=4字节
INSERT INTO test_byte VALUES ('一二三'); -- 报错!3个中文=9字节,远超4字节限制
什么时候该用CHAR?当你的数据长度基本一致的时候------手机号、身份证号、固定编码,这类场景用CHAR最合适,查询效率也更高。
1.2 VARCHAR2------用多少占多少,最灵活
VARCHAR2是日常开发中用得最多的变长字符串类型。跟CHAR不同,它不会用空格去填充,实际内容占多少就存多少。
sql
VARCHAR2(size [BYTE | CHAR])
最大能存多长?这取决于一个系统参数MAX_STRING_SIZE:
| 参数值 | 最大长度 |
|---|---|
| STANDARD | 4000字节 |
| EXTENDED | 32767字节 |
来看个实际的例子:
sql
CREATE TABLE user_info (
user_id INT,
username VARCHAR2(50 CHAR), -- 最多50个字符
email VARCHAR2(100 BYTE), -- 最多100个字节
bio VARCHAR2(500 CHAR) -- 个人简介,最多500个字符
);
INSERT INTO user_info VALUES (1, '张三', 'zhangsan@example.com', '全栈开发工程师');
-- VARCHAR2会原样存储,不会补空格
SELECT username, length(username) FROM user_info;
-- 结果:张三 | 2(实际字符长度)
顺便提一句,VARCHAR和VARCHAR2功能完全一样,可以互换使用。另外CHARACTER VARYING、CHAR VARYING也都是它的别名。
1.3 NCHAR与NVARCHAR2------多语言场景的标配
如果你的应用要同时支持中英日韩多国语言,那就得请出NCHAR和NVARCHAR2了。它们用Unicode国际字符集存储,不管什么语言的字符都能正确保存,不会出现乱码。
sql
-- NCHAR:定长的Unicode字符串
-- NVARCHAR2:变长的Unicode字符串
CREATE TABLE multilang (
label NCHAR(20), -- 固定20个Unicode字符位
content NVARCHAR2(500 CHAR) -- 变长,最多500个Unicode字符
);
-- 中英日文混合存储,完全没问题
INSERT INTO multilang VALUES ('Hello世界こんにちは', '多语言内容存储示例');
SELECT label, content FROM multilang;
两者的容量限制:
| 类型 | 编码方式 | 最大字符数 |
|---|---|---|
| NCHAR | AL16UTF16 | 1000字符 |
| NCHAR | UTF8 | 2000字符 |
| NVARCHAR2 | EXTENDED模式 | 最大32767字节 |
| NVARCHAR2 | STANDARD模式 | 最大4000字节 |
简单总结一下字符类型的选择思路:长度固定用CHAR,长度变化用VARCHAR2,涉及多语言就用NCHAR/NVARCHAR2。
二、数值类型:钱用NUMBER,计数用INT
数值类型看起来简单,但选错了后果很严重------尤其是涉及钱的时候。
数据库能存整数、小数、正数、负数、零,甚至还有几个特殊值:Infinity(正无穷)、-Infinity(负无穷)和NaN(不是数字)。
2.1 NUMBER------万能数值,精度你说了算
NUMBER是最通用的数值类型,你可以精确控制它保留多少位有效数字、多少位小数。
sql
NUMBER [(precision [, scale])]
- precision(精度):有效数字总共多少位,范围1~1000
- scale(标度):小数点后保留几位,范围-84~1000。标度还能是负数,表示从个位开始往左舍入
直接看例子最直观:
sql
CREATE TABLE financial_records (
record_id NUMBER(10), -- 10位整数,足够存ID
unit_price NUMBER(8,2), -- 8位有效数字、2位小数,如 999999.99
total NUMBER(12,2), -- 12位有效数字、2位小数,适合存大额金额
round_value NUMBER(12,-2) -- 标度为负,自动舍入到百位
);
-- 实际插入数据
INSERT INTO financial_records VALUES (100001, 29.90, 2990.00, 123456);
-- round_value 列存入123456,实际会被存为 123500(百位以下舍入)
SELECT * FROM financial_records;
精度和标度的效果演示:
sql
-- 四舍五入到3位有效数字
SELECT CAST(123.89 AS NUMBER(3)); -- 输出:124
-- 保留2位小数
SELECT CAST(123.89 AS NUMBER(5,2)); -- 输出:123.89
-- 保留1位小数,第2位小数四舍五入
SELECT CAST(123.89 AS NUMBER(6,1)); -- 输出:123.9
-- 小数点后5位,适合存精度很高的数据
SELECT CAST(0.000127 AS NUMBER(4,5)); -- 输出:0.00013
-- 超出精度直接报错
SELECT CAST(123.89 AS NUMBER(3,2));
-- ERROR: 超出精度范围
2.2 INT与SMALLINT------存整数,轻量又高效
如果你要存的只是整数(比如ID、数量、年龄),那用INT或SMALLINT比NUMBER更高效:
| 类型 | 取值范围 | 占用空间 |
|---|---|---|
| INT(别名INTEGER) | -2,147,483,648 ~ 2,147,483,647 | 4字节 |
| SMALLINT | -32,768 ~ 32,767 | 2字节 |
sql
CREATE TABLE orders (
order_id INT, -- 订单编号,21亿以内够用了
quantity SMALLINT, -- 购买数量,3万多个也够了
status SMALLINT -- 状态码:0待付款 1已付款 2已发货 3已完成
);
INSERT INTO orders VALUES (100001, 5, 0);
INSERT INTO orders VALUES (100002, 120, 1);
SELECT * FROM orders WHERE quantity > 10;
经验法则:一般业务数据用INT就够了,如果是状态码、排序号这种小范围的值,SMALLINT更省空间。
2.3 浮点类型------科学计算用,别拿来存钱
FLOAT是可变精度的浮点数。当你需要存科学数据、传感器读数这类对"绝对精确"要求不高的数值时,浮点类型就派上用场了。
sql
-- FLOAT:precision为1~24时是单精度(REAL),25~53时是双精度(DOUBLE PRECISION)
-- BINARY_FLOAT:32位单精度,范围 1.17549E-38 ~ 3.40282E+38
-- BINARY_DOUBLE:64位双精度,范围 2.22507E-308 ~ 1.79769E+308
实际使用:
sql
CREATE TABLE sensor_data (
sensor_id INT,
temperature BINARY_FLOAT, -- 温度传感器读数
pressure BINARY_DOUBLE, -- 气压传感器读数
recorded_at TIMESTAMP
);
-- 浮点数的字面量写法
INSERT INTO sensor_data VALUES (1, 36.5F, 101325.0D, CURRENT_TIMESTAMP);
INSERT INTO sensor_data VALUES (2, -0.1F, 101200.5D, CURRENT_TIMESTAMP);
-- 查询浮点数据
SELECT sensor_id, temperature, pressure FROM sensor_data;
-- 浮点数支持特殊值
SELECT 'Infinity'::float8; -- 正无穷
SELECT '-Infinity'::float8; -- 负无穷
SELECT 'NaN'::float8; -- 不是数字
重点提醒:浮点数用的是二进制存储,没法精确表示某些十进制小数(比如0.1)。所以,凡是跟钱沾边的,一律用NUMBER,千万别用FLOAT!
三、日期时间类型:不只是"存个日期"那么简单
时间看起来简单,但真要在业务里用好,得区分好几种不同的精度和场景。
3.1 DATE------基本的日期+时间
DATE存的是年、月、日、时、分、秒,从公元前4713年到公元5874897年,跨度大得离谱。
sql
CREATE TABLE events (
event_id INT,
event_name VARCHAR2(100),
event_date DATE
);
-- 用TO_DATE把字符串转成DATE
INSERT INTO events VALUES (1, '项目启动', TO_DATE('2024-12-01', 'YYYY-MM-DD'));
INSERT INTO events VALUES (2, '年终总结', TO_DATE('2024-12-31 14:00:00', 'YYYY-MM-DD HH24:MI:SS'));
-- 不指定时间的话,默认是午夜 00:00:00
SELECT event_name, event_date FROM events;
-- 输出:
-- 项目启动 | 2024-12-01 00:00:00
-- 年终总结 | 2024-12-31 14:00:00
-- DATE之间可以直接做加减
SELECT event_name, event_date - TO_DATE('2024-12-01','YYYY-MM-DD') AS days_diff
FROM events;
-- 输出:
-- 项目启动 | 0
-- 年终总结 | 30
3.2 TIMESTAMP------精确到毫秒甚至微秒
TIMESTAMP是DATE的"加强版",在年月日时分秒之外,还能存小数秒,精度最高到6位(微秒级)。
sql
TIMESTAMP [(fractional_seconds_precision)] -- 小数秒位数,0~6,默认6
sql
CREATE TABLE system_log (
log_id INT,
log_time TIMESTAMP(3), -- 精确到毫秒(3位小数)
level VARCHAR2(10),
message VARCHAR2(500)
);
-- 插入带毫秒精度的时间
INSERT INTO system_log VALUES (
1,
TO_TIMESTAMP('2024-01-01 09:30:53.123', 'YYYY-MM-DD HH24:MI:SS.FF3'),
'INFO',
'系统启动完成'
);
INSERT INTO system_log VALUES (
2,
TO_TIMESTAMP('2024-01-01 09:30:53.456', 'YYYY-MM-DD HH24:MI:SS.FF3'),
'ERROR',
'数据库连接超时'
);
-- 精确到毫秒的查询
SELECT log_id, log_time, level, message FROM system_log
WHERE log_time > TO_TIMESTAMP('2024-01-01 09:30:53.200', 'YYYY-MM-DD HH24:MI:SS.FF3');
3.3 带时区的时间戳------跨时区应用的必备
如果你的用户分布在北京、纽约、伦敦,时间戳就得带上时区信息,不然时间就全乱套了。
数据库提供了两种带时区的TIMESTAMP:
- TIMESTAMP WITH TIME ZONE:原样保存时区偏移,查询时显示带时区的时间
- TIMESTAMP WITH LOCAL TIME ZONE:自动转换为当前会话时区显示
sql
-- 场景:跨国会议安排
CREATE TABLE global_meetings (
meeting_id INT,
topic VARCHAR2(200),
start_time TIMESTAMP WITH TIME ZONE, -- 保留原始时区
end_time TIMESTAMP WITH LOCAL TIME ZONE -- 自动转本地时区
);
-- 北京时间下午3点开会
INSERT INTO global_meetings VALUES (
1,
'全球技术同步会',
TIMESTAMP '2024-06-15 15:00:00 +08:00',
TIMESTAMP '2024-06-15 17:00:00 +08:00'
);
-- 查询时能看到时区信息
SELECT meeting_id, topic, start_time FROM global_meetings;
-- 输出:1 | 全球技术同步会 | 2024-06-15 15:00:00 +08:00
3.4 INTERVAL------表示"一段时间"
INTERVAL不是某个时间点,而是一段时间的长度。比如"3个月"、"2天4小时30分钟"这种。
它有两种形式:
sql
-- INTERVAL YEAR TO MONTH:以"年+月"为单位
-- INTERVAL DAY TO SECOND:以"天+时+分+秒"为单位
-- 例子:2004年闰年2月29日,加4年后是哪天?
SELECT TO_DATE('29-FEB-2004', 'DD-MON-YYYY') + TO_YMINTERVAL('4-0') AS result;
-- 输出:2008-02-29 00:00:00
-- 实际建表使用
CREATE TABLE project_schedule (
project_name VARCHAR2(100),
start_time TIMESTAMP,
buffer_time INTERVAL DAY(3) TO SECOND(0), -- 缓冲时间,精确到天和秒
duration INTERVAL YEAR TO MONTH -- 项目周期,以年月为单位
);
INSERT INTO project_schedule VALUES (
'ERP系统建设',
TO_TIMESTAMP('2024-03-01 09:00:00', 'YYYY-MM-DD HH24:MI:SS'),
'0 12:30:00', -- 半天缓冲
'1-6' -- 1年6个月
);
SELECT project_name,
start_time,
buffer_time,
duration,
start_time + buffer_time AS adjusted_start
FROM project_schedule;
最后来个快速对照表:
| 类型 | 什么时候用 | 典型场景 |
|---|---|---|
| DATE | 不需要毫秒精度 | 生日、节假日、订单日期 |
| TIMESTAMP | 需要精确到毫秒/微秒 | 系统日志、审计追踪 |
| TIMESTAMP WITH TIME ZONE | 用户跨越多个时区 | 国际会议、全球化系统 |
| INTERVAL | 算时间差或设偏移 | 项目排期、定时任务 |
四、大对象类型:图片、长文、视频往里塞
普通字符串存几万字还行,但如果你要存一张高清图片或者一部视频,那就要靠LOB(Large Object)类型了。
4.1 BLOB------二进制文件专用
BLOB专门用来存二进制格式的大文件------图片、音频、视频都可以,最大能存到1GB。
sql
CREATE TABLE media_library (
file_id INT,
file_name VARCHAR2(200),
file_type VARCHAR2(20), -- image/audio/video
file_size INT, -- 文件大小(字节)
file_data BLOB -- 实际文件内容
);
-- 插入一条记录(实际开发中通常通过程序写入BLOB数据)
INSERT INTO media_library (file_id, file_name, file_type, file_size, file_data)
VALUES (1, 'banner.jpg', 'image', 204800, empty_blob());
4.2 CLOB与NCLOB------超长文本的归宿
- CLOB:用数据库字符集存大段文本,最大1GB
- NCLOB:用Unicode国际字符集存大段文本,最大1GB
sql
CREATE TABLE knowledge_base (
article_id INT PRIMARY KEY,
title VARCHAR2(200),
author VARCHAR2(50),
content CLOB, -- 文章正文,几万字不在话下
summary NCLOB, -- Unicode格式的摘要
created_at DATE
);
INSERT INTO knowledge_base VALUES (
1,
'数据库性能优化实战',
'技术团队',
'本文详细介绍了数据库性能优化的各个方面,包括索引设计、查询优化、缓存策略......(此处省略一万字)',
'从索引到缓存,全方位讲解性能优化方法',
TO_DATE('2024-06-01', 'YYYY-MM-DD')
);
-- CLOB也能用LIKE搜索
SELECT title FROM knowledge_base WHERE content LIKE '%索引设计%';
4.3 BFILE------不存文件,只存"文件在哪"
BFILE比较特殊------它不存文件内容,只存一个指向操作系统文件的"指针"。而且只能读,不能改。
sql
-- 先创建一个目录对象(相当于给文件路径起个别名)
CREATE DIRECTORY doc_dir AS '/data/documents';
-- 建表
CREATE TABLE external_docs (
doc_id INT,
doc_name VARCHAR2(200),
doc_ref BFILE -- 指向外部文件的引用
);
-- 用BFILENAME函数插入文件引用
INSERT INTO external_docs VALUES (1, '年度报告', BFILENAME('doc_dir', 'annual_report.pdf'));
INSERT INTO external_docs VALUES (2, '技术规范', BFILENAME('doc_dir', 'tech_spec.docx'));
-- 查询时看到的是引用信息,不是文件内容
SELECT doc_id, doc_name FROM external_docs;
使用建议:BFILE适合存那些"体积大但不需要在数据库里修改"的文件,比如归档的PDF、历史扫描件等。
五、RAW类型:二进制数据"原样搬运"
RAW类型用来存二进制数据,它最实用的特点是:在不同字符集的数据库或客户端之间传输时,数据不会被"自作主张"地转换编码。
sql
RAW [(size)] -- size范围1~32767字节
LONG RAW -- 跟RAW一样,但不能指定长度
sql
CREATE TABLE raw_data (
id INT,
bin_data RAW(1024), -- 最多1024字节的二进制数据
signature RAW(256) -- 256字节的数字签名
);
-- 用HEXTORAW把十六进制字符串转成RAW
INSERT INTO raw_data VALUES (1, HEXTORAW('0A1B2C3D4E5F'), HEXTORAW('AABBCCDD'));
-- 用RAWTOHEX把RAW转回十六进制查看
SELECT id, RAWTOHEX(bin_data) AS hex_data FROM raw_data;
-- 输出:1 | 0A1B2C3D4E5F
什么时候用RAW?加密数据、通信协议数据、设备原始报文------这类"就是一串字节,别给我做任何转换"的场景。
六、JSON类型:灵活数据的存储利器
现在几乎所有应用都在用JSON------配置信息、API返回值、动态表单,到处都是。数据库原生支持JSON,不用再把JSON当普通字符串存了。
数据库提供了两种JSON类型:JSON 和JSONB。
6.1 JSON和JSONB到底选哪个?
这是最常被问到的问题。简单说:
- JSON:原样存储,存得快,但每次查询都要重新解析
- JSONB:存的时候就解析成二进制,占空间稍大,但查询快得多,还支持索引
| 对比项 | JSON | JSONB |
|---|---|---|
| 存储方式 | 原始文本 | 二进制格式 |
| 空格处理 | 原样保留 | 自动去掉 |
| 键的顺序 | 保持输入顺序 | 自动排序 |
| 重复键 | 全部保留 | 只留最后一个 |
| 查询速度 | 每次要重新解析 | 直接查,快 |
| 能建索引吗 | 不能 | 能(GIN索引) |
结论:绝大多数场景下,直接用JSONB就对了。
6.2 JSON基本操作
sql
-- 创建一张存JSON数据的表
CREATE TABLE api_data (
id INT,
jdoc JSONB
);
-- 插入一条JSON数据
INSERT INTO api_data VALUES (1, '{
"guid": "9c36adc1-7fb5-4d5b-83b4-90356a46061a",
"name": "张三",
"is_active": true,
"score": 95.5,
"tags": ["developer", "architect", "leader"]
}');
-- 再插一条
INSERT INTO api_data VALUES (2, '{
"guid": "7f3b2a01-4e9c-4d2f-b8a1-6234567890ab",
"name": "李四",
"is_active": false,
"score": 88.0,
"tags": ["designer", "leader"]
}');
-- 用 -> 提取JSON对象(返回JSON类型)
-- 用 ->> 提取JSON对象(返回文本)
SELECT
jdoc->>'name' AS name,
jdoc->'score' AS score,
jdoc->'is_active' AS active
FROM api_data;
-- 输出:
-- name | score | active
-- ------+-------+--------
-- 张三 | 95.5 | true
-- 李四 | 88.0 | false
6.3 JSONB查询:包含、存在、嵌套
JSONB的查询能力非常强大,几个操作符就能搞定大部分需求:
sql
-- @> 包含查询:tags里有没有"leader"?
SELECT jdoc->>'name' AS name FROM api_data
WHERE jdoc @> '{"tags":["leader"]}'::jsonb;
-- 输出:张三 和 李四 都有leader标签
-- ? 存在查询:有没有某个键?
SELECT jdoc->>'name' AS name FROM api_data
WHERE jdoc ? 'is_active';
-- 输出:两条都有
-- ?| 任一存在:有没有tags或者email?
SELECT jdoc->>'name' AS name FROM api_data
WHERE jdoc ?| array['tags', 'email'];
-- ?& 全部存在:是不是同时有name和score?
SELECT jdoc->>'name' AS name FROM api_data
WHERE jdoc ?& array['name', 'score'];
嵌套查询也不在话下:
sql
-- 更复杂的嵌套数据
INSERT INTO api_data VALUES (3, '{
"name": "王五",
"tags": [
{"term": "backend", "level": "senior"},
{"term": "cloud", "level": "expert"}
]
}');
-- 查找tags中包含特定term的文档
SELECT jdoc->>'name' FROM api_data
WHERE jdoc @> '{"tags":[{"term":"backend"}]}';
6.4 给JSONB建索引,查询飞起来
数据量一大,JSONB查询也会变慢。这时候就得靠GIN索引了:
sql
-- 通用GIN索引,支持 @>、?、?&、?| 操作符
CREATE INDEX idx_jsonb ON api_data USING gin (jdoc);
-- jsonb_path_ops索引:只支持@>操作符,但更小更快
CREATE INDEX idx_jsonb_path ON api_data USING gin (jdoc jsonb_path_ops);
-- 针对特定键的表达式索引(最精准)
CREATE INDEX idx_jsonb_tags ON api_data USING gin ((jdoc -> 'tags'));
建完索引后,包含查询就能走索引了,速度快很多:
sql
-- 这个查询会走 idx_jsonb_tags 索引
SELECT jdoc->>'name', jdoc->'score' FROM api_data
WHERE jdoc -> 'tags' ? 'leader';
6.5 JSONPATH------更灵活的路径查询
除了基本的操作符,还可以用JSONPATH语法做更高级的查询:
sql
-- 用 @@ 操作符配合JSONPATH语法
SELECT jdoc->>'name' FROM api_data
WHERE jdoc @@ '$.tags[*] == "leader"';
-- 带条件的路径查询
SELECT jdoc->>'name' FROM api_data
WHERE jdoc @@ '$.score ? (@ > 90)';
-- 输出:张三(score=95.5)
七、XML类型:结构化文档怎么存
XML在金融、政务、企业集成这些领域用得很多。相比把XML随便塞进一个文本字段,用专门的XML类型有个好处------存储时会自动检查XML结构合不合法。
7.1 XML基础用法
sql
CREATE TABLE xml_docs (
doc_id INT,
content XML
);
-- 方式一:用XMLPARSE创建
INSERT INTO xml_docs VALUES (1,
XMLPARSE(DOCUMENT '<?xml version="1.0"?>
<book>
<title>数据库入门</title>
<chapter id="1">基础概念</chapter>
<chapter id="2">进阶技巧</chapter>
</book>')
);
-- 方式二:直接类型转换
INSERT INTO xml_docs VALUES (2, '<note><to>张三</to><body>明天开会</body></note>'::xml);
-- 查询XML内容
SELECT doc_id, content FROM xml_docs;
-- 把XML转回字符串
SELECT XMLSERIALIZE(DOCUMENT content AS text) FROM xml_docs WHERE doc_id = 1;
-- 判断是完整文档还是文档片段
SELECT content IS DOCUMENT AS is_full_doc FROM xml_docs;
7.2 XMLType------更强大的XML操作
XMLType提供了一套方法,可以用XPath表达式在XML里精准定位和提取数据:
sql
CREATE TABLE xml_orders (
order_id INT,
order_data XMLType
);
-- 插入XML格式的订单数据
INSERT INTO xml_orders VALUES (1, XMLType('<?xml version="1.0"?>
<Order>
<Customer>李四</Customer>
<Items>
<Item sku="A001">
<Name>键盘</Name>
<Price>299</Price>
<Qty>2</Qty>
</Item>
<Item sku="A002">
<Name>鼠标</Name>
<Price>89</Price>
<Qty>1</Qty>
</Item>
</Items>
<Total>687</Total>
</Order>'));
-- 使用XMLType的方法查询
SELECT x.order_data FROM xml_orders x;
注意:XML类型没有比较操作符,不能直接在上面建索引。需要快速搜索的话,要在XPath表达式上建函数索引。
八、几何类型:坐标、区域、距离计算
这组类型可能日常业务不太常用,但在地图应用、CAD设计、游戏开发这些场景中就非常关键了。
数据库内置了7种二维几何类型:
| 类型 | 占多大 | 干什么用 | 怎么写 |
|---|---|---|---|
| point | 16字节 | 平面上的一个点 | (x, y) |
| line | 32字节 | 无限长的直线 | {A, B, C} |
| lseg | 32字节 | 一段线段 | ((x1,y1),(x2,y2)) |
| box | 32字节 | 矩形 | ((x1,y1),(x2,y2)) |
| path | 16+16n字节 | 路径(开放或封闭) | [(x1,y1),...]或((x1,y1),...) |
| polygon | 40+16n字节 | 多边形 | ((x1,y1),...) |
| circle | 24字节 | 圆 | <(x,y),r> |
sql
-- 建一张带几何字段的表
CREATE TABLE locations (
name VARCHAR2(50),
center point, -- 中心坐标
area polygon, -- 区域多边形
boundary circle -- 圆形边界
);
INSERT INTO locations VALUES (
'总部大楼',
'(116.397, 39.908)', -- 一个点
'((116.39, 39.90), (116.40, 39.90),
(116.40, 39.91), (116.39, 39.91))', -- 四边形
'<(116.397, 39.908), 0.005>' -- 以点为圆心的圆
);
SELECT name, center, area, boundary FROM locations;
8.1 几何计算示例
sql
-- 计算两点之间的距离
SELECT point '(1,1)' <-> point '(4,5)' AS distance;
-- 输出:5(勾股定理:√(3²+4²) = 5)
-- 判断一个点是否在矩形内
SELECT box '((0,0),(2,2))' @> point '(1,1)' AS is_inside;
-- 输出:true
SELECT box '((0,0),(2,2))' @> point '(3,3)' AS is_inside;
-- 输出:false
-- 计算圆的面积
SELECT area(circle '<(0,0), 5>') AS circle_area;
-- 输出:78.5398163397448(π × 5²)
-- 两个矩形是否重叠
SELECT box '((0,0),(2,2))' && box '((1,1),(3,3))' AS overlaps;
-- 输出:true
-- 计算两点之间的线段中点
SELECT center(lseg '((0,0),(4,4))') AS midpoint;
-- 输出:(2,2)
九、网络地址类型:别再用字符串存IP了
很多人图省事,用VARCHAR存IP地址。但字符串存IP有个大问题------你没法判断"192.168.1.100是不是属于192.168.1.0/24这个网段",除非你自己写一大堆逻辑。
用专门的inet和cidr类型,这些事数据库帮你做了。
| 类型 | 占多大 | 存什么 |
|---|---|---|
| cidr | 7或19字节 | IPv4/IPv6网络地址 |
| inet | 7或19字节 | IPv4/IPv6主机地址 |
| macaddr | 6字节 | MAC地址 |
| macaddr8 | 8字节 | MAC地址(EUI-64格式) |
9.1 inet与cidr
sql
CREATE TABLE network_assets (
asset_name VARCHAR2(50),
ip_addr inet,
subnet cidr
);
INSERT INTO network_assets VALUES ('web-server-1', '192.168.1.100', '192.168.1.0/24');
INSERT INTO network_assets VALUES ('db-server', '10.0.0.5/16', '10.0.0.0/16');
INSERT INTO network_assets VALUES ('cache-server', '172.16.5.20', '172.16.0.0/16');
-- 检查IP是否属于某个网段(<< 表示"包含在")
SELECT asset_name FROM network_assets WHERE ip_addr << '192.168.1.0/24'::cidr;
-- 输出:web-server-1
-- 获取IP的网络部分
SELECT network('192.168.1.100/24');
-- 输出:192.168.1.0/24
-- 获取主机部分
SELECT host('192.168.1.100/24');
-- 输出:192.168.1.100
-- 判断两个网段是否重叠
SELECT inet '192.168.1.0/24' && inet '192.168.1.128/25' AS overlaps;
-- 输出:true
inet和cidr的区别:inet允许"非标准"写法(比如192.168.0.1/24,主机位不是全0),cidr则要求严格的网络地址。
9.2 MAC地址
sql
CREATE TABLE devices (
device_id INT,
device_name VARCHAR2(50),
mac macaddr
);
-- 下面这几种写法都是同一个MAC地址
INSERT INTO devices VALUES (1, '服务器A', '08:00:2b:01:02:03');
INSERT INTO devices VALUES (2, '服务器B', '08-00-2b-01-02-03');
INSERT INTO devices VALUES (3, '交换机', '08002b:010203');
INSERT INTO devices VALUES (4, '路由器', '0800.2b01.0203');
INSERT INTO devices VALUES (5, '防火墙', '08002b010203');
-- 查询时统一输出为冒号分隔格式
SELECT device_id, device_name, mac FROM devices;
-- 输出全部显示为 08:00:2b:01:02:03
-- EUI-64格式的MAC地址(8字节)
INSERT INTO devices VALUES (6, '无线AP', '08:00:2b:01:02:03:04:05'::macaddr8);
-- 把48位MAC转成EUI-64格式
SELECT macaddr8_set7bit('08:00:2b:01:02:03');
-- 输出:0a:00:2b:ff:fe:01:02:03
十、全文搜索:让数据库帮你"找文章"
如果你要在大量文本中搜索关键词,LIKE '%关键词%' 也能凑合用,但性能很差,而且没法按相关度排序。全文搜索类型就是专门解决这个问题的。
它靠两个类型配合工作:
- tsvector:把文档文本拆解、去重、排序后存储的"词袋"
- tsquery:你要搜索的关键词组合
10.1 tsvector------把文本变成可搜索的格式
sql
-- 直接转tsvector:自动去重和排序
SELECT 'a fat cat sat on a mat and ate a fat rat'::tsvector;
-- 输出:'a' 'and' 'ate' 'cat' 'fat' 'mat' 'on' 'rat' 'sat'
-- 注意:重复的a和fat只出现一次
-- 用to_tsvector进行语言相关的处理(推荐)
SELECT to_tsvector('english', 'The Fat Rats');
-- 输出:'fat':2 'rat':3
-- "The"被过滤(停用词),"Fat"变成小写"fat","Rats"变成词干"rat"
-- 冒号后面的数字是词在原文中的位置
10.2 tsquery------构建搜索条件
tsquery支持布尔操作符来组合搜索词:
| 操作符 | 含义 | 例子 |
|---|---|---|
| & | AND,同时包含 | fat & rat |
| | | OR,包含任一 | fat | cat |
| ! | NOT,不包含 | fat & !cat |
| <-> | 紧跟在后面 | quick <-> fox |
sql
-- 简单AND查询
SELECT 'fat & rat'::tsquery;
-- 输出:'fat' & 'rat'
-- 组合查询
SELECT 'fat & (rat | cat)'::tsquery;
-- 输出:'fat' & ( 'rat' | 'cat' )
-- 排除查询
SELECT 'fat & rat & !cat'::tsquery;
-- 输出:'fat' & 'rat' & !'cat'
-- 前缀匹配
SELECT 'super:*'::tsquery;
-- 会匹配superman、supernatural等所有以super开头的词
10.3 实战:创建全文搜索系统
sql
-- 建表
CREATE TABLE articles (
article_id INT,
title VARCHAR2(200),
body TEXT
);
INSERT INTO articles VALUES (1, '数据库性能优化',
'数据库性能优化是每个开发者的必修课。索引设计、查询优化、缓存策略都是关键技能。');
INSERT INTO articles VALUES (2, 'Python入门教程',
'Python是一门非常适合初学者的编程语言。它的语法简洁,社区活跃。');
INSERT INTO articles VALUES (3, '分布式数据库架构',
'分布式数据库通过数据分片和复制来提高系统的可用性和性能。');
-- 创建GIN索引加速全文搜索
CREATE INDEX idx_articles_body ON articles
USING gin (to_tsvector('english', body));
-- 搜索:找出提到"database"和"performance"的文章
SELECT title FROM articles
WHERE to_tsvector('english', body) @@ to_tsquery('database & performance');
-- 搜索:找出提到"database"或"python"的文章
SELECT title FROM articles
WHERE to_tsvector('english', body) @@ to_tsquery('database | python');
-- 按相关度排序(使用ts_rank)
SELECT title, ts_rank(to_tsvector('english', body), to_tsquery('database')) AS rank
FROM articles
WHERE to_tsvector('english', body) @@ to_tsquery('database')
ORDER BY rank DESC;
十一、范围类型:优雅地表达"从A到B"
生活中到处都是"区间"的概念------价格区间、日期区间、年龄范围。以前你可能用两个字段(start和end)来表示,现在可以用一个范围类型搞定,而且天然支持"是否重叠"、"是否包含"这类查询。
11.1 内置的6种范围类型
| 类型 | 子类型 | 说明 |
|---|---|---|
| int4range | integer | 整数范围 |
| int8range | bigint | 大整数范围 |
| numrange | numeric | 小数范围 |
| tsrange | timestamp | 时间戳范围(无时区) |
| tstzrange | timestamptz | 时间戳范围(带时区) |
| daterange | date | 日期范围 |
11.2 范围操作实战
范围用方括号[]表示包含边界,用圆括号()表示不包含边界:
sql
-- 会议室预约系统
CREATE TABLE room_booking (
room_id INT,
booked_by VARCHAR2(50),
during tsrange
);
INSERT INTO room_booking VALUES
(101, '张三', '[2025-01-15 09:00, 2025-01-15 10:00)'), -- 9点到10点
(101, '李四', '[2025-01-15 10:00, 2025-01-15 11:30)'), -- 10点到11点半
(102, '王五', '[2025-01-15 09:30, 2025-01-15 12:00)'); -- 9点半到12点
-- 检查101会议室有没有时间冲突
SELECT booked_by FROM room_booking
WHERE room_id = 101
AND during && '[2025-01-15 09:30, 2025-01-15 10:30)'::tsrange;
-- 输出:张三(他的9:00-10:00跟9:30-10:30有重叠)
-- 包含检查:15在不在[10,20)范围内?
SELECT int4range(10, 20) @> 15;
-- 输出:true
-- 重叠检查:两个范围有没有交集?
SELECT numrange(11.1, 22.2) && numrange(20.0, 30.0);
-- 输出:true(11.1~22.2 跟 20.0~30.0 在20.0~22.2处重叠)
-- 取交集
SELECT int4range(10, 20) * int4range(15, 25);
-- 输出:[15,20)
-- 取上界和下界
SELECT lower(int4range(10, 20)), upper(int4range(10, 20));
-- 输出:10 | 20
-- 范围是否为空
SELECT isempty(numrange(1, 5));
-- 输出:false
11.3 自定义范围类型
如果内置的几种不够用,你可以自己定义:
sql
-- 创建一个浮点数范围类型
CREATE TYPE floatrange AS RANGE (
subtype = float8,
subtype_diff = float8mi
);
-- 直接使用
SELECT '[1.234, 5.678]'::floatrange;
-- 输出:[1.234,5.678)
-- 也可以创建一个时间范围类型
CREATE FUNCTION time_subtype_diff(x time, y time) RETURNS float8 AS
$$ SELECT EXTRACT(EPOCH FROM (x - y)) $$ LANGUAGE sql STRICT IMMUTABLE;
CREATE TYPE timerange AS RANGE (
subtype = time,
subtype_diff = time_subtype_diff
);
-- 使用自定义时间范围
SELECT '[11:10, 23:00]'::timerange;
-- 输出:[11:10:00,23:00:00)
十二、其他实用类型
12.1 ROWID------每一行的"身份证号"
ROWID是数据库给表中每一行自动分配的逻辑标识。它以23个16进制字符展示,是单调递增的------后插入的数据ROWID一定更大。
sql
-- 查看每一行的ROWID
SELECT ROWID, employee_id, name FROM employees WHERE employee_id <= 5;
-- 用ROWID定位特定行(比用主键还快)
SELECT * FROM employees WHERE ROWID = 'AAAACMAABAAAAEqAAA';
-- ROWID支持比较操作符
SELECT ROWID, name FROM employees
WHERE ROWID > 'AAAACMAABAAAAEqAAA'
ORDER BY ROWID;
12.2 用户自定义类型
当内置类型没法满足你的业务模型时,可以自己定义类型:
sql
-- 定义一个对象类型:地址
CREATE TYPE address_type AS OBJECT (
province VARCHAR2(50),
city VARCHAR2(50),
street VARCHAR2(200),
post_code CHAR(6)
);
-- 在表中使用自定义类型
CREATE TABLE customers (
customer_id INT,
name VARCHAR2(50),
home_addr address_type
);
-- 可变数组(Varray):一组有序元素,需指定最大数量
-- 嵌套表(Nested Table):一组无序元素
总结:一张表选对数据类型
| 你要存什么 | 用这个类型 | 常见场景 |
|---|---|---|
| 长度固定的文本 | CHAR | 手机号、邮编、编码 |
| 长度不固定的文本 | VARCHAR2 | 姓名、地址、备注 |
| 多语言文本 | NVARCHAR2 | 国际化应用的界面文本 |
| 精确小数(尤其是钱) | NUMBER(p,s) | 价格、金额、折扣率 |
| 整数 | INT / SMALLINT | ID、数量、年龄、状态码 |
| 科学数据、传感器值 | BINARY_FLOAT / BINARY_DOUBLE | 温度、气压、坐标值 |
| 日期(不要求毫秒) | DATE | 生日、订单日期 |
| 精确时间戳 | TIMESTAMP | 日志、审计记录 |
| 跨时区时间 | TIMESTAMP WITH TIME ZONE | 全球化系统 |
| 大段文本 | CLOB / NCLOB | 文章、合同、日志文件 |
| 图片、音视频 | BLOB | 多媒体文件存储 |
| 二进制数据 | RAW | 加密数据、协议报文 |
| 灵活结构的数据 | JSONB | 配置信息、动态属性 |
| XML文档 | XML / XMLType | 金融报文、政务数据 |
| 平面坐标、区域 | point / polygon / circle | 地图标记、区域范围 |
| IP地址 | inet / cidr | 网络设备管理 |
| MAC地址 | macaddr | 设备管理 |
| 文本关键词搜索 | tsvector + tsquery | 文档搜索、内容检索 |
| 区间范围 | 各类range类型 | 排期、定价区间 |
最后记住三个选类型的原则:
- 精确优先:能用NUMBER就别用FLOAT,尤其是算钱的时候
- 够用就好:能用SMALLINT就别用INT,能用VARCHAR2(50)就别用VARCHAR2(4000),省空间就是省性能
- 语义匹配:IP地址别用字符串,时间别用字符串,让数据库帮你做数据校验