前两个月接了个活,给一个做进口零食的电商搭后台数据库。甲方指定用金仓数据库,说他们集团采购过授权。我之前没有接触过这个数据库,用起来还算顺手,前后做了两周多,现在上线跑了一周,还算稳。趁着记忆还热乎,把用过的SQL都记下来------主要是建表、插测试数据、各种查、改、删,还有改表结构的那些操作。都是实际跑过的,复制就能用,但你要根据自己项目改表名和字段名。
我随也算DBA老手,有些写法可能不是最优,但至少都能跑通,数据也对。如果发现哪里不合理,欢迎给我留言指出来。

第一步:建表,我习惯先画草图再敲代码
甲方给的字段清单很粗,就一张Excel,列了用户、商品、订单、订单明细四个实体。我拿到后在自己笔记本上先画了草图,标出哪些字段必填、哪些将来要索引、哪些可能变长。然后才开始敲建表语句。
用户表我建的是这样:
sql
CREATE TABLE "market"."user" (
"userid" SERIAL,
"username" VARCHAR(50) NOT NULL,
"password" VARCHAR(100) NOT NULL,
"phone" VARCHAR(20),
"email" VARCHAR(100),
"createtime" DATETIME NOT NULL,
"updatetime" DATETIME,
CONSTRAINT "pk_user_userid" PRIMARY KEY (userid)
);
`SERIAL` 自增主键,金仓支持得很好。用户名我限制50字符,因为一般手机号或邮箱登录,够用了。密码我给100,是因为甲方说要用bcrypt加密,那串东西可不短。手机号没强制唯一,因为允许一个手机号注册多个账号(甲方业务特殊,一个家庭共用一个手机号但各自要独立账号)。
插句话:我在测试环境忘了给 `username` 加唯一索引,结果测试人员重复注册了三个同名账号,查用户的时候出现多条,搞得我以为代码写错了。后来补了个 `CREATE UNIQUE INDEX idx_username ON market.user(username);` 才算完。
商品表:
sql
CREATE TABLE "market"."product" (
"proid" SERIAL,
"cateid" INTEGER NOT NULL,
"name" VARCHAR(100) NOT NULL,
"detail" TEXT,
"price" NUMERIC(20,2) NOT NULL,
"stock" INTEGER NOT NULL,
"status" SMALLINT NOT NULL,
"createtime" DATETIME NOT NULL,
"updatetime" DATETIME NOT NULL,
CONSTRAINT "pk_product_proid" PRIMARY KEY (proid)
);
价格类型我用 `NUMERIC(20,2)`,甲方说商品单价最高不会超过五位数,但做活动时可能有折扣,小数后两位够了。库存我没设默认值,因为每个商品入库时必须明确。状态我定了1在售、0下架,后来甲方又要求加"预售"状态,我就改了 `SMALLINT` 可以存2。
这里有一个细节:`detail` 用的是 `TEXT`,但实际商品详情里包含图片URL和HTML富文本,长度可能超,我后来改成 `VARCHAR(2000)` 了,因为 `TEXT` 在某些查询中性能稍差。不过这个改动是上线前两天才做的,幸好数据量不大,用 `ALTER TABLE` 改类型也没出问题。
订单主表和明细表:
sql
CREATE TABLE "market"."orders" (
"orderid" SERIAL,
"userid" INTEGER NOT NULL,
"order_no" VARCHAR(32) NOT NULL,
"total_amount" NUMERIC(20,2) NOT NULL,
"status" SMALLINT NOT NULL,
"address" VARCHAR(200),
"create_time" DATETIME NOT NULL,
"pay_time" DATETIME,
"ship_time" DATETIME,
CONSTRAINT "pk_orders_orderid" PRIMARY KEY (orderid)
);
CREATE TABLE "market"."order_item" (
"itemid" SERIAL,
"orderid" INTEGER NOT NULL,
"proid" INTEGER NOT NULL,
"quantity" INTEGER NOT NULL,
"price" NUMERIC(20,2) NOT NULL,
CONSTRAINT "pk_order_item_itemid" PRIMARY KEY (itemid)
);
订单号我生成规则是 `年月日时分秒毫秒+随机4位`,用Java代码生成,数据库只存,不负责生成。`total_amount` 是在下单时计算好的,避免以后价格变动影响历史订单。明细表里的 `price` 是下单时那个商品的单价,独立于商品表当前价格。
说实话,我一开始没给 `order_item` 加任何外键,因为甲方说可能频繁删除商品,但订单明细需要保留历史。后来为了数据一致性,我只加了 `orderid` 的外键,`proid` 不加,避免删商品时级联删除。这个取舍我纠结了一会儿,最终决定应用层保证 `proid` 的合法性。
一个偷懒的技巧:为了快速给运营做一个在售商品清单,我用 `CREATE TABLE AS` 复制了一个表:
sql
CREATE TABLE market.active_product AS
SELECT proid, name, price, stock
FROM market.product
WHERE status = 1;
这个表后来没用上,因为运营发现数据不是实时的,他们要求实时查,最后我直接给了视图。但 `CREATE TABLE AS` 备份数据确实方便,我后来备份历史订单也用的它。
第二步:塞数据,单条、批量、从查询往里怼
建完表,我得往里塞测试数据。甲方给了一个Excel,里面大概有50个用户、200个商品和一批订单。我写SQL批量导进去。
单条插入:
sql
INSERT INTO "market"."user" (username, password, phone, email, createtime, updatetime)
VALUES ('zhangsan', 'encrypted_pwd_123', '13800138001', 'zhangsan@example.com', NOW(), NOW());
商品也一样:
sql
INSERT INTO "market"."product" (cateid, name, detail, price, stock, status, createtime, updatetime)
VALUES (1, '华为Mate 60 Pro', '旗舰智能手机,配备麒麟芯片', 6999.00, 100, 1, NOW(), NOW());
这没啥好说的。但插了几十条之后,我发现 `NOW()` 在一条语句里多次调用,时间戳是一样的(同一个事务内),所以我的 `createtime` 和 `updatetime` 相同,后续更新时再改。
批量插入,节省时间:
甲方给了200个商品,我一条条写INSERT手都要断了。后来我用Excel公式拼成了批量格式,直接粘贴执行:
sql
INSERT INTO "market"."user" (username, password, phone, email, createtime, updatetime) VALUES
('lisi', 'encrypted_pwd_456', '13800138002', 'lisi@example.com', NOW(), NOW()),
('wangwu', 'encrypted_pwd_789', '13800138003', 'wangwu@example.com', NOW(), NOW()),
('zhaoliu', 'encrypted_pwd_abc', '13800138004', 'zhaoliu@example.com', NOW(), NOW());
这条语句一次性插了三条,实际我一次插50条,金仓没报错,速度也很快。我还试过插1000条,结果SQL文本太长,超过命令行限制,被截断了。后来我分批插,每批200条。
从现有表导入到另一张表:
我要把下架商品挪到一个归档表 `product_archive`(结构一致),于是:
sql
INSERT INTO market.product_archive (proid, name, price, status)
SELECT proid, name, price, status
FROM market.product
WHERE status = 0;
这个执行完后我查了 `product_archive`,数据正确,然后删除了原表的下架商品。但后来甲方说不能删除,只能标记,所以我白干了,又把数据插回去......这是题外话。
MERGE 语句解决"有则更新,无则插入":
库存同步的时候经常用到。比如每天早上从ERP系统导入库存,如果商品存在就更新,否则新建。我写成:
sql
MERGE INTO market.product AS target
USING (VALUES (1, '华为Mate 60 Pro', 150)) AS source (cateid, name, stock)
ON target.name = source.name
WHEN MATCHED THEN
UPDATE SET stock = source.stock, updatetime = NOW()
WHEN NOT MATCHED THEN
INSERT (cateid, name, stock, status, createtime, updatetime)
VALUES (source.cateid, source.name, source.stock, 1, NOW(), NOW());
这里有个坑:如果 `name` 有重复,`ON` 条件匹配多条,会报错。所以我确保 `name` 在商品表里是唯一的(后来加了唯一约束)。另外,`USING` 里的 `VALUES` 必须指定列别名,否则后面引用不了。
第三步:查询,写到手软的 SELECT
查询是用的最多的。我总结一下常用的几种。
全表查和指定列:
测试时我经常 `SELECT * FROM market.user;` 看有没有插进去。但正式代码里,我明确写列名,比如 `SELECT username, phone FROM market.user;`。
带条件过滤:
甲方要查3000~8000元的在售商品,我写:
sql
SELECT name, price, stock FROM market.product
WHERE status = 1 AND price BETWEEN 3000 AND 8000;
`BETWEEN` 包含边界,这个我确认过。如果要用 `LIKE` 模糊匹配,比如名字里带"手机"的:
sql
SELECT * FROM market.product WHERE name LIKE '%手机%';
注意 `%` 在前面会走不了索引,数据量大了会慢。我因为数据少,没在意。
排序分页:
列表页要分页显示商品,每页10条,看第2页:
sql
SELECT name, price, stock FROM market.product
WHERE status = 1
ORDER BY proid
LIMIT 10 OFFSET 10;
`OFFSET` 是从0开始,第一页是 `OFFSET 0`。我一开始写 `OFFSET 1`,结果第一页少了一条,后来才明白。
聚合统计:
统计每个分类的商品数和平均价格,并只显示商品数≥2的分类:
sql
SELECT
cateid,
COUNT(*) AS product_count,
AVG(price) AS avg_price
FROM market.product
WHERE status = 1
GROUP BY cateid
HAVING COUNT(*) >= 2;
`HAVING` 别和 `WHERE` 搞混,前者过滤分组后,后者过滤行。
多表连接:
订单列表要显示用户名,我连 `orders` 和 `user`:
sql
SELECT
o.order_no,
u.username,
o.total_amount,
o.create_time
FROM market.orders o
INNER JOIN market.user u ON o.userid = u.userid;
如果要看每个订单里的商品明细,再加 `order_item` 和 `product`:
sql
SELECT
o.order_no,
p.name AS product_name,
oi.quantity,
oi.price,
oi.quantity * oi.price AS subtotal
FROM market.order_item oi
INNER JOIN market.orders o ON oi.orderid = o.orderid
INNER JOIN market.product p ON oi.proid = p.proid;
这种三表连接,我经常忘记写别名,然后报错说列引用模糊,必须指定表名或别名。后来我统一用短别名,比如 `o`、`u`、`p`。
第四步:更新,我差点把整个价格表清零
有一次我想给一个商品打九折,结果忘了写 `WHERE`,执行了:
sql
UPDATE market.product SET price = price * 0.9;
还好是测试环境,我立刻 `ROLLBACK` 了。从此以后,我养成了习惯:先把 `SELECT` 查出来,确认影响行数,再把 `SELECT` 改成 `UPDATE`,而且 `WHERE` 一定要先写。
正常更新:
sql
UPDATE market.product
SET price = price * 1.1, updatetime = NOW()
WHERE proid = 1;
```
改用户手机号:
sql
UPDATE market.user
SET phone = '13900139001', updatetime = NOW()
WHERE username = 'zhangsan';
批量更新下架商品为在售(甲方说重新上架一批):
sql
UPDATE market.product
SET status = 1, updatetime = NOW()
WHERE status = 0;
子查询更新:
我后来给商品表加了 `sales_count` 字段,需要从订单明细汇总销量:
sql
UPDATE market.product p
SET sales_count = (
SELECT SUM(oi.quantity)
FROM market.order_item oi
WHERE oi.proid = p.proid
)
WHERE EXISTS (
SELECT 1 FROM market.order_item oi
WHERE oi.proid = p.proid
);
如果不加 `EXISTS`,那些没销量的商品会被更新成 `NULL`,所以我加了条件。
RETURNING 看结果:
更新库存时,我想知道新库存是多少:
sql
UPDATE market.product
SET stock = stock - 1, updatetime = NOW()
WHERE proid = 1 AND stock > 0
RETURNING proid, name, stock AS new_stock;
这个在应用程序里可以直接拿到新值,省了一次查询。
第五步:删除,逻辑删还是物理删?
甲方说所有数据都不能物理删除,只做逻辑删除。所以我加了 `is_deleted` 字段,默认0,删除时改为1。但测试环境我经常物理删除。
条件删除:
sql
DELETE FROM market.user WHERE username = 'zhaoliu';
这个只删一条。
清空表:
为了清理测试数据,我用 `TRUNCATE`:
sql
TRUNCATE TABLE market.temp_table;
这个很快,但不能回滚,所以只在测试库用。
子查询删除,小心 NOT IN:
我想删掉从未下过订单的用户,第一版写的是:
sql
DELETE FROM market.user
WHERE userid NOT IN (SELECT DISTINCT userid FROM market.orders);
但假如 `orders` 为空,子查询返回空,`NOT IN` 就变成 `WHERE userid NOT IN NULL`,结果是未知,删除不了任何行,但也不是报错。这会导致我以为删除了,实际上没删。后来我改成 `NOT EXISTS`:
sql
DELETE FROM market.user u
WHERE NOT EXISTS (
SELECT 1 FROM market.orders o WHERE o.userid = u.userid
);
这个更安全。
删除并返回:
我想记录删了哪些商品,以便写日志:
sql
DELETE FROM market.product
WHERE status = 0 AND stock = 0
RETURNING proid, name, price;
返回的结果集我保存到文件里了。
第六步:改表结构,上线前做,上线后尽量不动
开发过程中,甲方不断改需求,表结构跟着变。
加字段:
给用户加等级:
sql
ALTER TABLE market.user ADD COLUMN user_level SMALLINT DEFAULT 1;
默认值很重要,否则已有行会为 `NULL`,代码里要判空。
改字段类型:
手机号从20改为15,因为甲方说国内手机号就11位,留15足够:
sql
ALTER TABLE market.user ALTER COLUMN phone TYPE VARCHAR(15);
但如果有数据超过15位,会失败,我先清掉了测试数据。
重命名字段:
`phone` 改成 `mobile`,甲方说更符合他们的业务术语:
sql
ALTER TABLE market.user RENAME COLUMN phone TO mobile;
删字段:
删掉 `email`,因为甲方不打算用邮箱登录:
sql
ALTER TABLE market.user DROP COLUMN email;
我先确认没有视图依赖这个字段,才执行。
最后,一个完整的测试脚本
每次建新环境,我都跑一遍这个脚本,从建库到清空:
sql
CREATE DATABASE shopdb ENCODING = 'UTF8';
CREATE SCHEMA shop;
CREATE TABLE shop.user (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
real_name VARCHAR(50),
phone VARCHAR(20),
create_time TIMESTAMP DEFAULT NOW(),
update_time TIMESTAMP DEFAULT NOW()
);
CREATE TABLE shop.category (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
parent_id INTEGER DEFAULT 0,
sort_order INTEGER DEFAULT 0
);
CREATE TABLE shop.product (
id SERIAL PRIMARY KEY,
category_id INTEGER NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
price NUMERIC(20,2) NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
status SMALLINT DEFAULT 1,
create_time TIMESTAMP DEFAULT NOW(),
update_time TIMESTAMP DEFAULT NOW()
);
INSERT INTO shop.category (name, sort_order) VALUES
('电子产品', 1),
('家用电器', 2),
('图书文具', 3);
INSERT INTO shop.user (username, password, real_name, phone) VALUES
('admin', 'admin_encrypted', '管理员', '13800000001'),
('test_user', 'user_encrypted', '测试用户', '13800000002');
INSERT INTO shop.product (category_id, name, description, price, stock) VALUES
(1, '华为Mate 60 Pro', '旗舰手机,麒麟芯片', 6999.00, 50),
(1, 'iPhone 15', '苹果旗舰手机', 5999.00, 30),
(2, '小米空气净化器', '智能空气净化', 1299.00, 20),
(3, '《深入理解计算机系统》', '计算机经典教材', 89.00, 100);
SELECT
p.id,
c.name AS category_name,
p.name AS product_name,
p.price,
p.stock
FROM shop.product p
INNER JOIN shop.category c ON p.category_id = c.id
WHERE p.status = 1
ORDER BY p.price DESC;
UPDATE shop.product
SET price = price * 0.9, update_time = NOW()
WHERE category_id = 1;
DELETE FROM shop.product
WHERE stock = 0 AND status = 0;
ALTER TABLE shop.product ADD COLUMN sales_count INTEGER DEFAULT 0;
这个脚本我跑过不下十次,每次都能顺利执行完。
写在最后
金仓这个库,用下来感觉跟其他关系型数据库差不多,SQL标准支持得不错。我遇到的问题大多是自己粗心,跟库本身没关系。以后要是再做类似项目,这些SQL模板我还会复用,但肯定会再加一些索引和分区策略,目前数据量小,还没到那一步。
写这篇笔记的时候,刚给甲方演示完后台功能,他们还算满意。接下来可能要加报表功能,到时又要写一堆聚合查询,等搞完了我再来补充。