← quay lại đọcBản in thử6 × 9 in · khổ · trang đối diện24 tr · 5,101 chữ
ii
Maxubrq · GT - Chương II
Dấu hiệu nhận biết
“Dấu hiệu của graph problem không nằm trong dữ liệu, nó nằm trong cách câu hỏi được phát biểu.”
iii
Maxubrq · GT - Chương II

Dấu hiệu nhận biết

năm tín hiệu cho thấy requirement trước mặt bạn là graph problem
Hai requirement nghe gần giống nhau, một cái là lookup, một cái là graph problem sẽ ám đội bạn hàng quý. Ranh giới không nằm trong dữ liệu hay công nghệ, nó nằm trong cách câu hỏi được phát biểu, và kiểm tra được bằng năm dấu hiệu.
bởi maxubrq
tháng 6 năm 2026
Dấu hiệu nhận biết

Buổi grooming chiều thứ Năm, hai ticket nằm cạnh nhau trên màn hình chia sẻ. Ticket thứ nhất: tìm tất cả đơn hàng của khách hàng VIP. Ticket thứ hai: tìm tất cả tài khoản nhận tiền từ tài khoản X qua tối đa năm trung gian. Cùng một động từ "tìm tất cả", cùng một database, cùng một đội ngồi trong phòng, và product owner đọc cả hai bằng đúng một tông giọng, như thể chúng là hai biến thể của một việc.

Nhìn bề mặt thì đúng là vậy. Cả hai đều là lọc dữ liệu theo điều kiện. Cả hai đều estimate được trong buổi planning, ticket một được gắn ba point, ticket hai được gắn năm point vì nghe "phức tạp hơn một chút". Cả hai, trong đầu người estimate, đều kết thúc bằng một câu query.

Nhưng hai ticket này không phải hai biến thể của một việc. Ticket một là một phép lookup, một bước quan hệ, độ sâu cố định, viết buổi sáng xong buổi chiều, và không bao giờ quay lại. Ticket hai mang trong nó bộ gen của câu query mười hai phút ở chương trước, cái câu mà mỗi lần risk team hỏi "thế nếu sáu trung gian thì sao" lại phình thêm một tầng. Một ticket sẽ đóng trong ngày. Một ticket, nếu được giải như ticket một, sẽ quay lại ám đội này hàng quý.

Tương tác — thử trên web
Diagram
a question

Trước khi đọc tiếp, thử tự trả lời: nếu phải phân biệt hai ticket này ngay trong buổi planning, bạn sẽ dựa vào tiêu chí gì? Không phải cảm giác, mà là tiêu chí nói ra được thành lời.

answer.Không phân biệt được bằng dữ liệu, vì cùng một schema phục vụ cả hai. Không phân biệt được bằng công nghệ, vì cả hai đều chạy trên Postgres. Tiêu chí nằm trong chính câu chữ của requirement: độ sâu cố định hay để mở, và câu hỏi có sống trên các động từ nối hay không.
Nếu câu trả lời của bạn là một cảm giác ('cái này nghe nguy hiểm hơn'), đó chính là khoảng trống mà chương này muốn lấp: biến cảm giác thành tiêu chí nói ra được thành lời, kiểm tra được trong buổi planning.

Câu hỏi đáng giá là: phân biệt bằng cái gì? Không phải bằng dữ liệu, vì cùng một schema phục vụ được cả hai ticket, bảng accounts và bảng transfers không hề biết chúng sắp bị hỏi câu nào. Không phải bằng công nghệ, vì cả hai đều chạy được trên Postgres, chỉ là một cái chạy thong thả còn một cái chạy trong đau đớn. Nếu ranh giới không nằm trong dữ liệu, không nằm trong công cụ, thì nó phải nằm ở một tầng khác, tầng mà chúng ta ít khi nhìn vào một cách có chủ đích: cách câu hỏi được phát biểu.

Chương trước kết thúc với một khoảng trống, rằng nhận diện là kỹ năng không ai dạy. Chương này là một cách lấp khoảng trống đó. Không phải bằng trực giác sẽ đến sau mười năm, mà bằng một bài kiểm tra năm câu, chạy được trong một phút, trên chính cái câu mà product owner vừa đọc lên.

Postgres hay Neo4j là câu hỏi đến quá sớm

Trước khi đến bài kiểm tra, đáng dừng lại ở cách phân loại mà phần lớn chúng ta đang dùng, vì nó phổ biến đến mức gần như vô hình.

Cách đó diễn ra trong các cuộc họp kiến trúc, dưới dạng những câu hỏi nghe rất chuyên nghiệp: dữ liệu này nên để trong Postgres hay Neo4j, đây có phải use case cho graph database không, mình có nên đưa social data sang một graph store riêng không. Để ý điểm chung: tất cả đều xoay quanh nơi chứa dữ liệu. Phân loại bài toán bằng cách phân loại chỗ đặt dữ liệu.

Cách phân loại này hấp dẫn vì ba lý do chính đáng. Thứ nhất, nó actionable ngay lập tức, chọn xong công nghệ là có việc cụ thể để làm, có proof of concept để dựng, có buổi demo để hẹn. Thứ hai, nó khớp với cách cả ngành tổ chức kiến thức, tài liệu được viết theo công nghệ, conference talk được đặt tên theo công nghệ, vendor bán hàng theo công nghệ, nên tư duy theo công nghệ là tư duy thuận dòng. Thứ ba, nó từng đúng ở những nơi khác: dữ liệu time-series thì cân nhắc TSDB, dữ liệu full-text thì cân nhắc search engine, phản xạ "loại dữ liệu nào, loại store đó" đã được tưởng thưởng đủ nhiều lần để trở thành thói quen.

Rồi nó gãy, ở hai chỗ.

Chỗ gãy thứ nhất: cùng một dữ liệu phục vụ cả câu hỏi graph lẫn câu hỏi không phải graph. Quay lại bảng account_links của chương trước. Câu hỏi "tài khoản X có bao nhiêu liên kết" là một phép đếm, một bước, một GROUP BY lành mạnh, không có gì là graph ở đây cả. Câu hỏi "X nối tới cụm tài khoản gian lận nào, qua bao nhiêu bước" là traversal sâu trên đúng bảng đó, đúng các hàng đó. Một bảng, hai bản chất, tuỳ câu hỏi. Phân loại theo dữ liệu buộc mỗi bảng phải có một bản chất duy nhất, trong khi bản chất chưa bao giờ thuộc về bảng. Nó thuộc về câu hỏi.

Chỗ gãy thứ hai tinh vi hơn: phân loại theo công nghệ khoá quyết định vào hiện trạng. Đội đang vận hành Postgres sẽ có xu hướng nhìn mọi requirement thành bài toán quan hệ, vì lời giải quan hệ là lời giải không phải xin budget. Đội vừa dựng xong Neo4j sẽ có xu hướng ngược lại, mọi thứ bắt đầu trông như graph, vì cái store mới cần use case để biện minh cho sự tồn tại của nó. Trong cả hai trường hợp, công cụ đang định nghĩa vấn đề. Đáng lẽ phải ngược lại.

Vậy nếu không phân loại theo dữ liệu, không phân loại theo công nghệ, thì còn lại gì để bám vào? Còn lại chính cái thứ ít được coi trọng nhất trong buổi planning: câu chữ của requirement, đúng như nó được phát biểu. Hoá ra cái câu mà product owner đọc lên, trước khi bất kỳ kỹ sư nào kịp dịch nó sang ngôn ngữ bảng và cột, đã chứa đủ tín hiệu để phân loại. Việc cần làm là biết nghe.

Năm dấu hiệu

Năm dấu hiệu dưới đây không đòi hỏi trực giác. Mỗi dấu hiệu là một câu hỏi đặt lên requirement, trả lời được bằng có hoặc không, và mỗi dấu hiệu đi kèm lý do tại sao nó là dấu hiệu, vì một checklist mà không hiểu cơ chế thì chỉ dùng được đúng những ca giống hệt ví dụ.

Quan hệ là dữ liệu chính, không phải thuộc tính phụ

Dấu hiệu thứ nhất hỏi: câu hỏi này quan tâm đến bản thân các kết nối, hay quan tâm đến thuộc tính của từng thực thể? Có một phép thử nhanh: tưởng tượng xoá hết mọi thuộc tính, tên, số dư, ngày tạo, chỉ giữ lại các thực thể trần trụi và các kết nối giữa chúng. Nếu câu hỏi vẫn còn nghĩa, quan hệ là dữ liệu chính.

Những câu bạn sẽ nghe trong meeting khi dấu hiệu này có mặt: khách hàng nào được giới thiệu bởi khách hàng đã churn. Hai nhân viên này có từng chung manager nào trong quá khứ không. Hai câu này sống hoàn toàn trên kết nối, "được giới thiệu bởi", "chung manager", thuộc tính của từng người gần như không tham gia.

Phản ví dụ, nghe giống mà không phải: khách hàng nào có tổng chi tiêu trên một trăm triệu. Quan hệ mua hàng có mặt trong dữ liệu, mỗi đơn hàng là một kết nối giữa khách và sản phẩm, nhưng câu hỏi chỉ aggregate° một thuộc tính dọc theo các kết nối đó rồi vứt kết nối đi. Kết nối ở đây là đường ống, không phải nội dung.

Tại sao đây là dấu hiệu: khi quan hệ là dữ liệu chính, mọi biểu diễn giấu quan hệ vào foreign key sẽ bắt câu hỏi đi đường vòng, mỗi lần muốn chạm vào kết nối lại phải tái tạo nó bằng một phép join. Câu hỏi hỏi thẳng vào thứ mà mô hình coi là chi tiết phụ. Mismatch bắt đầu từ đó.

Độ sâu traversal không biết trước lúc viết query

Dấu hiệu thứ hai hỏi: số bước quan hệ cần đi có phải là hằng số tại thời điểm viết code không, hay nó phụ thuộc vào dữ liệu tại thời điểm chạy?

Dấu hiệu này lộ ra ngay trong từ ngữ. Hãy nghe những cụm này trong requirement: cho đến khi, toàn bộ chuỗi, trực tiếp và gián tiếp, đến tận gốc, tối đa N bước với N là tham số người dùng chỉnh được. Duyệt lên cây phê duyệt cho đến khi gặp người có thẩm quyền. Toàn bộ dependency của service này, trực tiếp và gián tiếp. Mỗi cụm như vậy là một lời thú nhận rằng không ai biết trước con số.

Phản ví dụ: lấy đơn hàng kèm thông tin shipping của nó. Nghe như đi qua hai bảng, và đúng là đi qua hai bảng, nhưng là hai bước cố định, biết trước, hôm nay là hai JOIN thì mười năm nữa vẫn là hai JOIN. Độ sâu cố định không phải graph problem, nó là schema đang làm đúng việc của schema.

Tại sao đây là dấu hiệu: đây chính là gen của câu query mười hai phút. Độ sâu cố định dịch sang JOIN được, dịch một lần là xong. Độ sâu mở thì không có bản dịch tĩnh nào cả, mỗi giá trị của N là một câu query khác nhau, và recursive CTE° chỉ là cách gói sự thật đó lại cho đỡ phải nhìn. Trong năm dấu hiệu, đây là dấu hiệu đáng tin nhất, ít false positive nhất. Khi nghe thấy "cho đến khi", gần như chắc chắn bạn đang đứng trước một graph problem.

a question

Đến đây bạn đã có hai dấu hiệu. Thử chạy chúng lên hai ticket ở đầu chương trước khi đọc tiếp: ticket nào khớp dấu hiệu nào?

answer.Ticket một không khớp dấu hiệu nào: quan hệ chỉ là đường ống dẫn đến thuộc tính, và một bước là một bước, mãi mãi. Ticket hai khớp dấu hiệu hai rất rõ, 'tối đa năm trung gian' là một con số có lịch sử leo thang. Dấu hiệu một thì mờ hơn: câu hỏi vẫn truy về danh sách tài khoản cụ thể, nhưng nội dung của nó sống trên các kết nối chuyển tiền.
Nếu bạn phân vân ở dấu hiệu một với ticket hai, sự phân vân đó là lành mạnh. Các dấu hiệu không phải cái nào cũng cho tín hiệu mạnh như nhau trên mọi requirement, và bài kiểm tra đầy đủ còn ba câu nữa.

Tính bắc cầu trong cách phát biểu

Dấu hiệu thứ ba hỏi: requirement có chứa những từ mang nghĩa lan truyền không? Qua, gián tiếp, thông qua, lan đến, kéo theo, ảnh hưởng dây chuyền, kế thừa. Đây là những động từ và giới từ của sự hợp thành, chúng mô tả quan hệ được xây từ quan hệ.

Câu meeting điển hình: user này được xem tài liệu vì user thuộc nhóm, nhóm thuộc tổ chức, tổ chức được share quyền đọc cả thư mục. Hoặc: nếu bảng này sai thì những dashboard nào sai theo. Cả hai câu đều có một chuỗi "vì... mà... nên" chạy xuyên qua nhiều tầng quan hệ, và câu trả lời không nằm ở bất kỳ tầng đơn lẻ nào.

Phản ví dụ: user nào có role admin. Quyền được gán trực tiếp, một bảng user_roles, một phép lọc, không có gì bắc cầu. Chữ "quyền" xuất hiện trong cả hai câu nhưng cấu trúc của hai câu hỏi khác nhau hoàn toàn, và đó chính là điểm của cả chương này: đừng phân loại theo danh từ trong câu, phân loại theo cấu trúc của câu.

Tại sao đây là dấu hiệu: quan hệ bắc cầu nghĩa là câu trả lời nằm trên đường đi, không nằm trên bản ghi. Một hệ thống chỉ index bản ghi sẽ phải tái tạo đường đi bằng tay, từng bước một, và thế là chúng ta quay lại dấu hiệu thứ hai, vì đường đi dài bao nhiêu thì thường không ai biết trước. Hai dấu hiệu này hay đi cùng nhau, bắc cầu là tính chất của câu hỏi, độ sâu mở là hệ quả vận hành của nó.

Cấu trúc kết nối tự nó mang thông tin

Dấu hiệu thứ tư hỏi: trong câu hỏi này, ai nối với ai có quan trọng hơn ai là ai không? Tức là, thông tin cần tìm có phải là một tính chất của vị trí trong mạng, thay vì tính chất của bản thân thực thể?

Câu meeting điển hình: service nào mà nếu chết thì nhiều luồng nghiệp vụ nhất bị đứt. Nhóm tài khoản này có giao dịch khép kín với nhau không. Để trả lời câu thứ nhất, bạn phải biết các luồng chạy qua đâu, tức là phải nhìn toàn bộ mạng, không có cột nào trong bảng services chứa câu trả lời. Để trả lời câu thứ hai, bạn phải so sánh mật độ giao dịch bên trong nhóm với giao dịch ra bên ngoài, lại là một tính chất của hình, không phải của hàng.

Phản ví dụ tinh vi hơn các dấu hiệu trước: service nào có nhiều caller nhất. Nghe rất giống câu "service nào quan trọng nhất", nhưng đếm caller trực tiếp là aggregate một bước, một câu GROUP BY trả lời được. Chỉ khi câu hỏi cần vị trí tương đối trong toàn mạng, điểm nghẽn, cây cầu duy nhất giữa hai nửa hệ thống, thì cấu trúc mới thật sự mang thông tin. Sự khác nhau giữa "nhiều kết nối nhất" và "đứng ở vị trí hiểm nhất" là một sự khác nhau mà phần sau của sách sẽ còn quay lại, ở đây chỉ cần ghi nhận: chúng là hai câu hỏi khác nhau.

Tại sao đây là dấu hiệu: loại thông tin này không tồn tại trong bất kỳ hàng nào của bất kỳ bảng nào. Nó là tính chất toàn cục, chỉ hiện ra khi toàn bộ dữ liệu được nhìn như một mạng. Không có phép tối ưu cục bộ nào tạo ra được thứ vốn không nằm trong từng mảnh.

Câu hỏi về đường đi, vòng lặp, hoặc cụm

Dấu hiệu thứ năm là dấu hiệu thẳng thắn nhất: requirement hỏi trực tiếp về các đối tượng hình học của mạng. Con đường nào, có vòng không, nhóm nào dính nhau.

Câu meeting điển hình: tiền đi từ A đến B qua những ngả nào. Chuỗi phê duyệt này có vòng không, kiểu A duyệt chi tiêu cho B trong khi B duyệt chi tiêu cho A. Khách hàng của mình tự chia thành những cụm hành vi nào. Đường, vòng, cụm, ba từ này không có định nghĩa nào bên ngoài ngôn ngữ của mạng lưới.

Phản ví dụ: có bao nhiêu giao dịch giữa A và B. Câu này hỏi về một cạnh trực tiếp, đếm trên một quan hệ, không hỏi về đường.

Tại sao đây là dấu hiệu: khi business nói ra những từ này, họ đang nói ngôn ngữ graph mà không biết. Họ không nói "hãy chạy thuật toán tìm chu trình", họ nói "hai ông này duyệt chéo cho nhau thì có vấn đề không", nhưng hai câu là một. Việc của kỹ sư lúc đó không phải là dịch ngược câu hỏi về ngôn ngữ bảng cho khớp với công cụ đang có, mà là nhận ra ngôn ngữ gốc của câu hỏi và giữ nó nguyên dạng càng lâu càng tốt.

Năm dấu hiệu, nhìn lại, không độc lập với nhau. Chúng là năm mặt của cùng một điều: câu hỏi được phát biểu trên quan hệ và trên cấu trúc của quan hệ, thay vì trên thuộc tính của từng thực thể. Một requirement khớp một dấu hiệu là đáng dừng lại nhìn kỹ. Khớp từ hai dấu hiệu trở lên thì gần như chắc chắn.

“

Dấu hiệu của graph problem không nằm trong dữ liệu, nó nằm trong cách câu hỏi được phát biểu.

”

Giờ quay lại hai ticket ở đầu chương và chạy bài kiểm tra. Ticket một, tìm đơn hàng của khách VIP: quan hệ chỉ là đường ống dẫn đến thuộc tính, độ sâu cố định một bước, không có từ bắc cầu nào, không hỏi gì về vị trí trong mạng, không đường không vòng không cụm. Zero trên năm. Ticket hai, tìm tài khoản nhận tiền từ X qua tối đa năm trung gian: độ sâu là tham số, dấu hiệu hai khớp. "Qua trung gian" là từ bắc cầu nguyên chất, dấu hiệu ba khớp. Và câu hỏi thực chất là về các đường đi của dòng tiền, dấu hiệu năm khớp. Ba trên năm. Bài kiểm tra chạy hết năm câu hỏi, trong chính buổi planning, trước khi ai kịp gắn story point°.

Twitter và câu hỏi follow

Bài kiểm tra trên giấy thì gọn. Đáng xem nó vận hành thế nào trên một bài toán thật, ở quy mô thật, nơi câu trả lời không phải lúc nào cũng là một chữ "graph" tròn trịa.

Đầu những năm 2010, quan hệ follow của Twitter sống trong MySQL. Và điều đầu tiên cần nói cho công bằng: nó sống ổn. Đây không phải câu chuyện về một sai lầm ngây thơ chờ được sửa, vì phần lớn câu hỏi mà hệ thống phải trả lời lúc đó là câu hỏi một bước, lấy danh sách follower của một người, kiểm tra A có follow B không, đếm số following. Chạy bài kiểm tra năm dấu hiệu lên các câu hỏi đó: độ sâu cố định, không bắc cầu, không hỏi về hình. Bảng quan hệ trong MySQL là lời giải đúng, không phải lời giải tạm.

Rồi quy mô đổi, và quan trọng hơn, câu hỏi đổi. Mutual follows, tức giao của hai tập kết nối, để hiển thị "những người cả hai cùng follow". Fanout cho timeline, từ một người đăng bài toả đến hàng triệu follower. Các phép giao, hợp, hiệu trên những tập quan hệ khổng lồ, ở throughput hàng chục nghìn thao tác mỗi giây. Chạy lại bài kiểm tra: dấu hiệu một khớp hẳn, quan hệ follow giờ là dữ liệu chính, bản thân các kết nối là sản phẩm, không phải metadata. Dấu hiệu bốn khớp một phần, vài câu hỏi bắt đầu chạm vào cấu trúc.

Nhưng dấu hiệu hai thì sao? Đây là chỗ case này trở nên đáng học. Những câu hỏi mà Twitter phải trả lời hàng triệu lần mỗi phút vẫn là câu hỏi một bước, chỉ là một bước trên dữ liệu khổng lồ với các phép tập hợp phức tạp. Friend-of-friend, traversal hai bước trở lên, không nằm trong đường găng của sản phẩm.

Và đội Twitter đã làm một việc mà nhìn lại, chính là chạy bài kiểm tra này một cách tỉnh táo dù không gọi tên nó như vậy: họ xây FlockDB, một graph store tự phát triển, và cố tình không hỗ trợ traversal nhiều bước.

FlockDB được Twitter công bố và open-source năm 2010, kèm bài giới thiệu trên engineering blog của họ. Tài liệu của dự án nói rõ hệ thống được thiết kế cho adjacency list rất lớn, truy vấn một bước với các phép tập hợp, throughput cao, và không nhắm đến multi-hop traversal hay phân tích graph. Số phận về sau của FlockDB, và bài học tự xây so với mua, thuộc về chương 9.
Không phải vì làm không nổi, mà vì họ xác định được phần nào của bài toán là graph và phần nào không. Phần graph thật của Twitter lúc đó là quan hệ-một-bước-ở-quy-mô-khủng. Traversal sâu là thứ trông có vẻ phải có trong một "graph store", nhưng không ai trả tiền cho nó cả.

Nhận diện đúng, hoá ra, không phải là hô lên chữ graph. Nhận diện đúng là vẽ được ranh giới: phần này của bài toán là graph, phần kia không phải, và lời giải chỉ cần phủ đúng phần thứ nhất.

Case ngược lại nằm ở Google, trong một domain° mà gần như không ai gọi là graph: phân quyền. Câu hỏi "user này có được xem document này không" nghe như một phép lọc, một dòng trong bảng quyền. Cho đến khi đọc kỹ cách quyền thực sự vận hành: user được xem vì user thuộc nhóm, nhóm thuộc tổ chức, tổ chức được share cả thư mục, và thư mục kế thừa quyền xuống từng file bên trong. Toàn bộ là dấu hiệu ba, bắc cầu nguyên chất, cộng thêm dấu hiệu hai vì chuỗi membership lồng nhau sâu bao nhiêu thì không ai quy định trước. Google phát biểu lại bài toán authorization thành các tuple quan hệ và câu hỏi kiểm tra quyền thành câu hỏi "có đường đi từ user đến object qua các tuple này không", rồi xây cả một hệ thống toàn cầu quanh cách phát biểu đó, đặt tên là Zanzibar.

Paper "Zanzibar: Google's Consistent, Global Authorization System", USENIX ATC 2019, mô tả mô hình relation tuple và check API phục vụ hàng trăm dịch vụ của Google. Ở đây chỉ kể phần nhận diện; góc nhìn reachability° của chính bài toán này sẽ quay lại ở chương 4.

Tương tác — thử trên web
Diagram

Hai case, hai kết luận ngược chiều, từ cùng một bài kiểm tra. Twitter nhìn vào bài toán trông rất graph và tìm ra phần không phải graph của nó. Google nhìn vào bài toán trông không hề graph và tìm ra phần graph nằm sâu bên trong. Bài kiểm tra có giá trị ở cả hai chiều, và chiều thứ hai mới là chiều khó, vì không có chữ "network" hay "mạng lưới" nào trong requirement để nhắc bạn.

Ba kiểu nhận nhầm

Kỹ năng nào dùng được thì cũng dùng sai được. Ba kiểu sai dưới đây phổ biến đến mức gần như mỗi đội sẽ gặp ít nhất một, và mỗi kiểu có cách bắt riêng.

Kiểu thứ nhất là false positive: thấy bảng many-to-many là hô graph. Nó trông như thế này: trong buổi review kiến trúc, ai đó chỉ vào user_groups, order_items, các bảng nối quen thuộc, và nói dữ liệu của mình thực chất là một graph, kèm theo đề xuất công nghệ mới. Cái khó của kiểu sai này là về mặt toán học, nó đúng. Mọi quan hệ many-to-many là một đồ thị hai phía, không cãi được. Nhưng chính sự đúng-về-toán đó là cái bẫy, vì biểu diễn được bằng graph và đáng hỏi như graph là hai chuyện khác nhau. Cách bắt: chạy lại dấu hiệu hai. Tuyệt đại đa số bảng many-to-many trong một hệ thống chỉ phục vụ câu hỏi một bước, user nào trong nhóm này, đơn này có những item nào. Độ sâu cố định, mãi mãi cố định. Đồ thị có tồn tại, câu hỏi về đồ thị thì không.

Kiểu thứ hai là false negative, và nó thường mặc bộ đồ hiền lành nhất: "nó chỉ là cây thôi mà". Org chart, cây danh mục sản phẩm, permission kế thừa theo thư mục. Vì cây là cấu trúc quen thuộc, nó không kích hoạt phản xạ graph, và lời giải hay gặp là một vòng lặp ở tầng ứng dụng, mỗi vòng một query, đi lên cha, đi lên cha nữa, cho đến gốc. Chạy được. Chậm dần khi cây sâu ra. Rồi một ngày, dữ liệu thật làm điều dữ liệu thật luôn làm: nhân viên kiêm nhiệm báo cáo cho hai manager ở hai nhánh, danh mục được gắn chéo vào hai cây cùng lúc, và cấu trúc hứa hẹn là cây lặng lẽ mọc một cạnh thành đồ thị có chu trình. Vòng lặp đi-lên-cha treo, hoặc tệ hơn, chạy mãi mãi. Cách bắt: đừng phân loại theo hình dáng hiện tại của dữ liệu, phân loại theo câu hỏi. "Đi lên cho đến khi" là dấu hiệu hai, "kế thừa qua" là dấu hiệu ba. Cây khớp bài kiểm tra thì cây là graph problem, bất kể nó đang hiền đến đâu, và lời giải phải sẵn sàng cho cái ngày nó thôi làm cây.

Kiểu thứ ba xảy ra sau khi đã nhận diện đúng: dán nhãn rồi dừng lại. Cả phòng họp đồng ý, đây là graph problem, mọi người gật gù, cuộc họp chuyển chủ đề, ai cũng mang về một cảm giác đã-hiểu. Ba tuần sau, lúc bắt tay vào giải, mới lộ ra mỗi người đang nghĩ một bài toán khác nhau trên cùng một graph. Người nghĩ đến chuyện tìm mọi thứ chạm tới X, người nghĩ đến chuyện tìm đường tốt nhất giữa X và Y, người nghĩ đến chuyện tìm các cụm. Cái nhãn "graph problem" nghe như một chẩn đoán nhưng thực ra là tên của cả một họ bệnh. Cách bắt: trước khi rời cuộc họp, ép phát biểu lại requirement thành một câu hỏi tường minh trên quan hệ, theo dạng "từ X, tìm mọi node đến được qua edge loại E, trong tối đa N bước, với ràng buộc T". Câu phát biểu lại đó, chứ không phải cái nhãn, mới là sản phẩm thật của kỹ năng nhận diện. Nhãn tạo cảm giác hoàn thành. Câu phát biểu lại tạo công việc tiếp theo.

❦

Khi graph problem không cần lời giải graph

Đến đây cần nói thẳng một điều, để tránh hiểu nhầm tích luỹ từ đầu chương: bài kiểm tra năm dấu hiệu trả lời câu hỏi "đây có phải graph problem không". Nó không trả lời câu hỏi "có nên dựng lời giải graph không". Hai câu hỏi này tách nhau, và sẽ còn tách nhau suốt phần còn lại của cuốn sách.

Có những requirement khớp dấu hiệu, là graph problem thật, và vẫn nên được giải bằng công cụ không phải graph. Ba điều kiện làm điều đó lành mạnh.

Điều kiện thứ nhất là traversal nông và cố định thật sự. Nếu câu hỏi là hai bước, hôm nay hai bước, và mọi hiểu biết về nghiệp vụ nói rằng nó sẽ mãi là hai bước, thì hai cái JOIN là lời giải đúng, không phải lời giải tạm. Chữ "tạm" chỉ xuất hiện khi độ sâu có lịch sử leo thang.

Điều kiện thứ hai là tần suất thấp. Một câu hỏi reachability mỗi quý chạy một lần cho báo cáo audit chịu được một recursive CTE chạy vài phút, và nên chịu như vậy. Nỗi đau phải đủ thường xuyên mới đáng trả tiền hạ tầng, một câu query xấu xí mà hiếm khi chạy rẻ hơn nhiều so với một hệ thống đẹp đẽ phải vận hành quanh năm.

Điều kiện thứ ba là dữ liệu nhỏ. Một graph vài chục nghìn node nằm gọn trong bộ nhớ của một script Python chạy trên laptop. Đọc dữ liệu ra, dựng graph trong RAM, hỏi xong, vứt đi. Đừng kiến trúc hoá thứ một script giải được.

Nhưng điều kiện thật sự quan trọng không phải từng cái riêng lẻ, mà là phép nhân của cả ba với một thứ không nằm trong dữ liệu: quỹ đạo của câu hỏi. Requirement hôm nay hai bước, nhưng nếu nó đến từ một risk team mà lịch sử cho thấy "thêm một bước nữa nhé" luôn luôn đến, thì con số hai đó có tuổi thọ ngắn. Nhận diện sớm không phải để xây sớm. Nhận diện sớm là để khi yêu cầu leo thang, đội không bị bất ngờ, và quyết định chuyển công cụ là một bước đi đã trù tính chứ không phải một cuộc tháo chạy.

Người đọc đến đây đã cầm được bài kiểm tra, và nó dùng được ngay, một buổi chiều rảnh chạy thử lên hai mươi ticket gần nhất trong backlog là biết hệ thống của mình đang giấu bao nhiêu graph problem.

Nhưng giữa "nhận ra đây là graph problem" và "có một graph để hỏi" còn một bước nữa, một bước nghe có vẻ hiển nhiên đến mức không ai nghĩ phải dạy: quyết định cái gì là node, cái gì là edge. Hai đội cùng nhận diện đúng một bài toán vẫn có thể dựng ra hai mô hình khác nhau, từ cùng một dữ liệu, và một trong hai mô hình sẽ trả lời được câu hỏi, mô hình còn lại thì câm.

✱ hết bản in thử