So sánh chi tiết từ MySQL single-tenant 4 DBs sang PostgreSQL 16 multi-tenant unified — 92 tables với tenant isolation + Trust Layer + AI Core.
Thay đổi lõi: DB engine · Tenancy model · Số lượng databases · Extensions.
| Dimension | V3 Current | V4 Target | Impact |
|---|---|---|---|
| DB Engine | MySQL 8.x | PostgreSQL 16 | JSONB native · GIN indexes · partitioning · pgvector |
| Tenancy Model | Single-tenant | Multi-tenant (row-level) | tenant_id trên mọi bảng + RLS policies |
| Databases | 4 (viecxanh + viecxanh_admin + chat_db MongoDB + Redis) | 2 (viecxanh_v4 unified + Redis; MongoDB optional cho chat messages) | Unified ops DB, no cross-DB queries |
| Migrations count | ~71 migrations split 2 DBs | ~92 migrations unified | Clean build order, không còn deferred FK hack |
| Cross-DB Queries | ⚠ Admin reads work_records từ public DB | ✅ Không còn (unified) | Bỏ PublicApiClient proxy complexity |
| FK Constraints | ⚠ Deferred (migration 999999) skip SQLite | ✅ Natural dependency order | Tests run in-memory DB được |
| Extensions | MySQL built-in only | pgvector · pg_trgm · unaccent · pgcrypto · uuid-ossp · pg_stat_statements | Vector AI + fuzzy search + encryption native |
| Row-Level Security | ❌ Không có (app-layer only) | ✅ RLS policies bật theo tenant_id | Defense-in-depth cho tenant isolation |
| Partitioning | ❌ Không có (single-tenant nhỏ) | ✅ Native range partition (work_date, created_at) | attendance_records 30M+ rows/tháng scale |
| Soft Deletes | ❌ Không dùng (hard delete) | ✅ deleted_at column + SoftDeletingScope | Audit recovery + compliance |
| ID Strategy | BigInt auto-increment | BigInt auto-increment (giữ nguyên) | Familiar, performant |
| Money Type | decimal(15,2) | NUMERIC(19,4) | Financial-grade, 4 decimals cho VND chia giờ |
| Time Types | timestamp (no TZ) | TIMESTAMPTZ | Timezone-aware cho audit, cross-region |
| Polymorphic Relations | ⚠ 15+ morphmap types (tight coupling) | Minimize, explicit relations preferred | Dễ refactor service boundaries |
Số lượng tables qua từng giai đoạn migration.
| Category | V3 | V4 | Change |
|---|---|---|---|
| Tenant & Organization | ~3 (employers, clusters, partners) | 9 | +6 new (tenants, subscriptions, shifts, etc.) |
| Identity & Access | ~5 (users, roles, permissions, tokens) | 7 | +2 login_logs, refined structure |
| Recruitment | ~6 (jobs, applications, candidates, sources) | 10 | +4 job_distributions, interview_rounds, etc. |
| Worker Lifecycle | ~8 (workers, employments, milestones) | 8 | ~0 structure refined, names unified |
| Attendance | ~6 (records, adjustments, imports) | 8 | +2 summaries, anomalies, holidays |
| Payroll & Finance | ~15 (wallets, transactions, payslips, loans) | 8 | -7 tách riêng financial products |
| Platform Infra | ~4 (audit, notifications, settings) | 6 | +2 domain_events outbox, file_attachments |
| Supplier Network | ~5 (ctv_tiers, commissions scattered) | 6 | +1 unified structure |
| Factory Operations | 0 | 5 | +5 ALL NEW (manpower, eval, feedback) |
| Reconciliation | ~3 (disputes, basic) | 7 | +4 sessions, items, settlements |
| AI & Knowledge | 0 | 8 | +8 ALL NEW (sessions, embeddings, tools) |
| Chat & Messaging | MongoDB (separate DB) | 4 | +4 in PG; messages optional MongoDB |
| Worker App + Profile | 0 | 10 | +10 ALL NEW (accounts, skills, certs, timeline) |
| Financial Products | 0 (salary advance only) | 4 | +4 loans, insurance, wallet |
| Workflow Engine | 0 (FSM scattered) | 3 | +3 ALL NEW |
| Entity Versioning | 0 | 1 | +1 ALL NEW |
| CTV Tree | 0 (adjacency list scattered) | 1 | +1 materialized path |
| Integration Hub | 0 | 3 | +3 ALL NEW |
| HRM (dropped) | ~30 (viecxanh_admin) | 0 | -30 Not in V4 brief |
| Housing (dropped) | 9 (landlords, rooms, contracts) | 0 | -9 Not in V4 brief |
| Community (dropped) | 16 (posts, groups, points) | 0 | -16 Not in V4 brief |
| Content (dropped) | 6 (SEO posts, policies) | 0 | -6 Move to headless CMS |
17 V3 domains (backend) + 9 V3 domains (backend-admin) → 15 V4 module groups.
| V3 Domain | V4 Module | Action | Notes |
|---|---|---|---|
| Application | M11 AI Analytics | MERGE | Aggregator → AI Core |
| Infrastructure | M2 IAM + M7 Platform | SPLIT | User mgmt → M2, utilities → M7 |
| Shared | Shared kernel | KEEP | Framework utilities |
| Dashboard | M11 AI Analytics | MERGE | Per-tenant rewrite |
| Worker | M4 Worker Lifecycle | KEEP | 70% reuse (21 models) |
| WorkerManagement | M4 Worker Lifecycle (extend) | MERGE | Groups/tags gộp M4 |
| Employer | M1 Tenant + Gap A Workflow | SPLIT | Employer → tenant entity |
| EmployerUser | M2 IAM users | MERGE | Unified user model |
| Cluster | M1 work_locations | RENAME | Geographic unit, not tenant |
| Factory (V2) | — | DROP | Legacy alias, redundant |
| Job | M3 Recruitment | KEEP | 80% reuse (Dispatch FSM) |
| Partner | M1 tenant_partnerships | RENAME | N:N tenant relationship |
| Content | — | DROP | Move to headless CMS |
| Attendance | M5 Attendance | KEEP | 80% reuse |
| Finance | M6 Payroll + Gap E Financial | SPLIT | Payroll ops vs Financial products |
| Incentive | M8 Supplier + M10 Reconciliation | SPLIT | Commission engine reuse |
| Housing | — | DROP | Not in V4 brief |
| Community | — | DROP | Not in V4 brief |
| V3 Domain | V4 Module | Action | Notes |
|---|---|---|---|
| Attendance (admin) | M5 Attendance (admin view) | MERGE | Same tables, admin UI |
| Campaign | M3 Recruitment (campaigns) | MERGE | Gộp vào recruitment |
| Crm | M3 + M4 + M8 | SPLIT | Lead flows tách domain phù hợp |
| Finance (staff view) | M6 Payroll + M10 Reconciliation | MERGE | Admin view shared tables |
| Hrm (35 models) | — | DROP | Không phải V4 focus |
| Infrastructure | M7 Platform + Integration Hub | SPLIT | User sync pattern → Integration Hub |
| RecruitmentSupport | M8 Supplier Network | KEEP | Recruiter tools |
| Shared | Shared kernel | KEEP | Framework utilities |
| Trust | M10 Reconciliation + Trust Layer | MERGE | Dispute + reputation → M10 |
Chi tiết từng bảng V3 → V4: rename, merge, add tenant_id, drop.
| V3 Table | V4 Table | Action | Key Changes |
|---|---|---|---|
| employers | tenants | RENAME | Employer → Tenant entity, thêm type (factory/supplier/hybrid) |
| employers_subscriptions | tenant_subscriptions | RENAME | Billing per tenant |
| — | tenant_modules | NEW | Module on/off per tenant |
| partners | tenant_partnerships | RENAME | NM ↔ NCC N:N relationship |
| employer_organizations | organizations | KEEP | Self-ref tree (HQ/branch/site) |
| departments (in HRM) | departments | RENAME | Tách từ HRM dropped, keep structure |
| positions (scattered) | positions | KEEP | Chức danh unified |
| clusters | work_locations | RENAME | Geographic unit, decoupled from tenant |
| shifts (in Attendance) | shifts | REFINE | Promoted to M1 shared, add OT rules JSONB |
| V3 Table | V4 Table | Action | Key Changes |
|---|---|---|---|
| job_postings | jobs | RENAME | Simpler name, remove target_factory_tenant_id |
| — | job_distributions | NEW | Tách ra bảng riêng cho NCC → NM dispatch |
| job_posting_versions | via entity_versions | MERGE | Use Trust Layer generic versioning |
| applications (V2: job_applications) | applications | KEEP | Drop V2 alias |
| candidate_profiles | candidates | RENAME | Shorter name, full-text index |
| interviews | interviews + interview_rounds | SPLIT | Support multi-round PV |
| dispatches | merged into applications + workflow_instances | MERGE | State machine → Workflow Engine |
| job_sources | candidate_sources | RENAME | Align to candidate-centric |
| recruitment_campaigns (admin) | recruitment_campaigns | KEEP | Move from admin to core |
| candidate_contacts_log | candidate_contact_logs | KEEP | Rename plural |
| candidate_media | candidate_documents | RENAME | Use dedicated table, not polymorphic |
| V3 Table | V4 Table | Action | Key Changes |
|---|---|---|---|
| workers | workers | REFINE | Add tenant_id + link to worker_account_id (cross-tenant) |
| worker_jobs | employments | RENAME | Clearer semantics (1 employment per job) |
| worker_milestones | retention_milestones | RENAME | Explicit retention focus |
| worker_status_changes | worker_status_logs | RENAME | Align với log convention |
| worker_documents | worker_documents | KEEP | Same structure + tenant_id |
| worker_addresses | JSON in workers | MERGE | Denormalize (1 address typical) |
| worker_bank_accounts | JSON in workers | MERGE | Same reason |
| worker_contacts | emergency_contact JSONB in workers | MERGE | Fewer joins |
| leave_requests | leave_requests | KEEP | + workflow_instance link |
| referrals | referrals | KEEP | Internal worker referrals |
| worker_tags, worker_groups | worker_tags, worker_groups | KEEP | Từ WorkerManagement domain |
| — | worker_onboardings | NEW | Explicit onboarding workflow |
| V3 Table | V4 Table | Action | Key Changes |
|---|---|---|---|
| attendance_periods | timesheet_periods | RENAME | Align to "timesheet" domain term |
| attendance_records | attendance_records | REFINE | Partition by work_date + tenant_id |
| attendance_imports | attendance_imports | KEEP | Excel/API import history |
| attendance_adjustments | attendance_adjustments | KEEP | With entity_versions integration |
| overtime_records | merged to attendance_records | MERGE | ot_hours column in main table |
| reconciliation_runs | moved to M10 | MERGE | Reconciliation domain separate |
| — | timesheet_summaries | NEW | Aggregate per period/worker |
| — | attendance_anomalies | NEW | AI anomaly detection output |
| — | shift_assignments | NEW | Worker-shift mapping history |
| — | holidays | NEW | Holidays per tenant |
| V3 Table | V4 Table | Action | Key Changes |
|---|---|---|---|
| payroll_cycles | payroll_cycles | KEEP | Monthly batches |
| payslips | payslips | REFINE | NUMERIC(19,4) + breakdown JSONB |
| payslip_items | payslip_items | KEEP | Detail lines |
| salary_structures | salary_structures | KEEP | Templates |
| wallet_transactions | split to payment_records + M12 wallet_balances | SPLIT | Operations vs aggregate view |
| wallets | wallet_balances (in M12) | MERGE | Cross-tenant aggregate (worker-centric) |
| bonuses | bonuses | KEEP | Standalone bonus records |
| salary_advances | salary_advances | KEEP | + Gap E expansion (loans catalog) |
| ledger_entries | split to payment_records + audit_logs | SPLIT | Operations vs audit trail |
| loans, lending_products | loan_products, loan_applications (in Gap E) | RENAME | Move to Financial Products |
| savings_accounts | via wallet_balances aggregate | MERGE | Balance aggregation |
| device_tokens | moved to M12 worker app | MERGE | Push notification per worker_account |
| V3 Table | V4 Table | Action | Key Changes |
|---|---|---|---|
| ctv_users, ctv_tiers | collaborators + collaborator_trees | MERGE | Adjacency → materialized path |
| commission_policies | commission_rules | RENAME | "Rules" clearer than "policies" |
| commission_formulas, commission_tiers, commission_conditions | merged into commission_rules.rules (JSONB) | MERGE | Simpler model |
| commission_runs, commission_lines | commission_payouts | MERGE | Single table per payout event |
| — | recruiters | NEW | Internal NCC recruiters |
| — | vendors | NEW | Sub-tier vendors |
| — | candidate_assignments | NEW | NCC → NM candidate dispatch |
| — | manpower_plans | NEW | Factory headcount forecasting |
| — | supplier_evaluations | NEW | NCC scorecard |
| — | factory_feedback | NEW | NM → NCC feedback |
| — | headcount_snapshots | NEW | Daily aggregates |
| disputes (in admin) | disputes (in M10) | REFINE | + dispute_evidences, settlements |
| — | reconciliation_sessions, reconciliation_items, settlement_records, policy_snapshots, receivables_payables | NEW | Full reconciliation model |
| trust_scores | via M10 + audit_logs | MERGE | Trust via policy_snapshots + history |
| V3 Table | V4 Table | Action | Key Changes |
|---|---|---|---|
| users (worker subset) | worker_accounts (NO tenant_id) | SPLIT | Cross-tenant identity |
| — | worker_account_links | NEW | Account ↔ worker × tenant junction |
| saved_jobs (scattered) | saved_jobs | KEEP | Per worker_account |
| — | job_applications_mobile | NEW | Mobile-sourced applications tracking |
| notifications (general) | worker_notifications | SPLIT | Worker-specific channel |
| — | worker_skills, worker_experiences, worker_certificates, worker_preferences, worker_timeline_events | NEW | Digital Profile 9 parts |
Những thay đổi pattern ảnh hưởng toàn bộ schema.
-- workers table CREATE TABLE workers ( id BIGINT PRIMARY KEY AUTO_INCREMENT, full_name VARCHAR(200), phone VARCHAR(20), id_number VARCHAR(12), status VARCHAR(20), created_at TIMESTAMP, updated_at TIMESTAMP -- NO tenant_id -- NO deleted_at -- NO RLS ); -- Query from app layer SELECT * FROM workers WHERE status = 'active';
-- workers table V4 CREATE TABLE workers ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, worker_account_id BIGINT, full_name VARCHAR(200), phone VARCHAR(20), id_number VARCHAR(12), status VARCHAR(20), deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE workers ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON workers USING (tenant_id = current_setting('app.current_tenant_id')::BIGINT);
-- Risk: precision loss on VND hourly CREATE TABLE payslips ( gross_pay DECIMAL(15,2), net_pay DECIMAL(15,2), ... ); -- Float used elsewhere (BUG prone) CREATE TABLE commission_formulas ( rate FLOAT -- DANGEROUS );
-- Financial-grade precision CREATE TABLE payslips ( gross_pay NUMERIC(19,4), net_pay NUMERIC(19,4), ... ); -- All rate columns also NUMERIC CREATE TABLE commission_rules ( amount NUMERIC(19,4) NOT NULL, percentage NUMERIC(5,3) -- safe ); -- Invariant enforced by CHECK ALTER TABLE payslips ADD CONSTRAINT chk_net_le_gross CHECK (net_pay <= gross_pay);
-- 15+ morphmap types tight coupling CREATE TABLE activity_log ( subject_type VARCHAR(255), subject_id BIGINT, ... ); -- morphmap in AppServiceProvider Relation::morphMap([ 'worker' => Worker::class, 'job' => JobPosting::class, 'application' => Application::class, ...15+ entries ]); -- Hard to split to services later
-- Generic via audit_logs + entity_versions CREATE TABLE audit_logs ( tenant_id BIGINT, entity_type VARCHAR(50), entity_id BIGINT, action VARCHAR(20), old_values JSONB, new_values JSONB, ... ); -- entity_type is plain string, no morphmap -- Services define their own entity types -- Easier to split to microservices later
-- 5M rows/month, no partition -- Full-table scan risk CREATE TABLE attendance_records ( id BIGINT PRIMARY KEY, worker_id BIGINT, work_date DATE, ... ); CREATE INDEX idx_attendance_date ON attendance_records(work_date); -- Eventually degrades without partition
-- PostgreSQL 16 native CREATE TABLE attendance_records ( id BIGSERIAL, tenant_id BIGINT NOT NULL, worker_id BIGINT NOT NULL, work_date DATE NOT NULL, ... ) PARTITION BY RANGE (work_date); CREATE TABLE attendance_2026_04 PARTITION OF attendance_records FOR VALUES FROM ('2026-04-01') TO ('2026-05-01'); -- Query planner auto-prunes partitions -- Easy archival (drop old partitions)
-- Partial audit trail activity_log table: - description (text) - subject (polymorphic) - causer (polymorphic) - properties (JSON) -- Missing: - Full entity snapshots - Version numbers - Policy snapshots at decision time - Replay capability
-- 3 tables complete coverage audit_logs -- what changed entity_versions -- full snapshot domain_events -- outbox pattern policy_snapshots -- (in M10) -- Critical for V4 financial: SELECT snapshot FROM entity_versions WHERE entity_type = 'payslips' AND entity_id = 42 AND version_number = 3; -- Replay any entity state at any point
Tables không có trong V3, phải build from zero.
60+ tables từ V3 KHÔNG chuyển sang V4. Lý do + migration path.
| V3 Domain | Tables dropped | Count | Lý do | Data handling |
|---|---|---|---|---|
| HRM (admin) | hrm_employees, hrm_departments, hrm_contracts, hrm_payrolls, hrm_attendance, hrm_leaves, hrm_kpi, hrm_training, ... | ~30 | V4 không phải HR internal — focus workforce ops | Export cho record, không migrate. Team HR nội bộ chuyển sang HR-dedicated tool. |
| Housing | landlords, rooms, rental_contracts, bookings, maintenance_requests, landlord_documents, room_media, ... | 9 | Không có trong V4 brief. Business case weak. | Sunset với 3-6 month notice. Optional: tách app riêng nếu demand cao. |
| Community | posts, groups, comments, reactions, points, post_media, group_members, moderation_logs, ... | 16 | Không phải core V4 | Sunset entirely. V4 Chat module đủ cho communication. |
| Content | content_posts, content_categories, content_tags, policies, training_materials, ... | 6 | SEO + policies → headless CMS riêng | Migrate to Strapi/Sanity. Next.js fetch via API. |
| Factory V2 alias | factories (VIEW alias) | 1 | Legacy backward-compat | Remove VIEW. Frontend uses work_locations. |
| V2 morphmap legacy | 5 morphmap entries | — | Cleanup polymorphic bloat | Code cleanup only, no data impact. |
| zalo-mini-app tables | Zalo-specific user linking | ~3 | Zalo app sunset | Export CTV data, freeze 12 months. |
Mapping từng bảng V3 → V4 + data migration method.
| Category | Count | Strategy | Risk |
|---|---|---|---|
| Direct copy | ~20 tables | 1:1 copy với tenant_id backfill (infer từ employer ownership) | LOW |
| Rename + copy | ~15 tables | Copy với tên mới, cùng structure, backfill tenant_id | LOW |
| Refine + copy | ~12 tables | Copy với columns mới (tenant_id, NUMERIC type cast, JSONB denormalize) | MEDIUM |
| Split | ~6 operations | 1 V3 table → 2+ V4 tables (employments, wallets, etc.) | MEDIUM |
| Merge | ~10 operations | Nhiều V3 tables → 1 V4 table (addresses/contacts → JSONB) | MEDIUM |
| New (no migrate) | ~30 tables | Empty table, populate qua usage | LOW |
| Drop (no migrate) | ~65 tables | Export CSV backup, không migrate | LOW |
| Step | Action | Duration | Downtime? |
|---|---|---|---|
| 1 | Schema V4 finalized + tested in staging | 1 week | No |
| 2 | Migration scripts developed + tested against dump | 2 weeks | No |
| 3 | Canary tenant selected + engaged | 1 week | No |
| 4 | Dry-run migration cho canary tenant (from backup) | 3-5 days | No |
| 5 | Canary cutover (V3 read-only → migrate → V4 live) | 4-8 hours | YES (canary only) |
| 6 | Monitor canary 2 weeks (metrics, bugs) | 2 weeks | No |
| 7 | Batch migrate remaining tenants (batch size 5-10) | 4-6 weeks | YES (per batch) |
| 8 | V3 sunset announcement + decommission | 2-4 weeks | No |
V3 ~70 tables scattered 2 DBs → V4 92 tables unified PostgreSQL
~60% data migratable (direct/rename/refine) · ~30% structural changes (split/merge) · ~65 tables dropped (HRM, Community, Housing, Content) · ~30 tables ALL NEW (gap areas)
Migration timeline: 3-4 months (canary + batch) · Downtime: ~8 hours per tenant · Zero data loss target.