據(jù)庫大作業(yè)超市管理系統(tǒng):從表設(shè)計(jì)到事務(wù)并發(fā)與查詢優(yōu)化實(shí)戰(zhàn))
簡介這份數(shù)據(jù)庫大作業(yè)文檔面向高校計(jì)算機(jī)相關(guān)專業(yè)學(xué)生提供一套小型超市管理系統(tǒng)的完整課程設(shè)計(jì)參考方案適合正在完成數(shù)據(jù)庫原理或程序設(shè)計(jì)大作業(yè)、需要項(xiàng)目實(shí)戰(zhàn)案例的學(xué)習(xí)者。資源包內(nèi)含1個docx文檔約249KB內(nèi)容涵蓋項(xiàng)目簡介、需求分析、編程環(huán)境、數(shù)據(jù)庫基本表與E-R圖、數(shù)據(jù)庫框架介紹、源代碼段分析及問題解決等模塊。文檔以C語言和SQL為開發(fā)語言基于Visual Studio 2013與MySQL數(shù)據(jù)庫配合Navicat可視化工具詳細(xì)闡述了顧客、員工、管理員三部分功能的設(shè)計(jì)與實(shí)現(xiàn)并給出員工表、商品表、貨架表、進(jìn)貨表、日銷售量表等實(shí)體結(jié)構(gòu)及一對多關(guān)聯(lián)關(guān)系。讀者可從中獲取完整的需求分析思路、E-R圖設(shè)計(jì)方法、數(shù)據(jù)庫連接與查詢的API調(diào)用示例以及建表、MFC界面調(diào)試等常見問題的排錯經(jīng)驗(yàn)。目前已有1727人學(xué)習(xí)下載適合作為課程設(shè)計(jì)參考或數(shù)據(jù)庫綜合實(shí)踐的學(xué)習(xí)材料。1. 從一份 docx 到能跑起來的超市管理系統(tǒng)數(shù)據(jù)庫大作業(yè)到底在考什么很多人拿到「數(shù)據(jù)庫大作業(yè)–超市管理系統(tǒng).docx」這個題目第一反應(yīng)是去網(wǎng)上找一份現(xiàn)成代碼改改交差。我?guī)н^幾屆課程設(shè)計(jì)見過太多這樣的翻車現(xiàn)場表建了七八張外鍵一個沒加庫存扣減靠前端算完再寫回答辯時老師一句「并發(fā)下超賣了算誰的」直接問穿。這個題目的本質(zhì)不是讓你寫一個收銀界面而是用超市這個業(yè)務(wù)場景把數(shù)據(jù)庫設(shè)計(jì)、增刪改查、事務(wù)與并發(fā)控制、查詢優(yōu)化這幾件事串起來落地。它適合數(shù)據(jù)庫課程設(shè)計(jì)階段的學(xué)生也適合想用一個完整小項(xiàng)目把 SQL 手感撿回來的開發(fā)者。下面我按「先立住設(shè)計(jì)、再動手建庫、最后壓測排錯」的順序把一份能過答辯、也能真跑的系統(tǒng)講清楚中間會帶上數(shù)據(jù)庫課程設(shè)計(jì)里最常被忽略的參數(shù)和坑。2. 需求拆解與表結(jié)構(gòu)設(shè)計(jì)超市管理系統(tǒng)該建哪幾張表2.1 先畫業(yè)務(wù)流再定實(shí)體別一上來就寫 CREATE TABLE超市管理系統(tǒng)的業(yè)務(wù)其實(shí)就四條主線商品進(jìn)貨入庫、商品銷售出庫、庫存盤點(diǎn)、會員與收銀。把這四條線畫成流程實(shí)體自然就出來了。我一般會先在紙上列實(shí)體清單再決定哪些是主表、哪些是流水表。核心實(shí)體有這些商品product、商品分類category、供應(yīng)商supplier、員工employee、會員member、進(jìn)貨單purchase_order及其明細(xì)purchase_item、銷售單sale_order及其明細(xì)sale_item、庫存流水stock_log。注意這里我把「庫存」拆成了兩個東西商品表上存一個當(dāng)前庫存冗余字段同時用 stock_log 記錄每一次增減。這是超市系統(tǒng)里最關(guān)鍵的一個設(shè)計(jì)決策后面講并發(fā)時會用到。為什么要有明細(xì)表因?yàn)橐粡堖M(jìn)貨單可以進(jìn)多種商品這是典型的一對多。很多同學(xué)圖省事把商品直接塞進(jìn)訂單表的一個字段里用逗號分隔這種設(shè)計(jì)在數(shù)據(jù)庫課程設(shè)計(jì)里是硬傷老師一眼就能看出來你不懂第一范式。2.2 表結(jié)構(gòu)落地字段、類型與約束怎么定下面是我常用的建表腳本以 MySQL 8.0 為例。字段類型的選擇有講究金額一律用 DECIMAL 而不是 FLOAT因?yàn)楦↑c(diǎn)數(shù)算錢會出現(xiàn) 0.10.2 這種玄學(xué)問題庫存數(shù)量用 INT時間用 DATETIME。-- 商品分類表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE COMMENT 分類名唯一 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品表 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, barcode VARCHAR(32) NOT NULL UNIQUE COMMENT 條形碼業(yè)務(wù)主鍵, name VARCHAR(100) NOT NULL, category_id INT NOT NULL, supplier_id INT, price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 售價(jià), cost DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 進(jìn)價(jià), stock INT NOT NULL DEFAULT 0 COMMENT 當(dāng)前庫存冗余字段, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_product_category FOREIGN KEY (category_id) REFERENCES category(id), INDEX idx_category (category_id), INDEX idx_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;這里有幾個參數(shù)必須說清楚。ENGINEInnoDB是硬性要求因?yàn)橹挥?InnoDB 支持事務(wù)和行級鎖MyISAM 在這個項(xiàng)目里直接出局。utf8mb4而不是utf8是因?yàn)樯唐访锟赡艹霈F(xiàn)生僻字或 emojiutf8在 MySQL 里其實(shí)是三字節(jié)的殘缺實(shí)現(xiàn)。DECIMAL(10,2)表示總共 10 位、小數(shù) 2 位最大能存到九千多萬對超市單品足夠。外鍵fk_product_category保證了不會出現(xiàn)「分類不存在」的臟數(shù)據(jù)。但要注意外鍵在高并發(fā)寫入時會有額外鎖開銷如果后面做壓測發(fā)現(xiàn)插入慢可以考慮在應(yīng)用層保證一致性、去掉物理外鍵這是常見的取舍。2.3 訂單與流水表把「一次交易」拆成主表加明細(xì)銷售單和進(jìn)貨單結(jié)構(gòu)類似我用銷售單舉例。主表存單據(jù)頭信息明細(xì)表存每一行商品。-- 銷售單主表 CREATE TABLE sale_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 業(yè)務(wù)單號, member_id INT, employee_id INT NOT NULL, total_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00, pay_type TINYINT NOT NULL DEFAULT 1 COMMENT 1現(xiàn)金 2掃碼 3會員卡, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_created (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 銷售明細(xì)表 CREATE TABLE sale_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, unit_price DECIMAL(10,2) NOT NULL COMMENT 成交單價(jià)快照, CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES sale_order(id), INDEX idx_order (order_id), INDEX idx_product (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;sale_item里的unit_price是成交時的價(jià)格快照不能靠關(guān)聯(lián) product 表實(shí)時取因?yàn)樯唐氛{(diào)價(jià)后歷史訂單金額會變這是財(cái)務(wù)上的大忌。order_no加唯一索引防止重復(fù)提交產(chǎn)生兩張單。idx_created索引是為了后面按日期做銷售統(tǒng)計(jì)查詢時能走索引不然全表掃描在數(shù)據(jù)量上來后會很慢。3. 增刪改查與事務(wù)把進(jìn)貨、銷售、退貨寫成能扛住并發(fā)的 SQL3.1 進(jìn)貨入庫一條事務(wù)里同時改庫存和寫流水進(jìn)貨的核心動作是插入進(jìn)貨單主表、插入明細(xì)、增加商品庫存、寫一條庫存流水。這四步必須在一個事務(wù)里任何一步失敗都要回滾否則會出現(xiàn)「單子建了但庫存沒加」這種對不上賬的情況。START TRANSACTION; INSERT INTO purchase_order (order_no, supplier_id, employee_id, total_amount) VALUES (PO20240101001, 1, 1, 500.00); SET order_id LAST_INSERT_ID(); INSERT INTO purchase_item (order_id, product_id, quantity, unit_price) VALUES (order_id, 1, 100, 5.00); -- 增加庫存 UPDATE product SET stock stock 100 WHERE id 1; -- 寫庫存流水change_type1 表示入庫 INSERT INTO stock_log (product_id, change_type, change_qty, ref_order_id) VALUES (1, 1, 100, order_id); COMMIT;LAST_INSERT_ID()拿到剛插入的主鍵避免再查一次。UPDATE product SET stock stock 100這種寫法是原子的數(shù)據(jù)庫層面保證不會丟更新千萬不要寫成「先 SELECT 出庫存在應(yīng)用里加 100再 UPDATE 回去」那種寫法在并發(fā)下必然出錯。3.2 銷售出庫用條件更新防超賣銷售比進(jìn)貨多一個約束庫存不能賣成負(fù)數(shù)。最穩(wěn)的寫法是在 UPDATE 的 WHERE 里帶上庫存判斷。START TRANSACTION; -- 扣庫存只有庫存足夠時才更新成功 UPDATE product SET stock stock - 5 WHERE id 1 AND stock 5; -- 檢查影響行數(shù)如果為 0 說明庫存不足 -- 應(yīng)用層判斷 affected_rows 0 則 ROLLBACK INSERT INTO sale_order (order_no, employee_id, total_amount) VALUES (SO20240101001, 1, 25.00); SET sale_id LAST_INSERT_ID(); INSERT INTO sale_item (order_id, product_id, quantity, unit_price) VALUES (sale_id, 1, 5, 5.00); INSERT INTO stock_log (product_id, change_type, change_qty, ref_order_id) VALUES (1, 2, -5, sale_id); COMMIT;關(guān)鍵在WHERE id 1 AND stock 5。這條語句在 InnoDB 行鎖下是串行執(zhí)行的兩個并發(fā)請求同時扣庫存第二個會等第一個提交后再判斷庫存不夠就更新 0 行。應(yīng)用層拿到affected_rows為 0 就回滾并提示「庫存不足」。這就是防超賣的標(biāo)準(zhǔn)做法比在應(yīng)用層加鎖可靠得多。3.3 退貨與庫存回滾別把退貨寫成負(fù)數(shù)的銷售退貨是很多同學(xué)容易寫亂的地方。常見錯誤是直接插一條數(shù)量為負(fù)的銷售明細(xì)這樣統(tǒng)計(jì)銷售額時會把退貨算成負(fù)銷售邏輯上勉強(qiáng)能用但報(bào)表會很難看。正確做法是單獨(dú)建退貨單或者用change_type3的庫存流水區(qū)分。START TRANSACTION; -- 退貨庫存加回 UPDATE product SET stock stock 5 WHERE id 1; -- 寫退貨流水change_type3 INSERT INTO stock_log (product_id, change_type, change_qty, ref_order_id) VALUES (1, 3, 5, return_id); -- 更新原銷售單狀態(tài)為已退貨如果整單退 UPDATE sale_order SET status 2 WHERE id sale_id; COMMIT;change_type用枚舉值區(qū)分入庫、銷售、退貨、盤點(diǎn)、報(bào)損這樣庫存流水表就是一本完整的賬任何時候都能通過SUM(change_qty)對出當(dāng)前庫存和 product.stock 做核對。如果兩者對不上說明有代碼繞過了流水直接改庫存這是排查數(shù)據(jù)問題的第一入口。3.4 常用查詢銷售統(tǒng)計(jì)與庫存預(yù)警怎么寫才走索引課程設(shè)計(jì)答辯最愛問的就是「你這個統(tǒng)計(jì)查詢怎么優(yōu)化的」。下面兩條是高頻查詢。-- 查詢某天的銷售額和單數(shù) SELECT COUNT(*) AS order_cnt, SUM(total_amount) AS total FROM sale_order WHERE created_at 2024-01-01 00:00:00 AND created_at 2024-01-02 00:00:00; -- 庫存預(yù)警低于安全庫存的商品 SELECT p.id, p.name, p.stock, c.name AS category FROM product p JOIN category c ON p.category_id c.id WHERE p.stock 10 AND p.status 1 ORDER BY p.stock ASC;第一條能走idx_created索引注意用和而不是BETWEEN或?qū)ψ侄巫龊瘮?shù)運(yùn)算因?yàn)镈ATE(created_at) 2024-01-01這種寫法會讓索引失效這是數(shù)據(jù)庫優(yōu)化里最經(jīng)典的坑。第二條的 JOIN 走idx_categorystock 10是范圍條件走不了索引但商品表數(shù)據(jù)量小問題不大如果商品上萬可以考慮給 stock 建索引或做定時任務(wù)預(yù)計(jì)算。4. 避坑與排查數(shù)據(jù)庫課程設(shè)計(jì)里最容易翻車的五件事4.1 中文亂碼建庫時沒指定字符集現(xiàn)象插入「可樂」顯示成問號或亂碼。原因數(shù)據(jù)庫、表、連接三處字符集不一致常見是建庫用了默認(rèn)的 latin1。解決建庫時顯式指定CREATE DATABASE supermarket DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;連接串里加characterEncodingutf8并且確認(rèn) MySQL 配置文件里character-set-serverutf8mb4。三處都對了才不會亂碼。4.2 外鍵報(bào)錯 1452插入順序搞反了現(xiàn)象插入商品時報(bào)Cannot add or update a child row。原因商品引用的 category_id 在分類表里不存在或者你先插商品后插分類。解決按依賴順序插入先 category、supplier再 product。批量導(dǎo)入數(shù)據(jù)時尤其要注意用SET FOREIGN_KEY_CHECKS0臨時關(guān)閉外鍵檢查是應(yīng)急手段但導(dǎo)完必須開回來否則臟數(shù)據(jù)會悄悄進(jìn)去。4.3 死鎖兩個事務(wù)更新順序不一致現(xiàn)象并發(fā)下報(bào)Deadlock found when trying to get lock。原因事務(wù) A 先鎖商品 1 再鎖商品 2事務(wù) B 反過來互相等對方釋放。解決約定所有事務(wù)按 product_id 升序更新把要改的商品先排序再依次 UPDATE。另外事務(wù)里不要做網(wǎng)絡(luò)請求或長時間計(jì)算鎖持有時間越短越好。數(shù)據(jù)庫死鎖是并發(fā)編程的必修課出現(xiàn)不可怕可怕的是不知道為什么。4.4 庫存對不上有人繞過了流水直接改 stock現(xiàn)象product.stock 和 stock_log 匯總值不一致。原因某段代碼直接UPDATE product SET stock 100覆蓋沒寫流水。解決把 stock 字段設(shè)為只允許通過增減語句修改代碼評審時重點(diǎn)查有沒有絕對賦值的寫法。定期跑一條核對 SQLSELECT p.id, p.stock, IFNULL(SUM(s.change_qty),0) FROM product p LEFT JOIN stock_log s ON p.id s.product_id GROUP BY p.id HAVING p.stock IFNULL(SUM(s.change_qty),0);能對不上的就是有問題。4.5 查詢慢統(tǒng)計(jì)報(bào)表把庫拖垮現(xiàn)象月底跑銷售報(bào)表時整個系統(tǒng)卡住。原因報(bào)表 SQL 全表掃描大表還和業(yè)務(wù)查詢搶鎖。解決給統(tǒng)計(jì)字段建索引報(bào)表走從庫或定時預(yù)計(jì)算到匯總表。如果只是課程設(shè)計(jì)至少要做到統(tǒng)計(jì)查詢走索引別在 WHERE 里對時間字段用函數(shù)。數(shù)據(jù)庫優(yōu)化不是玄學(xué)先看執(zhí)行計(jì)劃 EXPLAINtype 是 ALL 就是全表掃描得改。5. 從能跑到能答辯用 EXPLAIN 和壓測數(shù)據(jù)證明你的設(shè)計(jì)系統(tǒng)能跑起來只是及格線答辯時老師想看的是你有沒有想過「數(shù)據(jù)量大了會怎樣」。我一般會做兩件事來兜底。第一件是用 EXPLAIN 驗(yàn)證關(guān)鍵查詢。拿庫存預(yù)警那條 SQL 舉例前面加EXPLAIN看輸出。如果type是ref或range說明走了索引如果是ALL就得考慮加索引或改寫。銷售統(tǒng)計(jì)那條重點(diǎn)看key是不是idx_createdrows掃描行數(shù)是不是接近實(shí)際命中行數(shù)。這一步花十分鐘能擋掉答辯時一半的追問。第二件是造數(shù)據(jù)壓一壓。用存儲過程批量插入十萬條銷售明細(xì)然后跑統(tǒng)計(jì)查詢看耗時。-- 造測試數(shù)據(jù)插入10萬條銷售明細(xì) DELIMITER $$ CREATE PROCEDURE gen_test_data() BEGIN DECLARE i INT DEFAULT 0; WHILE i 100000 DO INSERT INTO sale_item (order_id, product_id, quantity, unit_price) VALUES (FLOOR(1 RAND() * 1000), FLOOR(1 RAND() * 50), FLOOR(1 RAND() * 5), 9.90); SET i i 1; END WHILE; END$$ DELIMITER ; CALL gen_test_data();RAND()生成隨機(jī)數(shù)模擬真實(shí)分布FLOOR(1 RAND() * 1000)保證 order_id 落在已有訂單范圍內(nèi)。造完數(shù)據(jù)再跑統(tǒng)計(jì)查詢?nèi)绻麖暮撩爰壍舻矫爰壘驼f明索引或查詢寫法有問題這時候優(yōu)化才有說服力。壓測數(shù)據(jù)不用多漂亮能說明「我加索引前后差了 20 倍」就夠了。最后說個我自己的習(xí)慣每次改完表結(jié)構(gòu)或 SQL我都會把建表腳本和關(guān)鍵查詢單獨(dú)存一份.sql文件用版本號命名。數(shù)據(jù)庫課程設(shè)計(jì)最怕的就是改著改著把能跑的版本覆蓋了回頭想找回滾都找不到。留一份后悔藥比什么都強(qiáng)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取