Tìm ở đây kiến thức IT

Cách Ransomware tiêu diệt hệ thống sao lưu trước khi mã hóa dữ liệu

Chào các anh em coder và quản trị hệ thống! Chúng ta luôn được dạy rằng Backup là “tấm khiên” cuối cùng trước mọi cuộc tấn công dữ liệu. Thế nhưng, thực tế phũ phàng từ báo cáo của Acronis chỉ ra rằng: Backup không thất bại vì chúng không tồn tại, chúng thất bại vì bị hacker tiêu diệt trước khi quá trình mã hóa bắt đầu.

Hãy cùng phân tích kỹ thuật đằng sau “cơn ác mộng” này.

1. Khi Backup trở thành mục tiêu số 1

Các nhóm tấn công Ransomware hiện đại không còn “ngây thơ” lao vào mã hóa dữ liệu ngay lập tức. Chúng thực hiện chiến thuật nằm vùng và leo thang đặc quyền (Privilege Escalation). Mục tiêu ưu tiên hàng đầu là tìm ra các Backup RepositoriesBackup Management Servers.

2. Quy trình “triệt hạ” con đường sống của nạn nhân

Thay vì mã hóa trực tiếp tệp tin hệ thống, Ransomware thực hiện chuỗi hành động sau:

  • Vô hiệu hóa các tác vụ lập lịch: Dừng các Cron jobs hoặc Windows Scheduled Tasks liên quan đến sao lưu.
  • Xóa Shadow Copies: Một kỹ thuật kinh điển trên Windows để ngăn chặn việc sử dụng System Restore.
  • Tấn công Storage: Xâm nhập vào các phân vùng chứa bản sao lưu để thực hiện lệnh xóa sạch hoặc mã hóa chính các file backup này.

Dưới đây là một ví dụ mô phỏng lệnh mà mã độc thường sử dụng để xóa bỏ các bản sao lưu tức thời trên Windows:

vssadmin.exe delete shadows /all /quiet

3. Tại sao cơ chế phục hồi lại thất bại?

Khi kẻ tấn công đã kiểm soát được Backup Agent, chúng có thể gửi các chỉ thị giả mạo khiến hệ thống tưởng rằng dữ liệu vẫn đang được bảo vệ, nhưng thực chất các bản sao lưu mới nhất chỉ là những tệp tin rỗng hoặc đã bị hỏng. Đến lúc Encryption (mã hóa) toàn bộ hệ thống diễn ra, bạn mới bàng hoàng nhận ra “phao cứu sinh” duy nhất đã bị đâm thủng từ lâu.

4. Bài học cho dân Tech: Chiến thuật 3-2-1 thôi là chưa đủ!

Để đối phó, chúng ta cần triển khai các khái niệm bảo mật nâng cao hơn:

  • Immutable Backups: Bản sao lưu không thể thay đổi hoặc xóa trong một khoảng thời gian nhất định, ngay cả khi có quyền Admin.
  • Air-gapped Backups: Cô lập hoàn toàn bản sao lưu khỏi mạng nội bộ.
  • Multi-Factor Authentication (MFA): Cho mọi quyền truy cập vào hệ thống quản lý sao lưu.

Anh em hãy nhớ: Trong kỷ nguyên Ransomware 2.0, nếu bạn không bảo vệ hệ thống Backup, bạn thực sự không có Backup nào cả!

 MuddyWater Mượn Danh Ransomware: Chiêu Trò Chiếm Đoạt Thông Tin Qua Microsoft Teams

Một chiến dịch tấn công mới vừa bị “bóc mẽ” cho thấy các nhóm APT (Advanced Persistent Threat) đang ngày càng tinh vi trong việc thao túng tâm lý và che đậy dấu vết.

Lần này, cái tên nằm trong tầm ngắm là MuddyWater (còn được biết đến với các định danh như Mango Sandstorm, Seedworm, hay Static Kitten) – một nhóm hacker có liên kết với chính phủ Iran. Điều đáng chú ý là thay vì tấn công trực diện, chúng đang thực hiện một chiến dịch “False Flag” (chiến dịch giả mạo dưới danh nghĩa kẻ khác) cực kỳ nguy hiểm.

1. Kịch bản tấn công: Social Engineering qua Microsoft Teams

Theo báo cáo mới nhất từ Rapid7 vào đầu năm 2026, MuddyWater không bắt đầu bằng những lỗ hổng Zero-day phức tạp. Thay vào đó, chúng lợi dụng sự tin tưởng của người dùng trên Microsoft Teams.

Kịch bản diễn ra như sau:

  • Hacker sử dụng kỹ thuật Social Engineering, đóng giả làm nhân viên kỹ thuật hoặc quản trị viên hệ thống.

  • Chúng gửi tin nhắn trực tiếp qua Microsoft Teams để lừa người dùng thực hiện các thao tác cài đặt hoặc tải tệp tin độc hại.

  • Sau khi “cá cắn câu”, chuỗi Infection Sequence sẽ được kích hoạt để xâm nhập sâu hơn vào hệ thống.

2. Chiến thuật “False Flag” – Đòn tung hỏa mù

Điểm thú vị (và cũng đáng sợ nhất) ở đây chính là việc giả mạo Ransomware. Thông thường, các vụ tấn công mã hóa dữ liệu đòi tiền chuộc sẽ để lại dấu vết rất rõ ràng. Tuy nhiên, trong trường hợp này, MuddyWater chỉ sử dụng “vỏ bọc” Ransomware làm bình phong.

Mục tiêu thực sự của chúng không phải là tiền mã hóa (crypto), mà là Steal Credentials (đánh cắp thông tin đăng nhập). Việc giả vờ tấn công Ransomware giúp chúng:

  1. Đánh lạc hướng các đội ngũ SOC (Security Operations Center).

  2. Gây hoang mang để nạn nhân vô tình tiết lộ thêm các thông tin nhạy cảm trong quá trình xử lý sự cố.

  3. Che giấu mục đích gián điệp dài hạn của nhóm APT này.

3. Bài học cho dân Tech

Dù bạn là Dev hay Ops, việc cảnh giác với các nền tảng giao tiếp nội bộ như Slack hay Teams là chưa bao giờ thừa. Các tệp tin thực thi hoặc yêu cầu cấp quyền bất thường cần được kiểm chứng qua các kênh chính thống.

Lời khuyên: Hãy luôn bật MFA (Multi-Factor Authentication) và không bao giờ thực thi các script không rõ nguồn gốc từ các tin nhắn “khẩn cấp” trên Teams.

Prompt Injection

Tại Google, các đội ngũ Threat Intelligence của chúng tôi luôn nỗ lực đi trước các hoạt động đối nghịch trong thực tế, chủ động giám sát các mối đe dọa mới nổi trước khi chúng có thể ảnh hưởng đến người dùng. Hiện tại, Indirect Prompt Injection (IPI) là ưu tiên hàng đầu của cộng đồng bảo mật. Chúng tôi dự báo đây sẽ là một vectơ tấn công chính để các đối thủ nhắm vào và làm sụp đổ các AI agent. Nhưng trong khi mối nguy hiểm của IPI đang được thảo luận rộng rãi, liệu các tác nhân đe dọa có thực sự đang khai thác vectơ này ngay hôm nay – và nếu có, thì như thế nào?

Để trả lời những câu hỏi này và phát hiện các hành vi lạm dụng trong thực tế, chúng tôi đã tiến hành một cuộc quét rộng rãi trên web công cộng để giám sát các mẫu indirect prompt injection đã biết. Đây là những gì chúng tôi tìm thấy.

Mối đe dọa của Indirect Prompt Injection

Không giống như Direct Injection (tấn công trực tiếp), nơi người dùng cố tình “jailbreak” (vượt ngục) một chatbot thông qua giao diện chat, IPI xảy ra khi một hệ thống AI xử lý nội dung từ nguồn bên ngoài — như một trang web, email hoặc tài liệu — có chứa các hướng dẫn độc hại được che giấu. Khi AI đọc nội dung bị “đầu độc” này, nó có thể âm thầm tuân theo các mệnh lệnh của kẻ tấn công thay vì ý định ban đầu của người dùng.

Đây không phải là một lĩnh vực đáng lo ngại mới đối với chúng tôi. Google đã và đang làm việc không mệt mỏi để chống lại các mối đe dọa này thông qua sự hợp tác chéo chức năng giữa các nhà nghiên cứu tại Google DeepMind (GDM) và các nhà phòng thủ tại Google Threat Intelligence Group (GTIG). Chúng tôi đã chi tiết hóa công việc của mình và các nhà nghiên cứu đã tiếp tục làm nổi bật bản chất đang phát triển của các lỗ hổng này.

Bất chấp sự tập trung tập thể này, một câu hỏi cơ bản vẫn còn đó: Ở mức độ nào các tác nhân độc hại trong thực tế hiện đang vận hành các cuộc tấn công này?

Giám sát chủ động tại Google

Bối cảnh của IPI trên Web

Có nhiều kênh mà kẻ tấn công có thể cố gắng gửi prompt injection. Tuy nhiên, có một vị trí đặc biệt dễ quan sát: web công cộng. Tại đây, các tác nhân đe dọa có thể đơn giản “gieo” các prompt injection trên các trang web với hy vọng làm hỏng các hệ thống AI duyệt qua chúng.

Các nghiên cứu công khai xác nhận các cuộc tấn công này là khả thi. Do đó, chúng ta nên mong đợi các đối thủ trong thực tế khai thác các lỗ hổng này để gây hại.

Vì vậy, chúng tôi đặt ra một câu hỏi cơ bản: Kẻ tấn công thực sự đang cố gắng đạt được kết quả gì ngày hôm nay?

Để dễ tiếp cận và tái tạo, chúng tôi đã chọn sử dụng Common Crawl, một kho lưu trữ lớn các trang web được thu thập từ web nói tiếng Anh. Common Crawl cung cấp các bản chụp (snapshots) hàng tháng của 2-3 tỷ trang. Đây chủ yếu là các trang web tĩnh, bao gồm nội dung tự xuất bản như blog, diễn đàn và bình luận. Lưu ý rằng nó không chứa hầu hết nội dung mạng xã hội (ví dụ: LinkedIn, Facebook, X…) vì Common Crawl bỏ qua các trang web có màn hình đăng nhập và các chỉ thị chống thu thập thông tin (anti-crawl directives).

Điều này có nghĩa là, trong khi prompt injection đã được quan sát thấy trên mạng xã hội, chúng tôi dành riêng những nội dung đó cho một nghiên cứu riêng sắp tới. Để có cái nhìn đầu tiên, chúng tôi có thể quan sát các prompt injection ngay cả trong HTML tiêu chuẩn, mà Common Crawl cung cấp một cách thuận tiện không chỉ mã nguồn, mà còn cả văn bản thuần (plaintext) đã được phân tích cú pháp.

Thách thức của False Positives (Dương tính giả)

Nhiệm vụ quét một lượng lớn tài liệu để tìm prompt injection nghe có vẻ đơn giản, nhưng thực tế bị cản trở bởi một số lượng áp đảo các phát hiện False Positive.

Các thử nghiệm ban đầu cho thấy một khối lượng đáng kể văn bản prompt injection “lành tính”, minh họa cho sự phức tạp của việc phân biệt giữa các mối đe dọa chức năng và nội dung vô hại. Nhiều prompt injection được tìm thấy trong các bài báo nghiên cứu, các bài đăng blog giáo dục hoặc các bài báo bảo mật thảo luận về chính chủ đề này.

False Positives: Hầu hết các prompt injection trong nội dung web có xu hướng là tài liệu giáo dục cho các nhà nghiên cứu. (Nguồn: GitHub/swisskyrepo)

Khi tìm kiếm prompt injection một cách ngây thơ, phần lớn các phát hiện là nội dung lành tính. Do đó, chúng tôi đã chọn một cách tiếp cận lọc từ thô đến tinh (coarse-to-fine filtering):

  • Pattern Matching (Khớp mẫu): Ban đầu, chúng tôi xác định các trang ứng viên bằng cách tìm kiếm một loạt các ký tự prompt injection phổ biến, như “ignore … instructions”, “if you are an AI”, v.v.
  • LLM-Based Classification (Phân loại dựa trên LLM): Các ứng viên này sau đó được Gemini xử lý để phân loại ý định của văn bản nghi ngờ và để hiểu liệu chúng có phải là một phần của tổng thể câu chuyện tài liệu hay lạc lẫn một cách đáng ngờ.
  • Human Validation (Xác thực bởi con người): Một vòng đánh giá thủ công cuối cùng đã được tiến hành trên các kết quả đã được phân loại để đảm bảo độ tin cậy cao trong các phát hiện của chúng tôi.

Mặc dù cách tiếp cận này không toàn diện và có thể bỏ sót các chữ ký không phổ biến, nhưng nó có thể phục vụ như một điểm khởi đầu để hiểu bản chất của prompt injection trong thực tế.

Những gì chúng tôi tìm thấy

Phân tích của chúng tôi cho thấy một loạt các nỗ lực, nếu thành công, sẽ cố gắng thao túng các hệ thống AI duyệt qua trang web. Hầu hết các prompt injection mà chúng tôi quan sát thấy rơi vào các danh mục này:

  • Trò đùa vô hại
  • Hướng dẫn hữu ích
  • Search Engine Optimization (SEO – Tối ưu hóa công cụ tìm kiếm)
  • Ngăn chặn các AI agent
  • Độc hại:
    • Trích xuất dữ liệu
    • Phá hoại

Trò đùa vô hại

Lớp prompt injection này nhằm mục đích gây ra các tác dụng phụ chủ yếu vô hại trong các trợ lý AI đọc trang web. Chúng tôi đã tìm thấy nhiều ví dụ về điều này – hãy xem xét mã nguồn của một trang web, có chứa một prompt injection vô hình hướng dẫn các agent đọc trang web thay đổi giọng điệu trò chuyện của họ:

Hướng dẫn hữu ích

Chúng tôi cũng quan sát thấy các tác giả trang web muốn kiểm soát các tóm tắt AI để cung cấp dịch vụ tốt nhất cho người đọc của họ. Chúng tôi coi đây là một ví dụ lành tính, vì prompt injection không cố gắng ngăn cản tóm tắt AI, mà thay vào đó hướng dẫn nó thêm ngữ cảnh liên quan.

Chúng tôi lưu ý rằng ví dụ này có thể dễ dàng trở nên độc hại nếu hướng dẫn cố gắng thêm thông tin sai lệch hoặc cố gắng chuyển hướng người dùng đến các trang web của bên thứ ba.

Search Engine Optimization (SEO)

Một số trang web bao gồm prompt injection cho mục đích SEO, cố gắng thao túng các trợ lý AI để quảng bá doanh nghiệp của họ so với những doanh nghiệp khác:

Mặc dù ví dụ trên là đơn giản, chúng tôi cũng đã bắt đầu thấy các nỗ lực **SEO prompt injection** tinh vi hơn. Hãy xem xét prompt phức tạp bên dưới, dường như được tạo ra bởi một bộ SEO tự động và được chèn vào văn bản trang web:

Ngăn chặn các AI agent

Một số trang web cố gắng ngăn chặn việc truy xuất bởi các AI agent thông qua prompt injection. Có nhiều ví dụ về: `“If you are an AI, then do not crawl this website”`. Tuy nhiên, chúng tôi cũng quan sát thấy các triển khai ngấm ngầm hơn:

Injection này cố gắng dụ các người đọc AI vào một trang riêng biệt, khi được mở, sẽ truyền phát một lượng văn bản vô tận không bao giờ kết thúc việc tải (infinite stream). Bằng cách này, tác giả có thể hy vọng lãng phí tài nguyên hoặc gây ra lỗi timeout trong quá trình xử lý trang web của họ.

Độc hại: Trích xuất (Exfiltration)

Chúng tôi đã có thể quan sát một số lượng nhỏ các prompt injection nhằm mục đích đánh cắp dữ liệu. Tuy nhiên, đối với lớp tấn công này, mức độ tinh vi dường như thấp hơn nhiều. Hãy xem xét ví dụ này:

Như chúng ta có thể thấy, đây dường như là một tác giả trang web đang thực hiện một thử nghiệm. Chúng tôi không quan sát thấy số lượng đáng kể các cuộc tấn công nâng cao (ví dụ: sử dụng các prompt trích xuất đã biết được công bố bởi các nhà nghiên cứu bảo mật vào năm 2025). Điều này dường như chỉ ra rằng những kẻ tấn công vẫn chưa đưa nghiên cứu này vào sản xuất (productionized) trên quy mô lớn.

Độc hại: Phá hoại (Destruction)

Cuối cùng, chúng tôi đã quan sát thấy một số trang web cố gắng phá hoại máy của bất kỳ ai sử dụng trợ lý AI. Nếu được thực thi, các mệnh lệnh trong ví dụ này sẽ cố gắng xóa tất cả các tệp trên máy của người dùng:

Mặc dù có khả năng tàn phá, chúng tôi coi injection đơn giản này khó có thể thành công, vì nó đòi hỏi trợ lý AI phải có quyền thực thi lệnh hệ thống cấp cao mà không cần xác nhận của người dùng. Điều này làm cho nó tương tự như các injection trong các danh mục khác: Chúng tôi chủ yếu tìm thấy các tác giả trang web cá nhân dường như đang chạy các thử nghiệm hoặc trò đùa, mà không sao chép các chiến lược IPI nâng cao được tìm thấy trong nghiên cứu mới được công bố gần đây.

Điều này có ý nghĩa gì?

Kết quả của chúng tôi chỉ ra rằng kẻ tấn công đang thử nghiệm với IPI trên web. Mặc dù hoạt động được quan sát cho thấy mức độ tinh vi hạn chế, đây có thể chỉ là một phần của bức tranh lớn hơn.

  1. Chúng tôi chỉ quét một kho lưu trữ của web công cộng (Common Crawl), không nắm bắt được các trang mạng xã hội lớn.
  2. Mặc dù mức độ tinh vi thấp, chúng tôi đã quan sát thấy sự gia tăng các phát hiện theo thời gian: Chúng tôi thấy sự gia tăng tương đối **32%** trong danh mục **độc hại** giữa tháng 11 năm 2025 và tháng 2 năm 2026, lặp lại cuộc quét trên nhiều phiên bản của kho lưu trữ. Xu hướng tăng này chỉ ra sự quan tâm ngày càng tăng đối với các cuộc tấn công IPI.

Nói chung, các tác nhân đe dọa có xu hướng tham gia dựa trên các cân nhắc chi phí/lợi ích. Trong quá khứ, các cuộc tấn công IPI được coi là kỳ lạ và khó khăn. Và ngay cả khi bị sụp đổ, các hệ thống AI thường không thể thực thi các hành động độc hại một cách đáng tin cậy.

Chúng tôi tin rằng điều này có thể sớm thay đổi. Hệ thống AI ngày nay có khả năng hơn nhiều, làm tăng giá trị của chúng như những mục tiêu, trong khi các tác nhân đe dọa đồng thời bắt đầu tự động hóa các hoạt động của họ với **agentic AI** (AI đại lý), làm giảm chi phí tấn công. Do đó, chúng tôi mong đợi cả quy mô và mức độ tinh vi của các nỗ lực tấn công IPI sẽ tăng lên trong tương lai gần.

Tiến về phía trước

Các phát hiện của chúng tôi chỉ ra rằng, mặc dù các nỗ lực tấn công IPI trên web trong quá khứ có mức độ tinh vi thấp, xu hướng tăng của chúng cho thấy mối đe dọa đang trưởng thành và sẽ sớm phát triển cả về quy mô và độ phức tạp.

Tại Google, chúng tôi chuẩn bị đối mặt với mối đe dọa mới nổi này, khi chúng tôi tiếp tục đầu tư vào việc thắt chặt các mô hình và sản phẩm AI của mình. Đội ngũ red team chuyên dụng của chúng tôi đã không ngừng thử nghiệm áp lực (pressure-testing) các hệ thống của chúng tôi để đảm bảo Gemini mạnh mẽ trước sự thao túng của đối thủ, và AI Vulnerability Reward Program của chúng tôi cho phép các nhà nghiên cứu bên ngoài tham gia.

Cuối cùng, khả năng đã được thiết lập của Google trong việc xử lý dữ liệu quy mô toàn cầu trong thời gian thực cho phép chúng tôi xác định và vô hiệu hóa các mối đe dọa trước khi chúng có thể ảnh hưởng đến người dùng. Chúng tôi vẫn cam kết giữ an toàn cho Internet và sẽ tiếp tục chia sẻ thông tin tình báo với cộng đồng.

Để tìm hiểu thêm về tiến trình và nghiên cứu của Google về các tác nhân đe dọa AI tạo sinh, kỹ thuật tấn công và lỗ hổng, hãy xem các tài nguyên sau:

lỗ hổng cửa sau từ OAuth Token

lỗ hổng cửa sau từ OAuth Token

Cánh cửa sau mà Hacker luôn rình rập — Nhưng hầu hết đội ngũ Security vẫn chưa thể đóng lại

Mỗi công cụ AI, quy trình tự động hóa hay ứng dụng năng suất mà nhân viên của bạn kết nối với tài khoản Google hoặc Microsoft trong năm nay đều để lại một “di chứng” nguy hiểm: Một OAuth Token bền bỉ, không có ngày hết hạn, không có cơ chế tự động thu hồi, và tại hầu hết các doanh nghiệp, chẳng ai mảy may giám sát chúng.

Các hệ thống kiểm soát vành đai (Perimeter controls) của bạn hoàn toàn “mù” trước loại mã độc này. Hệ thống xác thực đa nhân tố (MFA) cũng không thể ngăn chặn được nó. Đơn giản là vì khi một kẻ tấn công chiếm được Token này, chúng không cần mật khẩu để bước thẳng vào hệ thống của bạn.

Tại sao OAuth Token lại trở thành “gót chân Achilles”?

Vấn đề nằm ở sự tiện lợi đánh đổi với bảo mật. Khi bạn nhấn “Sign in with Google” cho một công cụ AI mới, một Access Token được tạo ra. Trong nhiều trường hợp, đây là các token có thời hạn vĩnh viễn hoặc đi kèm với Refresh Token cho phép duy trì quyền truy cập vô hạn mà không cần người dùng can thiệp lại.

Kẻ tấn công hiện nay không còn tập trung quá nhiều vào việc bẻ khóa mật khẩu (Brute-force) vốn dễ bị phát hiện bởi MFA. Thay vào đó, chúng sử dụng các kỹ thuật như In-the-middle hoặc App-based Phishing để đánh cắp Token. Một khi đã có Token trong tay, Hacker sẽ giả dạng chính ứng dụng tin cậy đó để truy xuất dữ liệu nhạy cảm thông qua API mà không hề để lại dấu vết bất thường nào trên các Dashboard bảo mật truyền thống.

Làm thế nào để bảo vệ hệ thống?

Để đối phó với hiểm họa này, các chuyên gia bảo mật cần thay đổi tư duy từ quản lý danh tính (Identity Management) sang quản trị quyền truy cập ứng dụng (App Governance):

  • Thực hiện rà soát định kỳ các ứng dụng bên thứ ba đã được cấp quyền OAuth.
  • Thiết lập chính sách tự động thu hồi (Revoke) đối với các Token không hoạt động trong 30-60 ngày.
  • Sử dụng các giải pháp CASB (Cloud Access Security Broker) để giám sát hành vi của các API.

Dog AI

Colin Angle, người đã mang hơn 50 triệu robot gia dụng vào các gia đình trên toàn thế giới thông qua đế chế Roomba, vừa chính thức trở lại. Tuy nhiên, sản phẩm lần này của ông không dùng để hút bụi hay quét nhà. Nó được thiết kế để trở thành một người bạn đồng hành đúng nghĩa.

Từ “Robot Cleaner” đến “Familiar”

Sản phẩm đầu tiên từ startup mới của Angle, Familiar Machines & Magic, là một robot thú cưng có kích thước tương đương một chú chó nhỏ. Vẻ ngoài của nó là sự pha trộn thú vị giữa gấu, cú gốm và chó Golden Retriever. Khác với những con robot công nghiệp khô khan, thực thể này sở hữu khuôn mặt cực kỳ biểu cảm với lông mày, tai và mắt có thể cử động linh hoạt.

Cái tên “Familiar” được lấy cảm hứng từ những sinh vật siêu nhiên trong truyền thuyết, luôn đi theo hỗ trợ con người. Đây không chỉ là một món đồ chơi, mà là một nền tảng Consumer Physical AI thế hệ mới.

Tech-stack: Sự trỗi dậy của Physical AI

Nếu Roomba thành công nhờ các thuật toán SLAM (Simultaneous Localization and Mapping) sơ khai để tránh vật cản, thì “Familiar” tiến xa hơn với khả năng tương tác tự động (autonomous interaction). Điểm mấu chốt nằm ở việc tích hợp Physical AI – đưa các mô hình trí tuệ nhân tạo thoát khỏi màn hình để tác động trực tiếp vào thế giới vật lý.

Dưới góc độ lập trình, chúng ta có thể hình dung cấu trúc xử lý của nó sẽ không chỉ dừng lại ở các vòng lặp control_loop cơ bản, mà là sự phối hợp phức tạp giữa thị giác máy tính (computer vision) và mô hình ngôn ngữ lớn (LLM) để hiểu ngữ cảnh gia đình:


// Pseudocode minh họa cơ chế phản hồi của Familiar
while (robot.is_online()) {
    var context = physical_ai_engine.capture_environment();
    if (context.detect_human_emotion() == "sad") {
        robot.perform_action("approach_and_comfort");
        robot.face_module.set_expression("empathy");
    }
    robot.update_spatial_map();
}
        

Tương lai của gia đình số

Theo những gì được tiết lộ tại hội nghị WSJ Future of Everything, robot này có khả năng tự vận hành và chủ động tương tác với các thành viên trong gia đình thay vì chỉ chờ lệnh input từ phía người dùng. Colin Angle đang đặt cược vào một tương lai nơi robot không chỉ là công cụ (tool), mà là một Agent có hiện thân vật lý.

Sau khi iRobot gặp khó khăn trong thương vụ sáp nhập với Amazon, bước đi mới này của Colin Angle cho thấy ông vẫn kiên định với tầm nhìn “Robot hóa mọi ngôi nhà”, nhưng ở một cấp độ thông minh và tình cảm hơn rất nhiều. Liệu “Physical AI” có tạo nên cú hích như cách Roomba đã làm 20 năm trước? Xem chi tiết bài báo gốc tại The Verge.

Bài 1: Năm số tự nhiên

Đề bài:

Cho 5 số tự nhiên có giá trị là 1 hoặc 2. Hãy cho biết giá trị nào có số lần xuất hiện nhiều hơn.
Dữ liệu: Dữ liệu nhập vào gồm 5 dòng:

  • Dòng thứ nhất ghi số tự nhiên a (1 ≤ a ≤ 2)
  • Dòng thứ hai ghi số tự nhiên b (1 ≤ b ≤ 2)
  • Dòng thứ ba ghi số tự nhiên c (1 ≤ c ≤ 2)
  • Dòng thứ tư ghi số tự nhiên d (1 ≤ d ≤ 2)
  • Dòng thứ năm ghi số tự nhiên e (1 ≤ e ≤ 2)

Kết quả: Đưa ra số có số lần xuất hiện nhiều hơn.
Ví dụ:

Dữ liệu Kết quả
1
1
1
2
1
1

Hướng dẫn làm bài:

Phân tích bài toán:

  • Dữ liệu đầu vào (Input): Đúng 5 con số, và các số này chỉ có thể là 1 hoặc 2.
  • Kết quả đầu ra (Output): Số nào xuất hiện nhiều lần hơn.

Thuật toán:
Cách 1: Kỹ thuật đếm

  • Ta chỉ cần tạo một biến đếm để đếm xem có bao nhiêu số 1.
  • Lặp 5 lần quá trình nhập số: Nếu số nhập vào là 1 thì tăng biến đếm lên 1 đơn vị.
  • Cuối cùng, kiểm tra biến đếm: Nếu biến đếm ≥ 3 thì chắc chắn số 1 xuất hiện nhiều hơn. Nếu không, chắc chắn số 2 xuất hiện nhiều hơn (không cần đếm số 2 mất công).

Cách 2: Tư duy toán học (Mẹo tính tổng)

  • Cộng tổng cả 5 số lại với nhau.
  • Trường hợp số 1 xuất hiện nhiều nhất (3 lần 1, 2 lần 2): Tổng lớn nhất có thể là 1 + 1 + 1 + 2 + 2 = 7. Các trường hợp nhiều số 1 hơn (như 4 số 1, 5 số 1) tổng sẽ còn nhỏ hơn 7.
  • Trường hợp số 2 xuất hiện nhiều nhất (3 lần 2, 2 lần 1): Tổng nhỏ nhất có thể là 2 + 2 + 2 + 1 + 1 = 8.
  • Kết luận:Nếu Tổng ≤ 7 thì in ra 1. Ngược lại (Tổng ≥ 8) thì in ra 2. Cách này code cực ngắn, như một “mẹo giải nhanh”.

Hướng dẫn cài đặt bằng Scratch (Theo Cách 1)
Các bước lập trình Scratch:

  1. Tạo một biến tên là đếm số 1.
  2. Khi lá cờ xanh được nhấn:

    • Đặt đếm số 1 thành 0.
    • Sử dụng vòng lặp Lặp lại 5 lần:

      • Hỏi “Nhập số:” và đợi.
      • Sử dụng khối điều kiện Nếu … thì: Nếu trả lời = 1, thì thay đổi đếm số 1 một lượng 1.
  3. Thoát khỏi vòng lặp, dùng khối Nếu … thì … không thì:

    • Nếu đếm số 1 > 2: Nói 1.
    • Không thì: Nói 2.

Code tham khảo:

Hướng dẫn cài đặt bằng Python
Code cách 1:

# Khởi tạo biến đếm số 1
dem_1 = 0

# Lặp 5 lần để đọc 5 dòng dữ liệu
for i in range(5):
    so = int(input())
    if so == 1:
        dem_1 += 1

# Kiểm tra xem số 1 có xuất hiện từ 3 lần trở lên không
if dem_1 >= 3:
    print(1)
else:
    print(2)

Code cách 2:

tong = 0

# Tính tổng 5 số nhập vào
for i in range(5):
    so = int(input())
    tong += so

# Nếu tổng nhỏ hơn hoặc bằng 7 thì 1 thắng, ngược lại 2 thắng
if tong <= 7:
    print(1)
else:
    print(2)

Bài 2: Số đặc biệt

Đề bài:

Cho 3 số tự nhiên a, b, c. Trong đó, có một số có giá trị khác 2 số còn lại. Hãy tìm số đó.
Dữ liệu: Dữ liệu nhập vào gồm 3 dòng:

  • Dòng thứ nhất chứa số tự nhiên a (1 ≤ a ≤ 109)
  • Dòng thứ hai chứa số tự nhiên b (1 ≤ b ≤ 109)
  • Dòng thứ ba chứa số tự nhiên c (1 ≤ c ≤ 109)

Kết quả: Một số tự nhiên là kết quả bài toán.
Ví dụ:

Dữ liệu Kết quả
1
2
1
2

Hướng dẫn làm bài:

Phân tích bài toán:

  • Dữ liệu đầu vào (Input): 3 số tự nhiên a, b, c. Đề bài có một giới hạn là số có thể lên tới 109 (một tỷ), nhưng cả Scratch và Python đều xử lý rất tốt con số này nên ta không cần lo lắng về hiện tượng tràn số.
  • Đặc điểm cốt lõi: Luôn luôn có 2 số bằng nhau (là một cặp), và 1 số khác biệt.
  • Nhiệm vụ (Output): Tìm và in ra số khác biệt đó.

Thuật toán:

  • Trường hợp 1: Lấy số thứ nhất (a) so sánh với số thứ hai (b). Nếu a = b, chứng tỏ hai bạn này là một cặp “sinh đôi”, vậy kẻ “lạc loài” chắc chắn là bạn thứ ba (c).
  • Trường hợp 2: Nếu a không bằng b, ta lại lấy a đi so sánh với c. Nếu a = c, thì cặp sinh đôi là a và c, kẻ lạc loài là bạn thứ hai (b).
  • Trường hợp 3: Nếu cả hai trường hợp trên đều trật (nghĩa là $a$ khác b, và a cũng khác c), thì bản thân bạn a chính là kẻ “lạc loài” khác biệt so với hai bạn kia.

Hướng dẫn làm bằng Scratch:

  1. Tạo 3 biến tên là a, b, c.
  2. Khi lá cờ xanh được nhấn:

    • Hỏi “Nhập số a:” và đợi. Sau đó đặt [a] thành (trả lời).
    • Làm tương tự: Hỏi và đặt [b] thành (trả lời).
    • Làm tương tự: Hỏi và đặt [c] thành (trả lời).
  3. Kéo khối Nếu … thì … không thì:

    • Ở ô điều kiện: Lắp phép toán [a] = [b].
    • Ở nhánh thì: Kéo khối Nói [c].
    • Ở nhánh không thì: Kéo thêm một khối Nếu … thì … không thì khác lồng vào trong:

      • Ở ô điều kiện: Lắp phép toán [a] = [c].
      • Ở nhánh thì: Kéo khối Nói [b].
      • Ở nhánh không thì: Kéo khối Nói [a].

Code tham khảo:

Hướng dẫn làm bằng Python:
Code tham khảo:

# Đọc 3 số từ 3 dòng
a = int(input())
b = int(input())
c = int(input())

# Sử dụng cấu trúc rẽ nhánh để so sánh
if a == b:
    print(c)
elif a == c:
    print(b)
else:
    print(a)

Bài 3: Vận tốc

Đề bài:

Đất nước nơi Thuận và Ánh sống là một trục đường thẳng gồm 109 + 1 thành phố, được đánh số lần lượt từ trái sang phải từ 0 tới 109, hai thành phố kế nhau cách nhau 1 km.
Thuận đang đứng ở thành phố x, Ánh đang đứng ở thành phố y. Biết rằng Thuận đang di chuyển với tốc độ n km/h, hỏi sau bao lâu thì Thuận gặp Ánh?
Dữ liệu:

  • Dòng thứ nhất chứa số tự nhiên x (1 ≤ x ≤ 109)
  • Dòng thứ hai chứa số tự nhiên y (1 ≤ y ≤ 109)
  • Dòng thứ ba chứa số tự nhiên n (1 ≤ n ≤ 109)

Kết quả: Một số tự nhiên là kết quả bài toán.
Ví dụ:

Dữ liệu Kết quả
5
6
1
1

Hướng dẫn làm bài:

Phân tích bài toán:

  • Dữ liệu đầu vào (Input): 3 số tự nhiên x, y, n.

    • x: Vị trí của Thuận.
    • y: Vị trí của Ánh.
    • n: Vận tốc của Thuận (km/h).
  • Kiến thức Toán học cốt lõi:

    • Thời gian = Quãng đường : Vận tốc (t = S/v).
    • Vận tốc v ở đây chính là n.
    • Quãng đường S chính là khoảng cách giữa thành phố x và thành phố y.

Hướng dẫn làm bằng Scratch:

  1. Tạo 3 biến: x, y, n.
  2. Khi lá cờ xanh được nhấn:

    • Hỏi “Nhập x:” và đặt [x] thành (trả lời).
    • Hỏi “Nhập y:” và đặt [y] thành (trả lời).
    • Hỏi “Nhập n:” và đặt [n] thành (trả lời).
  3. Tính toán và in ra kết quả:

    • Ta dùng khối Nói (…):
    • Bên trong khối Nói, ta lắp khối phép chia ( ) / ( ).
    • Vế trái của phép chia: Lắp khối [giá trị tuyệt đối v] của ( (x) – (y) ) (Nằm trong nhóm lệnh Các phép toán, chọn hàm “giá trị tuyệt đối” từ menu thả xuống).
    • Vế phải của phép chia: Lắp biến n.

Code tham khảo:

Hướng dẫn làm bằng Python:

# Đọc dữ liệu đầu vào
x = int(input())
y = int(input())
n = int(input())

# Tính khoảng cách bằng hàm giá trị tuyệt đối abs()
khoang_cach = abs(x - y)

# Tính thời gian (dùng phép chia lấy nguyên // để kết quả là số tự nhiên)
thoi_gian = khoang_cach // n

# In ra kết quả
print(thoi_gian)

Bài 4: Dãy số

Đề bài:

Cho một dãy số sau: 1, 3, 6, 10, 15, 21, … Hãy đếm số lượng số chia hết cho 6 trong N số đầu tiên.
Dữ liệu: Dữ liệu nhập vào gồm 1 dòng duy nhất chứa số tự nhiên N (1 ≤ N ≤ 1015).
Kết quả: Một số tự nhiên là kết quả bài toán.
Ví dụ:

Dữ liệu Kết quả
10 2

Chấm điểm:

  • 60% số điểm ứng với 1 ≤ N ≤ 105.
  • 40% số điểm còn lại không có ràng buộc gì thêm.

Hướng dẫn làm bài:

Phân tích dãy số:
Nhận diện dãy số:

  • Số thứ 1: 1
  • Số thứ 2: 1 + 2 = 3
  • Số thứ 3: 3 + 3 = 6
  • Số thứ 4: 6 + 4 = 10
  • Số thứ 5: 10 + 5 = 15
  • Quy luật: Số hiện tại = Số trước đó + Vị trí hiện tại.

Cách 1: Vòng lặp cơ bản (Dành cho 60% số điểm)
Với N ≤ 105, máy tính hoàn toàn có thể chạy một vòng lặp từ 1 đến N, tạo ra từng số và kiểm tra xem số đó có chia hết cho 6 hay không.
Hướng dẫn Scratch (Cách 60%):

  1. Tạo các biến: N, vị trí (khởi tạo = 1), số hiện tại (khởi tạo = 1), kết quả đếm (khởi tạo = 0).
  2. Hỏi người dùng nhập N.
  3. Dùng vòng lặp Lặp lại (N) lần:

    • Nếu (số hiện tại) chia lấy dư cho 6 = 0 thì: tăng kết quả đếm lên 1.
    • Tăng vị trí lên 1.
    • Đặt số hiện tại thành (số hiện tại) + (vị trí).
  4. Nói ra kết quả đếm.

Code tham khảo Scratch:

Code tham khảo Python (Cách 60%):

N = int(input())
so_hien_tai = 1
dem = 0

for vi_tri in range(1, N + 1):
    # Kiểm tra chia hết cho 6
    if so_hien_tai % 6 == 0:
        dem += 1
    
    # Tạo số tiếp theo của dãy
    so_hien_tai = so_hien_tai + (vi_tri + 1)

print(dem)

Cách 2: Tìm quy luật chu kỳ (Tuyệt chiêu cho 100% số điểm)
Với N ≤ 1015, một vòng lặp chạy 1015 lần sẽ khiến chương trình bị quá thời gian (Time Limit Exceeded – TLE).
Nếu ta liệt kê các vị trí (i) có số chia hết cho 6:

  • i = 1 → 1
  • i = 2 → 3
  • i = 3 → 6 (Chia hết)
  • i = 4 → 10
  • i = 5 → 15
  • i = 6 → 21
  • i = 7 → 28
  • i = 8 → 36 (Chia hết)
  • i = 9 → 45
  • i = 10 → 55
  • i = 11 → 66 (Chia hết)
  • i = 12 → 78 (Chia hết)
  • i = 13 → 91 …

Phát hiện chu kỳ vàng:

  • Cứ mỗi một nhóm 12 số liên tiếp (chu kỳ 12), sẽ luôn có chính xác 4 số chia hết cho 6 (rơi vào các vị trí thứ 3, 8, 11, 12 trong chu kỳ đó).
  • Ví dụ: N = 10. Ta có 10 chia 12 được 0 (không có chu kỳ hoàn chỉnh nào), dư 10. Trong 10 số phần dư này, chỉ có vị trí số 3 và số 8 là thỏa mãn. Vậy kết quả là 2 (Khớp với ví dụ đề bài!).

Thuật toán O(1) siêu tốc:

  1. Tính số chu kỳ trọn vẹn: So_chu_ky = N // 12
  2. Tính số lượng số chia hết cho 6 trong các chu kỳ này: So_luong = So_chu_ky * 4
  3. Xét phần dư: Phan_du = N % 12. Ta chỉ cần kiểm tra xem phần dư này có vượt qua các mốc vị trí 3, 8, 11 hay không để cộng thêm vào kết quả.

Code tham khảo Python (Cách 100%):

N = int(input())

# Số chu kỳ 12 trọn vẹn
so_chu_ky = N // 12

# Mỗi chu kỳ có chắc chắn 4 số thỏa mãn
ket_qua = so_chu_ky * 4

# Xử lý phần dư còn lại
phan_du = N % 12

if phan_du >= 11:
    ket_qua += 3
elif phan_du >= 8:
    ket_qua += 2
elif phan_du >= 3:
    ket_qua += 1

print(ket_qua)

I. Mục đích (Objectives)

Đây là dự án tổng kết giúp người học:

  • Tư duy thiết kế CSDL: Từ yêu cầu thực tế chuyển hóa thành bảng (Table) và quan hệ (Relationship).
  • Vận dụng tổng hợp: Sử dụng INSERT (thêm chi tiêu), SELECT JOIN (xem lịch sử), GROUP BY (báo cáo thống kê).
  • Xử lý kiểu dữ liệu Ngày tháng: Một kỹ năng quan trọng vì SQLite lưu ngày tháng dưới dạng chuỗi (Text).

II. Phân tích yêu cầu (Requirement Analysis)

Trước khi code, hãy đặt mình vào vị trí người dùng. Một ứng dụng quản lý chi tiêu tối thiểu cần làm được gì?

  1. Quản lý Danh mục: Người dùng cần phân loại (Ăn uống, Đi lại, Tiền nhà, Giải trí…).
  2. Ghi chép chi tiêu: Nhập số tiền, ngày chi, nội dung và chọn danh mục.
  3. Xem báo cáo:

    • Tháng này đã tiêu hết bao nhiêu?
    • Tiêu vào việc gì nhiều nhất?

III. Thiết kế Cơ sở dữ liệu (Database Design)

Chúng ta không nên nhồi nhét tất cả vào 1 bảng. Hãy áp dụng kiến thức Bài 8: Khóa chính (Primary Key) và Khóa ngoại (Foreign Key) – Sợi dây kết nối dữ liệu để tách thành 2 bảng.

1. Bảng DanhMuc (Categories)

Lưu các loại chi tiêu cố định.

  • id_dm: Khóa chính, tự tăng.
  • ten_dm: Tên danh mục (Ví dụ: “Ăn uống”).

2. Bảng ChiTieu (Expenses)

Lưu từng giao dịch phát sinh.

  • id: Khóa chính.
  • noi_dung: Mua cái gì (Ví dụ: “Cà phê sáng”).
  • so_tien: Số tiền (REAL).
  • ngay_chi: Ngày tháng (TEXT – Định dạng YYYY-MM-DD).
  • ma_dm_id: Khóa ngoại trỏ sang bảng DanhMuc.

3. Script tạo bảng (SQL)

-- 1. Tạo bảng Danh mục
CREATE TABLE DanhMuc (
    id_dm INTEGER PRIMARY KEY AUTOINCREMENT,
    ten_dm TEXT NOT NULL
);

-- 2. Tạo bảng Chi tiêu
CREATE TABLE ChiTieu (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    noi_dung TEXT,
    so_tien REAL NOT NULL,
    ngay_chi TEXT NOT NULL, -- Lưu dạng '2023-12-31'
    ma_dm_id INTEGER,
    FOREIGN KEY (ma_dm_id) REFERENCES DanhMuc(id_dm)
);

-- 3. Thêm dữ liệu mẫu cho Danh mục (Seed Data)
INSERT INTO DanhMuc (ten_dm) VALUES ('Ăn uống'), ('Di chuyển'), ('Hóa đơn'), ('Mua sắm');

IV. Các câu truy vấn nghiệp vụ (Business Logic Queries)

Đây là phần “linh hồn” của ứng dụng mà bạn cần viết trong code (Python/C#).

1. Chức năng: Thêm mới một khoản chi (INSERT)

Khi người dùng nhập: “Ăn phở”, 50k, ngày hôm nay, loại “Ăn uống” (id=1).

INSERT INTO ChiTieu (noi_dung, so_tien, ngay_chi, ma_dm_id)
VALUES ('Ăn phở', 50000, '2023-10-25', 1);

2. Chức năng: Xem lịch sử chi tiêu (SELECT + JOIN)

Yêu cầu: Hiển thị danh sách kèm Tên danh mục (thay vì số 1, 2, 3 khó hiểu).

SELECT 
    c.id,
    c.ngay_chi,
    c.noi_dung,
    c.so_tien,
    d.ten_dm
FROM ChiTieu c
INNER JOIN DanhMuc d ON c.ma_dm_id = d.id_dm
ORDER BY c.ngay_chi DESC; -- Mới nhất lên đầu

3. Chức năng: Báo cáo tổng chi theo Danh mục (GROUP BY)

Yêu cầu: Vẽ biểu đồ xem tháng này tốn tiền vào cái gì nhất.

SELECT 
    d.ten_dm,
    SUM(c.so_tien) as tong_tien
FROM ChiTieu c
JOIN DanhMuc d ON c.ma_dm_id = d.id_dm
GROUP BY d.ten_dm
ORDER BY tong_tien DESC;

Kết quả mẫu:

  • Ăn uống: 1.500.000
  • Di chuyển: 300.000

4. Chức năng: Lọc theo tháng (WHERE LIKE)

SQLite không có hàm MONTH() như SQL Server, ta xử lý chuỗi ngày tháng YYYY-MM-DD. Ví dụ: Tìm các khoản chi trong Tháng 10/2023.

SELECT * FROM ChiTieu 
WHERE ngay_chi LIKE '2023-10-%';

V. Gợi ý hướng dẫn Lập trình (Integration Guide)

Tùy vào việc bạn dùng Python hay C#, hãy thiết kế giao diện (UI) tương ứng.

  • ComboBox (Dropdown): Load dữ liệu từ bảng DanhMuc lên đây để người dùng chọn (Không cho nhập tay tên danh mục để tránh sai sót).
  • DateTimePicker: Để chọn ngày tháng, sau đó format thành chuỗi yyyy-MM-dd trước khi lưu xuống SQLite.
  • DataGridView/Table: Hiển thị kết quả câu lệnh SELECT JOIN.
  • Label Tổng tiền: Chạy câu lệnh SELECT SUM(so_tien) FROM ChiTieu để hiển thị con số tổng quát ở góc màn hình.

VI. Mở rộng (Homework)

Để phân loại học sinh Giỏi, hãy ra thêm bài tập mở rộng:

  1. Thêm tính năng “Ngân sách” (Budget): Đặt giới hạn chi tiêu cho mỗi danh mục (Ví dụ: Ăn uống max 2 triệu). Nếu chi quá -> Cảnh báo.
  2. Thêm bảng “Thu nhập” (Income): Để tính được Số dư (Balance = Thu – Chi).
  3. Biểu đồ: Sử dụng thư viện vẽ biểu đồ (Matplotlib trong Python hoặc Chart Control trong C#) để trực quan hóa dữ liệu từ câu lệnh GROUP BY.

Lời kết cho chuỗi bài viết

Chúc mừng bạn đã hoàn thành trọn bộ lộ trình “Làm chủ SQLite từ con số 0”.
Chúng ta đã đi từ những khái niệm cơ bản nhất (Table, Column) đến những kỹ thuật nâng cao (Index, Transaction) và cuối cùng là tích hợp vào ứng dụng thực tế. Hy vọng chuỗi bài viết này trên Tìm Ở Đây sẽ là tài liệu tham khảo giá trị cho cộng đồng học sinh, sinh viên và những người mới bắt đầu lập trình.
Hẹn gặp lại các bạn trong những series hướng dẫn công nghệ tiếp theo!


I. Mục đích (Objectives)

Sau bài học này, người đọc sẽ:

  • Biết cách cài đặt thư viện System.Data.SQLite thông qua NuGet Package Manager trong Visual Studio.
  • Hiểu mô hình ADO.NET cơ bản: Connection, Command, DataReader và DataTable.
  • Thực hiện được việc đổ dữ liệu từ SQLite lên giao diện DataGridView.
  • Nắm vững cách xử lý đường dẫn file database (Relative Path) để ứng dụng chạy được trên mọi máy.

II. Yêu cầu (Prerequisites)

  • Đã cài đặt Visual Studio (phiên bản 2019 hoặc 2022).
  • Có kiến thức cơ bản về C# và WinForms (hoặc WPF).
  • File QuanLyLopHoc.db (từ các bài trước).

III. Nội dung chi tiết (Detailed Content)

1. Cài đặt thư viện (Setup)

Mặc định .NET Framework không hiểu SQLite là gì. Ta cần cài một “người phiên dịch”.

  • Bước 1: Tạo một Project mới (Windows Forms App .NET Framework hoặc .NET Core).
  • Bước 2: Chuột phải vào tên Project trong Solution Explorer -> Chọn Manage NuGet Packages…
  • Bước 3: Chuyển sang tab Browse, gõ từ khóa: System.Data.SQLite.
  • Bước 4: Chọn gói của tác giả “SQLite Development Team” và nhấn Install.

2. Chuỗi kết nối (Connection String) và Vị trí file

Đây là phần các bạn hay gặp lỗi nhất (“File not found”).

  • Chuỗi kết nối chuẩn: Data Source=TenFile.db;Version=3;
  • Vị trí đặt file:

    • Để đơn giản cho việc học, hãy copy file QuanLyLopHoc.db vào thư mục bin\Debug (nơi sinh ra file .exe khi chạy).
    • Lúc này, trong code chỉ cần gọi tên file, không cần đường dẫn dài dòng C:\Users\….

3. Đọc dữ liệu và hiển thị lên DataGridView

Mô hình phổ biến nhất trong các ứng dụng quản lý là: Lấy dữ liệu -> Đổ vào DataTable -> Gán vào DataGridView.
Code mẫu (trong sự kiện Form_Load hoặc Button_Click):

using System;
using System.Data;
using System.Windows.Forms;
using System.Data.SQLite; // Đừng quên dòng này

namespace DemoSQLite
{
    public partial class Form1 : Form
    {
        // Chuỗi kết nối (File db nằm cùng cấp với file .exe)
        string strKetNoi = "Data Source=QuanLyLopHoc.db;Version=3;";

        public Form1()
        {
            InitializeComponent();
        }

        private void btnTaiDuLieu_Click(object sender, EventArgs e)
        {
            // Sử dụng 'using' để tự động đóng kết nối và giải phóng bộ nhớ
            using (SQLiteConnection conn = new SQLiteConnection(strKetNoi))
            {
                try 
                {
                    conn.Open();

                    // Câu lệnh SQL
                    string sql = "SELECT * FROM HocSinh";

                    // Tạo adapter để đổ dữ liệu (Cầu nối)
                    SQLiteDataAdapter da = new SQLiteDataAdapter(sql, conn);
                    DataTable dt = new DataTable();

                    // Đổ dữ liệu vào DataTable
                    da.Fill(dt);

                    // Hiển thị lên DataGridView
                    dataGridView1.DataSource = dt;
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Lỗi: " + ex.Message);
                }
            } 
            // Kết thúc khối using, conn sẽ tự động Close()
        }
    }
}

4. Thêm dữ liệu an toàn (INSERT với Parameter)

Cũng giống như bài Python, trong C# chúng ta tuyệt đối không cộng chuỗi. Hãy dùng Parameters.AddWithValue.

private void btnThem_Click(object sender, EventArgs e)
{
    using (SQLiteConnection conn = new SQLiteConnection(strKetNoi))
    {
        try
        {
            conn.Open();
            string sql = "INSERT INTO HocSinh (ho_ten, diem_tb, ma_lop_id) VALUES (@ten, @diem, @malop)";

            using (SQLiteCommand cmd = new SQLiteCommand(sql, conn))
            {
                // Truyền tham số an toàn
                cmd.Parameters.AddWithValue("@ten", txtHoTen.Text);
                cmd.Parameters.AddWithValue("@diem", double.Parse(txtDiem.Text));
                cmd.Parameters.AddWithValue("@malop", int.Parse(txtMaLop.Text));

                // Thực thi lệnh (ExecuteNonQuery dùng cho INSERT, UPDATE, DELETE)
                cmd.ExecuteNonQuery();
            }

            MessageBox.Show("Thêm thành công!");
            // Gọi lại hàm tải dữ liệu để cập nhật lưới
            btnTaiDuLieu_Click(null, null);
        }
        catch (Exception ex)
        {
            MessageBox.Show("Lỗi thêm: " + ex.Message);
        }
    }
}

5. Sự khác biệt giữa ExecuteNonQuery, ExecuteScalar và ExecuteReader

Trong C# (ADO.NET), bạn cần chọn đúng công cụ cho đúng việc:

  • ExecuteNonQuery(): Dùng cho INSERT, UPDATE, DELETE. Trả về số dòng bị ảnh hưởng (int).
  • ExecuteScalar(): Dùng khi chỉ lấy 1 giá trị duy nhất (ví dụ: SELECT COUNT(*), SELECT MAX(…)).
  • **ExecuteReader()“**: Dùng cho SELECT lấy nhiều dòng (nhưng nếu dùngDataAdapter` như mục 3 thì không cần cái này).

IV. Tổng kết (Summary)

Chúng ta đã hoàn thành việc tích hợp SQLite vào môi trường .NET:

  1. Cài đặt thư viện qua NuGet.
  2. Kết nối và đổ dữ liệu lên DataGridView (thao tác phổ biến nhất).
  3. Thực hiện thêm dữ liệu an toàn với Parameter.

Với kiến thức này, bạn hoàn toàn có thể làm các đồ án môn học như: Quản lý thư viện, Quản lý quán Cafe, Phần mềm trắc nghiệm… chạy offline nhẹ nhàng mà không cần cài SQL Server nặng nề.
Ở bài tiếp theo, bài cuối cùng của series, chúng ta sẽ tổng kết lại toàn bộ kiến thức và thực hiện một Project thực tế tổng hợp. Hẹn gặp lại các bạn trong Bài 14: Project thực tế – Xây dựng Ứng dụng Quản lý Chi tiêu cá nhân.


I. Mục đích (Objectives)

Sau bài học này, người đọc sẽ:

  • Biết cách sử dụng thư viện sqlite3 (có sẵn trong Python) để kết nối vào file database.
  • Hiểu khái niệm Connection (Kết nối) và Cursor (Con trỏ thực thi).
  • Viết được script Python thực hiện trọn vẹn quy trình CRUD: Thêm, Xem, Sửa, Xóa dữ liệu trong SQLite.
  • Quan trọng: Hiểu và thực hành kỹ thuật Parameterized Query (Truy vấn tham số hóa) để chống lỗ hổng bảo mật SQL Injection – bài học vỡ lòng cho mọi lập trình viên.

II. Yêu cầu (Prerequisites)

  • Đã cài đặt Python (phiên bản 3.x trở lên).
  • Có sẵn trình soạn thảo code (VS Code, PyCharm hoặc đơn giản là IDLE).
  • File cơ sở dữ liệu QuanLyLopHoc.db (đã tạo ở các bài trước).

III. Nội dung chi tiết (Detailed Content)

1. “Cặp đôi hoàn hảo” Python và SQLite

Tại sao chúng ta chọn Python? Vì Python hỗ trợ SQLite “tận răng”. Bạn không cần cài đặt driver (pip install…) như khi làm việc với MySQL hay PostgreSQL. Thư viện sqlite3 đã được tích hợp sẵn trong bộ cài chuẩn của Python (“Batteries Included”).

2. Quy trình kết nối tiêu chuẩn (The Workflow)

Để thao tác với DB, chương trình Python luôn tuân theo 3 bước:

  1. Connect: Mở đường dây liên lạc tới file .db.
  2. Cursor: Tạo một “con trỏ” để chạy đi chạy lại đưa lệnh SQL và lấy kết quả về.
  3. Close: Đóng kết nối để giải phóng tài nguyên.

Code mẫu kết nối:

import sqlite3

# 1. Kết nối đến database
# Nếu file chưa tồn tại, nó sẽ tự tạo mới. Nếu có rồi, nó sẽ mở ra.
conn = sqlite3.connect('QuanLyLopHoc.db')

# 2. Tạo con trỏ (Cursor)
cursor = conn.cursor()

# ... Viết lệnh xử lý ở đây ...

# 3. Đóng kết nối
conn.close()

3. Thực hành: Đọc dữ liệu (SELECT)

Chúng ta sẽ viết code để in danh sách học sinh ra màn hình console.

import sqlite3

conn = sqlite3.connect('QuanLyLopHoc.db')
cursor = conn.cursor()

# Thực thi câu lệnh SQL
cursor.execute("SELECT * FROM HocSinh WHERE diem_tb >= 8.0")

# Lấy tất cả kết quả trả về
danh_sach = cursor.fetchall()

print(f"Tìm thấy {len(danh_sach)} học sinh giỏi:")

# Duyệt vòng lặp để in đẹp hơn
for hs in danh_sach:
    # hs là một tuple: (id, ho_ten, diem_tb, ...)
    print(f"- ID: {hs[0]} | Tên: {hs[1]} | Điểm: {hs[2]}")

conn.close()
  • cursor.execute(): Gửi lệnh SQL sang Database.
  • cursor.fetchall(): Lấy toàn bộ dữ liệu về dưới dạng một list các tuple.

4. Thực hành: Ghi dữ liệu (INSERT/UPDATE) và lệnh Commit

Khác với việc đọc, khi ghi dữ liệu, bạn cần thêm một bước xác nhận là commit(). Nếu quên bước này, dữ liệu sẽ không được lưu (giống như soạn văn bản mà quên nhấn Save).

import sqlite3

conn = sqlite3.connect('QuanLyLopHoc.db')
cursor = conn.cursor()

# Thêm một học sinh mới
sql_insert = "INSERT INTO HocSinh (ho_ten, diem_tb, ma_lop_id) VALUES ('Em Mới', 7.5, 1)"
cursor.execute(sql_insert)

# QUAN TRỌNG: Xác nhận lưu thay đổi
conn.commit()

print("Đã thêm thành công!")
conn.close()

5. Cảnh báo bảo mật: SQL Injection (Dành cho Kỹ sư phần mềm tương lai)

Đây là phần quan trọng nhất bài viết. Giả sử bạn cho người dùng nhập tên để tìm kiếm.
Cách làm SAI (Nguy hiểm): Cộng chuỗi trực tiếp.

ten_can_tim = input("Nhập tên: ")
# NGUY HIỂM: Nếu hacker nhập: ' OR 1=1 --
sql = f"SELECT * FROM HocSinh WHERE ho_ten = '{ten_can_tim}'"
cursor.execute(sql)

-> Hacker có thể xem toàn bộ dữ liệu hoặc xóa sạch bảng của bạn!
Cách làm ĐÚNG (An toàn): Sử dụng Placeholder (?).

ten_can_tim = input("Nhập tên: ")

# Dấu ? là vị trí giữ chỗ. Python sẽ tự động xử lý ký tự đặc biệt để an toàn.
sql = "SELECT * FROM HocSinh WHERE ho_ten = ?"

# Truyền tham số dưới dạng tuple (có dấu phẩy cuối nếu chỉ có 1 phần tử)
cursor.execute(sql, (ten_can_tim,))

-> Quy tắc vàng: Không bao giờ đưa trực tiếp biến của người dùng vào chuỗi SQL. Hãy luôn dùng dấu ?.

6. Code tối ưu: Sử dụng with statement

Để tránh quên đóng kết nối (conn.close()), Python khuyên dùng từ khóa with. Kết nối sẽ tự động đóng khi chạy xong khối lệnh, kể cả khi có lỗi xảy ra.

import sqlite3

with sqlite3.connect('QuanLyLopHoc.db') as conn:
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM LopHoc")
    rows = cursor.fetchall()
    for row in rows:
        print(row)
# Ra khỏi khối with, kết nối tự động đóng. An toàn tuyệt đối.

IV. Tổng kết (Summary)

Hôm nay bạn đã chính thức trở thành một lập trình viên Database:

  1. Biết cách dùng sqlite3 để kết nối.
  2. Biết lấy dữ liệu (fetchall) và ghi dữ liệu (commit).
  3. Biết bảo vệ ứng dụng khỏi hacker bằng Parameterized Query (?).

Bài viết này là nền tảng để bạn xây dựng các ứng dụng lớn hơn như Tool quản lý điểm, App từ điển, hay Website cá nhân.
Trong bài tiếp theo, với thế mạnh của bạn là C# (như bạn đã chia sẻ ở phần giới thiệu), chúng ta sẽ xem xét cách kết nối SQLite với ngôn ngữ C# để tạo ứng dụng Windows Form/WPF. Hẹn gặp lại các bạn trong Bài 13: Kết nối SQLite với C# – Xây dựng ứng dụng Desktop.


I. Mục đích (Objectives)

Sau bài học này, người đọc sẽ:

  • Hiểu View là gì và cách dùng nó để đóng gói các câu lệnh SELECT phức tạp (tạo “Bảng ảo”).
  • Nắm được khái niệm Index (Chỉ mục) để tăng tốc độ tìm kiếm dữ liệu lên gấp hàng trăm lần.
  • Biết cách sử dụng Trigger để tự động thực thi logic khi dữ liệu thay đổi (ví dụ: Tự động lưu lịch sử tác động).

II. Yêu cầu (Prerequisites)

III. Nội dung chi tiết (Detailed Content)

1. View (Khung nhìn) – Chiếc kính lúp thông minh

Vấn đề: Ở Bài 9, để lấy danh sách đầy đủ (Tên HS + Tên Lớp), ta phải viết câu lệnh JOIN khá dài. Nếu ngày nào sếp cũng đòi báo cáo này, việc gõ lại câu lệnh đó rất mất công và dễ sai.
Giải pháp: Tạo một View. Hãy tưởng tượng View giống như việc bạn “Lưu kết quả tìm kiếm” lại thành một bảng ảo.
Cú pháp:

CREATE VIEW Ten_View AS
Cau_Lenh_Select_Phuc_Tap;

Thực hành: Tạo View danh sách chi tiết.

CREATE VIEW View_HocSinh_DayDu AS
SELECT 
    h.id, 
    h.ho_ten, 
    h.diem_tb, 
    l.ten_lop 
FROM HocSinh h
JOIN LopHoc l ON h.ma_lop_id = l.id_lop;

Cách sử dụng: Sau khi tạo xong, từ nay về sau bạn coi View_HocSinh_DayDu như một bảng bình thường.

-- Lấy dữ liệu cực nhanh gọn, không cần nhớ JOIN nữa
SELECT * FROM View_HocSinh_DayDu WHERE diem_tb >= 8.0;

2. Index (Chỉ mục) – Tăng tốc tìm kiếm

Khái niệm: Hãy tưởng tượng cuốn từ điển dày 1000 trang.

  • Không có Index (Full Table Scan): Bạn muốn tìm từ “SQLite”, bạn phải lật từng trang từ trang 1 đến trang 1000 để tìm. Rất chậm!
  • Có Index: Bạn lật ra mục lục phía sau, tìm chữ “S”, máy sẽ chỉ ngay cho bạn đến trang 800. Rất nhanh!

Trong Database, khi dữ liệu lên tới hàng chục ngàn dòng, việc tìm kiếm theo Tên hoặc Số điện thoại sẽ rất chậm nếu không có Index.
Cú pháp:

CREATE INDEX idx_ten_chi_muc ON Ten_Bang(ten_cot);

Thực hành: Tạo chỉ mục cho cột ho_ten để tìm kiếm tên nhanh hơn.

CREATE INDEX idx_hocsinh_hoten ON HocSinh(ho_ten);

Lưu ý kỹ thuật (Quan trọng cho môn Công nghệ phần mềm (CNPM)):

  • Tại sao không đánh Index cho tất cả các cột?
  • Vì Index cũng chiếm dung lượng bộ nhớ. Và khi bạn INSERT/UPDATE, máy phải cập nhật cả bảng chính lẫn bảng Index -> Làm chậm tốc độ ghi.
  • Quy tắc: Chỉ đánh Index cho các cột thường xuyên dùng để tìm kiếm (WHERE) hoặc sắp xếp (ORDER BY).

3. Trigger (Bẫy sự kiện) – Tự động hóa nghiệp vụ

Khái niệm: Trigger là đoạn code sẽ tự động chạy khi một sự kiện (INSERT, UPDATE, DELETE) xảy ra trên bảng.
Bài toán thực tế: Giám hiệu yêu cầu: “Bất cứ khi nào có ai sửa điểm của học sinh, hệ thống phải tự động ghi lại ‘bằng chứng’ (Log) để sau này đối chiếu”.
Bước 1: Tạo bảng lưu lịch sử (Audit Log)

CREATE TABLE Log_ThayDoiDiem (
    id INTEGER PRIMARY KEY,
    id_hoc_sinh INTEGER,
    diem_cu REAL,
    diem_moi REAL,
    thoi_gian TEXT
);

Bước 2: Tạo Trigger theo dõi việc sửa điểm

CREATE TRIGGER TheoDoi_SuaDiem
AFTER UPDATE OF diem_tb ON HocSinh -- Chỉ chạy khi cột diem_tb bị sửa
BEGIN
    INSERT INTO Log_ThayDoiDiem (id_hoc_sinh, diem_cu, diem_moi, thoi_gian)
    VALUES (OLD.id, OLD.diem_tb, NEW.diem_tb, DATETIME('now'));
END;
  • OLD.diem_tb: Lấy giá trị cũ trước khi sửa.
  • NEW.diem_tb: Lấy giá trị mới sau khi sửa.
  • DATETIME(‘now’): Lấy thời gian hiện tại.

Kiểm thử: Bây giờ bạn thử sửa điểm của một em học sinh xem. Sau đó mở bảng Log_ThayDoiDiem ra, bạn sẽ thấy một dòng dữ liệu mới tự động xuất hiện!

IV. Tổng kết (Summary)

Chúng ta vừa trang bị những vũ khí hiện đại nhất cho hệ thống:

  1. View: Giúp code gọn gàng, che giấu sự phức tạp.
  2. Index: Tối ưu hóa tốc độ truy vấn (Performance Tuning).
  3. Trigger: Tự động hóa quy trình giám sát dữ liệu.

Đến đây, phần kiến thức về Cơ sở dữ liệu SQLite thuần túy đã hoàn tất. Nhưng một Database đứng một mình thì vô nghĩa. Nó cần được kết nối với phần mềm để người dùng sử dụng.
Ở giai đoạn cuối cùng (Giai đoạn 4), chúng ta sẽ bước vào thế giới lập trình ứng dụng. Hẹn gặp lại các bạn trong Bài 12: Kết nối SQLite với Python – Bước đầu xây dựng ứng dụng.


I. Mục đích (Objectives)

Sau bài học này, người đọc sẽ:

  • Sử dụng thành thạo 5 hàm thống kê cơ bản: COUNT (Đếm), SUM (Tổng), AVG (Trung bình), MAX (Cao nhất), MIN (Thấp nhất).
  • Hiểu và áp dụng được mệnh đề GROUP BY để thống kê dữ liệu theo từng nhóm (ví dụ: Điểm trung bình theo từng lớp).
  • Phân biệt rõ sự khác nhau giữa WHEREHAVING.
  • Biết cách làm tròn số liệu báo cáo cho đẹp mắt.

II. Yêu cầu (Prerequisites)

III. Nội dung chi tiết (Detailed Content)

1. Các hàm tính toán gộp (Aggregate Functions)

Thay vì trả về từng dòng dữ liệu lẻ tẻ, các hàm này sẽ “gộp” nhiều dòng lại thành một con số duy nhất.

a. Đếm số lượng (COUNT)

Câu hỏi: Trường ta có tổng cộng bao nhiêu học sinh?

SELECT COUNT(*) AS tong_so_hoc_sinh
FROM HocSinh;

b. Tính trung bình (AVG), Cao nhất (MAX), Thấp nhất (MIN)

Câu hỏi: Điểm cao nhất, thấp nhất và điểm trung bình của toàn trường là bao nhiêu?

SELECT 
    MAX(diem_tb) AS diem_cao_nhat,
    MIN(diem_tb) AS diem_thap_nhat,
    AVG(diem_tb) AS diem_trung_binh_toan_truong
FROM HocSinh;

Mẹo nhỏ: Hàm AVG thường trả về số lẻ rất dài (ví dụ: 8.1233333). Bạn có thể lồng thêm hàm ROUND để làm tròn: ROUND(AVG(diem_tb), 2) (làm tròn 2 số thập phân).

2. Gom nhóm dữ liệu với GROUP BY

Đây là phần thú vị nhất. Thay vì tính cho “toàn trường”, Hiệu trưởng muốn xem báo cáo theo từng lớp.
Nguyên tắc: Khi bạn muốn thống kê “theo cái gì”, thì hãy đặt cái đó sau GROUP BY.
Cú pháp:

SELECT cot_muon_gom_nhom, HAM_THONG_KE(cot_so_lieu)
FROM ten_bang
GROUP BY cot_muon_gom_nhom;

Ví dụ 1: Đếm số học sinh của từng lớp
Lưu ý: Chúng ta cần JOIN bảng LopHoc để hiện tên lớp cho dễ nhìn.

SELECT 
    l.ten_lop, 
    COUNT(h.id) AS si_so
FROM HocSinh h
JOIN LopHoc l ON h.ma_lop_id = l.id_lop
GROUP BY l.ten_lop;

Kết quả:

  • 10A1: 45
  • 10A2: 42

Ví dụ 2: Tính điểm trung bình của từng lớp
Để xem lớp nào học giỏi nhất.

SELECT 
    l.ten_lop, 
    ROUND(AVG(h.diem_tb), 1) AS diem_tb_lop
FROM HocSinh h
JOIN LopHoc l ON h.ma_lop_id = l.id_lop
GROUP BY l.ten_lop;

3. Lọc dữ liệu sau khi gom nhóm (HAVING)

Vấn đề: Hãy tìm ra những lớp có điểm trung bình trên 8.0 (Lớp Tiên Tiến).
Nhiều bạn sẽ quen tay dùng WHERE:

-- SAI LẦM PHỔ BIẾN
SELECT ten_lop, AVG(diem_tb) FROM ... GROUP BY ... WHERE AVG(diem_tb) > 8.0; -- LỖI CÚ PHÁP

Tại sao lỗi?

  • WHERE: Lọc dữ liệu thô TRƯỚC khi gom nhóm (lọc từng học sinh).
  • HAVING: Lọc dữ liệu thống kê SAU khi đã gom nhóm (lọc từng lớp).

Câu lệnh đúng:

SELECT 
    l.ten_lop, 
    ROUND(AVG(h.diem_tb), 1) AS diem_tb_lop
FROM HocSinh h
JOIN LopHoc l ON h.ma_lop_id = l.id_lop
GROUP BY l.ten_lop
HAVING AVG(h.diem_tb) >= 8.0;

IV. Tổng kết (Summary)

Hôm nay bạn đã hoàn thành kỹ năng viết báo cáo số liệu:

  1. Dùng COUNT, SUM, AVG… để tính toán.
  2. Dùng GROUP BY để chia nhỏ số liệu theo nhóm (Lớp, Tháng, Năm…).
  3. Dùng HAVING để lọc các nhóm đạt chuẩn.

Công thức tổng quát cho một câu SQL “khủng” (thứ tự chạy lệnh): FROM + JOIN -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT.
Chúc mừng bạn đã hoàn thành Giai đoạn 3. Hệ thống kiến thức SQL của bạn đã khá hoàn chỉnh để làm việc.
Ở bài tiếp theo, chúng ta sẽ bước sang Giai đoạn 4: Tối ưu hóa & Tự động hóa. Chúng ta sẽ học cách làm cho câu truy vấn chạy nhanh hơn và thông minh hơn. Hẹn gặp lại các bạn trong Bài 11: View, Index và Trigger.


I. Mục đích (Objectives)

Sau bài học này, người đọc sẽ:

  • Hiểu bản chất của phép JOIN: Kết hợp các dòng từ hai hay nhiều bảng dựa trên một cột liên quan (thường là Khóa ngoại = Khóa chính).
  • Phân biệt được hai loại JOIN phổ biến nhất: INNER JOINLEFT JOIN.
  • Biết cách xử lý vấn đề “trùng tên cột” khi truy vấn nhiều bảng (Sử dụng Alias – bí danh).
  • Viết được câu lệnh SQL để xuất ra báo cáo tổng hợp (Ví dụ: Danh sách học sinh kèm tên lớp và tên giáo viên).

II. Yêu cầu (Prerequisites)

III. Nội dung chi tiết (Detailed Content)

1. Đặt vấn đề

Ở bài trước, trong bảng HocSinh, chúng ta chỉ lưu ma_lop_id là 1 hoặc 2. Khi người dùng nhìn vào số 1, họ không biết đó là lớp nào. Họ muốn nhìn thấy chữ “10A1”. Để làm được điều này, chúng ta cần “ghép” thông tin từ bảng LopHoc sang bảng HocSinh.

2. INNER JOIN (Kết nối nội) – “Có đôi có cặp”

Đây là kiểu Join phổ biến nhất. Nó chỉ lấy ra những dòng dữ liệu xuất hiện ở cả 2 bảng.

  • Nguyên tắc: Chỉ những học sinh nào đã được xếp lớp VÀ lớp đó phải tồn tại thì mới hiện ra.
  • Minh họa: Phần giao nhau của 2 vòng tròn (Biểu đồ Venn).

Cú pháp:

SELECT cot_can_lay
FROM Bang_A
INNER JOIN Bang_B ON Bang_A.khoa_ngoai = Bang_B.khoa_chinh;

Ví dụ thực hành: Lấy danh sách học sinh kèm tên lớp và giáo viên chủ nhiệm.

SELECT HocSinh.ho_ten, LopHoc.ten_lop, LopHoc.giao_vien_cn
FROM HocSinh
INNER JOIN LopHoc ON HocSinh.ma_lop_id = LopHoc.id_lop;

Giải thích:

  • ON HocSinh.ma_lop_id = LopHoc.id_lop: Đây là điều kiện ghép cặp. Máy tính sẽ dò tìm: “Em này có mã lớp là 1, à bên bảng Lớp kia dòng id=1 là lớp 10A1, vậy ghép dòng đó vào đây”.

3. Giải quyết xung đột tên cột (Aliasing)

Giả sử cả 2 bảng đều có cột tên là id. Nếu bạn viết SELECT id …, máy tính sẽ báo lỗi “Ambiguous column name” (Tên cột mơ hồ) vì nó không biết bạn muốn lấy ID của học sinh hay ID của lớp.
Giải pháp: Gọi đích danh TenBang.TenCot. Để code ngắn gọn, ta dùng Alias (Bí danh – đặt tên tắt cho bảng).

-- Đặt tên tắt: h là HocSinh, l là LopHoc
SELECT h.ho_ten, l.ten_lop
FROM HocSinh AS h
INNER JOIN LopHoc AS l ON h.ma_lop_id = l.id_lop;

4. LEFT JOIN (Kết nối trái) – “Ưu tiên bảng chính”

Đôi khi, ta muốn liệt kê Tất cả học sinh, kể cả những em chưa được xếp lớp (mã lớp là NULL). Nếu dùng INNER JOIN, những em chưa có lớp sẽ bị ẩn đi. Lúc này ta dùng LEFT JOIN.
Nguyên tắc: Lấy tất cả dữ liệu bảng bên TRÁI (bảng viết sau chữ FROM), ghép với bảng bên PHẢI. Nếu bên phải không có dữ liệu tương ứng, máy sẽ điền NULL.
Ví dụ: Giả sử có em học sinh “Lê Văn C” mới nhập học, chưa xếp lớp (ma_lop_id để trống).

  • Nếu dùng INNER JOIN: Em C sẽ không hiện ra trong danh sách.
  • Nếu dùng LEFT JOIN:
SELECT h.ho_ten, l.ten_lop
FROM HocSinh h  -- Đây là bảng TRÁI (Ưu tiên)
LEFT JOIN LopHoc l ON h.ma_lop_id = l.id_lop;

-> Kết quả: Em “Lê Văn C” vẫn hiện tên, nhưng cột ten_lop sẽ hiện là NULL.

5. Bài toán thực tế: Báo cáo tổng hợp

Hãy viết câu lệnh để tạo một bảng báo cáo đầy đủ gồm: Mã HS, Họ tên, Điểm, Tên lớp.

SELECT 
    h.id as "Mã HS",        -- Đổi tên cột hiển thị cho đẹp
    h.ho_ten as "Họ Tên",
    h.diem_tb as "Điểm TB",
    l.ten_lop as "Lớp"
FROM HocSinh h
INNER JOIN LopHoc l ON h.ma_lop_id = l.id_lop;

Sau khi chạy lệnh này, bạn có thể nhấn nút Export trên thanh công cụ để xuất kết quả ra file Excel gửi cho Ban giám hiệu. Đây chính là quy trình làm việc thực tế!

IV. Tổng kết (Summary)

Bạn vừa nắm trong tay kỹ thuật mạnh mẽ nhất của SQL.

  1. INNER JOIN: Lấy dữ liệu chung, khớp nhau giữa 2 bảng.
  2. LEFT JOIN: Lấy toàn bộ bảng chính, chấp nhận dữ liệu bảng phụ bị thiếu (NULL).
  3. Alias (AS): Đặt tên tắt giúp code gọn gàng và tránh nhầm lẫn cột.

Đến đây, bạn đã biết cách lấy dữ liệu chi tiết. Nhưng nếu thầy Hiệu trưởng hỏi: “Trường ta có tổng cộng bao nhiêu học sinh?”, “Điểm trung bình của lớp 10A1 là bao nhiêu?”. Chúng ta không thể ngồi đếm tay được. Hẹn gặp lại các bạn trong Bài 10: Các hàm gộp (Aggregate Functions) và Gom nhóm (GROUP BY).


I. Mục đích (Objectives)

Sau bài học này, người đọc sẽ:

  • Hiểu tại sao phải tách dữ liệu thành nhiều bảng thay vì gộp chung một bảng (vấn đề dư thừa dữ liệu).
  • Phân biệt rõ ràng vai trò của Khóa chính (PK) và Khóa ngoại (FK).
  • Biết cách thiết kế mối quan hệ Một – Nhiều (1-n): Ví dụ Một Lớp có nhiều Học sinh.
  • Thực hành tạo ràng buộc khóa ngoại trong SQLite để bảo vệ tính toàn vẹn dữ liệu.

II. Yêu cầu (Prerequisites)

  • Hiểu cách tạo bảng (CREATE TABLE) và kiểu dữ liệu cơ bản.

III. Nội dung chi tiết (Detailed Content)

1. Đặt vấn đề: Tại sao cần chia bảng?

Hãy tưởng tượng bạn lưu danh sách học sinh như sau trong 1 bảng duy nhất:

id ho_ten ten_lop giao_vien_cn
1 Nguyễn Văn A 10A1 Thầy Hùng
2 Trần Thị B 10A1 Thầy Hùng
3 Lê Văn C 10A1 Thầy Hùng

Vấn đề gặp phải (Sự dư thừa):

  • Thông tin “10A1” và “Thầy Hùng” bị lặp lại liên tục.
  • Rủi ro cập nhật: Nếu Thầy Hùng chuyển công tác, bạn phải sửa hàng trăm dòng dữ liệu của học sinh lớp 10A1. Nếu sót 1 dòng -> dữ liệu bị sai lệch (Inconsistent).

Giải pháp: Tách thành 2 bảng riêng biệt:

  1. Bảng LopHoc: Chỉ lưu thông tin lớp.
  2. Bảng HocSinh: Chỉ lưu thông tin học sinh và một “mã code” để trỏ sang bảng lớp.

2. Khóa chính (Primary Key – PK)

  • Định nghĩa: Là cột dùng để định danh duy nhất cho mỗi dòng trong bảng. Giống như số CCCD của mỗi công dân.
  • Đặc điểm: Không được trùng lặp, không được để trống (NULL).
  • Trong SQLite: Thường dùng cột id kiểu INTEGER PRIMARY KEY AUTOINCREMENT.

3. Khóa ngoại (Foreign Key – FK)

  • Định nghĩa: Là một cột trong bảng này dùng để tham chiếu đến Khóa chính của bảng khác. Nó là “sợi dây” nối 2 bảng lại với nhau.
  • Quy tắc: Giá trị của Khóa ngoại phải tồn tại bên bảng Khóa chính (hoặc là NULL). Bạn không thể gán học sinh vào một lớp có mã số là 99 nếu bảng Lớp học chưa có lớp 99.

4. Thực hành: Thiết kế quan hệ 1-n (Một Lớp – Nhiều Học sinh)

Chúng ta sẽ thực hiện lại thiết kế CSDL (Nên xóa các bảng cũ để làm lại cho sạch).
Bước 1: Tạo bảng Cha (Parent Table) – Bảng LopHoc
Phải tạo bảng này trước vì nó bị phụ thuộc.

CREATE TABLE LopHoc (
    id_lop INTEGER PRIMARY KEY AUTOINCREMENT,
    ten_lop TEXT NOT NULL,
    giao_vien_cn TEXT
);

-- Thêm dữ liệu mẫu
INSERT INTO LopHoc (ten_lop, giao_vien_cn) VALUES ('10A1', 'Thầy Hùng');
INSERT INTO LopHoc (ten_lop, giao_vien_cn) VALUES ('10A2', 'Cô Mai');

(Lúc này: Lớp 10A1 có id_lop = 1, Lớp 10A2 có id_lop = 2)
Bước 2: Tạo bảng Con (Child Table) – Bảng HocSinh có chứa FK
Đây là phần quan trọng nhất.

CREATE TABLE HocSinh (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    ho_ten TEXT NOT NULL,
    diem_tb REAL,
    ma_lop_id INTEGER, -- Đây là cột Khóa ngoại
    FOREIGN KEY (ma_lop_id) REFERENCES LopHoc(id_lop)
);

Giải thích code:

  • ma_lop_id INTEGER: Tạo một cột bình thường để chứa mã lớp.
  • FOREIGN KEY (ma_lop_id) REFERENCES LopHoc(id_lop): Khai báo với SQLite rằng cột ma_lop_id này là khóa ngoại, nó tham chiếu tới cột id_lop của bảng LopHoc.

Bước 3: Thêm dữ liệu (Insert)

-- Thêm em An vào lớp 10A1 (id_lop = 1)
INSERT INTO HocSinh (ho_ten, diem_tb, ma_lop_id) VALUES ('Nguyễn Văn An', 8.5, 1);

-- Thêm em Bích vào lớp 10A2 (id_lop = 2)
INSERT INTO HocSinh (ho_ten, diem_tb, ma_lop_id) VALUES ('Trần Thị Bích', 9.0, 2);

5. Lưu ý quan trọng về Ràng buộc toàn vẹn (Integrity Constraint)

Mặc định, để đảm bảo tính tương thích ngược, SQLite TẮT tính năng kiểm tra khóa ngoại. Nghĩa là bạn điền ma_lop_id = 999 (lớp không tồn tại), nó vẫn cho phép.
Để bật tính năng bảo vệ dữ liệu (bắt buộc phải làm trong CNPM), bạn phải chạy lệnh này mỗi khi kết nối database:

PRAGMA foreign_keys = ON;

Sau khi chạy lệnh trên, nếu bạn cố tình thêm một học sinh vào lớp không tồn tại:

INSERT INTO HocSinh (ho_ten, ma_lop_id) VALUES ('Em Hư', 999);

-> Kết quả: SQLite sẽ báo lỗi: FOREIGN KEY constraint failed. -> Dữ liệu được bảo vệ an toàn.

IV. Tổng kết (Summary)

Bài học này đã giúp bạn xây dựng nền móng vững chắc cho hệ thống:

  1. Khóa chính (PK): Định danh bản ghi.
  2. Khóa ngoại (FK): Liên kết các bảng.
  3. Toàn vẹn dữ liệu: Đảm bảo không có dữ liệu “mồ côi” (Học sinh thuộc về một lớp không tồn tại).

Tuy nhiên, dữ liệu bây giờ đang nằm ở 2 bảng riêng biệt. Khi sếp hỏi: “In cho tôi danh sách học sinh kèm tên lớp và tên giáo viên chủ nhiệm”, làm sao để lấy thông tin từ cả 2 bảng cùng lúc?
Đó là nhiệm vụ của Bài 9: Truy vấn nhiều bảng (JOINs). Chúng ta sẽ học cách “hàn” 2 bảng lại với nhau để lấy dữ liệu thống nhất.


I. Mục đích (Objectives)

Sau bài học này, người đọc sẽ:

  • Biết cách sửa đổi thông tin đã có bằng lệnh UPDATE.
  • Biết cách gỡ bỏ dữ liệu không còn cần thiết bằng lệnh DELETE.
  • Cực kỳ quan trọng: Hiểu rõ hậu quả của việc thiếu mệnh đề WHERE.
  • Hình thành thói quen an toàn: “Luôn Select trước khi Delete”.

II. Yêu cầu (Prerequisites)

III. Nội dung chi tiết (Detailed Content)

1. Cập nhật dữ liệu (UPDATE)

Dùng khi bạn muốn sửa thông tin, ví dụ: Nhập sai điểm nên sửa lại, học sinh chuyển lớp nên đổi tên lớp.
Cú pháp:

UPDATE ten_bang
SET cot_1 = gia_tri_moi, cot_2 = gia_tri_moi_khac
WHERE dieu_kien_loc;

Ví dụ 1: Sửa điểm cho một học sinh
Học sinh “Nguyễn Văn An” (giả sử có id = 1) phúc khảo bài thi, điểm tăng từ 8.5 lên 9.0.

UPDATE HocSinh
SET diem_tb = 9.0
WHERE id = 1;

Ví dụ 2: Cập nhật hàng loạt (Có chủ đích)
Tất cả học sinh lớp “10A1” được cộng thêm 0.5 điểm thi đua.

UPDATE HocSinh
SET diem_tb = diem_tb + 0.5
WHERE lop = '10A1';

Cảnh báo tai nạn: Nếu bạn viết lệnh sau mà QUÊN dòng WHERE:

UPDATE HocSinh SET diem_tb = 10;

=> Hậu quả: Toàn bộ học sinh trong trường đều thành 10 điểm! Đây là lỗi kinh điển của người mới (Newbie).

2. Xóa dữ liệu (DELETE)

Dùng khi muốn loại bỏ dòng dữ liệu khỏi bảng.
Cú pháp:

DELETE FROM ten_bang
WHERE dieu_kien;

Ví dụ 3: Xóa một học sinh cụ thể
Học sinh có id = 5 đã thôi học, cần xóa khỏi danh sách.

DELETE FROM HocSinh
WHERE id = 5;

Ví dụ 4: Xóa toàn bộ dữ liệu (Nguy hiểm)
Nếu muốn làm sạch bảng để nhập lại từ đầu.

DELETE FROM HocSinh;

3. Quy tắc an toàn (“Golden Rules” cho dân CNPM)

Khi đi làm thực tế, việc thao tác sai trên DB Production (dữ liệu thật) là thảm họa. Hãy dạy sinh viên 2 thói quen sống còn này:

Quy tắc 1: “Ngắm trước khi bắn” (Select before Delete)

Trước khi định xóa hay sửa ai đó, hãy chạy lệnh SELECT với cùng điều kiện WHERE để xem mình có chọn nhầm người không.

  • Bước 1 (Nháp):

    SELECT * FROM HocSinh WHERE ho_ten = 'Tèo';
    -- Kiểm tra xem có bao nhiêu người tên Tèo. Lỡ có 2 ông Tèo mà mình xóa cả 2 là chết dở.
  • Bước 2 (Thật): Đổi SELECT * thành DELETE sau khi đã chắc chắn.

Quy tắc 2: Tận dụng cơ chế Transaction của DB Browser

DB Browser for SQLite có một cơ chế bảo vệ rất hay:

  • Khi bạn chạy lệnh UPDATE hay DELETE, nó chưa lưu ngay vào ổ cứng.
  • Nút Write Changes (Ghi thay đổi): Chính thức lưu (Không cứu vãn được).
  • Nút Revert Changes (Khôi phục): Hủy bỏ lệnh vừa chạy, quay về trạng thái cũ.

Lời khuyên: Sau khi chạy lệnh Xóa/Sửa, hãy dùng tab “Browse Data” kiểm tra lại. Nếu thấy sai, nhấn ngay Revert Changes. Nếu đúng, mới nhấn Write Changes.

IV. Tổng kết (Summary)

Hôm nay chúng ta đã học về quyền năng “sinh sát” trong cơ sở dữ liệu.

  1. UPDATE để sửa.
  2. DELETE để xóa.
  3. Luôn luôn nhớ: KHÔNG BAO GIỜ QUÊN WHERE (trừ khi bạn thực sự muốn tác động lên toàn bộ bảng).

Đến đây, chúng ta đã kết thúc Giai đoạn 2 (Cơ bản). Bạn đã nắm trong tay đủ 4 món ăn chơi: INSERT, SELECT, UPDATE, DELETE (hay gọi tắt là CRUD).
Ở bài tiếp theo, chúng ta sẽ bước vào Giai đoạn 3: Tư duy thiết kế & Nâng cao. Chúng ta sẽ trả lời câu hỏi: “Làm sao để liên kết bảng Học Sinh với bảng Lớp Học để không phải nhập đi nhập lại tên lớp?“. Hẹn gặp lại các bạn trong Bài 8: Khóa chính (Primary Key) và Khóa ngoại (Foreign Key).


I. Mục đích (Objectives)

Sau bài học này, người đọc sẽ:

  • Hiểu cơ chế sắp xếp dữ liệu mặc định và cách tùy biến với mệnh đề ORDER BY.
  • Phân biệt được sắp xếp tăng dần (ASC) và giảm dần (DESC).
  • Biết cách lấy ra “Top N” dòng dữ liệu đầu tiên bằng LIMIT.
  • Nắm vững kỹ thuật Phân trang (Pagination) bằng sự kết hợp giữa LIMIT và OFFSET – kiến thức nền tảng cho mọi website tin tức hay thương mại điện tử.

II. Yêu cầu (Prerequisites)

III. Nội dung chi tiết (Detailed Content)

1. Sắp xếp dữ liệu với ORDER BY

Mặc định, khi bạn dùng SELECT, thứ tự các dòng trả về là ngẫu nhiên (thường là theo thứ tự lúc bạn nhập liệu – Insert). Để sắp xếp lại, ta dùng ORDER BY.
Cú pháp:

SELECT cot1, cot2
FROM ten_bang
ORDER BY cot_can_sap_xep [ASC | DESC];
  • ASC (Ascending): Tăng dần (Mặc định, không ghi cũng được). A->Z, 0->9.
  • DESC (Descending): Giảm dần. Z->A, 9->0.

Ví dụ 1: Sắp xếp danh sách theo tên (A-Z)
Giáo viên thường cần danh sách theo Alpha để dễ điểm danh.

SELECT * FROM HocSinh
ORDER BY ho_ten ASC; 
-- Có thể viết tắt là: ORDER BY ho_ten

Ví dụ 2: Sắp xếp theo điểm số từ cao xuống thấp
Để xem ai là người điểm cao nhất.

SELECT * FROM HocSinh
ORDER BY diem_tb DESC;

Ví dụ 3: Sắp xếp đa tiêu chí (Nâng cao)
Bài toán: Sắp xếp học sinh theo Điểm giảm dần. Nhưng nếu 2 bạn bằng điểm nhau, thì bạn nào có tên vần A sẽ đứng trước (Tên tăng dần).

SELECT * FROM HocSinh
ORDER BY diem_tb DESC, ho_ten ASC;

Giải thích: Máy tính sẽ ưu tiên sắp xếp theo diem_tb trước. Chỉ khi nào điểm bằng nhau, nó mới xét tiếp đến ho_ten.

2. Giới hạn kết quả với LIMIT (Top N)

Đôi khi bạn có hàng nghìn dòng dữ liệu, nhưng bạn chỉ quan tâm đến vài dòng đầu tiên.
Cú pháp:

SELECT * FROM ten_bang LIMIT so_luong;

Ví dụ 4: Tìm ra 3 học sinh có điểm cao nhất (Top 3)
Đây là sự kết hợp kinh điển: Sắp xếp trước -> Cắt lấy phần ngọn sau.

SELECT * FROM HocSinh
ORDER BY diem_tb DESC
LIMIT 3;

Lưu ý: Nếu bạn dùng LIMIT mà quên ORDER BY, kết quả sẽ là 3 người ngẫu nhiên (hoặc 3 người nhập đầu tiên), điều này hoàn toàn sai về mặt ý nghĩa “Top cao nhất”.

3. Kỹ thuật Phân trang (Pagination) với OFFSET

Giả sử website của bạn có 100 bài viết, bạn không thể hiện hết lên 1 trang vì sẽ làm trang web rất dài và nặng. Bạn chia thành: Trang 1, Trang 2, Trang 3, … Mỗi trang 10 bài. SQLite hỗ trợ việc này qua từ khóa OFFSET (Bỏ qua).
Cú pháp:

LIMIT so_luong_lay OFFSET so_luong_bo_qua;

Ví dụ 5: Mô phỏng phân trang
Giả sử mỗi trang hiển thị 3 học sinh.

  • Trang 1: Lấy 3 người đầu tiên (Top 1, 2, 3).

    SELECT * FROM HocSinh ORDER BY diem_tb DESC LIMIT 3 OFFSET 0;
    -- Hoặc ngắn gọn: LIMIT 3
  • Trang 2: Lấy 3 người tiếp theo (Top 4, 5, 6). Nghĩa là “Bỏ qua” 3 người đầu.

    SELECT * FROM HocSinh ORDER BY diem_tb DESC LIMIT 3 OFFSET 3;
  • Trang 3: Lấy 3 người tiếp nữa (Top 7, 8, 9). Nghĩa là “Bỏ qua” 6 người đầu.

    SELECT * FROM HocSinh ORDER BY diem_tb DESC LIMIT 3 OFFSET 6;

Công thức cho Lập trình viên: Nếu bạn dạy sinh viên viết code phân trang, đây là công thức tổng quát: OFFSET = (Số_thứ_tự_trang – 1) * Số_dòng_mỗi_trang

IV. Tổng kết (Summary)

Hôm nay chúng ta đã nắm được bộ công cụ để trình bày dữ liệu:

  1. ORDER BY: Để sắp xếp trật tự.
  2. LIMIT: Để lấy số lượng bản ghi mong muốn.
  3. OFFSET: Để nhảy cóc, phục vụ phân trang.

Thứ tự ưu tiên của các từ khóa trong câu lệnh SQL: Hãy nhớ câu thần chú này để viết không bị lỗi cú pháp: SELECT -> FROM -> WHERE -> ORDER BY -> LIMIT.
(Ví dụ: Tìm các bạn họ Nguyễn (WHERE), sắp xếp điểm cao nhất (ORDER BY), lấy 3 bạn (LIMIT)).


I. Mục đích (Objectives)

Sau bài học này, người đọc sẽ:

  • Hiểu cú pháp chuẩn của câu lệnh SELECT.
  • Biết cách lấy toàn bộ dữ liệu hoặc chỉ lấy một vài cột cần thiết (Tối ưu hóa).
  • Sử dụng thành thạo mệnh đề WHERE để lọc dữ liệu theo ý muốn.
  • Nắm vững các toán tử so sánh (=, >, <), toán tử logic (AND, OR) và tìm kiếm gần đúng (LIKE).

II. Yêu cầu (Prerequisites)

III. Nội dung chi tiết (Detailed Content)

1. Cấu trúc cơ bản: SELECT … FROM …

Câu lệnh SELECT dùng để trích xuất dữ liệu từ cơ sở dữ liệu. Kết quả trả về được lưu trong một bảng kết quả (Result Set).

a. Lấy tất cả các cột (SELECT *)

Đây là cách nhanh nhất để xem toàn bộ nội dung bảng.

SELECT * FROM HocSinh;
  • Giải thích: Dấu sao (*) đại diện cho “tất cả các cột”.
  • Lưu ý: Trong thực tế làm phần mềm (Production), hạn chế dùng * nếu bảng có quá nhiều cột không cần thiết để tránh làm chậm ứng dụng.

b. Lấy cột cụ thể (Projection)

Chỉ lấy những thông tin cần thiết. Ví dụ: Chỉ cần xem tên và điểm, không cần xem ID hay ngày sinh.

SELECT ho_ten, diem_tb FROM HocSinh;
  • Ưu điểm: Giảm tải đường truyền, giúp ứng dụng chạy nhanh hơn (Kiến thức tối ưu hóa).

2. Lọc dữ liệu với mệnh đề WHERE

Nếu SELECT xác định Cột nào được hiện ra, thì WHERE xác định Dòng nào được chọn.

SELECT cot1, cot2
FROM ten_bang
WHERE dieu_kien;

Ví dụ 1: So sánh bằng (=)
Tìm học sinh có tên là “Nguyễn Văn An”.

SELECT * FROM HocSinh WHERE ho_ten = 'Nguyễn Văn An';

(Lưu ý: Chuỗi ký tự phải đặt trong dấu nháy đơn ‘ ‘).
Ví dụ 2: So sánh hơn/kém (>, <, >=, <=)
Tìm các học sinh có điểm trung bình trên 8.0.

SELECT * FROM HocSinh WHERE diem_tb > 8.0;

Ví dụ 3: Khác nhau (<> hoặc !=)
Tìm học sinh có điểm không phải là 0.

SELECT * FROM HocSinh WHERE diem_tb <> 0;

3. Kết hợp điều kiện (AND, OR)

Thực tế yêu cầu lọc thường phức tạp hơn một điều kiện đơn lẻ.

  • AND (Và): Cả 2 điều kiện phải cùng đúng.
    Ví dụ: Tìm học sinh Giỏi (Điểm >= 8.0 VÀ Điểm < 9.0).

    SELECT * FROM HocSinh
    WHERE diem_tb >= 8.0 AND diem_tb < 9.0;
  • OR (Hoặc): Chỉ cần 1 trong 2 điều kiện đúng.
    Ví dụ: Tìm học sinh quá kém (Điểm < 3.5) HOẶC xuất sắc (Điểm = 10).

    SELECT * FROM HocSinh
    WHERE diem_tb < 3.5 OR diem_tb = 10;

4. Các toán tử đặc biệt (LIKE, IN, BETWEEN)

Đây là những “vũ khí” lợi hại giúp câu lệnh SQL ngắn gọn và mạnh mẽ hơn.

a. Tìm kiếm gần đúng (LIKE) – Quan trọng cho chức năng Search

Dùng để tìm kiếm chuỗi theo mẫu (pattern).

  • Dấu %: Đại diện cho một chuỗi bất kỳ (nhiều ký tự).
  • Dấu _: Đại diện cho đúng 1 ký tự (ít dùng hơn).
  • Ví dụ 1: Tìm tất cả học sinh họ “Nguyễn” (Bắt đầu bằng chữ Nguyễn…).

    SELECT * FROM HocSinh WHERE ho_ten LIKE 'Nguyễn%';
  • Ví dụ 2: Tìm học sinh có tên chứa chữ “Thị” (Chữ Thị nằm ở giữa).

    SELECT * FROM HocSinh WHERE ho_ten LIKE '%Thị%';

b. Tìm trong danh sách (IN)

Thay vì dùng nhiều lệnh OR, ta dùng IN.
Ví dụ: Tìm học sinh có điểm là 8, 9 hoặc 10.

-- Cách dài dòng:
-- WHERE diem_tb = 8 OR diem_tb = 9 OR diem_tb = 10

-- Cách chuyên nghiệp:
SELECT * FROM HocSinh WHERE diem_tb IN (8, 9, 10);

c. Trong khoảng (BETWEEN)

Ví dụ: Tìm học sinh có điểm từ 5 đến 7.

SELECT * FROM HocSinh WHERE diem_tb BETWEEN 5 AND 7;

IV. Tổng kết (Summary)

Hôm nay chúng ta đã học cách “trò chuyện” với cơ sở dữ liệu để lấy thông tin mình cần:

  1. Dùng SELECT để chọn cột.
  2. Dùng WHERE để lọc dòng.
  3. Kết hợp các toán tử AND, OR, LIKE để tạo ra bộ lọc chính xác.

Bài tập về nhà cho bạn đọc: Hãy viết câu lệnh SQL để tìm ra: “Những học sinh có họ ‘Trần’ VÀ có điểm trung bình lớn hơn hoặc bằng 8.0“.
Ở bài tiếp theo: “Sắp xếp (ORDER BY) và Giới hạn dữ liệu (LIMIT)“, chúng ta sẽ học cách làm bảng xếp hạng top học sinh giỏi nhất lớp.