1. PL/pgSQL数据类型深度解析
在PostgreSQL的存储过程和函数开发中,PL/pgSQL作为其默认的过程语言,数据类型的选择和使用直接影响着代码的性能、安全性和可维护性。与标准SQL相比,PL/pgSQL提供了更丰富的数据类型系统和更灵活的类型处理方式。
1.1 基础数据类型体系
PL/pgSQL继承了PostgreSQL强大的类型系统,主要包含以下几类:
-
标量类型:最基础的数据单元
- 数值类型:INTEGER, BIGINT, NUMERIC(p,s), REAL, DOUBLE PRECISION
- 字符类型:CHAR(n), VARCHAR(n), TEXT
- 布尔类型:BOOLEAN
- 日期时间:DATE, TIME, TIMESTAMP, INTERVAL
- 货币类型:MONEY
- 二进制数据:BYTEA
-
复合类型:
- 记录类型(RECORD)
- 行类型(行变量)
- 自定义复合类型(CREATE TYPE)
-
特殊类型:
- JSON/JSONB
- 数组类型
- 枚举类型
- 几何类型
- 网络地址类型
关键提示:NUMERIC(10,2)这类精确数值类型在金融计算中比浮点类型更可靠,能避免舍入误差。
1.2 类型声明与初始化
在PL/pgSQL中声明变量时,类型声明有几种典型方式:
sql复制-- 显式类型声明
DECLARE
user_id INTEGER;
user_name VARCHAR(50) := 'anonymous';
account_balance NUMERIC(15,2) DEFAULT 0.0;
-- 基于表字段类型
DECLARE
product_price products.price%TYPE;
-- 基于行类型
DECLARE
customer_record customers%ROWTYPE;
类型推导在实际开发中非常实用:
sql复制-- 使用:=进行类型推导
DECLARE
inferred_var := some_expression;
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高级类型特性实战
2.1 记录与行类型应用
记录类型(RECORD)和行类型是PL/pgSQL中处理多列数据的利器:
sql复制CREATE OR REPLACE FUNCTION get_employee_details(emp_id INT)
RETURNS TEXT AS $$
DECLARE
emp_rec RECORD;
output_text TEXT;
BEGIN
SELECT * INTO emp_rec FROM employees WHERE employee_id = emp_id;
-- 安全访问字段
IF emp_rec IS NULL THEN
RETURN 'Employee not found';
END IF;
output_text := 'Name: ' || emp_rec.full_name ||
', Department: ' || emp_rec.department;
RETURN output_text;
END;
$$ LANGUAGE plpgsql;
行类型变量会自动继承表结构的所有字段:
sql复制DECLARE
customer_row customers%ROWTYPE;
BEGIN
SELECT * INTO customer_row FROM customers WHERE customer_id = 100;
-- 直接访问字段
RAISE NOTICE 'Customer email: %', customer_row.email;
END;
2.2 数组类型操作技巧
PostgreSQL数组的强大功能在PL/pgSQL中得到了完整支持:
sql复制CREATE OR REPLACE FUNCTION calculate_average(p_values NUMERIC[])
RETURNS NUMERIC AS $$
DECLARE
total NUMERIC := 0;
arr_length INTEGER;
BEGIN
arr_length := array_length(p_values, 1);
IF arr_length IS NULL OR arr_length = 0 THEN
RETURN NULL;
END IF;
FOR i IN 1..arr_length LOOP
total := total + p_values[i];
END LOOP;
RETURN total / arr_length;
END;
$$ LANGUAGE plpgsql;
数组常用操作示例:
sql复制-- 数组构造
DECLARE
tags TEXT[] := ARRAY['urgent','backend'];
matrix INTEGER[][] := ARRAY[[1,2],[3,4]];
-- 数组函数使用
BEGIN
-- 追加元素
tags := array_append(tags, 'high-priority');
-- 查找元素
IF 'urgent' = ANY(tags) THEN
RAISE NOTICE 'Urgent tag found';
END IF;
END;
3. JSON/JSONB类型处理
3.1 JSON与JSONB的选择
两者关键区别:
- JSON:存储精确副本,保留空格和键顺序,插入快但查询慢
- JSONB:二进制格式,处理速度更快,支持索引
sql复制CREATE OR REPLACE FUNCTION process_order(order_data JSONB)
RETURNS TEXT AS $$
DECLARE
customer_name TEXT;
order_total NUMERIC;
items JSONB;
BEGIN
-- 安全提取字段
customer_name := order_data->>'customerName';
order_total := (order_data->>'total')::NUMERIC;
items := order_data->'items';
-- 验证必要字段
IF customer_name IS NULL OR order_total IS NULL THEN
RAISE EXCEPTION 'Invalid order data';
END IF;
-- 处理JSONB数组
FOR i IN 0..jsonb_array_length(items)-1 LOOP
DECLARE
item JSONB := items->i;
product_id INT := (item->>'productId')::INT;
quantity INT := (item->>'quantity')::INT;
BEGIN
-- 库存检查等业务逻辑
-- ...
END;
END LOOP;
RETURN 'Order processed for ' || customer_name;
END;
$$ LANGUAGE plpgsql;
3.2 JSON路径查询
PostgreSQL 12+ 引入了强大的JSON路径查询:
sql复制DECLARE
doc JSONB := '{
"store": {
"book": [
{"title": "Database Design", "price": 25},
{"title": "SQL Mastery", "price": 30}
]
}
}';
expensive_books JSONB;
BEGIN
-- 使用JSON路径查询价格>28的书籍
expensive_books := jsonb_path_query_array(doc, '$.store.book[*] ? (@.price > 28)');
RAISE NOTICE 'Expensive books: %', expensive_books;
END;
4. 类型转换与处理
4.1 显式类型转换
PL/pgSQL提供了多种类型转换方式:
sql复制-- CAST语法
DECLARE
str_val TEXT := '123.45';
num_val NUMERIC;
BEGIN
num_val := CAST(str_val AS NUMERIC);
-- ::快捷语法
num_val := str_val::NUMERIC;
-- 格式化转换
RAISE NOTICE 'Formatted: %', to_char(num_val, 'L999.99');
END;
4.2 处理NULL值
NULL处理是类型安全的重要方面:
sql复制CREATE OR REPLACE FUNCTION safe_divide(
numerator NUMERIC,
denominator NUMERIC
) RETURNS NUMERIC AS $$
BEGIN
-- 防御性编程
IF denominator IS NULL OR denominator = 0 THEN
RETURN NULL;
END IF;
RETURN numerator / denominator;
END;
$$ LANGUAGE plpgsql;
使用COALESCE提供默认值:
sql复制DECLARE
user_level TEXT;
BEGIN
SELECT COALESCE(level, 'standard') INTO user_level
FROM user_settings
WHERE user_id = 1001;
END;
5. 自定义类型开发
5.1 创建复合类型
sql复制-- 创建自定义类型
CREATE TYPE inventory_item AS (
name TEXT,
supplier_id INTEGER,
price NUMERIC
);
-- 在PL/pgSQL中使用
CREATE OR REPLACE FUNCTION get_item_info(item_id INTEGER)
RETURNS inventory_item AS $$
DECLARE
item inventory_item;
BEGIN
SELECT name, supplier_id, unit_price
INTO item.name, item.supplier_id, item.price
FROM items WHERE id = item_id;
RETURN item;
END;
$$ LANGUAGE plpgsql;
5.2 枚举类型应用
枚举类型适合固定值集合:
sql复制CREATE TYPE order_status AS ENUM (
'new', 'processing', 'shipped', 'completed', 'cancelled'
);
CREATE OR REPLACE FUNCTION update_order_status(
order_id INTEGER,
new_status order_status
) RETURNS VOID AS $$
BEGIN
UPDATE orders SET status = new_status
WHERE id = order_id;
-- 验证有效状态
IF new_status = 'cancelled' THEN
RAISE NOTICE 'Order % was cancelled', order_id;
END IF;
END;
$$ LANGUAGE plpgsql;
6. 类型相关性能优化
6.1 选择合适的数据类型
-
整数选择:
- SMALLINT (-32768 to +32767)
- INTEGER (-2147483648 to +2147483647)
- BIGINT (-9223372036854775808 to +9223372036854775807)
-
字符串类型:
- CHAR(n):固定长度,适合短且长度一致的字符串
- VARCHAR(n):可变长度,有长度限制
- TEXT:无限长度,最佳通用选择
实际经验:在PostgreSQL中,TEXT类型通常比VARCHAR(n)性能更好,因为后者有额外的长度检查开销。
6.2 数组与JSONB索引
sql复制-- 为数组列创建GIN索引
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT,
tags TEXT[]
);
CREATE INDEX idx_products_tags ON products USING GIN(tags);
-- JSONB索引
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
data JSONB
);
CREATE INDEX idx_orders_customer ON orders USING BTREE((data->>'customerId'));
CREATE INDEX idx_orders_items ON orders USING GIN((data->'items'));
7. 调试与类型检查
7.1 运行时类型检查
sql复制CREATE OR REPLACE FUNCTION calculate_discount(
original_price NUMERIC,
discount_rate NUMERIC
) RETURNS NUMERIC AS $$
BEGIN
-- 参数验证
IF original_price IS NULL OR discount_rate IS NULL THEN
RAISE EXCEPTION 'Parameters cannot be NULL';
END IF;
IF discount_rate < 0 OR discount_rate > 1 THEN
RAISE EXCEPTION 'Discount rate must be between 0 and 1';
END IF;
RETURN original_price * (1 - discount_rate);
END;
$$ LANGUAGE plpgsql;
7.2 使用RAISE NOTICE调试
sql复制CREATE OR REPLACE FUNCTION complex_calculation(input JSONB)
RETURNS NUMERIC AS $$
DECLARE
temp_value NUMERIC;
BEGIN
RAISE NOTICE 'Input data: %', input;
temp_value := (input->>'baseValue')::NUMERIC;
RAISE NOTICE 'Base value: %', temp_value;
-- 复杂计算过程
-- ...
RETURN temp_value;
END;
$$ LANGUAGE plpgsql;
8. 最佳实践与常见陷阱
8.1 类型安全实践
- 始终为函数参数和返回值声明类型
- 使用%TYPE引用表字段类型,保持同步
- 对用户输入进行严格的类型验证
- 使用TRY_CAST进行安全类型转换
sql复制-- 安全转换示例
CREATE OR REPLACE FUNCTION safe_string_to_int(input TEXT)
RETURNS INTEGER AS $$
BEGIN
RETURN CASE
WHEN input ~ '^[0-9]+$' THEN input::INTEGER
ELSE NULL
END;
END;
$$ LANGUAGE plpgsql;
8.2 常见类型错误
-
隐式类型转换导致的精度丢失:
sql复制-- 错误示例 DECLARE price NUMERIC := 19.99; discount INTEGER := 30; BEGIN -- 这里会先进行整数除法,导致结果错误 RAISE NOTICE 'Final price: %', price * (1 - discount/100); END; -
字符串与数字比较问题:
sql复制-- 可能不按预期工作 IF user_input > '100' THEN -- 字符串比较 -- 应该使用 IF user_input::INTEGER > 100 THEN -
NULL处理不当:
sql复制-- 错误处理NULL IF variable = NULL THEN -- 总是返回NULL -- 正确方式 IF variable IS NULL THEN
9. 高级类型模式
9.1 多态类型
PostgreSQL支持多态类型,允许函数处理多种类型:
sql复制CREATE OR REPLACE FUNCTION array_length(
anyarray,
dimension INTEGER DEFAULT 1
) RETURNS INTEGER AS $$
BEGIN
RETURN array_length($1, $2);
END;
$$ LANGUAGE plpgsql;
9.2 域类型(Domains)
域类型是在现有类型基础上添加约束:
sql复制CREATE DOMAIN email_address AS TEXT
CHECK (
VALUE ~ '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
);
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email email_address NOT NULL
);
-- 在PL/pgSQL中使用
DECLARE
user_email email_address := 'test@example.com';
BEGIN
-- 会自动验证格式
-- ...
END;
10. 实际案例:类型系统在电商系统中的应用
10.1 产品库存管理
sql复制CREATE TYPE inventory_adjustment AS (
product_id INTEGER,
warehouse_id INTEGER,
quantity_change INTEGER,
reason_code VARCHAR(10)
);
CREATE OR REPLACE FUNCTION update_inventory(
adjustments inventory_adjustment[]
) RETURNS INTEGER AS $$
DECLARE
total_updated INTEGER := 0;
adj inventory_adjustment;
BEGIN
FOREACH adj IN ARRAY adjustments LOOP
-- 验证数量
IF adj.quantity_change = 0 THEN
CONTINUE;
END IF;
-- 更新库存
UPDATE inventory
SET quantity = quantity + adj.quantity_change
WHERE product_id = adj.product_id
AND warehouse_id = adj.warehouse_id;
-- 记录变动
INSERT INTO inventory_history (
product_id, warehouse_id,
change_amount, reason_code
) VALUES (
adj.product_id, adj.warehouse_id,
adj.quantity_change, adj.reason_code
);
total_updated := total_updated + 1;
END LOOP;
RETURN total_updated;
END;
$$ LANGUAGE plpgsql;
10.2 订单处理流程
sql复制CREATE TYPE order_line_item AS (
product_id INTEGER,
quantity INTEGER,
unit_price NUMERIC(10,2),
discount NUMERIC(3,2)
);
CREATE OR REPLACE FUNCTION create_order(
customer_id INTEGER,
items order_line_item[],
shipping_info JSONB
) RETURNS INTEGER AS $$
DECLARE
order_id INTEGER;
total_amount NUMERIC(12,2) := 0;
item order_line_item;
BEGIN
-- 计算总金额
FOREACH item IN ARRAY items LOOP
total_amount := total_amount +
(item.quantity * item.unit_price * (1 - COALESCE(item.discount, 0)));
END LOOP;
-- 创建订单
INSERT INTO orders (
customer_id,
order_date,
total_amount,
shipping_address
) VALUES (
customer_id,
CURRENT_TIMESTAMP,
total_amount,
shipping_info->>'address'
) RETURNING id INTO order_id;
-- 添加订单项
FOREACH item IN ARRAY items LOOP
INSERT INTO order_items (
order_id, product_id,
quantity, unit_price,
discount
) VALUES (
order_id, item.product_id,
item.quantity, item.unit_price,
item.discount
);
END LOOP;
RETURN order_id;
END;
$$ LANGUAGE plpgsql;
