为什么数据类型如此重要
目录
在《在开始之前》,简要地提到了数据类型。编程语言里,数据类型是语言本身的重要组成部分,程序在运行过程,就是操作数据的过程,在 SQL 中也一样。如果这样说,程序处理的对象,是某个具体实例化的、带有数据类型并受之约束的变量,会把你绕晕,那让我们从另外的角度看来待数据类型。
你可能不是专业的编程人员,对一些概念不太理解。不要紧,这些会在使用数据库和编写 SQL 脚本的过程中慢慢学习到。若有些段落不太理解的,请先跳过,但不建议完全忽略本文的其它内容。
什么数据类型 #
数据类型是基础数据结构,由编程语言所提供的内置实现的最小数据单元。数据类型定义了程序中可以操作的最小数据单位。复合数据类型或者用户自定义数据结构,都是由基础数据类型组成的。
在编写 SQL 时,查询结果的每一列数据,都带有数据类型。它有可能是表字段的数据类型,或者经过计算转换,得到新的数据类型。
与在编写 pl/sql 存储过程或者函数时,遇到变量定义,需要指定它的具体数据类型一样,在对建模、表设计时,我们需要将逻辑模型中的字段进行物理化设计,此时需要结合数据库支持的数据类型,来定义每个字段的具体数据类型。这个数据类型将决定数据库如何存储数据,并且在 SQL 中如何体现。
-- postgres
CREATE TABLE employees (
emp_no INT NOT NULL,
birth_date DATE NOT NULL,
first_name VARCHAR(14) NOT NULL,
last_name VARCHAR(16) NOT NULL,
-- gender ENUM ('M','F') NOT NULL,
gender char(1) not null CHECK(gender in ('M','F')),
hire_date DATE NOT NULL,
PRIMARY KEY (emp_no)
);
上例是
employees 示例库中
的雇员表,使用 int(整数)
作为员工号的类型,性别使用带有条件约束的 char(定长字符串)
作为类型。
平常接触最多的要数字符(串)、数字等类型(因为它们覆盖了绝大部分字段),往往不会去深究数据库所提供的数据类型说明文档。我们了解数据类型,可以不去了解数据库如何存储不同类型的数据,但不能不了解数据类型的种类。
例个粟子。我们使用 “(经度, 纬度)” 来表示地理位置,如 “-99.5075448 27.5064117”。如果使用传统的方法,可能会使用两个 numeric 类型的字段,分别表示经纬度。再改进一下,可能会把这个经纬度的字面值转为字符串后保存在一个 varchar 可变长字符串字段中。请注意,字符串在转换为数字之前,是不能进行数值计算的。倘若有一个数据类型,假设它叫 GEO,可以让它保存这个经纬度的值,又提供一些经纬度类型专用的数据操作函数,会不会更香呢?
当然,最终使用什么数据类型,是受目标需求与使用的数据库而定的。不是使用越多个性化、越强大越显摆的类型,就一定是对的。如果只是为了显示,没有什么比字符、字符串更省事了。
再来一个粟子。在编程中,“ (key, (key, value…)) ” 这种带有嵌套结构的数据非常常见。如果数据是存储在数据库中,你会非常希望,读到了这个字段,自动就可得到这个嵌套结构,或者从数据库中对这个嵌套数据进行筛选,使用 “key.key…” 提取其中的值。而没有专用数据类型之前,还只能把整个值当成是一个长长的字符串,塞到一个字段,等取出来之后,再使用专门的格式解析器来分析这段特别格式文本,何止用麻烦可以形容。还好,当前主流数据库,在这方面下了功夫,比如 xml、json、地理数据这类结构复杂的类型,都有专门的支持。
这确实涉及到了数据结构的范畴。不过不用担心,本文不是指导你来设计新的数据结构类型,而是解释数据库的数据类型、并如何运用它来帮助完成工作。
基础数据类型 #
一般地,常见的数据类型有数字类型、字符类型,以及日期时间类型、XML、JSON、GEO地理等几大类,有的数据库还支持集合类型。
本节所提数据类型,并非所有的数据库均有支持。
数字类型 #
类型名称 | 描述 |
---|---|
int2/smallint | 使用2个字节空间的有符号整数,值范围为 -2(7+8) ~ 2(7+8)-1 |
int/int4/integer | 使用4个字节空间的有符号整数,值范围为 -2(7+24) ~ 2(7+24)-1 |
int8/bigint | 使用4个字节空间的有符号整数,值范围为 -2(7+56) ~ 2(7+56)-1 |
numeric/decimal | 任意精度十进制数字,自定义整数与小数位 |
float/real/double | 浮点型/单精度/双精度 |
有整数型、通用型之分。
因整数的存储占用空间,有 2字节、4字节、8字节,分别对应 int2、int4、int8,一般的 integer
对应 int4。
通用型数字类型,有的数据库叫 numeric,也有叫 decimal,或者两者都支持。它们通常定义整数与小数据部分,如 numeric(p,s)
,p
为整数位长,s
为小数位数。
使用数字类型时,要记得 整数 与 非整数数字混合计算时的小数位问题;单精度双精度等浮点数计算时的精度问题。设计表字段时,要注意类型的精度与取值是否满足实际的需要。
若需要有精确的存储和计算的(如表示货币金额)的,请使用 numeric/decimal 类型。但为什么呢 🤔(搜索了解浮点数计算问题)
字符类型 #
类型名称 | 描述 |
---|---|
character(n), char(n) | 定长字符串,使用空格补齐 |
character varying(n), varchar(n) | 可变长字符串 |
text, clob | 超长文本 |
字符串实际上一个结构体,它是一个字节列表以及指向这个列表的指针所构成。因长度,有 可变成(varchar) 与 *不可变长(char)*之分。这个区分,并不是需求场景所致,而是底层实现的差别。定长的字符串,系统会为所有值预分配空间,当有值且字符值不足长时,使用空格在右边补齐。
而 text 或 clob 是超长文本类型,用于存储超过 varchar 最大长度限制的文本。
⚠️ 开卷:
- 查阅数据库文档后思考,定长字符串、可变长字符串类型分别在什么场景下使用?什么时候使用 text 或者 clob?
- 大部分人初学者都犯错误的问题 – 空字符串与空值
日期时间类型 #
date, 日期
time,时间
datetime / timestamp,日期+时间、时间戳
interval,日期、时间间隔
日期时间类型可细分为日期、时间、日期时间(或时间戳)、间隔。日期类是非常有趣的数据类型,它与数字、字符构成了重要的三种类型。以日期为例,数据库通常使用三个部位来分别表示年、月、日,配合对应的日期函数,可以玩出非常多的花样,月初、月末。部分数据库还提供间隔类型(interval),表示两个日期之间的差,也可用来计算如 “几天前”、“几天后” 等这些常见的日期计算。
使用日期类型时,需要注意时区对时间值的影响,数据库有具体的对应方法。
⚠️ 开卷:
- 在 postgres 上查出今天的日期、当前的时间戳,并使用函数计算月初、月末、年初、年末,提取当前月份。然后再到 mysql 数据库上实现同时的计算。并观察不同数据库上的差异。
集合类型 #
表示集合的有数组 与集类型。
数组类型是指具有相同数据类型元素组成的列表,集是有相同数据类型元素组成的集合。数组与集的区别在于,数据中的元素有排列顺序,元素值可以相同,而集内元素是无序且唯一的。
集类型可以用于表示码值字段的可选值选项,数组类型用于表示一个对象或者事物的多种情况。当数组元素为数组时,该数组就变成矩阵。
二进制类型 #
通常我们使用数据库来保存的数据,有文本、数字,使用文字串、长文本、数字类型足够。但有时需要存储一些非 ascii 的数据,如图片,这里就需要使用到二进制类型。数据库的最小存储单位是字节,8位的二进制数可使用2位的十六进制数表示。在数据传输时,需要使用数值的文本化表示(序列化),那么往往会使用十六进制来替代二进制(两者值相同,但字面文本的长度是1:4)
-- postgres
select b'1100'
, x'C'
, 'C'::bytea
, E'\x43'
, ascii('C')
, to_hex(ascii('C'))
当见到十六进制的数据时,把它称为 binary data
是正确的,它是二进制数据十六进制形式。
⚠️ 开卷:
- 你的日常使用的数据库是否有上面例中这些 bit 类型、escape string( E’\x…’)、十六进制函数 hex?
- 什么是 ascii 码,它与二进制、十进制之间的关系?
- 与 SQL 无关的数学题,编写一个用于将二进制数字转为十进制的转换函数。
- 编写一个用于将十进制数字转二进制的转换函数。
其他复杂类型 #
用于存储 xml 数据。它较使用 text 长文本类型直接存储 xml 数据的字面值形式的优势在于可以约束校验来检查数据的格式,还有一些类型安全的专用函数。
-- postgres
select xmlcomment('hello youwu.today');
结果为:
xmlcomment
--------------------------
<!--hello youwu.today-->
(1 row)
与 xml 类型类似,数据库也提供了专用的操作函数。
--- postgres
SELECT '{"domain": "youwu.today", "options": {"type": "site", "active": true}}' ::json;
几何类型包含 点 point
、直线 line
、线段 line segment
、矩形 box
、圆 circle
、路径 path
、多边形 polygon
、曲线 curve
、面 surface
。这些类型也是计算图形学中重要的基本数据结构。
postgres 的 几何类型:
类型名称 | 描述 | 展示形式 |
---|---|---|
point | Point on a plane | (x,y) |
line | Infinite line | {A,B,C} |
lseg | Finite line segment | ((x1,y1),(x2,y2)) |
box | Rectangular box | ((x1,y1),(x2,y2)) |
path | Closed path (similar to polygon) | ((x1,y1),…) |
path | Open path | [(x1,y1),…] |
polygon | Polygon (similar to closed path) | ((x1,y1),…) |
circle | Circle | <(x,y),r> (center point and radius) |
比如在平面上,矩形 box '(1, 1), (0,0)'
在 x 轴上右向2个单位,即与点 point '(2, 0)'
相加,select box '(1,1),(0,0)' + point '(2,0)'
,得到 box '(3,1), (2,0)'
⚠️ 开卷:
- 线
path + path
会得到什么 - 矩形
box
除以 点point
又是什么鬼 😵
再来看看几何类型的配套函数,比如 求面积 area(几何类型)
、中心点 center()
、矩形高度 height(矩形)
等等。有兴趣的同事可以找些资料看看,顺便复习下初中的几何知识。
以上特殊的复杂类型,是有特定的应用场景。因为场景应用比较突出,数据库产品提供了原生支持,使用它们时,往往需要配合数据库所提供的专用函数来使用,因此,学习这些数据类型时,应配合阅读这些类型对应的操作函数。
⚠️ 开卷:
- 从 xml 类型操作函数说明中,找到可以实现字符串聚合(拼接)的函数(如xmlagg),并编写出完整的SQL。
- 从 json 类型操作函数中,找出类似实现上述功能的函数,并编写出完整的SQL。
自定义类型 #
自定义类型,即自定义数据结构,并且为该类型定义专用操作函数,类似于在 java 中定义类型的属性与方案。 它分为定义数据结构、定义算法操作函数两个部分。如果没有类型专属的函数,那么查询后得到的结构化结果是非常难以使用的。
当所用数据库内置的数据类型无法满足使用时,可以考虑自已来定义类型,特别是这个类型有非常多的专用操作算法函数的时候。
⚠️ 开卷:
使用 postgres 的自定义类型,设计一个 year
类型,用于表示年份。
各数据库中的数据类型 #
数据类型 | postgres | mysql | sqlite | oracle |
---|---|---|---|---|
整数型 | int2 / int / int8 | samllint / int / bigint | integer | integer / number |
数字型 | numeric / deciaml | numeric / deciaml | numeric | number / deciaml |
浮点型 | real / double | double | ||
字符型 | character / character varying | char / varchar | ||
长文本 | text | text | clob | |
二进制 | bit type, bytea | blob | blob | blob |
数组 | array, type[] | |||
集 | ✘ | set | ✘ | |
枚举 | enum | enum | ✘ | |
布尔 | ✔ | ✔ | ✔ | ✔ |
xml | ✔ | ✔ | ✔ | ✔ |
json | ✔ | ✔ | ✔ | ✔ |
几何类型 | ✔ | ✔ | ✘ |
数据类型的转换 #
前文提到,程序过程是操作数据的过程。计算机在底层计算是二进制的位操作,程序中的计算是针对具体数据类型的操作。编写SQL时也一样,对于初学者来说,这可能会增加心智负担,但这确实是一个无法回避而需要迈过去的坎。至少理解数据类型与转换,有助的排除SQL错误。
看看在 postgres 中的这个例子:
select t.col::int
from ( select '' col) t
得到的是这样的错误提示:
ERROR: invalid input syntax for type integer: ""
假如,有一个张表,其中有一个字段是字符类型,该字段实际上是存储数字型的值,当无值时填了空字符串(’’),当SQL很长的时候,这个错误是非常难定位的,postgres 只提示“转换 integer 时出现无效的输入”。很难能联系到问题所在。
上面的例子中,至少有如下两个问题:
1. 字符类型的字段,空值与字字符串的差别
2. 字符类型转整数类型
数据类型的转换有显式转换、隐式转换两种。显式转换,即使用数据库的数据类型转换函数强制转换数据类型;隐式转换,在SQL为了计算,执行引擎会将数据按照实际的计算进行数据类型转换,或者在函数调用时,会将输入值转为函数参数的数据类型。
当转换时出现无效值(即无法转换),数据库则会报错。
- 显式转换
-- postgres
-- 将表示有效日期的 yyyy-mm-dd 字符串转为 date 类型
select cast('2022-01-16' as date), date '2022-01-16'
- 隐藏转换
select '1' + 2, ascii('1')
ascii('1')
计算出字段“1” 的 ascii 码是 49,并不是 1。当运算 '1' + 2
时,数据库会尝试将第一个操作数(字符串“1”)按字面量转换为数字,然后与第二个操作数(数字 2)进行相加操作。
⚠️ 开卷:
- 你日常使用的数据库,是如何处理
select 'a' + 1
这个计算的?会有什么结果?
小结 #
通过本文,简单了解了数据库中的基础数据类型。若稍微有编程经验的,可能会出现,这些数据类型与编程语言中的基础数据类型非常接近。
与编写一般程序一样,编写 SQL 与编写存储过程、函数时使用数据类型,搭配对应的函数,非常强大,一通便通。通常会使用或需要注意:
- 数字型:
- 精度: 单精度、双精度问题
- 类型转换: 数字字符串与数字之间的隐性类型转换
- 常用函数: 四舍五入、零存整取
- 字符类型:
- 字符串拼接
- 字符串模板
- 与各种数据类型间的格式转换
- 日期类型:
- 特定日期: 月初、月末、年初、季末
- 日期计算: … x 以后、… x 以前、间隔
- 当前时间、当前日期、当前时间戳
- unix 时间戳与日期之间有转换
- 二进制:
- 使用场景
- 字节数组与字符串间的关系
- 十六进制字符串、UNICONDE字符串