0

Bóc trần "thế giới ngầm" của Nested Query: Khi các luồng truy vấn đan xen

Chào anh em Viblo, lại là mình đây!

Tiếp nối series dọn dẹp những "góc khuất" của Database, hôm nay chúng ta sẽ cùng mổ xẻ một kỹ thuật mà chắc chắn anh em backend nào cũng từng đụng độ, thậm chí là từng "ôm hận" vì nó: Nested Query (Truy vấn lồng nhau).

Trong quá trình xây dựng các hệ thống backend từ những framework quen thuộc như Laravel cho đến việc tối ưu hóa kiến trúc với Golang, dữ liệu hiếm khi nào nằm gọn gàng trên một mặt phẳng. Chúng có tính phân cấp, cha-con, và chằng chịt các mối quan hệ. Nested Query sinh ra để giải quyết bài toán đó, nhưng cách nó hoạt động ở thế giới SQL truyền thống và thế giới NoSQL (như Elasticsearch) lại có những cú "twist" rất khác nhau.

1. Dạng "cổ điển": Nested Query trong SQL (Subquery)

Trong thế giới cơ sở dữ liệu quan hệ (RDBMS), Nested Query thường được biết đến với cái tên Subquery (Truy vấn con). Hiểu đơn giản, nó là một câu lệnh SELECT nằm chui rúc bên trong một mệnh đề WHERE, HAVING, hoặc FROM của một câu truy vấn lớn hơn.

Bối cảnh thực tế: Giả sử bạn đang cần xuất một báo cáo: "Tìm tất cả các khách hàng có tổng giá trị đơn hàng lớn hơn mức trung bình của toàn bộ hệ thống".

Bạn không thể dùng một câu query phẳng thông thường được, vì bạn phải tìm "mức trung bình" trước. Vậy là Nested Query xuất hiện:

SELECT customer_id, customer_name 
FROM Customers 
WHERE customer_id IN (
    SELECT customer_id 
    FROM Orders 
    GROUP BY customer_id 
    HAVING SUM(total_amount) > (
        SELECT AVG(total_amount) FROM Orders
    )
);

Nhìn thì rất logic, đúng kiểu tư duy con người: đi từ trong ra ngoài. Lớp truy vấn trong cùng tính trung bình, lớp giữa gom nhóm khách hàng, lớp ngoài cùng lấy thông tin chi tiết.

Cạm bẫy "Truy vấn lồng tương quan" (Correlated Subquery): Đây là thứ khiến server DB của bạn khóc thét. Nếu truy vấn con bên trong phụ thuộc vào một giá trị của truy vấn cha bên ngoài, DB sẽ phải thực thi lại cái truy vấn con đó cho mỗi một dòng mà truy vấn cha quét qua. Trông thì gọn, nhưng performance thì tỷ lệ thuận với số lượng bản ghi theo cấp số nhân. Kinh nghiệm của mình là: Nếu có thể, hãy luôn tìm cách chuyển Subquery thành JOIN. Hệ quản trị CSDL tối ưu JOIN tốt hơn rất nhiều.

2. Dạng "hiện đại": Nested Query trong Elasticsearch

Nếu bạn làm việc với Elasticsearch, khái niệm Nested Query mang một ý nghĩa đặc thù và giải quyết một bài toán vô cùng hóc búa mang tên: Cross-object matching (Khớp chéo đối tượng).

Bối cảnh thực tế: Giả sử hệ thống backend của bạn phục vụ cho một nền tảng thương mại điện tử chuyên về mỹ phẩm. Một sản phẩm (Product) sẽ có nhiều biến thể (Variants) như màu sắc và dung tích, được lưu dưới dạng một mảng các object:

{
  "product_name": "Kem chống nắng",
  "variants": [
    { "color": "Blue", "size": "50ml" },
    { "color": "Red", "size": "100ml" }
  ]
}

Mặc định, Elasticsearch sẽ "làm phẳng" (flatten) cái mảng này ra khi đánh index. Nó sẽ hiểu là: sản phẩm này có màu [Blue, Red] và có dung tích [50ml, 100ml].

Hậu quả? Khi khách hàng search: "Tìm kem chống nắng màu Blue và dung tích 100ml", sản phẩm này vẫn lọt vào kết quả dù thực tế nó không hề có biến thể nào đáp ứng cả 2 điều kiện đó cùng lúc (Blue đi với 50ml, còn Red mới đi với 100ml).

Giải pháp với Nested Data Type & Nested Query: Để giữ được tính toàn vẹn của từng object con trong mảng, bạn phải khai báo trường variants là kiểu nested. Và khi truy vấn, bạn không dùng query thông thường mà phải bọc nó trong một nested query:

{
  "query": {
    "nested": {
      "path": "variants",
      "query": {
        "bool": {
          "must": [
            { "match": { "variants.color": "Blue" }},
            { "match": { "variants.size": "100ml" }}
          ]
        }
      }
    }
  }
}

Lúc này, Elasticsearch sẽ kiểm tra nghiêm ngặt từng phần tử trong mảng, đảm bảo màu Blue và size 100ml phải cùng nằm trong một biến thể duy nhất. Khách hàng sẽ không còn phàn nàn vì search ra kết quả "ảo" nữa.

3. Kinh nghiệm "tồn tại" khi dùng Nested Query

Dù ở môi trường SQL hay NoSQL, Nested Query luôn đòi hỏi bạn phải cân nhắc kỹ về hiệu năng:

  • Trong thế giới Backend (PHP/Laravel, Go): Khi dùng các ORM (như Eloquent trong Laravel với hàm whereHas), thực chất framework đang sinh ra các câu query lồng nhau EXISTS (...) ở dưới background. Hãy bật query log lên để xem thực sự framework đang sinh ra mã SQL gì, tránh tình trạng dính N+1 query hoặc full table scan.
  • Với Elasticsearch: Nested documents được lưu dưới dạng các hidden documents tách biệt ở dưới Lucene engine. Có nghĩa là khi bạn update một object con trong mảng nested, Elasticsearch sẽ phải re-index lại toàn bộ document cha và tất cả các document con khác. Do đó, hạn chế tối đa việc thiết kế cấu trúc nested quá sâu hoặc mảng nested chứa hàng ngàn phần tử nếu dữ liệu đó bị update liên tục.

Lời kết

Nested Query là một công cụ mạnh mẽ giúp chúng ta biểu diễn và truy vấn các cấu trúc dữ liệu phức tạp sát với logic nghiệp vụ nhất. Hiểu được cơ chế hoạt động đằng sau những truy vấn lồng nhau này sẽ giúp anh em tự tin hơn khi "đối rập" với những bài toán tối ưu dữ liệu khó nhằn. Chúc anh em áp dụng thành công và không bị những câu query lồng nhau làm cho "lú não"!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí