Chương 2 — Data Models and Query Languages
Phần I — Foundations of Data Systems

Chương 2 — Data Models and Query Languages

17 phút đọc DDIA · Martin Kleppmann

🎯 Mục tiêu chương: So sánh các data model đa dụng — relational, document, graph (property graph, triple-store) — cùng các query language đi kèm (SQL, MapReduce, aggregation pipeline, Cypher, SPARQL, Datalog), để biết model nào hợp với loại quan hệ dữ liệu nào và vì sao declarative query lại thắng.

Bối cảnh: Data model là các lớp abstraction

  • Data model có lẽ là phần quan trọng nhất của phát triển phần mềm: nó quyết định không chỉ cách viết code mà cả cách ta tư duy về bài toán.
  • Ứng dụng được xây bằng cách xếp chồng data model; mỗi lớp trả lời câu hỏi "biểu diễn mình bằng lớp bên dưới như thế nào?":
    • Lớp 1 — Application developer: mô hình hóa thế giới thật (người, tổ chức, hàng hóa, dòng tiền…) thành object/data structure + API.
    • Lớp 2 — Lưu trữ bằng một general-purpose data model: JSON/XML document, bảng relational, graph. (Trọng tâm của chương này.)
    • Lớp 3 — Engineer của database biểu diễn model đó thành bytes trên memory/disk/network. (Chương 3.)
    • Lớp 4 — Hardware biểu diễn bytes bằng dòng điện, xung ánh sáng, từ trường…
  • Mỗi lớp che giấu complexity của lớp dưới, giúp các nhóm người khác nhau (vendor DB vs application dev) làm việc cùng nhau.
  • Mỗi data model mang theo giả định về cách dùng: có thao tác dễ, có thao tác không được hỗ trợ; có thao tác nhanh, có thao tác chậm. Vì vậy chọn model phù hợp là quyết định quan trọng.

Relational Model Versus Document Model

  • Relational model do Edgar Codd đề xuất năm 1970: dữ liệu tổ chức thành relations (table trong SQL), mỗi relation là tập hợp không thứ tự các tuples (row).
  • Ban đầu bị nghi ngờ là không implement hiệu quả được, nhưng đến giữa thập niên 1980, RDBMS + SQL trở thành lựa chọn mặc định, thống trị khoảng 25–30 năm.
  • Gốc rễ: business data processing trên mainframe những năm 60–70 — transaction processing (bán hàng, ngân hàng, đặt vé máy bay, kho) và batch processing (hóa đơn, lương, báo cáo).
  • Mục tiêu của relational model: che implementation detail (cách dữ liệu được lưu bên trong) sau một interface sạch.
  • Các đối thủ lần lượt đến rồi đi: network model và hierarchical model (70s–đầu 80s), object databases (cuối 80s–đầu 90s), XML databases (đầu 2000s, chỉ niche). Relational tổng quát hóa rất tốt ra ngoài phạm vi ban đầu: web publishing, mạng xã hội, e-commerce, game, SaaS…

The Birth of NoSQL

  • "NoSQL" ra đời năm 2009 như một hashtag Twitter cho một meetup về database phân tán, mã nguồn mở, phi quan hệ; về sau được diễn giải lại thành "Not Only SQL". Nó không chỉ một công nghệ cụ thể nào.
  • Động lực áp dụng NoSQL:
    • Cần scalability lớn hơn relational dễ đạt được (dataset rất lớn, write throughput rất cao).
    • Ưu tiên phần mềm free/open source.
    • Các thao tác query chuyên biệt mà relational hỗ trợ kém.
    • Bực bội với schema cứng nhắc, muốn model động và biểu cảm hơn.
  • Kết luận: relational sẽ tiếp tục tồn tại song song với nhiều datastore phi quan hệ — gọi là polyglot persistence.

The Object-Relational Mismatch

  • Code ứng dụng thường là OOP; dữ liệu lưu trong bảng/row/column → cần một lớp dịch "vụng về" giữa hai thế giới. Sự lệch pha này gọi là impedance mismatch (mượn từ điện tử).
  • ORM (ActiveRecord, Hibernate) giảm boilerplate nhưng không che hết khác biệt.
  • Ví dụ LinkedIn profile / résumé (Figure 2-1): user_id, first_name, last_name xuất hiện một lần → cột trong bảng users. Nhưng positions, education, contact info là quan hệ one-to-many. Ba cách biểu diễn:
CáchMô tảHạn chế
Normalized tables (SQL truyền thống, trước SQL:1999)Tách positions, education, contact_info thành bảng riêng, có foreign key về usersLấy một profile cần nhiều query hoặc multi-way join
Structured/XML/JSON datatype trong SQLLưu dữ liệu multi-valued trong một row, có thể query và index bên trong (Oracle, DB2, SQL Server, PostgreSQL; JSON ở DB2, MySQL, PostgreSQL)Mức hỗ trợ khác nhau giữa các DB
Encode JSON/XML vào cột textỨng dụng tự parse cấu trúcDatabase không thể query giá trị bên trong
  • Với résumé — dữ liệu gần như self-contained document — JSON khá hợp (Example 2-1). Các document database hỗ trợ model này: MongoDB, RethinkDB, CouchDB, Espresso.
  • Ưu điểm JSON ở đây:
    • Một số dev thấy nó giảm impedance mismatch (dù JSON có vấn đề riêng khi làm encoding format — Chương 4).
    • Locality tốt hơn: toàn bộ profile nằm một chỗ, một query là đủ; relational phải query nhiều bảng hoặc join phức tạp.
    • Quan hệ one-to-many tạo thành cấu trúc cây, JSON biểu diễn cây này một cách tường minh (Figure 2-2).

Many-to-One and Many-to-Many Relationships

  • Trong Example 2-1, region_id và industry_id là ID chứ không phải chuỗi "Greater Seattle Area" / "Philanthropy". Lợi ích của danh sách chuẩn hóa + ID:
    • Style và chính tả nhất quán.
    • Tránh mơ hồ (nhiều thành phố trùng tên).
    • Dễ cập nhật — tên chỉ lưu một chỗ (ví dụ thành phố đổi tên vì chính trị).
    • Localization — dịch danh sách chuẩn sang ngôn ngữ người xem.
    • Search tốt hơn — biết Seattle thuộc bang Washington.
  • Bản chất là câu hỏi về duplication: ID không mang nghĩa với con người nên không bao giờ cần đổi; thông tin có nghĩa với con người có thể đổi, và nếu bị nhân bản thì phải cập nhật mọi bản sao → tốn chi phí ghi và rủi ro inconsistency. Loại bỏ duplication là ý tưởng cốt lõi của normalization.
    • Rule of thumb: nếu bạn nhân bản giá trị có thể lưu ở một chỗ, schema chưa normalized.
  • Normalize đòi hỏi quan hệ many-to-one (nhiều người sống ở một region) — thứ không hợp tự nhiên với document model. Relational join dễ; document DB thường hỗ trợ join yếu (lúc viết sách: RethinkDB có join, MongoDB không, CouchDB chỉ qua predeclared view).
  • Nếu DB không hỗ trợ join → phải emulate join trong application code bằng nhiều query → công việc dịch chuyển từ DB sang app.
  • Dữ liệu có xu hướng ngày càng liên kết khi thêm feature. Ví dụ mở rộng résumé:
    • Organizations và schools thành entity — có trang riêng, logo, news feed; résumé link tới chúng (Figure 2-3).
    • Recommendations — user A viết giới thiệu cho user B, hiển thị tên + ảnh của A; A đổi ảnh thì mọi recommendation phải cập nhật → cần reference tới profile của A.
  • Các feature này đòi hỏi many-to-many (Figure 2-4): phần trong mỗi khung có thể gom thành document, nhưng tham chiếu tới organization, school, user khác phải là reference và cần join khi query.

Are Document Databases Repeating History?

  • Tranh luận về cách biểu diễn many-to-many cũ hơn NoSQL rất nhiều.
  • IMS của IBM — DB phổ biến nhất cho business data processing thập niên 70, ban đầu làm quản lý kho cho chương trình Apollo, phát hành thương mại 1968, vẫn còn chạy trên mainframe đến nay.
  • IMS dùng hierarchical model — dữ liệu là cây record lồng nhau, rất giống JSON. Giống document DB: tốt cho one-to-many, khó với many-to-many, không hỗ trợ join → dev phải chọn giữa denormalize hoặc tự resolve reference. Vấn đề thập niên 60–70 y hệt vấn đề với document DB ngày nay.
  • Hai giải pháp nổi bật: relational model (thắng, thành SQL) và network model (từng được ưa chuộng rồi chìm vào quên lãng). "Great debate" kéo dài gần hết thập niên 70.

The network model (CODASYL)

  • Chuẩn hóa bởi ủy ban CODASYL (Conference on Data Systems Languages). Là tổng quát hóa của hierarchical: trong cây mỗi record có đúng 1 parent; trong network model, record có thể có nhiều parent → mô hình hóa được many-to-one và many-to-many (ví dụ mọi user ở Seattle link tới record "Greater Seattle Area").
  • Liên kết giữa record không phải foreign key mà giống pointer (lưu trên disk). Cách duy nhất truy cập record là đi theo đường từ root record qua chuỗi link → gọi là access path.
  • Query = di chuyển cursor qua database, lặp qua list record và theo access path. Với nhiều đường dẫn tới cùng record, lập trình viên phải tự nhớ hết trong đầu — chính thành viên CODASYL ví như "điều hướng trong không gian n chiều".
  • Tối ưu tốt cho phần cứng hạn chế của thập niên 70 (tape drive seek rất chậm), nhưng code query/update phức tạp và cứng nhắc. Không có path tới dữ liệu cần? Phải đổi access path và viết lại nhiều code query viết tay → rất khó thay đổi data model.

The relational model

  • Bày mọi dữ liệu ra ngoài: một relation chỉ là tập các tuple, hết. Không có cấu trúc lồng nhau, không có access path phức tạp. Đọc bất kỳ row nào theo điều kiện tùy ý; insert row vào bảng bất kỳ mà không lo foreign key (constraint là tùy chọn; join trên foreign key thực hiện lúc query time, còn CODASYL thực chất "join" lúc insert time).
  • Query optimizer tự quyết định thứ tự thực thi và index nào dùng — đó chính là "access path", nhưng do máy chọn chứ không phải dev.
  • Muốn query kiểu mới → chỉ cần khai báo index mới, query tự dùng index phù hợp mà không cần sửa query → dễ thêm feature.
  • Insight quan trọng: optimizer rất phức tạp và tốn nhiều năm R&D, nhưng chỉ cần xây một lần, mọi ứng dụng đều hưởng lợi. Viết tay access path cho một query dễ hơn viết optimizer tổng quát, nhưng giải pháp tổng quát thắng về lâu dài.

So sánh với document databases

  • Document DB quay lại hierarchical model ở một điểm: lưu nested record (one-to-many) ngay trong parent thay vì bảng riêng.
  • Nhưng với many-to-one và many-to-many, relational và document không khác nhau về bản chất: đều dùng unique identifier — foreign key (relational) hay document reference (document) — và resolve lúc đọc bằng join hoặc follow-up query. Document DB không đi theo con đường CODASYL.

Relational Versus Document Databases Today

  • Chương này chỉ so sánh ở mức data model (fault-tolerance xem Chương 5, concurrency xem Chương 7).
Tiêu chíDocument modelRelational model
Điểm mạnh chínhSchema flexibility, locality, gần với data structure của appJoin tốt, hỗ trợ many-to-one và many-to-many
Hợp với dữ liệuCây one-to-many, thường load cả cây một lầnDữ liệu liên kết, cần tham chiếu chéo
SchemaSchema-on-read (implicit)Schema-on-write (explicit, DB enforce)
Tham chiếu phần tử lồngKhông trực tiếp — phải nói kiểu "item thứ 2 trong positions của user 251"Trực tiếp qua key
JoinYếu → denormalize hoặc join trong appMạnh, tối ưu bởi DB
Nhược điểmMany-to-many làm code app phức tạp và chậm hơnShredding document thành nhiều bảng → schema và code rườm rà

Which data model leads to simpler application code?

  • Dữ liệu dạng document (cây one-to-many, load cả cây) → dùng document model. Shredding (chẻ document thành nhiều bảng như Figure 2-1) gây schema cồng kềnh và code phức tạp không cần thiết.
  • Giới hạn của document: không tham chiếu trực tiếp phần tử lồng (giống access path của hierarchical) — thường không sao nếu document không lồng quá sâu.
  • Join yếu có thể không thành vấn đề — ví dụ ứng dụng analytics ghi lại sự kiện nào xảy ra lúc nào có thể không bao giờ cần many-to-many.
  • Nếu app cần many-to-many: denormalize thì app phải giữ dữ liệu nhất quán; emulate join bằng nhiều request thì phức tạp và thường chậm hơn join trong DB.
  • Kết luận: tùy vào loại quan hệ giữa các item. Dữ liệu liên kết cao: document thì vụng, relational thì chấp nhận được, graph là tự nhiên nhất.

Schema flexibility in the document model

  • Phần lớn document DB (và JSON trong relational DB) không enforce schema. Gọi là "schemaless" là gây hiểu lầm: code đọc dữ liệu vẫn giả định một cấu trúc → có implicit schema, chỉ là DB không enforce.
Schema-on-readSchema-on-write
Cấu trúcNgầm định, được diễn giải khi đọcTường minh, DB đảm bảo mọi dữ liệu ghi vào đều tuân thủ
Tương tự trong ngôn ngữ lập trìnhDynamic (runtime) type checkingStatic (compile-time) type checking
Đổi format dữ liệuGhi document mới với field mới; code app xử lý document cũ khi đọcChạy migration: ALTER TABLE + UPDATE
Phù hợp khiDữ liệu heterogeneous: nhiều loại object, cấu trúc do hệ thống bên ngoài quyết địnhMọi record cùng cấu trúc — schema giúp document hóa và enforce
  • Ví dụ tách name thành first_name + last_name:
    • Document DB: code app kiểm tra — nếu document có name mà thiếu first_name (document ghi trước một ngày nào đó) thì tự split name lúc đọc.
    • Relational: ALTER TABLE users ADD COLUMN first_name rồi UPDATE bằng split_part (PostgreSQL) hoặc substring_index (MySQL).
  • Schema change bị mang tiếng chậm và cần downtime — không hoàn toàn đúng: đa số RDBMS chạy ALTER TABLE trong vài millisecond. Ngoại lệ đáng chú ý: MySQL copy toàn bộ bảng khi ALTER → có thể mất phút đến giờ với bảng lớn (có tool để lách).
  • UPDATE trên bảng lớn thì chậm ở mọi DB (phải ghi lại mọi row). Giải pháp: để first_name mặc định NULL và fill lúc đọc — y như document DB.

Data locality for queries

  • Document thường lưu thành một chuỗi liên tục (JSON, XML, hoặc binary như BSON của MongoDB). Nếu app thường cần toàn bộ document (ví dụ render trang web) → lợi thế storage locality; dữ liệu chia nhiều bảng thì cần nhiều index lookup, nhiều disk seek hơn.
  • Nhưng locality chỉ có lợi khi cần phần lớn document cùng lúc:
    • DB thường phải load cả document dù chỉ cần một phần nhỏ → lãng phí với document lớn.
    • Update thường phải ghi lại cả document; chỉ sửa không đổi kích thước encode mới làm in-place được.
    • → Khuyến nghị: giữ document nhỏ, tránh write làm tăng kích thước document. Những giới hạn này thu hẹp đáng kể phạm vi document DB phát huy tác dụng.
  • Locality không độc quyền của document model:
    • Google Spanner: schema khai báo row của bảng con được interleave (lồng) trong bảng cha.
    • Oracle: multi-table index cluster tables.
    • Bigtable (dùng trong Cassandra, HBase): khái niệm column-family để quản lý locality.

Convergence of document and relational databases

  • Relational (trừ MySQL) hỗ trợ XML từ giữa những năm 2000, có thể sửa cục bộ, index và query bên trong document.
  • JSON: PostgreSQL từ 9.3, MySQL từ 5.7, IBM DB2 từ 10.5.
  • Phía document: RethinkDB hỗ trợ join kiểu relational; một số driver MongoDB tự resolve reference (thực chất là client-side join — chậm hơn do thêm network round-trip và ít tối ưu).
  • Thú vị: mô tả gốc của Codd đã cho phép giá trị trong row là một relation lồng nhau (nonsimple domains) — gần giống JSON trong SQL, hơn 30 năm trước.
  • Hai model đang hội tụ và bổ sung cho nhau — hybrid relational + document là hướng đi tốt cho tương lai.

Query Languages for Data

  • SQL là declarative; IMS và CODASYL query bằng code imperative.
  • Ví dụ lọc cá mập (sharks) từ danh sách động vật:
    • Imperative: vòng for, kiểm tra family === "Sharks", push vào mảng.
    • Relational algebra: phép chọn σ với điều kiện family = "Sharks".
    • SQL: SELECT * FROM animals WHERE family = 'Sharks' — bám sát relational algebra.
Khía cạnhImperativeDeclarative
Nói gìLàm các bước nào, theo thứ tự nàoMuốn dữ liệu có pattern gì (điều kiện, sort, group, aggregate) — không nói làm thế nào
Ai chọn index, join method, thứ tựLập trình viênQuery optimizer
Độ ngắn gọnDàiNgắn, dễ dùng hơn
DB tự cải tiến performanceKhó — không biết code có phụ thuộc chi tiết nào (ví dụ thứ tự record)Dễ — không cần sửa query
Song song hóaRất khó vì chỉ định thứ tự cụ thểDễ hơn vì chỉ mô tả kết quả
  • Ví dụ quan trọng về ordering: nếu DB muốn thu hồi disk space bằng cách di chuyển record (đổi thứ tự), query SQL không đảm bảo thứ tự nên không bị ảnh hưởng; còn code imperative có thể ngầm phụ thuộc thứ tự → DB không dám tối ưu. SQL hạn chế hơn về chức năng nên DB có nhiều không gian tối ưu hơn.
  • Parallelism: CPU nhanh hơn nhờ thêm core chứ không tăng xung nhịp nhiều → declarative có lợi thế lớn khi chạy song song trên nhiều core, nhiều máy.

Declarative Queries on the Web

  • Ví dụ ngoài database: trang web về sinh vật biển, mục "Sharks" đang được chọn (li class="selected"), muốn tiêu đề của trang được chọn có nền xanh.
    • CSS (declarative): selector li.selected > p với background-color: blue.
    • XSL/XPath (declarative): li[@class='selected']/p — tương đương.
    • JavaScript DOM API (imperative): lặp mọi li, kiểm tra className, lặp children, kiểm tra tagName P, set style → dài, khó đọc.
  • Vấn đề của cách imperative:
    • Khi bỏ class selected (user chuyển trang), nền xanh không tự mất kể cả chạy lại code; CSS thì trình duyệt tự phát hiện rule không còn áp dụng.
    • Muốn dùng API mới nhanh hơn (getElementsByClassName, document.evaluate()) → phải viết lại code; còn browser vendor có thể tối ưu CSS/XPath mà không phá tương thích.
  • → Trong browser, CSS declarative tốt hơn JS imperative; trong database, SQL declarative tốt hơn query API imperative (IMS/CODASYL dùng COBOL duyệt từng record).

MapReduce Querying

  • MapReduce: mô hình lập trình xử lý dữ liệu lớn theo lô trên nhiều máy, phổ biến bởi Google. MongoDB và CouchDB hỗ trợ một dạng giới hạn để làm read-only query trên nhiều document. (Chi tiết ở Chương 10.)
  • Nằm giữa declarative và imperative: logic query viết bằng đoạn code được framework gọi lặp lại; dựa trên map (collect) và reduce (fold/inject) của lập trình hàm.
  • Ví dụ: nhà sinh vật biển đếm số cá mập nhìn thấy mỗi tháng:
    • PostgreSQL: WHERE family = 'Sharks', GROUP BY date_trunc('month', observation_timestamp), sum(num_animals).
    • MongoDB mapReduce: filter query: {family: "Sharks"} (khai báo — extension riêng của MongoDB); map emit key "năm-tháng" và value numAnimals; framework group theo key; reduce cộng tổng; kết quả ghi vào collection monthlySharkReport.
    • Với 2 document tháng 12/1995 có 3 và 4 con: map emit ("1995-12", 3) và ("1995-12", 4); reduce("1995-12", [3, 4]) = 7.
  • map và reduce phải là pure function: chỉ dùng input được truyền vào, không query DB thêm, không side effect → DB có thể chạy chúng ở bất kỳ đâu, theo thứ tự bất kỳ, và chạy lại khi lỗi. Vẫn đủ mạnh: parse string, gọi thư viện, tính toán.
  • Lưu ý:
    • MapReduce là mô hình low-level. SQL có thể implement bằng pipeline MapReduce, nhưng nhiều SQL phân tán không dùng MapReduce. SQL không bị giới hạn chạy trên một máy, MapReduce không độc quyền distributed query.
    • Dùng JavaScript giữa query không chỉ có ở MapReduce — một số SQL DB cũng cho mở rộng bằng JS.
  • Vấn đề usability: phải viết hai hàm phối hợp cẩn thận, khó hơn một query; lại ít cơ hội cho optimizer. → MongoDB 2.2 thêm aggregation pipeline — declarative, cú pháp JSON ($match, $group, $sum…), biểu đạt tương đương một tập con SQL.
  • Bài học: một hệ NoSQL có thể vô tình phát minh lại SQL trong lớp vỏ khác.

Graph-Like Data Models

  • Many-to-many là đặc điểm phân biệt các data model. Chủ yếu one-to-many hoặc không quan hệ → document. Relational xử lý được many-to-many đơn giản; khi kết nối phức tạp hơn → graph tự nhiên hơn.
  • Graph gồm vertices (nodes/entities) và edges (relationships/arcs). Ví dụ:
    • Social graph: vertex là người, edge là quen biết.
    • Web graph: vertex là trang web, edge là link HTML.
    • Road/rail network: vertex là giao lộ, edge là đường/đường ray.
  • Thuật toán nổi tiếng: tìm shortest path (dẫn đường), PageRank (xếp hạng trang web).
  • Graph không chỉ cho dữ liệu homogeneous: có thể lưu các loại object hoàn toàn khác nhau trong một datastore. Ví dụ Facebook duy trì một graph duy nhất với vertex là người, địa điểm, sự kiện, check-in, comment; edge là bạn bè, check-in ở đâu, ai comment post nào, ai tham dự event nào…
  • Ví dụ xuyên suốt (Figure 2-5): Lucy (sinh ở Idaho, Mỹ) và Alain (sinh ở Beaune, Pháp), đã kết hôn và sống ở London.
  • Hai model: property graph (Neo4j, Titan, InfiniteGraph) và triple-store (Datomic, AllegroGraph). Ba ngôn ngữ declarative: Cypher, SPARQL, Datalog. Ngoài ra có ngôn ngữ imperative như Gremlin và framework xử lý graph như Pregel (Chương 10).

Property Graphs

Vertex gồmEdge gồm
Unique identifierUnique identifier
Tập outgoing edgesTail vertex (nơi edge bắt đầu)
Tập incoming edgesHead vertex (nơi edge kết thúc)
Properties (key-value)Label mô tả loại quan hệ
Properties (key-value)
  • Có thể hình dung graph store là hai bảng relational: vertices và edges (Example 2-2, properties dùng kiểu json của PostgreSQL), với index trên cả tail_vertex và head_vertex để tìm edge đi ra/đi vào.
  • Đặc điểm quan trọng:
    • Vertex nào cũng nối được với vertex nào — không schema giới hạn loại gì được liên kết.
    • Từ một vertex tìm được hiệu quả cả incoming và outgoing edge → traverse graph cả xuôi lẫn ngược.
    • Dùng label khác nhau cho các loại quan hệ → lưu nhiều loại thông tin trong một graph mà model vẫn sạch.
  • Figure 2-5 cho thấy những thứ khó biểu diễn bằng relational schema truyền thống:
    • Cấu trúc hành chính khác nhau giữa các nước (Pháp: départements và régions; Mỹ: counties và states).
    • "Nước trong nước" do lịch sử.
    • Độ chi tiết khác nhau: nơi ở hiện tại của Lucy là thành phố, nơi sinh chỉ ở mức bang.
  • Dễ mở rộng: thêm vertex cho allergen, edge người–allergen, và allergen–thực phẩm → query xem ai ăn được gì. Graph rất tốt cho evolvability.

The Cypher Query Language

  • Cypher: ngôn ngữ declarative cho property graph, tạo ra cho Neo4j (tên theo nhân vật trong The Matrix, không liên quan mật mã).
  • Tạo dữ liệu (Example 2-3): khai báo vertex có tên tượng trưng và label, ví dụ (USA:Location {name:'United States', type:'country'}), rồi tạo edge bằng ký hiệu mũi tên (Idaho) -[:WITHIN]-> (USA), (Lucy) -[:BORN_IN]-> (Idaho).
  • Query "tìm tên những người di cư từ Mỹ sang châu Âu" (Example 2-4): MATCH hai pattern — person có edge BORN_IN tới một vertex, từ đó theo chuỗi WITHIN*0.. tới Location "United States"; và person có edge LIVES_IN rồi WITHIN*0.. tới "Europe"; RETURN person.name.
    • :WITHIN*0.. = "đi theo edge WITHIN 0 hoặc nhiều lần" — giống toán tử * trong regex.
  • Nhiều cách thực thi: quét mọi người rồi kiểm tra nơi sinh/nơi ở; hoặc bắt đầu từ hai vertex Location (dùng index trên name), đi ngược các edge WITHIN để tìm mọi địa điểm con, rồi tìm người qua edge BORN_IN/LIVES_IN. Là declarative nên optimizer tự chọn chiến lược hiệu quả nhất.

Graph Queries in SQL

  • Graph lưu được trong relational (Example 2-2) — query được bằng SQL không? Được, nhưng khó.
  • Vấn đề: trong relational, bạn thường biết trước cần bao nhiêu join; trong graph query, có thể phải đi qua số lượng edge không cố định (LIVES_IN có thể trỏ tới phố, thành phố, quận, vùng, bang…).
  • Từ SQL:1999, dùng recursive common table expressions (WITH RECURSIVE) — hỗ trợ bởi PostgreSQL, IBM DB2, Oracle, SQL Server. Example 2-5 xây dần các tập: in_usa, in_europe (theo incoming within đệ quy), born_in_usa, lives_in_europe, rồi join để lấy giao.
  • Cùng một query: 4 dòng Cypher vs 29 dòng SQL → các data model được thiết kế cho các use case khác nhau; hãy chọn model phù hợp.

Triple-Stores and SPARQL

  • Triple-store về cơ bản tương đương property graph, chỉ dùng từ ngữ khác. Mọi thông tin lưu dưới dạng (subject, predicate, object), ví dụ (Jim, likes, bananas).
  • Subject ≈ vertex. Object là một trong hai:
    • Giá trị nguyên thủy (string, number) → predicate + object ≈ key + value của property. Ví dụ (lucy, age, 33) ≈ vertex lucy có {"age": 33}.
    • Vertex khác → predicate là edge, subject là tail, object là head. Ví dụ (lucy, marriedTo, alain).
  • Định dạng Turtle (tập con của Notation3/N3) — Example 2-6/2-7: vertex viết dạng _:lucy, có thể dùng dấu chấm phẩy để nói nhiều điều về cùng subject, rất dễ đọc.

The semantic web

  • Triple-store độc lập với semantic web — ví dụ Datomic là triple-store không liên quan gì semantic web (kỹ thuật: Datomic dùng 5-tuple, thêm 2 field metadata cho versioning).
  • Ý tưởng semantic web: website ngoài việc xuất bản nội dung cho người đọc, cũng xuất bản dữ liệu máy đọc được. RDF (Resource Description Framework) là cơ chế để các site publish dữ liệu theo format nhất quán, kết hợp thành "web of data".
  • Bị overhyped đầu những năm 2000, chưa thành hiện thực, nhiều acronym, chuẩn quá phức tạp. Nhưng có nhiều thành quả tốt: triples có thể là data model nội bộ tốt cho ứng dụng.

The RDF data model

  • Turtle là format dễ đọc cho RDF; còn có RDF/XML (dài dòng hơn nhiều — Example 2-8). Công cụ như Apache Jena chuyển đổi giữa các format.
  • Đặc thù vì thiết kế cho trao đổi dữ liệu toàn Internet: subject/predicate/object thường là URI (ví dụ <http://my-company.com/namespace#within>) để khi gộp dữ liệu với người khác không bị xung đột nghĩa của từ within. URI không cần resolve — chỉ là namespace; khai báo prefix một lần ở đầu file.

The SPARQL query language

  • SPARQL (SPARQL Protocol and RDF Query Language, đọc là "sparkle") — query language cho triple-store dùng RDF. Ra đời trước Cypher; pattern matching của Cypher mượn từ SPARQL nên trông giống nhau.
  • Query di cư Mỹ → châu Âu (Example 2-9) còn ngắn hơn Cypher: ?person :bornIn / :within* / :name "United States" và tương tự cho :livesIn… "Europe".
  • Vì RDF không phân biệt property và edge (đều là predicate), cùng cú pháp dùng để match cả hai: ?usa :name "United States" tương đương (usa {name:'United States'}) trong Cypher.

Graph Databases Compared to the Network Model

  • Graph DB có phải CODASYL "tái sinh"? Không:
Khía cạnhCODASYL (network model)Graph database
SchemaQuy định record type nào được lồng trong record type nàoKhông giới hạn — vertex nào cũng có edge tới vertex nào
Truy cập recordChỉ qua access pathTrực tiếp qua unique ID, hoặc qua index theo giá trị
Thứ tựChildren là ordered set; DB phải duy trì thứ tự, app phải lo vị trí khi insertVertex và edge không có thứ tự (chỉ sort khi query)
QueryImperative, khó viết, dễ vỡ khi schema đổiCó thể imperative, nhưng thường có declarative (Cypher, SPARQL)

The Foundation: Datalog

  • Datalog cũ hơn nhiều so với SPARQL/Cypher — được nghiên cứu học thuật kỹ trong thập niên 1980. Ít người biết nhưng là nền tảng cho các query language sau này.
  • Dùng thực tế trong: Datomic (query language chính), Cascalog (Datalog cho dataset lớn trên Hadoop). Hai hệ này dùng cú pháp Clojure S-expression; sách dùng cú pháp Prolog cho dễ đọc.
  • Data model giống triple-store tổng quát hóa: viết predicate(subject, object) thay vì (subject, predicate, object). Ví dụ: within(usa, namerica), born_in(lucy, idaho), name(idaho, 'Idaho').
  • Query (Example 2-11) được xây bằng rules định nghĩa predicate mới:
    • Rule 1: within_recursive(Location, Name) nếu name(Location, Name).
    • Rule 2: within_recursive(Location, Name) nếu within(Location, Via) và within_recursive(Via, Name) — đệ quy.
    • Rule 3: migrated(Name, BornIn, LivingIn) nếu người đó có tên, sinh ở nơi nằm trong BornIn, sống ở nơi nằm trong LivingIn.
    • Query: ?- migrated(Who, 'United States', 'Europe') → Who = 'Lucy'.
  • Cú pháp: từ viết hoa đầu là biến; rule áp dụng nếu mọi predicate vế phải của :- đều match; khi đó coi như vế trái được thêm vào DB.
  • Cách áp dụng (Figure 2-6): name(namerica, 'North America') → rule 1 sinh within_recursive(namerica, 'North America') → cùng within(usa, namerica), rule 2 sinh within_recursive(usa, 'North America') → tiếp tục sinh cho idaho. Lặp lại để biết mọi địa điểm thuộc North America.
  • Khác biệt tư duy: Cypher/SPARQL "nhảy vào" SELECT ngay; Datalog đi từng bước nhỏ, rule có thể kết hợp và tái sử dụng giữa các query (như function gọi function). Kém tiện cho query một lần đơn giản, nhưng xử lý tốt hơn khi dữ liệu phức tạp.
Ngôn ngữModelHệ thống tiêu biểuKiểuGhi chú
SQLRelationalPostgreSQL, MySQL, Oracle…DeclarativeGraph query được qua WITH RECURSIVE nhưng rườm rà
MapReduceDocument / batchMongoDB, CouchDB, HadoopLai (code snippet)map/reduce phải là pure function
Aggregation pipelineDocumentMongoDB 2.2+DeclarativeCú pháp JSON, ≈ tập con SQL
CypherProperty graphNeo4jDeclarativePattern bằng mũi tên, *0.. cho traversal độ dài biến thiên
SPARQLTriple-store / RDFAllegroGraph, Apache JenaDeclarativeCó trước Cypher; cú pháp path như :within*
DatalogTriple-store tổng quátDatomic, CascalogDeclarative (rule-based)Rule đệ quy, tái sử dụng được
GremlinProperty graph—ImperativeChỉ được nhắc tới

Summary — Tóm tắt chương

  • Lịch sử: bắt đầu bằng một cây lớn (hierarchical) → không tốt cho many-to-many → relational ra đời. Gần đây một số ứng dụng không hợp relational → NoSQL phân nhánh hai hướng:
    • Document DB: dữ liệu là document tự chứa, quan hệ giữa document hiếm.
    • Graph DB: ngược lại — mọi thứ đều có thể liên quan tới mọi thứ.
  • Cả ba model đều được dùng rộng rãi, mỗi cái tốt ở lĩnh vực của nó. Có thể giả lập model này bằng model kia (graph trong relational) nhưng thường vụng về → không có giải pháp one-size-fits-all.
  • Document và graph thường không enforce schema → dễ thích nghi, nhưng app vẫn giả định cấu trúc; chỉ khác ở chỗ schema explicit (on write) hay implicit (on read).
  • Còn nhiều model khác: genome (sequence-similarity search, ví dụ GenBank), vật lý hạt (LHC với hàng trăm petabyte, cần giải pháp custom), full-text search (Chương 3 và Part III).

⚠️ Hiểu lầm & cạm bẫy thường gặp

  • "Schemaless" = không có schema — sai; luôn có implicit schema trong code đọc dữ liệu (schema-on-read).
  • "NoSQL" là một công nghệ — sai; ban đầu chỉ là hashtag, bao gồm nhiều model rất khác nhau (document, graph, key-value…).
  • Chọn document DB vì "dữ liệu giống JSON" hôm nay mà quên rằng dữ liệu thường ngày càng liên kết khi thêm feature (organizations, recommendations) → cuối cùng phải join trong app.
  • Denormalize để tránh join mà không tính chi phí giữ các bản sao nhất quán.
  • Nghĩ document DB luôn nhanh hơn nhờ locality — chỉ đúng khi cần phần lớn document; document lớn + update thường xuyên thì phải load và ghi lại toàn bộ.
  • Nghĩ locality là độc quyền của document model — Spanner interleaved tables, Oracle cluster tables, column-family của Bigtable/Cassandra/HBase cũng làm được.
  • Nghĩ ALTER TABLE luôn chậm, cần downtime — đa số DB chỉ vài ms; MySQL (bản cũ) mới copy cả bảng. Cái chậm là UPDATE toàn bảng.
  • Nghĩ graph DB là CODASYL tái sinh — khác ở schema, truy cập trực tiếp bằng ID/index, không có thứ tự, và có query declarative.
  • Nghĩ MapReduce là cách duy nhất để query phân tán — SQL cũng chạy phân tán được; MapReduce chỉ là mô hình low-level.
  • Nghĩ relational không lưu được document — PostgreSQL (JSON/JSONB), MySQL 5.7+, DB2 đều hỗ trợ JSON; hai phía đang hội tụ.
  • Viết query phụ thuộc thứ tự ngầm (ví dụ giả định kết quả SQL có thứ tự khi không có ORDER BY).

💼 Áp dụng thực tế & phỏng vấn

  • Câu hỏi kinh điển "SQL hay NoSQL?": đừng trả lời theo trào lưu. Phân tích theo (1) loại quan hệ dữ liệu (one-to-many vs many-to-many), (2) access pattern (có load cả khối không), (3) mức độ heterogeneous của schema, (4) nhu cầu join/transaction. Relational (PostgreSQL) vẫn là default an toàn; PostgreSQL JSONB cho phần dữ liệu linh hoạt.
  • Ví dụ phù hợp document model: user profile, product catalog với thuộc tính khác nhau theo loại, CMS content, event log/analytics.
  • Ví dụ phù hợp graph: social network (friends-of-friends), recommendation, fraud detection (vòng giao dịch), knowledge graph, phân quyền phức tạp, routing.
  • Thiết kế schema trong phỏng vấn: thể hiện được trade-off normalize vs denormalize (ví dụ lưu author_name trong post để đọc nhanh vs chỉ lưu author_id để nhất quán) và cách giữ dữ liệu denormalized đồng bộ.
  • Schema evolution thực tế: migration trên bảng lớn cần công cụ online (gh-ost, pt-online-schema-change cho MySQL); pattern "thêm cột nullable, backfill dần, xử lý NULL khi đọc" chính là kỹ thuật trong sách.
  • MongoDB thực tế: giữ document nhỏ, tránh mảng tăng không giới hạn (unbounded arrays); dùng aggregation pipeline thay vì mapReduce (đã bị deprecate ở các bản sau).
  • Hierarchy/tree trong SQL (danh mục, cây tổ chức, comment lồng nhau): dùng WITH RECURSIVE — câu hỏi hay gặp trong phỏng vấn SQL.
  • Declarative mindset: lý do SQL, CSS, React (UI declarative), Kubernetes manifests, Terraform thành công — hệ thống bên dưới được tự do tối ưu và song song hóa.

❓ Câu hỏi ôn tập

Bấm vào câu hỏi để xem đáp án
1Impedance mismatch là gì và document model giảm nó như thế nào?
Là sự lệch pha giữa object trong code OOP và bảng/row/column của relational DB, cần lớp dịch (ORM). Document JSON biểu diễn cây one-to-many (như résumé) gần với cấu trúc object hơn và có locality tốt, một query lấy được cả profile.
2Vì sao lưu region_id thay vì chuỗi "Greater Seattle Area"? Điều này liên quan gì tới normalization?
ID không mang nghĩa với người nên không bao giờ phải đổi; tên chỉ lưu một chỗ giúp nhất quán, tránh mơ hồ, dễ cập nhật, hỗ trợ localization và search. Loại bỏ duplication chính là normalization, nhưng nó cần quan hệ many-to-one mà document DB hỗ trợ kém.
3Network model (CODASYL) khác relational model ở điểm cốt lõi nào?
CODASYL dùng pointer và access path do lập trình viên tự điều hướng (imperative), khó thay đổi. Relational bày dữ liệu thành bảng phẳng, query optimizer tự chọn access path; thêm index là query tự dùng mà không cần sửa code.
4Phân biệt schema-on-read và schema-on-write. Khi nào schema-on-read có lợi?
Schema-on-read: cấu trúc ngầm, diễn giải khi đọc (như dynamic typing). Schema-on-write: DB enforce khi ghi (như static typing). Schema-on-read có lợi khi dữ liệu heterogeneous: nhiều loại object hoặc cấu trúc do hệ thống bên ngoài quyết định.
5Nêu giới hạn của lợi thế locality trong document DB.
Chỉ có lợi khi cần phần lớn document; DB thường phải load cả document và ghi lại cả document khi update (trừ khi kích thước không đổi), nên nên giữ document nhỏ. Locality cũng có ở Spanner (interleaved tables), Oracle cluster tables, column-family của Bigtable.
6Vì sao declarative query language tốt hơn imperative API?
Ngắn gọn hơn; che implementation detail nên DB có thể tối ưu (đổi index, đổi thứ tự lưu trữ) mà không phá query; dễ song song hóa vì chỉ mô tả kết quả chứ không mô tả thuật toán.
7Tại sao graph query khó viết bằng SQL, và SQL giải quyết bằng gì?
Graph traversal cần số lượng join không biết trước (ví dụ WITHIN lặp 0..n lần). SQL:1999 dùng recursive CTE (WITH RECURSIVE), nhưng rất rườm rà: 29 dòng so với 4 dòng Cypher.
8Graph database khác CODASYL ở những điểm nào?
Không có schema giới hạn liên kết; truy cập vertex trực tiếp bằng ID hoặc index thay vì chỉ qua access path; vertex/edge không có thứ tự; hỗ trợ query declarative như Cypher, SPARQL.

Đây là bản tóm tắt và ghi chú, không thay thế sách gốc. Hãy ủng hộ tác giả bằng cách đọc bản gốc.