
1. 项目概述与核心价值最近在整理自己过往的项目资料翻到了一个用纯SQL和PL/SQL实现的库存管理系统。这个项目虽然技术栈看起来“古老”但恰恰是这种基于数据库原生能力的系统最能体现一个后端开发者对数据模型、业务逻辑和性能优化的基本功。很多朋友一提到管理系统第一反应就是上Spring Boot、上微服务这当然没错但对于理解数据如何在最底层被组织、处理和流转亲手用存储过程、触发器和游标去实现业务规则是无可替代的体验。这个名为“inventory-management-system-sql”的项目就是一个从零开始用SQL脚本构建完整业务系统的实战案例。它涵盖了从产品、品牌、门店、类目的基础信息管理到客户购物车、交易记录的业务流程再到库存预警、自动补货的后台逻辑。整个系统不依赖任何外部应用服务器业务核心完全内聚在Oracle数据库中。对于正在学习数据库设计、想深入理解PL/SQL编程或者需要为一个轻量级、高数据一致性的场景寻找技术方案的朋友来说这个项目有很强的参考价值。你会发现用好数据库自带的能力往往能用最少的架构复杂度解决相当实际的业务问题。2. 系统架构与数据模型设计解析2.1 核心实体关系与设计思路一个库存管理系统的核心在于清晰定义“物”与“流”。“物”是静态的资源比如产品、仓库“流”是动态的过程比如入库、出库、交易。这个项目的设计很好地把握了这一点。其数据模型围绕几个关键实体展开产品Product这是系统的中心实体。每条记录代表一个具体的、可销售的商品项。它必须包含唯一标识如产品ID、名称、描述、所属品牌Brand、所属类别Category以及最关键的成本价与销售价。这里的设计要点在于产品信息应该是相对稳定的价格变动需要通过专门的流水或历史表来记录以支持财务审计。库存Inventory这是连接“产品”与“地点”的纽带。它记录了某个特定产品Product在某个特定门店Store的当前数量。这是一个典型的多对多关系分解表。将库存独立成表而不是作为产品表的一个字段是因为一个产品可能分布在多个门店。这个表是业务逻辑最密集的地方几乎所有出入库操作最终都会映射到对这张表某条记录的quantity字段的更新。门店Store与类目Category门店是库存的物理归属地类目是产品的逻辑分类。它们与产品、库存共同构成了静态资源网络。客户与交易Customer, Transaction, Transaction_Detail这是业务流的体现。客户Customer发起交易Transaction每笔交易包含多条明细Transaction_Detail每条明细对应一个产品的购买数量和当时快照的单价。这里采用“交易总表明细表”的设计是标准做法明细表记录了交易的具体内容同时通过外键关联到产品确保了数据的可追溯性。这种设计模式的优势在于它通过外键约束强制保证了数据的引用完整性。例如你无法删除一个还有库存记录的产品也无法为一条不存在的交易添加明细。数据库在底层帮你守护了业务规则的第一道防线。2.2 表结构定义深度解读仅仅知道有哪些表还不够每个字段的数据类型、约束条件的选择都直接影响着系统的健壮性和性能。我们以核心的inventory表和transaction_detail表为例看看设计时的考量。库存表inventoryCREATE TABLE inventory ( inventory_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, product_id NUMBER NOT NULL, store_id NUMBER NOT NULL, quantity NUMBER(10, 2) NOT NULL CHECK (quantity 0), last_restocked DATE DEFAULT SYSDATE, CONSTRAINT fk_inv_product FOREIGN KEY (product_id) REFERENCES product(product_id) ON DELETE CASCADE, CONSTRAINT fk_inv_store FOREIGN KEY (store_id) REFERENCES store(store_id) ON DELETE CASCADE, CONSTRAINT uq_product_store UNIQUE (product_id, store_id) );quantity字段类型为NUMBER(10,2)这里允许小数是为了应对某些按重量、长度计量的产品如散装食品、布料。CHECK (quantity 0)约束确保了库存数量永远不会为负这是一个重要的业务规则。last_restocked字段记录了最后一次补货时间这对于分析库存周转率、制定补货计划非常有价值。默认值设为SYSDATE方便插入数据。唯一约束UNIQUE (product_id, store_id)这是整个设计的精髓。它强制保证了同一个产品在同一个门店只能有一条库存记录。这避免了数据冗余和更新异常确保当你更新库存时目标明确且唯一。外键的ON DELETE CASCADE这是一个需要谨慎使用的选项。它意味着如果某个产品或门店被删除与之相关的所有库存记录也会被自动删除。在正式生产环境中我们更倾向于使用ON DELETE RESTRICT禁止删除或ON DELETE SET NULL设为空并在应用层实现更复杂的归档逻辑以防止误操作导致数据丢失。本项目中使用CASCADE是为了演示和测试的便利性。交易明细表transaction_detailCREATE TABLE transaction_detail ( detail_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, transaction_id NUMBER NOT NULL, product_id NUMBER NOT NULL, quantity NUMBER(10, 2) NOT NULL CHECK (quantity 0), unit_price NUMBER(10, 2) NOT NULL CHECK (unit_price 0), line_total NUMBER(12, 2) GENERATED ALWAYS AS (quantity * unit_price) VIRTUAL, CONSTRAINT fk_detail_trans FOREIGN KEY (transaction_id) REFERENCES transaction(transaction_id) ON DELETE CASCADE, CONSTRAINT fk_detail_product FOREIGN KEY (product_id) REFERENCES product(product_id) );生成列line_total这是一个计算列其值由quantity * unit_price自动得出。使用VIRTUAL关键字意味着它不占用物理存储空间只在查询时计算。这保证了数据的一致性总额永远等于数量乘以单价也简化了插入操作无需手动计算和填写。在Oracle 11g及以后版本中这个特性非常实用。存储unit_price而非引用产品当前价这是关键设计。交易发生时必须记录成交时的单价快照而不是关联到产品表里实时变动的价格。这是因为产品价格可能会调整但历史交易金额必须固定不变以供对账和财务审计。这体现了事务性数据Transaction与主数据Master Data分离的原则。2.3 索引策略规划好的数据模型需要好的索引来支撑性能。基于上述业务场景我们至少应该创建以下索引在inventory(product_id, store_id)上创建唯一索引已通过唯一约束隐含创建这是查询特定门店某产品库存的最主要路径。在transaction(customer_id, transaction_date)上创建复合索引用于快速查询某个客户的历史订单。在transaction_detail(transaction_id)上创建索引用于在查询交易详情时快速关联到明细。在inventory(quantity)上创建普通索引如果low_stock_view低库存视图查询频繁这个索引能加速WHERE quantity threshold这类条件筛选。索引不是越多越好每个索引都会增加数据插入、更新和删除的开销。上述索引是基于典型查询模式按客户查订单、按产品-门店查库存、查低库存提出的起点在真实生产环境中需要结合具体的SQL执行计划EXPLAIN PLAN和性能监控数据来不断调整优化。3. PL/SQL业务逻辑封装实战当业务逻辑变得复杂单纯依靠应用层发送多条SQL语句不仅网络开销大更难以保证操作的原子性要么全成功要么全失败。PL/SQL正是为了解决这个问题而生它允许我们将一系列数据库操作封装成一个完整的、在数据库服务器端执行的逻辑单元。3.1 存储过程实现自动补货补货是一个典型的多步骤事务检查当前库存计算补货量生成补货单更新库存记录日志。用存储过程实现能确保这些步骤作为一个整体执行。CREATE OR REPLACE PROCEDURE restock_product ( p_product_id IN inventory.product_id%TYPE, p_store_id IN inventory.store_id%TYPE, p_restock_quantity IN NUMBER, p_restock_threshold IN NUMBER DEFAULT 10 ) AS v_current_quantity inventory.quantity%TYPE; v_new_quantity inventory.quantity%TYPE; v_restock_id restock_log.restock_id%TYPE; BEGIN -- 步骤1: 查询当前库存并锁定该行记录FOR UPDATE SELECT quantity INTO v_current_quantity FROM inventory WHERE product_id p_product_id AND store_id p_store_id FOR UPDATE; -- 防止并发补货导致超卖 -- 步骤2: 业务判断只有库存低于阈值时才触发补货 IF v_current_quantity p_restock_threshold THEN DBMS_OUTPUT.PUT_LINE(库存充足无需补货。当前库存: || v_current_quantity); RETURN; -- 提前退出过程 END IF; -- 步骤3: 计算补货后新库存 v_new_quantity : v_current_quantity p_restock_quantity; -- 步骤4: 更新库存 UPDATE inventory SET quantity v_new_quantity, last_restocked SYSDATE WHERE product_id p_product_id AND store_id p_store_id; -- 步骤5: 记录补货日志假设有restock_log表 INSERT INTO restock_log (product_id, store_id, quantity_before, quantity_added, quantity_after, restock_date) VALUES (p_product_id, p_store_id, v_current_quantity, p_restock_quantity, v_new_quantity, SYSDATE) RETURNING restock_id INTO v_restock_id; -- 获取刚插入的ID -- 步骤6: 提交事务在自动提交关闭的环境下 -- COMMIT; -- 注意通常不在过程中显式提交由调用者控制事务 DBMS_OUTPUT.PUT_LINE(补货成功产品ID: || p_product_id || , 门店ID: || p_store_id); DBMS_OUTPUT.PUT_LINE(补货前: || v_current_quantity || , 补货量: || p_restock_quantity || , 补货后: || v_new_quantity); DBMS_OUTPUT.PUT_LINE(补货日志ID: || v_restock_id); EXCEPTION WHEN NO_DATA_FOUND THEN DBMS_OUTPUT.PUT_LINE(错误未找到指定产品在对应门店的库存记录。); WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE(发生未知错误: || SQLERRM); RAISE; -- 将异常重新抛出给调用者 END restock_product; /关键点解析FOR UPDATE子句在查询当前库存时使用FOR UPDATE会对查到的这行记录加锁。这在高并发场景下至关重要它能防止两个并发的补货或销售操作同时读到同一个旧库存值然后基于它进行更新导致数据不一致这就是经典的“丢失更新”问题。参数默认值p_restock_threshold IN NUMBER DEFAULT 10设置了默认补货阈值为10。这样调用时如果不传这个参数就会使用默认值提高了过程的灵活性。业务逻辑前置判断在过程开始不久就判断库存是否低于阈值如果充足则直接RETURN。这避免了不必要的资源占用和后续操作。日志记录与RETURNING子句补货操作必须留有痕迹。RETURNING ... INTO ...子句能在插入后立即获取自增的主键ID非常方便。异常处理使用EXCEPTION块捕获NO_DATA_FOUND等特定异常并处理未知异常WHEN OTHERS最后用RAISE重新抛出是一种良好的实践。它既向调用者提供了友好的错误提示又不掩盖真实的异常信息。事务控制注意过程中通常不直接COMMIT。事务应该由调用这个过程的上级应用或匿名块来控制这样多个过程调用可以被组合进一个更大的事务单元符合“原子性”要求。3.2 游标批量处理与报表生成游标提供了对查询结果集进行逐行或批量处理的能力。在库存管理中一个常见的场景是生成所有门店所有产品的库存状态报告或者批量处理所有低库存产品的补货。示例使用游标遍历所有低库存产品并生成报告DECLARE -- 定义游标查询所有库存量低于20的产品 CURSOR c_low_stock IS SELECT i.product_id, p.product_name, i.store_id, s.store_name, i.quantity FROM inventory i JOIN product p ON i.product_id p.product_id JOIN store s ON i.store_id s.store_id WHERE i.quantity 20 ORDER BY i.quantity ASC; -- 按库存从低到高排序 v_product_id product.product_id%TYPE; v_product_name product.product_name%TYPE; v_store_id store.store_id%TYPE; v_store_name store.store_name%TYPE; v_quantity inventory.quantity%TYPE; BEGIN DBMS_OUTPUT.PUT_LINE( 低库存产品报告 ); DBMS_OUTPUT.PUT_LINE(产品ID | 产品名称 | 门店ID | 门店名称 | 当前库存); DBMS_OUTPUT.PUT_LINE(--------------------------------------); OPEN c_low_stock; LOOP FETCH c_low_stock INTO v_product_id, v_product_name, v_store_id, v_store_name, v_quantity; EXIT WHEN c_low_stock%NOTFOUND; -- 当没有更多行时退出循环 -- 格式化输出每一行数据 DBMS_OUTPUT.PUT_LINE( RPAD(v_product_id, 8) || | || RPAD(v_product_name, 12) || | || RPAD(v_store_id, 8) || | || RPAD(v_store_name, 12) || | || v_quantity ); -- 这里可以加入更复杂的业务逻辑例如自动调用补货过程 -- IF v_quantity 5 THEN -- 紧急低库存 -- restock_product(v_product_id, v_store_id, 50); -- 调用补货过程 -- END IF; END LOOP; CLOSE c_low_stock; DBMS_OUTPUT.PUT_LINE( 报告结束 ); DBMS_OUTPUT.PUT_LINE(共发现 || c_low_stock%ROWCOUNT || 条低库存记录。); END; /游标使用心得显式游标 vs 隐式游标上述例子是显式游标需要DECLARE、OPEN、FETCH、CLOSE。对于简单的单行查询直接用SELECT ... INTO的隐式游标更简洁。显式游标更适合多行结果集处理和复杂控制逻辑。%ROWCOUNT属性在循环结束后c_low_stock%ROWCOUNT给出了游标获取的总行数非常便于生成统计信息。游标FOR循环PL/SQL还提供了更简洁的FOR record IN cursor_name LOOP ... END LOOP;语法能自动打开、获取、关闭游标并定义行记录变量。上述代码用这种写法会更简洁。我在这里使用基础写法是为了清晰展示游标工作的每个环节。性能考虑如果处理的数据量非常大例如数十万行逐行FETCH和DBMS_OUTPUT可能会成为瓶颈。此时应考虑使用BULK COLLECT和FORALL进行批量提取和操作或者将结果直接插入到一张报告表中再由前端工具展示。3.3 触发器实现审计追踪触发器是一种特殊的存储过程它在特定的数据库事件INSERT, UPDATE, DELETE发生时自动执行。它非常适合用于实现审计日志、数据校验和复杂的默认值计算。示例创建登录审计触发器假设我们有一个user_login表记录每次用户登录尝试另一个login_audit_log表用于审计所有登录操作包括成功和失败。-- 首先创建审计日志表 CREATE TABLE login_audit_log ( audit_id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, username VARCHAR2(50), attempt_time TIMESTAMP DEFAULT SYSTIMESTAMP, ip_address VARCHAR2(45), -- 支持IPv6 action VARCHAR2(20), -- LOGIN_SUCCESS, LOGIN_FAILURE, LOGOUT details CLOB ); -- 然后在user_login表上创建触发器 CREATE OR REPLACE TRIGGER trg_audit_user_login AFTER INSERT OR UPDATE ON user_login FOR EACH ROW DECLARE v_action login_audit_log.action%TYPE; v_details CLOB; BEGIN -- 根据操作类型和状态决定审计动作 IF INSERTING THEN IF :NEW.login_success Y THEN v_action : LOGIN_SUCCESS; v_details : 用户 || :NEW.username || 于 || TO_CHAR(:NEW.login_time, YYYY-MM-DD HH24:MI:SS) || 成功登录。; ELSE v_action : LOGIN_FAILURE; v_details : 用户 || :NEW.username || 于 || TO_CHAR(:NEW.login_time, YYYY-MM-DD HH24:MI:SS) || 登录失败。原因 || :NEW.failure_reason; END IF; ELSIF UPDATING THEN -- 假设更新只发生在登出时记录登出时间 IF :NEW.logout_time IS NOT NULL AND :OLD.logout_time IS NULL THEN v_action : LOGOUT; v_details : 用户 || :NEW.username || 于 || TO_CHAR(:NEW.logout_time, YYYY-MM-DD HH24:MI:SS) || 安全登出。; ELSE -- 其他更新可能不需要审计或记录不同动作 RETURN; -- 直接返回不记录审计日志 END IF; END IF; -- 插入审计日志记录 INSERT INTO login_audit_log (username, attempt_time, ip_address, action, details) VALUES (:NEW.username, :NEW.login_time, :NEW.ip_address, v_action, v_details); END trg_audit_user_login; /触发器设计注意事项谨慎使用触发器是“隐式”执行的业务逻辑分散在触发器中会降低代码的可读性和可维护性。应仅将其用于审计、强制复杂约束、维护衍生数据等“辅助性”功能核心业务逻辑最好放在显式调用的存储过程中。避免递归触发确保触发器中的操作不会导致对同一张表的再次触发形成死循环。例如在table_a的触发器中向table_a插入数据。性能影响FOR EACH ROW行级触发器对每一行数据都会执行一次在大批量数据操作时可能带来显著开销。务必确保触发器内的逻辑高效。:NEW与:OLD伪记录在触发器中你可以通过:NEW访问正在插入或更新后的新值通过:OLD访问更新前或删除前的旧值。这是实现数据变化追踪的关键。事务性触发器是触发它的DML语句所在事务的一部分。如果触发器失败整个DML语句也会回滚。4. 视图、函数与系统测试4.1 视图简化复杂查询与数据安全视图是一个虚拟表其内容由查询定义。对于库存系统视图有两个主要用途一是简化频繁使用的复杂查询二是控制数据访问权限。低库存视图CREATE OR REPLACE VIEW low_stock_view AS SELECT p.product_id, p.product_name, b.brand_name, c.category_name, i.store_id, s.store_name, i.quantity, i.last_restocked, -- 计算库存周转天数简化示例假设日均销售量为5 ROUND(i.quantity / 5, 1) AS estimated_days_of_supply FROM inventory i JOIN product p ON i.product_id p.product_id JOIN brand b ON p.brand_id b.brand_id JOIN category c ON p.category_id c.category_id JOIN store s ON i.store_id s.store_id WHERE i.quantity 20 -- 低库存阈值 ORDER BY i.quantity ASC, i.last_restocked DESC;现在业务人员或报表系统只需要执行简单的SELECT * FROM low_stock_view;就能获得一份包含产品、品牌、类目、门店信息和预估可供应天数的完整低库存清单而无需理解背后复杂的多表连接逻辑。如果基础业务逻辑发生变化比如阈值调整、计算方式改变也只需要修改视图定义所有依赖此视图的查询都会自动生效。带检查选项的视图还可以创建用于数据更新的视图并通过WITH CHECK OPTION来保证通过视图更新数据时更新后的数据仍然满足视图的查询条件。CREATE OR REPLACE VIEW active_products AS SELECT product_id, product_name, price, is_active FROM product WHERE is_active Y WITH CHECK OPTION;通过这个视图更新数据时你不能将is_active改为N否则会违反视图的WHERE条件操作会被拒绝。这提供了一种轻量级的数据完整性保护。4.2 函数封装可重用的计算逻辑函数与存储过程类似但主要目的是返回一个值并且可以在SQL语句中直接调用。它非常适合封装纯计算逻辑。示例计算订单总金额的函数CREATE OR REPLACE FUNCTION calculate_order_total ( p_transaction_id IN transaction.transaction_id%TYPE ) RETURN NUMBER AS v_total_amount NUMBER : 0; BEGIN SELECT NVL(SUM(line_total), 0) INTO v_total_amount FROM transaction_detail WHERE transaction_id p_transaction_id; RETURN v_total_amount; EXCEPTION WHEN NO_DATA_FOUND THEN RETURN 0; END calculate_order_total; /这个函数可以在查询中像内置函数一样使用SELECT transaction_id, transaction_date, calculate_order_total(transaction_id) AS order_total FROM transaction WHERE customer_id 1001;函数让业务逻辑的复用变得非常自然和清晰。需要注意的是函数中应避免执行DML操作INSERT/UPDATE/DELETE除非是自治事务否则可能会在查询中引发不可预料的结果或错误。4.3 系统集成测试与验证脚本8_test_cases.sql的价值就在于此。它不是一个简单的示例而是一套验证系统各个组件是否按预期工作的脚本。一个好的测试脚本应该包括数据完整性测试插入违反外键约束或检查约束的数据验证是否会抛出预期错误。存储过程功能测试用不同的参数调用restock_product验证正常补货、库存充足跳过补货、无效产品/门店等边界情况。触发器效果验证执行登录、登出操作然后查询login_audit_log表确认审计记录被正确创建。游标逻辑测试运行低库存报告游标确认其输出的数据范围和格式正确。视图查询测试对low_stock_view进行复杂查询验证其性能和数据准确性。并发操作模拟进阶使用两个会话同时尝试对同一低库存产品进行销售和补货验证FOR UPDATE锁机制是否能防止超卖。在运行测试前务必记住在SQL*Plus或SQL Developer中执行SET SERVEROUTPUT ON;这样才能看到DBMS_OUTPUT.PUT_LINE打印的调试信息。5. 部署、运维与性能考量5.1 脚本部署顺序与依赖管理项目提供的脚本顺序1_create, 2_insert, 3_procedures...是合理的。但实际部署时尤其是存在对象间循环依赖时比如过程A调用函数B而函数B又引用视图C可能需要更精细的处理。一种稳健的做法是先创建所有表1_create_tables.sql。创建无需依赖的视图和函数有些简单的视图和函数可能只依赖于表可以先创建。插入基础数据2_insert_data.sql。注意如果表上有外键插入顺序必须遵循父子关系先父表后子表。创建复杂的程序化对象按照依赖关系依次创建函数、过程、包。如果对象间有复杂依赖可能需要先创建空壳CREATE OR REPLACE PROCEDURE xxx AS BEGIN NULL; END;然后再依次替换定义。最后创建触发器因为触发器可能依赖于过程和函数。可以使用数据字典视图如USER_DEPENDENCIES来查询对象间的依赖关系。5.2 性能监控与优化建议系统上线后需要关注以下性能点监控长时间运行的SQL定期检查V$SQL或DBA_HIST_SQLSTAT视图找出执行时间长、消耗资源多的SQL语句特别是涉及全表扫描的查询。索引使用情况使用EXPLAIN PLAN分析关键查询的执行计划确认是否使用了预期的索引。有时需要创建新的复合索引或函数索引。PL/SQL代码性能对于循环操作检查是否可以使用BULK COLLECT和FORALL进行批量处理。避免在循环内执行SQL查询即“逐行处理”问题。触发器开销评估触发器的执行时间特别是行级触发器。如果触发器逻辑复杂且表更新频繁可能成为瓶颈。定期统计信息收集确保Oracle优化器有最新的统计信息来生成高效的执行计划。可以安排定时任务如使用DBMS_SCHEDULER在业务低峰期收集统计信息。5.3 安全与权限管理在真实环境中不应使用超级管理员账户如SYS, SYSTEM来运行应用。应该创建一个专用的数据库用户并授予其最小必需的权限。-- 创建应用用户 CREATE USER inventory_app IDENTIFIED BY strong_password DEFAULT TABLESPACE users QUOTA UNLIMITED ON users; -- 授予基本权限 GRANT CREATE SESSION TO inventory_app; GRANT CREATE TABLE, CREATE VIEW, CREATE PROCEDURE, CREATE TRIGGER, CREATE SEQUENCE TO inventory_app; -- 如果对象已由其他用户创建则需要授予对象权限 -- GRANT SELECT, INSERT, UPDATE, DELETE ON owner.table_name TO inventory_app; -- GRANT EXECUTE ON owner.procedure_name TO inventory_app;对于存储过程可以利用AUTHID DEFINER定义者权限或AUTHID CURRENT_USER调用者权限来控制其执行时访问数据库对象的权限这可以实现权限的封装和提升。6. 从项目到生产经验总结与避坑指南做完这个项目我最大的体会是数据库不只是个存数据的地方它本身就是一个强大的应用平台。PL/SQL让你能把复杂的、需要高一致性的业务逻辑牢牢地锁在数据旁边减少了网络往返提升了事务可靠性。但与之相对的是把业务逻辑“写死”在数据库里的风险比如业务规则变更时需要修改数据库对象这通常比更新应用代码更谨慎、更复杂。几个关键的避坑点事务边界要清晰存储过程里别随便COMMIT。让调用者应用来决定事务的提交或回滚。否则多个过程调用就无法组成一个原子操作。异常处理要周全PL/SQL里未处理的异常会导致整个块回滚。务必用EXCEPTION块捕获你能预见的异常如NO_DATA_FOUND,TOO_MANY_ROWS,DUP_VAL_ON_INDEX并记录有意义的错误信息。对于WHEN OTHERS至少要把错误堆栈DBMS_UTILITY.FORMAT_ERROR_STACK记到日志表里方便排查。游标记得关闭显式游标用完后一定要CLOSE否则会占用游标资源可能导致“超出最大打开游标数”的错误。养成在EXCEPTION块中也关闭游标的习惯。触发器不宜过重触发器里的逻辑要轻量。避免在触发器里执行长时间的操作或调用复杂的网络服务。记住触发器是同步执行的会拖慢主业务的DML速度。性能测试必不可少用真实规模的数据至少是未来一年预估的数据量进行压力测试。看看那些漂亮的视图和关联查询在百万级数据下是否还能秒出结果。索引不是建完就一劳永逸的。版本控制数据库脚本DDL, DML, PL/SQL一定要纳入Git这样的版本控制系统。每次变更都要有脚本并且有回滚的方案。直接在生产环境用GUI工具点来点去是灾难的开始。这个库存管理系统项目是一个绝佳的起点。你可以基于它继续深化引入“库存预留”机制来处理购物车实现“批次管理”来跟踪不同批次的成本用物化视图来预聚合复杂的报表数据甚至探索Oracle Advanced Queuing来实现库存变更的异步通知。把这些基础打牢无论以后面对多么复杂的业务系统你都能清晰地看到数据流动的脉络。