PostgreSQL是以加州大学伯克利分校计算机系开发的 POSTGRES,现在已经更名为PostgreSQL,版本 4.2为基础的对象关系型数据库管理系统(ORDBMS)。PostgreSQL支持大部分 SQL标准并且提供了许多其他现代特性:复杂查询、外键、触发器、视图、事务完整性、MVCC。同样,PostgreSQL 可以用许多方法扩展,比如, 通过增加新的数据类型、函数、操作符、聚集函数、索引。免费使用、修改、和分发 PostgreSQL,不管是私用、商用、还是学术研究使用。
PostgreSQL 是一个免费的对象-关系数据库服务器(数据库管理系统),它在灵活的 BSD-风格许可证下发行。它提供了相对其他开放源代码数据库系统(比如 MySQL 和 Firebird),和专有系统(比如 Oracle、Sybase、IBM 的 DB2 和 Microsoft SQL Server)之外的另一种选择。
PostgreSQL 不寻常的名字导致一些读者停下来尝试拼读它,特别是那些把SQL拼读为"sequel"的人。PostgreSQL 开发者把它拼读为 "post-gress-Q-L"。它也经常被简略念为 "postgres"。 [1]
事实上, PostgreSQL 的特性覆盖了 SQL-2/SQL-92 和 SQL-3/SQL-99,首先,它包括了可以说是目前世界上最丰富的数据类型的支持,其中有些数据类型可以说连商业数据库都不具备, 比如 IP 类型和几何类型等;其次,PostgreSQL 是全功能的自由软件数据库,很长时间以来,PostgreSQL 是唯一支持事务、子查询、多版本并行控制系统(MVCC)、数据完整性检查等特性的唯一的一种自由软件的数据库管理系统。 Inprise 的 InterBase 以及SAP等厂商将其原先专有软件开放为自由软件之后才打破了这个唯一。最后,PostgreSQL拥有一支非常活跃的开发队伍,而且在许多黑客的努力下,PostgreSQL 的质量日益提高。
从技术角度来讲,PostgreSQL 采用的是比较经典的C/S(client/server)结构,也就是一个客户端对应一个服务器端守护进程的模式,这个守护进程分析客户端来的查询请求,生成规划树,进行数据检索并最终把结果格式化输出后返回给客户端。为了便于客户端的程序的编写,由数据库服务器提供了统一的客户端 C 接口。而不同的客户端接口都是源自这个 C 接口,比如ODBC,JDBC,Python,Perl,Tcl,C/C++,ESQL等, 同时也要指出的是,PostgreSQL 对接口的支持也是非常丰富的,几乎支持所有类型的数据库客户端接口。这一点也可以说是 PostgreSQL 一大优点。
PostgreSQL强壮的一个原因源于它的架构。和商业数据库一样,PostgreSQL可以用于C/S(客户/服务器)环境。这对于用户和开发人员有很多好处。
PostgreSQL安装核心是数据库服务端进程。它允许在一个独立服务器上。需要访问存储在数据库中的数据的应用程序必须通过数据库进程。这些客户端程序无法直接访问数据,即使它们和服务程序在同一台机器上。
事务
子查询
视图
外键参照完整性
复杂的锁
用户自定义类型
继承
规则
多版本并发控制
Microsoft Windows 原生支持
表空间
可以修改列类型
时间点恢复
PostgreSQL 二进制安装包:
包 |
描述 |
---|---|
postgresql | 包含客户端和工具的基础包 |
postgresql-libs | 客户端需要的共享库 |
postgresql-server | 建立和运行服务端的程序 |
postgresql-contrib | 贡献的扩展程序 |
postgresql-devel | 开发用的头文件和库 |
postgresql-docs | 文档 |
postgresql-jdbc | PostgreSQL 的数据库连接库 |
postgresql-odbc | PostgreSQL 的 ODBC 接口库 |
postgresql-pl | Perl 的 PostgreSQL 服务器支持 |
postgresql-python | Pyton 的 PostgreSQL 服务器支持 |
postgresql-tcl | Tcl 的 PostgreSQL 服务器支持 |
postgresql-test | PostgreSQL 测试套件 |
备注:为了安装数据库和客户端,需要下载和安装至少base,libs和server包。
PostgreSQL 配置脚本选项:
选项 |
描述 |
---|---|
--prefix=prefix | 安装到 prefix 指向的目录;默认为/usr/local/pgsql |
--bindir=dir | 安装应用程序到 dir;默认为 prefix/bin |
--with-docdir=dir | 安装文档到 dir;默认为 prefix/doc |
--with-pgport=port | 设置默认的服务器端网络连接服务 TCP 端口号 |
--with-tcl | 为服务端提供 Tcl 存储过程支持 |
--with-perl | 为服务端提供 Perl 存储过程支持 |
--with-python | 为服务端提供 Python 存储过程支持 |
备注:本文档更多的是面向后端开发同学,更加详细的数据库配置以及命令可以参考《PostgreSQL从菜鸟到专家》和《PostgreSQL HighPerformance 》
select <逗号分隔的列的列表> FROM <表名>;
select town,lname AS "last_name" FROM customer;
SELECT <都好分隔的列名列表> FROM <表名> ORDER BY <列名> [ASC | DESC]
备注:通常,你可以用来排序的列被强制限制于你选择用于输出的列,它可以接受用于 ORDER BY 后面的列不在你选择的列的列表里头的情况。但是,这是非标准 SQL,所以建议避免使用这个功能。
select DISTINCT town FROM customer;
备注:
第一,使用 DISTINCT,意味着 PostgreSQL 需要在提取数据的时候需要做更多的事情来查重。除非知道需要移除的重复数据,否则不应该使用 DISTINCT 从句。
第二个原因更加注重于实用。有时候 DISTINCT 会隐藏 SQL 或者数据中的错误,而这种错误在显示重复行的时候却很容易看出。
SELECT description,cast((cost_price * 100) AS int )AS "cost" From table_name
select <逗号分隔的列> FROM <表名> WHERE <条件>
备注:可能有很多可以通过关键字 AND、OR 以及 NOT 组合的条件。
标准比较运算符:
运算符 |
描述 |
---|---|
< | 小于 |
<= | 小于或等于 |
= | 等于 |
>= | 大于或等于 |
> | 大于 |
<> | 不等于 |
假设我们想获得一个首字母为 B 和 N 之间的所有不同城镇的列表。所有的城镇的开始字母都是大写的。可以像下面这么写:
SELECT DISTINCT town FROM customer WHERE town BETWEEN 'B' AND 'Nz'
当使用 LIKE 比较字符串,你可以使用百分号(%)表示任何字符串,你还可以使用下划线表示匹配一个字符。
匹配使用字母 B 开始的城镇:...WHERE town LIKE 'B%'
匹配以字母 e 结束的名字:...WHERE fname LIKE '%e'
匹配只有四个字符长的名字,我们可以使用四个下划线:...WHERE fname LIKE '____'
只显示五条符合条件的行:
select customer_id from customer limit 5;
跳过结果的前两行,直接返回之后的五行:
select customer_id from customer limit 5 offset 2;
单独使用offset
select customer_id from customer offset 2;
备注:如果相与其他的SELECT从句一起使用limit,limit从句应该总跟随在某普通的SELECT语句之后,只有在你使用offset的时候,在后面跟随offset从句。
检查一个值为NULL
select * from table where name IS NULL;
还可以通过添加一个NOT来反转测试这个值是不是NULL以外的值
select * from table where name IS NOT NULL;
PostgreSQL 有两种基本类型用来处理日期和时间信息:
timestamp :保存完整日期和时间信息
date :保存年月日信息
设置时间和日期的风格:
使用psql的命令的语法如下:
SET datestyle TO 'value';
备注:为了设置月份和日期处理的顺序,需要将 datestyle 的值设置成 US 或者 European,分别表示月份在前(02/01/1997表示 2 月 1 日)和日期在前(01/02/1997 表示 2 月 1 日)。
为了修改显示格式,你也需要设置 datestyle,但是要设置成以下四个值之一:
ISO,设置成ISO-8601标准,使用-作为分隔符,格式类似于1997-02-01
SQL,用于传统样式,格式类似于02/01/1997
Postgres,用于默认的PostgreSQL样式,格式类似于Sat Feb 01
Geman,用于德国样式,格式类似于01.02.1997
备注:在最新的发布版本,默认的日期和时间戳样式使用SQL样式。
函数cast,将一种数据格式转换成另一种格式
转换成日期:
cast ('string' AS date)
转换一个包含时间的值:
cast('string' AS timestamp)
在比较日期的时候需要的函数:
date_part(units required,value to use)允许提取出日期的某一部分,例如月份
Now很简单地获得当前的日期和时间,它实际上和更标准的“魔法变量”current_timestamp相等。
加入想要在表中取出在九月份下订单的行:
select * from table_name where date_part('month',order_time)=9;
PostgreSQL为我们提取出了正确的行。注意日期是按照ISO格式显示的。我们可以提取日期和时间戳的以下部分:
Year(年)
Month(月)
Day(日)
Hour(小时)
Minute(分钟)
Second(秒钟)
也可以使用标准比较运算符来比较日期,以下是一个示例:
select * from table_name where order_time >= cast('2004 04 04' AS date);
也可以对日期进行简单的计算,例如计算从下单到收货所间隔的天数,我们可以使用类似这样的查询:(这返回数据库中存储的这两天之间所间隔的天数。)
select order_time_end - order_time_begin from table_name;
标准语法:
select <列的列表> from <表的列表> where <连接条件> and <行选择条件>
例子:
select customer.fname , order.oname from customer , order where customer.id = 8 and customer.id = order.customer_id;
SQL92的select语法
select <列的列表> from <表> join <表> on <连接条件> where <行选择条件>
语法:insert into table_name values(每列的值的列表);
我们提供一个由逗号分隔的列的值的列表,它的顺序必须和表中列的顺序相同。
语法:insert into table_name (列名的列表) values (跟列的列表相对应的列的数值);
在这个 INSERT 语句的变体中,我们必须列出列名以及那些列的相同顺序的数值,但这些可以和我们当初建表的顺 序不同。使用这种变体,我们不再需要知道列在表中的顺序。我们也拥有了一个将要插入到表中的优美、清晰和基本上并行的列名的列表和数据列表。
serial:它实际上是一个整数,但是可以通过自动增长来给我们一种为每一行建立唯一 ID 数字的方法。在实际使用中我们一定要避免在插入数据的时候为serial类型的数据提供数值。
但是如果真的出现了serial类型的值混乱的情况,那我们想要恢复,就需要将PostgreSQL内部的序列号改的和实际数据一致。
访问序列生成器
PostgreSQL 为这个列建立了一个特别的计数器,一个序列生成器,它可以用于产生唯一 ID。注意这个序列生成器总 是被命名为<表名><列名>seq。这个列的默认行为被 PostgreSQL 自动指定为函数 nextval(‘customer_customer_id_seq’) 的结果。当我们的 INSERT 语句没有提供这个列的数据,这个函数被 PostgreSQL 为我们自动执行。通过插入或提供数据 到这个列,我们破坏了这种自动机制,因为如果提供了数据,函数将不会被调用。幸运的是,我们没有被迫从这个表删除 所有数据并从头开始,因为 PostgreSQL 允许我们直接控制序列生成器。
当这样插入数据的时候,你通常会通过 currval 函数获得序列生成器的值:
currval('序列生成器名称');
PostgreSQL 将告诉你序列生成器当前的值:
select currval ('customer_customer_id_seq')
备注:严格来说,currval 告诉我们的是上一次调用 nextval 后返回的值,所以为了让它工作,我们需要要么插入一个新行或者直接在当前的 psql 会话中调用一次 nextval 函数。
如果,PostgreSQL 认为最后一行的当前数字式 16,但实际上,最后一行 是 19。当我们尝试插入数据到customer 表,空出 customer_id 让 PostgreSQL 处理时,它尝试通过调用 nextval 函数为这个列提供一个值:
nextval ('序列生成器名');
这个函数首先将提供的序列生成器的值加以,然后返回结果。我们可以这样直接尝试:
select nextval('customer_customer_id_seq');
我们当然可以通过反复针对序列生成器调用 nextval 以达到需要的值,但如果数值很大,这就帮不上太多忙了。作为替代方案,我们可以使用 serval 函数:
setval ('序列生成器名',新的值);
插入表中的空值用NULL表示,注意NULL没有引号包裹。
但是我们最好在插入语句中表明所要插入的列,如果即将插入此列的数据为NULL,那么我们就可以不插入此列,将其默认为NULL.
虽然 INSERT 是用于添加数据到数据库的标准 SQL 方法,但它不总是最合适的假设我们有大量的行需要加入到数据库,但是实际上的数据,可能是在一个电子表格里。将数据插入数据库的一种方法先是使用电子表格的导出功能,所以我们可以可能将电子表格导出为 CSV(逗号分隔值)文件。然后我们可以使用类似于 Eacs 的文本编辑器,或者至少支持宏的其他工具,将所有的数据转换成 INSERT 语句。
有一个 PostgreSQL 的命令叫做 COPY,它可以通过扁平文件存储和还原数据,但它限于数据库管理员使用,因为文件必须在服务器上操作,而普通用户可能没有访问权限。另一个更有用的是通用的\copy 命令,它基本上实现了 COPY 的全部功能,而且可以被任何人使用,而且数据是在客户机上读写的。所以基于 SQL 的 COPY 命令基本上是多余的。
\copy 命令有以下语法用于导入数据:
\copy table_name from '文件名' [USING DELIMITERS '作为分隔符的单个字符'] [WITH NULL AS '代表NULL的字符串']
方括号“[]”中的部分是可选的,所以你只有在需要时使用它们。但是,注意文件名需要用单引号括起来。
选项[USING DELIMITERS '作为分隔符的单个字符']允许你指定输入文件中的每个列是怎么分隔的。默认情况下,输入文件的被假设为使用制表符(tab)分隔列的。在我们的例子中,我们假设我们从一个从电子表格里头导出的 CSV 文件开始。通常,CSV 格式不是一个好的选择因为逗号可能出现在数据中,地址数据特别倾向于使用逗号字符。不幸的是,电子表格通产不提供合理的除了 CSV 文件之外的候选方案,所以你可能需要用好你拥有的。我们可以给出一个替代方案,使用管道符“|”,它经常被用作终止符,因为它极少出现在用户数据中。
选项[WITH NULL AS '代表 NULL 的字符串']允许你制定一个可以被翻译为 NULL 的字符串。默认情况下,假设为\N。注意在\copy 命令中,你必须用单引号包裹这个字符串,因为这才能告诉 PostgreSQL 这是一个字符串,虽然在实际数据中不会有引号。所以如果你希望使用 NOTHING 作为空值加载到数据库,你应该使用选项“WITH NULL AS 'NOTHING'”。
语法:
update table_name set 列名 = 值 where 条件
如果我们想一次性修改很多列:
update customer set town = 'Leicester',zipcode = 'LW4 2WQ' where 条件
提示:永远在执行update之前测试where从句。where从句中一个简单的错误会导致表中的很多甚至全部被更新为相同的值,如果这种情况发生在公司的生产环境,那无疑是灾难性的。
PostgreSQL 有一个扩展用法允许通过另一个表更新,使用这样的语法:
update table_name1 from table_name2 where 条件
示例:
update table1 set name = table2.name from table2 where table1.id = 100
语法:
delete from table_name where 条件
还有另一种方法从一个表删除数据。它从一个表中删除所有数据。除非它是包含在 PostgreSQL 7.4 以及以后版本中的事务中,否则你将没有办法恢复被删除的数据。这个命令是 TRUNCATE,它的语法是:
TRUNCATE table table_name
使用这个命令需要非常小心,只有当你非常确定要永久删除表中的数据才能使用。从某些方面说,它非常类似于删除表并重建它,除了它更容易操作且不会重置序列生成器。
聚集函数 |
描述 |
---|---|
count(*) | 提供行的计数 |
count(列名) | 提供指定字段中值不是NULL的行的计数 |
min(列名) | 返回指定列中的最小值 |
max(列名) | 返回指定列中的最大值 |
sum(列名) | 返回指定列的值的合计总数 |
avg(列名) | 返回指定列的值的平均数 |
使用任何聚集函数 SELECT 语句都可以包含两个可选的从句:GROUP BY 和 HAVING。语法如下(在这里使用了 count(*)函数):
select count(*),列名列表 from 表名 where 条件 [GROUP BY 列名 [HAVING 聚集条件]]
它有两种格式:count(*)和 count(列名)
count(*):
只获得了包含总数的一行,count(*)函数允许我们获取一个对象的计数,而不是获得对象本身。它比获得数据本身提高了很多性能。
GROUP BY 和 count(*)
按照GROUP BY后的条件来统计总数。
示例:
select count(*) ,sex from student group by sex;
HAVING 从句和 count(*)
SELECT 语句的最后一项可选部分是 HAVING 从句。只要记住 HAVING 是一种用于聚集函数的 WHERE 从句。我们使用 HAVING 来约束返回的结果 为针对特定的聚集的条件为真的行,例如 count(*) > 1。它的使用方法和我们通过列值限制行的方法一样。
注意:聚集无法使用在where从句中,他们只能用在HAVING从句中。
示例:
select count(*) ,town FROM customer GROPU BY town HAVING count(*)>1;
count(列名):
count(列名)统计表中提供的列值不为 NULL 的计数。
如果student表中一共有19人,我们想要查询出填写了家庭住址的学生人数(默认没有填写为NULL),那么可以这样写:
select count(address) from student;
count(DISTINCT列名)
聚集函数 count 支持 DISTINCT 关键字,它约束函数只考虑一个列中值唯一的情况,不统计重复的。例如我们想要统计student表中不同名的学生数量:
select count(DISTINCT name) AS "distinct",count(name) AS "all";
min 函数使用一个列名做参数且返回这个列中最小的值。对于 numeric 类型的列,结果应该和预期一样。对于时态类型,例如 date 的值,它返回最小的日期,日期既可以是过去也可以是未来。对于变长的字符串(varchar 类型),结果可能和预期有点不同:它在字符串右边添加空白后再进行比较。
比如想要查询年龄最小的同学的年龄:
select min(age) from student;
你可能预期结果会是 NULL,或者一个空串。因为 NULL 通常指位置,因此,min 函数忽略 NULL 值。忽略 NULL 值是所有的聚集函数的一个特点,除了 count(*)
max 函数使用一个列名作为参数且返回那个列的最大值。
比如想要查询年龄最大的同学的年龄:
select max(age) from student;
sum 函数使用一个列名作为参数并提供列的内容的合计。和 min 和 max 一样,NULL 值被忽略
比如想要查询学生年龄的总和:
select sum(age) from student;
它使用一个列名做参数并返回这个列数值的平均值。同样也是忽略NULL值的。
比如想要查询学生的平均年龄:
select avg(age) from student;
子查询是指一个 SELECT 查询的一个或多个 WHERE 条件中的 SELECT 语句。
示例:
select * from item where cost_price > (select avg(cost_price) from item) and sell_price < (select avg(sell_price) from item);
PostgreSQL 首先扫描查询并发现有两个在括号中的子查询。它独自评价每个子查询,然后在主查询被执行前,将答 案放回到主查询 WHERE 从句中的适当部分。
我们也可以使用附加的 WHERE 从句或者 ORDER BY 从句。它们可以完美有效地使用更常规的条件结合到子查询中 的 WHERE 条件中。
另一种格式的子查询在 WHERE 中使用 EXISTS 关键字来检查是否存在,而不需要知道数据的内容。
对于我们的查询,使用 EXISTS 才是将两个 SELECT 语句结合到一起的正确方法,因为我们只需要知道子查询是否返回了记录:
select fname , lname from customer c where exists(select * from order o where o.customer_id = c.id);
EXISTS 从句通常比其他类型的关联或者 IN 条件更高效。因此,在你选择怎么写一个子查询的时候,通常值得优先使用它而不是其他类型的连接。
示例:
SELECT town FROM tcust UNION SELECT town FROM customer;
注意:使用union连接查询返回的结果消除了所有重复数据,如果想要得到不去重的数据,可以使用union all 来替换union。
一种非常特殊的连接叫做自连接,它在我们想针对同一个表中的两个列使用连接时被使用。
示例:
select s1.des as "Student Name",s2.des as "Parent Name" from student s1,student s2 where s1.student_id = s2.parent_student_id;
语法:
select <列的列表> from <表1> LEFT OUTER JOIN <表2> on <连接条件> where <条件>
布尔
字符
数字
时间(基于时钟)
PostgreSQL扩展类型
二进制大对象(BLOB)
布尔类型可能是最简单的类型。它只可以存储两个值,true 和 false,以及在值未知的时候,存储 NULL。
翻译为true |
翻译为false |
---|---|
'1' | '0' |
'yes' | 'no' |
'y' | 'n' |
'true' | 'false' |
't' | 'f' |
单个字符
固定长度字符串
长度可变字符串
定义 |
意义 |
---|---|
char | 单个字符 |
char(n) | 一组长度固定为n的字符,长度不足用空白填充。如果存储过长的字符串,将会发生一个错误。 |
varchar(n) | 一组长度不超过n的字符,长度不足也不需要填充。PostgreSQL扩展了SQL标准,允许指定没有长度的varchar,这实际上使长度不受限制。 |
text | 实际上是一个长度不受限制的字符串,就像varchar一样,只是不需要定义最大长度。这是一个 PostgreSQL 针对 SQL 标准做的扩展。 |
整数
浮点数字
PostgreSQL 整数数据类型:
子类型 |
标准名 |
描述 |
---|---|---|
Small integer | Smallint | 一个2字节的符号型整数,可以存储-32768到32767的数字 |
Integer | Int | 一个4字节的符号型整数,可以存储-2147483648 到 2147473647 的数字 |
Serial | 和 integer 一样,除了它的值通常是由 PostgreSQL 自动输入的。 | |
PostgreSQL 浮点数据类型:
子类型 |
标准名 |
描述 |
---|---|---|
float | float(n) | 支持最少精度为 n,存储为最多 8 字节的浮点数。 |
float8 | real | 双精度(8 字节)浮点数字 |
numeric | numeric(p,s) | 拥有 p 个数字的实数,其中小数点后有 s 位。不像 float,这始终是一个确切的数字,但工作效率比普通浮点数字低。 |
money | numeric(9,2) | PostgreSQL 特有的类型,但在其他数据库里也普遍存在。Money 类型从PostgreSQL 8.0 开始不赞成使用,且可能在以后版本中取消。你应该使用number 类型代替。 |
定义 |
意义 |
---|---|
date | 日期 |
time | 时间 |
timestamp | 日期和时间 |
interval | 存储 timestamp 之间差别的信息 |
timestamptz | PostgreSQL 扩展的类型,存储包含时区信息的 timestamp |
由于 PostgreSQL 源于一个用于研究的数据库系统,PostgreSQL 拥有一些少见的数据类型用于存储几何和网络数据类型,使用 PostgreSQL 的这些任何一种特殊功能都会使一个 PostgreSQL 数据库的可移植性变得非常的差,所以通常,我们趋向于避免这些扩展。
定义 |
意义 |
---|---|
box | 矩形盒子 |
line | 一组点 |
point | 一对几何学的数字 |
lseg | 一条线段 |
polygon | 一条封闭的几何线 |
cidr或inet | 一个 IPv4 的地址,录入 192.168.0.1 |
macaddr | 以 MAC 地址(以太网卡物理地址) |
通常,一个数组需要通过使用一个附加表实现。但是,数组的能力有时候很有用,特别是当你需要存储固定数量的重复元素时,而且它非常容易使用。
使用 PostgreSQL 语法定义数组:
示例:创建可以存储工作日的表
CREATE TABLE empworkday(refcode char(5),workdays int[]);
插入数据:
INSERT INTO empworkday VALUES('val01', '{0,1,0,1,1,1,1}')
使用 SQL99 语法的数组:
示例:创建可以存储工作日的表
CREATE TABLE empworkday(refcode char(5),workdays int array[7]);
关系数据库如何进行类型转换存在很大的差异。PostgreSQL 使用 cast 转换符:
cast(column-name AS type-definition-to-convert-to)
另一种更简洁的双冒号语法可以用在 SELECT 语句中的简单的列名所在位置使用:
column-name::type-definition-to-convert-to
假设我们想要从我们原来的 bpsimple 数据库中的 orderinfo 表中以 char(10)格式获取 data 数据,我们可以这样写:
SELECT cast(date_placed AS char(10)) FROM orderinfo;
PostgreSQL 提供一些通用功能函数,你可以使用它们来操作列.
函数 |
描述 |
---|---|
length(column-name) | 返回一个字符串的长度 |
trim(column-name) | 移除字符串开始和结尾的空格 |
strpos(column-name, string) | 返回子串在列中的位置 |
substr(column-name, position, length) | 根据指定位置和长度截取子串。第一个字符算作位置 1 |
round(column-name, length) | 根据指定小数点位置四舍五入一个数字 |
abs(number) | 获得一个数字的绝对值 |