
Một thiết kế database quản lý kho cho doanh nghiệp nhỏ thường chỉ cần khoảng 6 bảng: danh_muc, san_pham, kho, nha_cung_cap, phieu_kho và phieu_kho_chi_tiet. Nguyên tắc quan trọng nhất: số tồn không phải một con số ai cũng gõ tay được, mà là kết quả cộng trừ từ các dòng phiếu nhập – xuất. Làm đúng hai điều đó, bạn sẽ ra được báo cáo tồn theo kho, nhập – xuất – tồn theo kỳ và cảnh báo hàng sắp hết mà không phải đập đi làm lại.
Tôi đã dựng kiểu dữ liệu này nhiều lần cho app AppSheet lẫn web app viết tay. Bài này tôi viết lại đầy đủ: bảng nào, cột nào, kiểu dữ liệu gì, quan hệ ra sao, kèm câu SQL mẫu để bạn tự làm được ngay.
Nghiệp vụ kho cần ghi lại những gì?
Chỉ có bốn loại việc xảy ra trong kho: nhập, xuất, chuyển kho và kiểm kê. Mọi thứ khác đều là biến thể của bốn loại này — nhập mua hàng, nhập trả lại, xuất bán, xuất hỏng, xuất sản xuất… đều quy về "hàng vào" hoặc "hàng ra" kèm một lý do.
Trước khi vẽ bảng, hãy viết ra những câu hỏi mà sếp sẽ hỏi app. Thực tế tôi hay gặp đúng bốn câu này:
- Tồn hiện tại theo từng kho — mặt hàng X còn bao nhiêu ở kho Bình Dương, bao nhiêu ở kho công ty?
- Nhập – xuất – tồn theo kỳ — tháng 9 tồn đầu bao nhiêu, nhập bao nhiêu, xuất bao nhiêu, tồn cuối bao nhiêu?
- Hàng sắp hết — mặt hàng nào đang dưới mức tồn tối thiểu để đặt mua tiếp?
- Hàng tồn lâu — mặt hàng nào không phát sinh xuất trong 90 ngày?
Bốn câu hỏi này quyết định cấu trúc bảng. Ví dụ muốn trả lời câu 2, bạn buộc phải lưu ngày của từng phiếu chứ không chỉ lưu số tồn hiện tại. Muốn trả lời câu 1, bạn buộc phải gắn kho vào từng lần phát sinh.
App quản lý kho cần bao nhiêu bảng?
Tối thiểu 6 bảng cho một kho nhỏ, và thêm 1–2 bảng nữa khi phát sinh nhu cầu bán hàng hoặc kiểm kê. Dưới đây là bộ khung tôi dùng làm điểm xuất phát cho hầu hết dự án:
| Bảng | Cột chính | Quan hệ |
|---|---|---|
danh_muc | id, ten, ghi_chu | 1 danh mục có nhiều sản phẩm |
san_pham | id, ma_sp, ten, don_vi_tinh, gia_von, ton_toi_thieu, danh_muc_id | thuộc 1 danh mục |
kho | id, ten_kho, dia_chi, nguoi_quan_ly | 1 kho có nhiều phiếu |
nha_cung_cap | id, ten, dien_thoai, dia_chi, ma_so_thue | 1 NCC có nhiều phiếu nhập |
phieu_kho | id, so_phieu, loai, kho_id, ngay, nha_cung_cap_id, trang_thai, nguoi_tao | thuộc 1 kho, có nhiều dòng chi tiết |
phieu_kho_chi_tiet | id, phieu_id, san_pham_id, so_luong, don_gia | thuộc 1 phiếu, trỏ tới 1 sản phẩm |
Hai bảng thêm khi cần: khach_hang (khi bắt đầu xuất bán, không chỉ xuất nội bộ) và ton_kho (bảng tổng hợp tồn, dùng khi dữ liệu lớn — tôi sẽ nói kỹ ở phần sau).
Điểm mấu chốt của bộ khung này là cặp phiếu – chi tiết phiếu. Một phiếu nhập ngày 05/10 có thể chứa 20 mặt hàng; nếu bạn nhét tất cả vào một bảng phẳng kiểu Excel, bạn sẽ phải lặp lại ngày, số phiếu, nhà cung cấp 20 lần và không khoá được "một phiếu đã duyệt". Tách ra hai bảng là cách chuẩn và cũng là cách AppSheet, Access hay bất kỳ web app nào đều làm.

Chi tiết từng bảng và kiểu dữ liệu
Chọn sai kiểu dữ liệu là lỗi tốn thời gian sửa nhất. Quy tắc của tôi: khoá chính dùng TEXT dạng UUID hoặc INTEGER tự tăng; tiền và số lượng dùng DECIMAL chứ không dùng FLOAT; ngày dùng DATE hoặc DATETIME chứ không dùng text.
Bảng san_pham — xương sống của cả hệ thống
Bảng này nên gọn và ổn định, vì mọi bảng khác đều trỏ về nó:
id— khoá chính, sinh tự động, không dùng mã sản phẩm làm khoá chính (vì mã có thể bị sửa).ma_sp— mã nội bộ, đặt ràng buộc UNIQUE để tránh trùng.ten— TEXT.don_vi_tinh— TEXT: cái, hộp, thùng, kg, mét.gia_von— DECIMAL(15,2), giá vốn tham chiếu mới nhất.ton_toi_thieu— DECIMAL, ngưỡng cảnh báo đặt hàng.danh_muc_id— khoá ngoại tớidanh_muc.id.dang_su_dung— BOOLEAN, để ẩn hàng ngừng kinh doanh mà không xoá dữ liệu lịch sử.
Bảng phieu_kho và phieu_kho_chi_tiet — nơi ghi mọi biến động
Bảng phieu_kho giữ phần đầu phiếu: loai (nhap / xuat / chuyen / kiem_ke), kho_id, ngay, nha_cung_cap_id, ly_do, trang_thai (nhap_lieu / da_duyet / da_huy), nguoi_tao, thoi_diem_tao.
Bảng phieu_kho_chi_tiet giữ từng dòng hàng: phieu_id, san_pham_id, so_luong, don_gia, ghi_chu.
Với chuyển kho, có hai cách xử lý. Cách gọn: thêm cột kho_den_id vào phieu_kho, phiếu loại chuyen sẽ trừ ở kho_id và cộng ở kho_den_id. Cách "sạch" hơn cho báo cáo: một lần chuyển sinh ra hai phiếu — một phiếu xuất ở kho đi, một phiếu nhập ở kho đến, nối với nhau bằng cột phieu_lien_ket_id. Tôi thường chọn cách hai cho doanh nghiệp có từ ba kho trở lên vì công thức tính tồn không phải xử lý trường hợp đặc biệt.
Tính tồn kho: lưu cột tồn hay tính từ phiếu?
Mặc định nên tính từ phiếu, chỉ lưu cột tồn khi dữ liệu đủ lớn để báo cáo chạy chậm. Tồn tính từ phiếu luôn khớp với chứng từ; tồn lưu sẵn thì nhanh nhưng dễ lệch nếu một lần ghi bị lỗi giữa chừng.
Câu SQL tính tồn hiện tại theo từng sản phẩm và từng kho:
SELECT ct.san_pham_id, p.kho_id, SUM(CASE WHEN p.loai = 'nhap' THEN ct.so_luong ELSE -ct.so_luong END) AS ton FROM phieu_kho p JOIN phieu_kho_chi_tiet ct ON ct.phieu_id = p.id WHERE p.trang_thai = 'da_duyet' GROUP BY ct.san_pham_id, p.kho_id;
Muốn ra danh sách hàng sắp hết, bọc kết quả trên lại và so với ton_toi_thieu:
SELECT s.ma_sp, s.ten, t.ton FROM san_pham s JOIN (…câu trên…) t ON t.san_pham_id = s.id WHERE t.ton < s.ton_toi_thieu;
Muốn báo cáo nhập – xuất – tồn theo kỳ, thêm điều kiện WHERE p.ngay BETWEEN '2026-09-01' AND '2026-09-30' và tách hai cột SUM riêng cho nhập và xuất.
| Tiêu chí | Tính từ phiếu | Lưu cột tồn (bảng ton_kho) |
|---|---|---|
| Độ chính xác | Luôn khớp chứng từ | Lệch nếu ghi sót hoặc sửa tay |
| Tốc độ khi dữ liệu lớn | Chậm dần theo số dòng phiếu | Nhanh, đọc một dòng là ra |
| Độ phức tạp code | Thấp | Cao: phải cập nhật mỗi lần duyệt/huỷ phiếu |
| Truy vết sai sót | Dễ, lần ngược về phiếu | Khó, phải đối chiếu lại toàn bộ |
Giải pháp dung hoà tôi hay dùng: vẫn tính từ phiếu, nhưng chốt tồn đầu kỳ mỗi tháng vào bảng ton_dau_ky (san_pham_id, kho_id, thang, so_luong). Báo cáo chỉ cộng trừ các phiếu trong tháng hiện tại lên số đầu kỳ — nhanh mà vẫn kiểm tra ngược được.
Mở rộng sang thiết kế cơ sở dữ liệu quản lý bán hàng
Để chuyển từ app kho sang thiết kế cơ sở dữ liệu quản lý bán hàng, bạn thêm ba bảng: khach_hang, don_hang và chi_tiet_don. Cấu trúc don_hang / chi_tiet_don giống hệt cặp phiếu – chi tiết phiếu ở trên, nên nếu đã hiểu phần kho thì phần này bạn làm trong một buổi.
Ba điểm cần nhớ khi nối bán hàng với kho:
- Đơn hàng không trừ kho, phiếu xuất mới trừ kho. Khi đơn chuyển sang trạng thái "đã giao", hệ thống sinh một bản ghi trong
phieu_khovớiloai = 'xuat'và cộtdon_hang_idtrỏ về đơn. Nhờ vậy đơn mới đặt chưa làm hụt tồn, còn tồn vẫn chỉ có một nguồn tính duy nhất. - Lưu đơn giá tại thời điểm bán vào
chi_tiet_don.don_gia. Giá trong bảngsan_phamsẽ thay đổi; nếu hoá đơn tháng trước đọc giá hiện tại thì doanh thu quá khứ sẽ sai. Tương tự, nên lưu cảgia_von_tai_thoi_diemnếu muốn tính lãi gộp theo đơn. - Tách trạng thái đơn và trạng thái thanh toán. Một đơn có thể đã giao nhưng chưa thu tiền. Hai cột riêng, hoặc thêm bảng
thanh_toannếu khách trả làm nhiều lần.
Ví dụ quen thuộc: một cửa hàng vật liệu xây dựng ở tỉnh bán xi măng và sắt thép, vừa bán lẻ tại cửa hàng vừa giao công trình. Họ cần biết lãi từng đơn giao công trình, trong khi giá xi măng thay đổi theo từng lô nhập. Nếu không lưu giá vốn theo thời điểm, báo cáo lãi sẽ nhảy loạn mỗi lần nhập hàng mới — đây là lỗi tôi gặp lại nhiều nhất khi tiếp nhận file Excel cũ của khách.
Thiết kế database web bán hàng khác gì app nội bộ?
Khác ở chỗ khách hàng tự thao tác. Một thiết kế database web bán hàng phải gánh thêm những thứ mà app nội bộ không có, vì nhân viên thì được huấn luyện còn khách thì không:
- Giỏ hàng — bảng
gio_hangvàgio_hang_chi_tiettách riêng khỏi đơn hàng, vì giỏ có thể bị bỏ dở và cần dọn định kỳ. - Biến thể sản phẩm — một áo thun có size S/M/L và ba màu. Đừng tạo chín sản phẩm rời; tạo bảng
bien_the(id, san_pham_id, size, mau, sku, gia, ton_toi_thieu) và chophieu_kho_chi_tiettrỏ tớibien_the_idthay vìsan_pham_id. Đây là thay đổi tốn công nhất nếu làm muộn, nên quyết sớm. - Mã khuyến mãi — bảng
khuyen_mai(ma, loai_giam, gia_tri, ngay_bat_dau, ngay_ket_thuc, so_lan_toi_da) và bảng ghi nhận lần dùng để chặn xài lại. - Trạng thái thanh toán và giao hàng tách đôi, kèm mã vận đơn của đơn vị vận chuyển.
- Địa chỉ giao nhiều lần — bảng
dia_chinối tớikhach_hang, và đơn hàng sao chép địa chỉ vào lúc đặt. Nếu chỉ lưu khoá ngoại, khách đổi địa chỉ là đơn cũ đổi theo, rất phiền khi đối chiếu giao nhận.
Lỗi hay gặp khi thiết kế database kho
Năm lỗi dưới đây chiếm phần lớn số lần tôi phải sửa dữ liệu cho người khác:
- Sửa thẳng cột tồn không qua phiếu. Ai đó gõ tay số tồn cho "khớp", từ đó trở đi không ai biết số thật là bao nhiêu. Chênh lệch phải xử lý bằng một phiếu kiểm kê có lý do, không phải bằng việc sửa ô.
- Không lưu quy đổi đơn vị. Nhập theo thùng, xuất theo hộp, bán theo cái. Hãy chọn một đơn vị cơ bản cho mỗi sản phẩm và thêm bảng
quy_doi_dvt(san_pham_id, don_vi, he_so). Mọi phép cộng trừ tồn quy về đơn vị cơ bản. - Xoá phiếu thay vì huỷ phiếu. Dùng cột
trang_thai = 'da_huy'; dữ liệu đã xoá thì không kiểm toán ngược được, mà kho là nơi hay phải giải trình nhất. - Không ghi người tạo và thời điểm. Hai cột
nguoi_taovàthoi_diem_taogần như miễn phí lúc thiết kế nhưng vô giá lúc truy vấn ai làm lệch tồn. - Thiếu quản lý lô và hạn dùng với ngành thực phẩm, dược, mỹ phẩm, hoá chất. Nếu ngành của bạn cần, hãy làm ngay từ đầu — bổ sung sau nghĩa là viết lại toàn bộ logic tồn.
Tóm tắt và bước tiếp theo
Ba ý cần nhớ: (1) bắt đầu với 6 bảng, thêm dần khi có nhu cầu thật; (2) mọi biến động đi qua cặp phieu_kho – phieu_kho_chi_tiet, tồn là kết quả tính chứ không phải số gõ tay; (3) khi mở rộng sang bán hàng, nhớ lưu đơn giá và giá vốn tại thời điểm phát sinh.
Việc nên làm tiếp, theo thứ tự: viết ra 4 câu hỏi báo cáo của riêng doanh nghiệp bạn → vẽ 6 bảng ra giấy kèm khoá ngoại → tạo 10 dòng dữ liệu mẫu → chạy thử câu SQL tính tồn ở trên và đối chiếu bằng tay. Nếu con số khớp, cấu trúc của bạn đã đứng vững.
Nếu bạn mới làm quen với khái niệm bảng – khoá chính – khoá ngoại, đọc trước bài thiết kế cơ sở dữ liệu cho người mới: 6 bước làm đúng từ đầu. Muốn tự viết được các câu truy vấn báo cáo trong bài này, tham khảo lộ trình học SQL 4 tuần cho người mới. Còn nếu bạn định để dữ liệu trên Google Sheet, bài dùng Google Sheet làm database: khi nào ổn, khi nào phải chuyển sẽ giúp bạn biết ngưỡng nên chuyển sang cơ sở dữ liệu thật.
Câu hỏi thường gặp
›App quản lý kho cần bao nhiêu bảng?
Tối thiểu khoảng 6 bảng cho kho nhỏ: danh_muc, san_pham, kho, nha_cung_cap, phieu_kho và phieu_kho_chi_tiet. Khi bắt đầu xuất bán cho khách thì thêm khach_hang, don_hang, chi_tiet_don; khi dữ liệu lớn thì thêm bảng tổng hợp tồn hoặc tồn đầu kỳ. Quan trọng không phải số lượng bảng mà là tách được cặp phiếu – chi tiết phiếu để một phiếu chứa nhiều mặt hàng.
›Có nên lưu số tồn vào một cột riêng không?
Mặc định nên tính tồn từ các dòng phiếu nhập – xuất bằng câu SUM, vì cách này luôn khớp chứng từ và truy vết được sai sót. Chỉ lưu cột tồn riêng khi số dòng phiếu lớn tới mức báo cáo chạy chậm thấy rõ. Giải pháp dung hoà là chốt tồn đầu kỳ mỗi tháng, rồi chỉ cộng trừ các phiếu trong tháng hiện tại.
›Quản lý hàng theo lô và hạn dùng thì thêm bảng gì?
Thêm bảng lo_hang gồm id, san_pham_id, so_lo, ngay_san_xuat, han_su_dung, và cho phieu_kho_chi_tiet trỏ tới lo_hang_id thay vì chỉ trỏ tới san_pham_id. Khi đó tồn được tính theo từng lô, bạn mới lọc được hàng sắp hết hạn và xuất theo nguyên tắc nhập trước xuất trước. Nếu ngành của bạn là thực phẩm, dược hay mỹ phẩm, nên làm ngay từ đầu vì bổ sung sau phải viết lại toàn bộ logic tồn.
›Thiết kế này dùng trên Google Sheet được không?
Được, mỗi bảng là một sheet và khoá ngoại là cột chứa mã của sheet kia — AppSheet hoạt động đúng theo mô hình này. Hạn chế là Google Sheet không tự ép ràng buộc khoá ngoại, không có transaction nên dễ lệch khi nhiều người ghi cùng lúc, và chậm dần khi số dòng lớn. Khi lượng dòng phiếu tăng mạnh hoặc nhiều người nhập liệu đồng thời, nên chuyển sang cơ sở dữ liệu thật như PostgreSQL hoặc MySQL.
Tác giả
Chuyên gia chuyển đổi số · Đại diện 5F Edu
Hơn 8 năm triển khai và tư vấn chuyển đổi số cho doanh nghiệp vừa và nhỏ: chuẩn hoá quy trình, xây phần mềm quản lý bằng AppSheet và web app, tự động hoá bằng N8N. Người làm kênh YouTube Nghiện AppSheet.



