Một buổi sáng thứ ba, đội risk gửi cho một kỹ sư backend một yêu cầu chỉ dài đúng một câu: liệt kê tất cả khách hàng có liên hệ với tài khoản gian lận X, trong vòng ba bước.
Câu hỏi đơn giản đến mức gần như xúc phạm. Ba bước nghĩa là gì thì ai cũng
hiểu, khách hàng A dùng chung thẻ với B, B chuyển tiền cho C, vậy C cách A
hai bước. Dữ liệu nằm sẵn trong database, bảng accounts, bảng
account_links, mỗi dòng là một mối liên hệ đã được hệ thống ghi lại từ
trước, không thiếu một mảnh nào. Người kỹ sư đọc xong yêu cầu, ước lượng
trong đầu, trả lời trong channel rằng chiều nay sẽ có.
Lời giải tự nhiên đến mức gần như tự viết. Một bước là một JOIN. Hai bước là hai JOIN. Ba bước, ba JOIN, thêm điều kiện loại các tài khoản đã đi qua để không đếm trùng. Xong trước giờ ăn trưa, kết quả khớp với vài ca mà đội risk đã điều tra tay, mọi người trong channel thả tim.
Cú trượt đầu tiên đến vào tuần sau. Đội risk quay lại: kết quả tốt lắm, nhưng bọn này hay giấu mình sau bốn năm lớp trung gian, chạy được năm bước không?
Mỗi mức sâu thêm là một lần self-JOIN nữa, và đến mức này thì cách chồng JOIN bằng tay bắt đầu bốc mùi, năm bước là năm tầng JOIN lồng nhau, mỗi tầng lặp lại gần nguyên một khối điều kiện, sửa một chỗ phải sửa năm chỗ. Cách làm cho tử tế là viết lại toàn bộ thành một câu recursive CTE°, để độ sâu trở thành tham số thay vì cấu trúc cứng của câu lệnh. Người kỹ sư chọn cách tử tế. Câu SQL phình từ tám dòng thành bốn mươi dòng, có phần khởi tạo, phần đệ quy, phần chống lặp vô hạn vì dữ liệu liên kết ngoài đời luôn có vòng, hai tài khoản trỏ vào nhau là chuyện thường ngày, và phần lọc kết quả cuối. Vẫn đọc được, nếu nheo mắt. Vẫn đúng, đã test kỹ. Có điều, lúc review, một đồng nghiệp nhìn câu query một lúc rồi hỏi nửa đùa, sao một câu hỏi một dòng lại cần bốn mươi dòng để hỏi. Không ai trả lời, vì câu đùa không có vẻ gì cần trả lời.
Trên production, nó chạy mười hai phút.
Index đã đánh đủ. Execution plan đã đọc đi đọc lại. Bảng liên kết có vài chục triệu dòng, lớn nhưng không phải lớn bất thường, phần cứng không thiếu thốn gì. Mười hai phút, cho một câu hỏi mà đội risk phát biểu gọn trong một dòng tiếng người.
Ở đây có một thứ đáng dừng lại lâu hơn cảm giác khó chịu thông thường về một câu query chậm. Mọi nguyên liệu của lời giải đều có mặt: dữ liệu đầy đủ và sạch, kỹ sư có nghề, công cụ là relational database, thứ đã được tinh luyện suốt bốn mươi năm và đang vận hành phần lớn dữ liệu của thế giới. Không có mảnh nào thiếu, không có mảnh nào tồi. Vậy thì thứ gì trong bài toán này đang kháng cự lại tất cả những mảnh tốt đó?
Người kỹ sư trong câu chuyện không tự hỏi như vậy. Họ mở execution plan ra xem tiếp. Và đó là chi tiết quan trọng nhất của cả câu chuyện: người viết câu SQL đó không nghĩ mình đang làm graph theory. Họ nghĩ mình đang JOIN.
Nếu bạn đang nghĩ "thì tối ưu tiếp chứ sao", bạn đang nghĩ đúng như một kỹ sư có nghề.
Đó là phản xạ được rèn qua nhiều năm: query chậm thì đọc plan, plan xấu thì xem index, index đủ rồi thì xét denormalize, denormalize chưa đủ thì materialized view, vẫn chưa đủ thì cache, và nếu tất cả vẫn chưa đủ thì quay lại thương lượng với business, ba bước thôi được không, năm bước thật sự cần thiết à. Mỗi bước trong chuỗi đó đều hợp lý, đều có giáo trình, đều đã cứu hàng nghìn hệ thống ngoài kia. Chương này không có ý định chế giễu chuỗi phản xạ đó. Nó là phản xạ nghề nghiệp lành mạnh, và trong câu chuyện của chúng ta, nó còn thắng được một trận.
Người kỹ sư đánh thêm một composite index theo đúng hướng truy vấn, viết lại phần đệ quy để lọc sớm hơn, đẩy điều kiện chống lặp lên trước thay vì để cuối. Mười hai phút xuống còn chín mươi giây. Một cải thiện gần mười lần, xứng đáng được khen trong sprint review, và thực tế là được khen thật. Đáng khen không phải vì con số, mà vì cách đạt được nó: không đổi công nghệ, không xin thêm máy, không bắt business hạ yêu cầu, chỉ bằng việc hiểu công cụ của mình sâu hơn. Đó chính xác là loại chiến thắng mà nghề này dạy chúng ta theo đuổi, và nó củng cố một niềm tin ngầm: bài toán này thuộc về thế giới quen thuộc, chỉ cần đào sâu thêm là kiểm soát được. Câu chuyện đáng lẽ kết thúc ở đây, như hàng nghìn câu chuyện tối ưu query vẫn kết thúc, êm đẹp và không ai phải nghĩ thêm gì.
Nó không kết thúc, vì đội risk không đứng yên. Kẻ gian không đứng yên, nên đội risk không thể đứng yên.
Tuần sau nữa, yêu cầu mới: ngoài liên hệ qua giao dịch, tính thêm liên hệ qua thiết bị, hai tài khoản đăng nhập từ cùng một điện thoại thì coi như có dây với nhau. Thêm một loại edge, sửa lại phần đệ quy, query chậm đi một chút, tối ưu lại một chút. Rồi thêm liên hệ qua địa chỉ giao hàng. Rồi câu hỏi đảo chiều, không phải "ai liên hệ với X" mà "trong toàn bộ khách hàng, những ai nằm trong vòng ba bước của bất kỳ tài khoản nào trong danh sách đen". Mỗi yêu cầu mới đều nghe rất nhỏ trong miệng người hỏi, một câu nói thêm trong cuộc họp, và mỗi yêu cầu đều buộc viết lại một phần đáng kể của câu query đã được tối ưu kỹ đến mức không còn chỗ thừa.
Đến tháng thứ ba thì hình dạng của vấn đề bắt đầu lộ ra, không phải trong code mà trong nhịp làm việc. Chi phí của mỗi thay đổi không giảm dần theo kinh nghiệm như lẽ thường, nó tăng. Câu query càng được tối ưu sâu cho hình dạng hiện tại của câu hỏi thì càng giòn trước hình dạng tiếp theo. Người kỹ sư nhận ra mình đang tối ưu một thứ rất cụ thể: câu query này, cho câu hỏi này, ở độ sâu này. Còn đội risk thì đang sống trong một thế giới khác, nơi thứ họ cần là cả một loại câu hỏi, và loại câu hỏi đó cứ mọc thêm biến thể mỗi tuần.
Có một sự bất đối xứng trong cuộc đua này, và nó không đứng về phía người kỹ sư. Mỗi biến thể mới của câu hỏi, phía risk chỉ tốn một câu nói. Phía engineering tốn một lần viết lại, một lần tối ưu lại, một lần test lại. Một bên trả giá bằng lời, một bên trả giá bằng tuần. Khi hai bên cùng nói về một thứ mà chi phí phát biểu chênh nhau cỡ đó, vấn đề thường không nằm ở năng lực của bên trả giá đắt. Nó nằm ở chỗ bên đó đang phải nói về thứ ấy bằng một thứ tiếng không sinh ra cho nó.
Tối ưu đúng, mà vẫn thua. Nếu kỹ năng tối ưu không phải là thứ đang thua, thì thứ gì đang thua?
Đặt hai thứ cạnh nhau và nhìn thật chậm.
Câu hỏi của đội risk được phát biểu bằng quan hệ: ai liên hệ với ai, qua bao nhiêu trung gian, lan đến đâu. Toàn bộ nội dung của câu hỏi nằm trong các động từ nối: liên hệ với, qua, trong vòng. Còn relational database, bất chấp cái tên, tính giá theo một đơn vị khác: mỗi lần bạn muốn đi thêm một bước quan hệ, nó thực hiện thêm một lần JOIN, nghĩa là thêm một lần đặt cả tập dữ liệu này cạnh cả tập dữ liệu kia rồi lọc ra những cặp khớp nhau.
Hai cấu trúc chi phí này không cùng một thế giới. Trong cách biểu diễn quan hệ dạng bảng, "đi thêm một bước" là một thao tác toàn cục, chi phí của nó dính với kích thước của cả bảng liên kết, dù bạn chỉ quan tâm đến vùng lân cận của một tài khoản duy nhất. Trong cách biểu diễn mà mỗi node giữ danh sách hàng xóm của chính nó, "đi thêm một bước" là một thao tác cục bộ, từ chỗ đang đứng nhìn quanh, chi phí dính với số hàng xóm thật sự có mặt ở đó.
Cùng một câu hỏi, một bên trả giá theo kích thước của thế giới, một bên trả giá theo kích thước của khu phố.Hình dung bằng đời thường thì thế này. Hỏi "hàng xóm của nhà số 14 là những ai" theo kiểu của bảng quan hệ là cầm danh bạ của cả thành phố, lật từng trang, gạch chân những ai khai địa chỉ cạnh số 14. Index làm việc lật nhanh hơn rất nhiều, nhưng bản chất vẫn là tra danh bạ thành phố để trả lời một câu hỏi về một con phố. Còn theo kiểu của đồ thị thì là đi bộ đến nhà số 14 và gõ hai cánh cửa bên cạnh. Khi chỉ hỏi một lần, hai cách chẳng khác nhau bao nhiêu. Khi câu hỏi trở thành "rồi hàng xóm của hàng xóm, rồi tầng nữa, cho đến khi gặp người quen", cách thứ nhất tra danh bạ thành phố lặp đi lặp lại ở mỗi tầng, cách thứ hai chỉ đơn giản là tiếp tục đi bộ.
Khi độ sâu cố định và nông, hai cách trả giá chưa kịp tách xa nhau, ba lần JOIN trên bảng có index tốt là chuyện nhỏ. Nhưng câu hỏi của đội risk có một tính chất mà lúc đầu không ai để ý: độ sâu không phải là hằng số. Ba bước, rồi năm, rồi "cứ lan đến đâu thì theo đến đó". Độ sâu là một phần của dữ liệu, nó thuộc về kẻ gian, không thuộc về người viết query. Và relational model° không có cách phát biểu tự nhiên cho một câu hỏi mà số lần JOIN chỉ biết được lúc chạy. Recursive CTE là cách SQL thừa nhận điều đó, một cách thừa nhận trung thực nhưng gượng, như một ngôn ngữ phải mượn từ của ngôn ngữ khác để nói về thứ nó không có từ riêng.
Đến đây thì có thể gọi tên lại toàn bộ câu chuyện. Vấn đề chưa bao giờ là hiệu năng, hiệu năng chỉ là triệu chứng đến sớm nhất. Vấn đề là một cú lệch ngôn ngữ: đội risk hỏi bằng thứ tiếng của quan hệ và đường lan truyền, còn người kỹ sư buộc phải dịch sang thứ tiếng của tập hợp và phép kết. Mỗi lần dịch, câu hỏi bị biến dạng một chút, và phần biến dạng nặng nhất rơi đúng vào phần quan trọng nhất, cái độ sâu không biết trước kia. Chín mươi giây hay mười hai phút chỉ là số đo của độ vênh giữa hai thứ tiếng.
Người kỹ sư trong câu chuyện không kém. Họ chỉ đang trả nợ cho một quyết định mà không ai trong phòng nhận ra là quyết định: chọn biểu diễn dữ liệu theo bảng cho một câu hỏi sống bằng đường đi.
Nhưng nếu đây thật sự là một cú lệch cấu trúc, không phải lỗi của một cá nhân hay một team, thì nó phải để lại dấu vết ở những nơi khác. Những công ty khác nhau, thập kỷ khác nhau, domain° không liên quan gì đến nhau, lẽ ra phải từng vấp đúng vào chỗ này, theo đúng kiểu này. Có không?
Có. Và ba lần vấp dưới đây không được chọn vì chúng nổi tiếng, chúng được chọn vì chúng không giống nhau ở mọi chi tiết bề mặt, chỉ giống nhau ở đúng một chỗ.
Đầu những năm 2000, PayPal suýt chết vì fraud. Không phải kiểu fraud lẻ tẻ vài giao dịch, mà là tội phạm có tổ chức coi hệ thống thanh toán non trẻ này như một cái máy rút tiền, tốc độ mất tiền có lúc đe doạ trực tiếp khả năng sống sót của công ty. Mọi lớp phòng thủ hiển nhiên đều đã được thử, và kẻ gian đi xuyên qua chúng theo cùng một cách: bất kỳ quy tắc nào xét từng giao dịch riêng lẻ, chúng đều học được và lách dưới ngưỡng. Điều làm đội của Max Levchin mất ngủ chính là chỗ đó, từng giao dịch gian lận, nhìn riêng, thường trông hoàn toàn bình thường, số tiền vừa phải, tài khoản có lịch sử, không có gì để một cái rule vịn vào. Thứ bất thường chỉ hiện ra khi đặt các tài khoản cạnh nhau: nhóm tài khoản này dùng chung dải thẻ, nhóm kia trỏ về cùng địa chỉ, nhóm nọ có kiểu hành vi giống nhau đến mức không thể là trùng hợp. Kẻ gian không vận hành tài khoản, kẻ gian vận hành mạng lưới tài khoản, và mạng lưới là thứ duy nhất chúng không thể làm cho trông vô tội, vì chính mạng lưới là cỗ máy kiếm tiền của chúng. Đội PayPal xây một công cụ nội bộ để điều tra viên lần theo và nhìn thấy các mạng lưới đó, đặt tên nó là Igor, theo tên một fraudster người Nga từng trêu ngươi họ.
Trong các tài liệu kể lại thời kỳ đó, gần như không ai dùng chữ graph theory. Họ nói về việc lần theo các tài khoản dính nhau. Cái việc không gọi tên này, xin giữ lại, lát nữa sẽ cần đến.Nhảy sang một domain khác hẳn. LinkedIn, những năm xây tính năng People You May Know, đối mặt với một câu hỏi nghe còn hiền hơn cả câu hỏi của đội risk ban nãy: gợi ý cho mỗi người dùng những người mà họ có thể quen. Trực giác đơn giản nhất, và hoá ra cũng là trực giác mạnh nhất, là bạn của bạn: những người có nhiều bạn chung với bạn. Hai bước. Chỉ hai bước thôi, độ sâu còn chẳng phải ẩn số. Nhưng làm một phép nhân nhẩm thì hai bước đó đổi tính chất. Một người có vài trăm kết nối, mỗi kết nối lại có vài trăm kết nối, vùng hai bước của một người bình thường đã là hàng chục nghìn ứng viên cần chấm điểm và xếp hạng. Nhân tiếp với hàng trăm triệu thành viên, mỗi người cần danh sách gợi ý riêng, danh sách phải tươi khi mạng lưới đổi từng giờ, thì bài toán friend-of-friend hiền lành trở thành một trong những tải nặng nhất toàn công ty, thứ không một câu query nào, dù tối ưu đến đâu, gánh nổi như một câu query. LinkedIn rốt cuộc phải xây hạ tầng chuyên dụng để phục vụ việc đi lại trên đồ thị quan hệ, và PYMK được nhắc đến trong chính các tài liệu kỹ thuật của họ như một trong những lý do hạ tầng graph trở thành hạ tầng cốt lõi.
Một tính năng gợi ý kết bạn, nghe như bài tập cuối tuần, lặng lẽ định hình kiến trúc của cả một công ty.Case thứ ba đảo chiều. Tháng ba năm 2016, một lập trình viên gỡ các package của mình khỏi npm sau một tranh chấp về tên gọi. Trong số đó có left-pad, một package làm đúng một việc là chèn ký tự vào đầu chuỗi, mười một dòng code. Trong vài giờ sau đó, build của hàng nghìn dự án JavaScript trên khắp thế giới đỏ rực, trong đó có những cái tên khổng lồ của hệ sinh thái.
Gần như không nạn nhân nào trong số đó từng nghe tên left-pad. Họ không phụ thuộc vào nó, theo nghĩa mà con người hiểu chữ phụ thuộc. Họ phụ thuộc vào A, A phụ thuộc vào B, B phụ thuộc vào left-pad, một chuỗi mắt xích nằm đầy đủ trong các file lockfile, máy đọc được từng dòng, chỉ có con người là chưa từng nhìn nó như một tổng thể. Hai case trước là chuyện không nhìn ra đồ thị nên không giải được bài toán. Case này là mặt còn lại của cùng đồng xu: không nhìn ra đồ thị nên không biết mình đang đứng trên nó, cho đến khoảnh khắc một mắt xích biến mất và cả sàn nhà nghiêng đi.Một công ty thanh toán chống tội phạm có tổ chức, một mạng xã hội nghề nghiệp gợi ý kết bạn, một hệ sinh thái mã nguồn mở sập build vì mười một dòng code. Ba thập kỷ chồng lấn, ba domain không chung nổi một danh từ, ba kết cục khác nhau, một nơi sống sót nhờ nhìn ra kịp lúc, một nơi xây cả hạ tầng quanh cái nhìn đó, một nơi chỉ nhìn ra sau khi đã ngã. Nhưng đặt ba câu chuyện cạnh nhau và lột hết lớp da bề mặt, còn lại một bộ xương: thứ mang thông tin quan trọng nhất không nằm trong từng bản ghi mà nằm trong cách các bản ghi nối với nhau, câu hỏi then chốt luôn có dạng lan truyền qua nhiều bước, và số bước thì hoặc không biết trước, hoặc biết trước nhưng nhân với quy mô thì thành chuyện khác. Ở cả ba nơi, bộ xương đó tồn tại rất lâu trước khi có ai gọi tên nó. Nó chỉ được nhìn thấy khi đủ đau. Và nếu sự lặp lại này có thật, thì nó kéo theo một câu hỏi không dễ chịu lắm về chính người đang đọc: bộ xương đó còn đang nằm ở những đâu nữa, ngay lúc này, chưa đủ đau để hiện hình?
Giờ có thể quay lại câu hỏi treo từ đầu chương. Mọi nguyên liệu đều tốt, kỹ sư giỏi, dữ liệu đủ, công cụ trưởng thành, vậy thứ gì kháng cự?
Câu trả lời là không có thứ gì kháng cự cả. Câu hỏi bị đặt sai chỗ ngay từ đầu, và đặt sai theo đúng một kiểu mà chúng ta được huấn luyện để đặt sai. Suốt nhiều năm, câu hỏi của chúng ta về graph theory là câu hỏi của sinh viên trước kỳ thi: môn này dùng để làm gì. Câu hỏi đó giả định graph là một công cụ nằm trên kệ, đợi một ngày nào đó có bài toán dán nhãn "graph" rơi xuống thì lấy ra dùng. Ngày đó không bao giờ đến, vì bài toán ngoài đời không mang nhãn.
Câu hỏi chưa bao giờ là graph theory dùng để làm gì. Câu hỏi là: những bài toán bạn đang giải bằng công cụ khác, bao nhiêu trong số đó vốn là graph.
“Graph problem không bao giờ tự giới thiệu nó là graph problem, nó chỉ xuất hiện như một câu query càng ngày càng chậm.
”
Thử kiểm kê một lượt, không phải kho công cụ, mà chính backlog của bạn.
Cái org chart trong hệ thống HR, và câu hỏi mỗi mùa nghỉ phép "request này
escalate lên ai khi cả manager lẫn skip-level đều out of office": đó là một
phép đi dọc cây, với điều kiện dừng phụ thuộc dữ liệu. Hệ thống phân quyền,
nơi user thuộc group, group thuộc group lớn hơn, group được cấp role, role
mở quyền trên folder, folder thừa kế từ folder cha, và câu hỏi tưởng đơn
giản "rốt cuộc ai đọc được file này" hoá ra phải lần qua năm sáu tầng thừa
kế: đó là reachability°. Cái sơ đồ microservice trên Confluence đã lỗi thời
từ quý trước, và câu hỏi làm cả phòng im lặng trước mỗi lần deploy lớn, "đổi
API này thì những service nào vỡ": vẫn là reachability°, chỉ là đi ngược
chiều mũi tên. Tính năng "khách mua sản phẩm này cũng mua" mà đội growth vẫn
đòi: một đồ thị hai phía giữa người và sản phẩm, được chiếu xuống một phía.
Câu hỏi của đội data "bảng nguồn này sai thì những dashboard nào đang nói
dối": lan truyền trên đồ thị lineage. Câu chuyện fraud mở đầu chương. Cái
thuật toán tìm đường trong tính năng giao hàng. Bảng entity_relationships
nào đó đang phình lên từng tháng trong schema của bạn, kèm một vòng while ở
tầng ứng dụng cứ tìm tiếp một tầng nữa cho đến khi hết thứ để tìm.
Danh sách này không cần đầy đủ, nó chỉ cần đủ dài để một điều trở nên khó chối: bạn không thỉnh thoảng gặp graph problem. Bạn giải chúng hàng tuần, bằng JOIN, bằng vòng lặp, bằng cache, bằng những cuộc họp. Và phần lớn các lời giải đó vẫn đang chạy được, đó mới là chỗ khiến mọi thứ khó nhìn ra. Không có còi báo động nào kêu khi một bài toán graph bị giải bằng công cụ khác, chỉ có một lớp ma sát mỏng phủ lên mọi thứ: query hơi chậm, code hơi rối, mỗi yêu cầu mới hơi đắt hơn mức nó đáng ra phải đắt, một vài câu hỏi của business bị âm thầm xếp vào loại "cái này khó, để sau". Ma sát không ai chết, nên ma sát không ai điều tra.
Và đến đây mới thấy hết vị của chi tiết đã xin giữ lại ở case PayPal: họ không gọi thứ họ làm là graph theory, họ gọi là lần theo các tài khoản dính nhau. Người kỹ sư đầu chương cũng vậy, họ không nghĩ mình đang làm graph theory, họ nghĩ mình đang JOIN. Cả hai đều không thiếu kiến thức. Hỏi định nghĩa BFS, người kỹ sư đó trả lời trôi chảy, có khi còn viết được trên bảng trắng không cần nháp, vì từng luyện để vượt vòng phỏng vấn. Thứ họ thiếu không nằm trong sách thuật toán. Thứ họ thiếu là cây cầu nối giữa chương sách đó và cái ticket JIRA sáng nay, cái phản xạ nhìn một yêu cầu phát biểu bằng tiếng người và nhận ra cấu trúc toán học đang mặc thường phục đứng trong đó. Lý thuyết học ở trường và vấn đề gặp ở công ty là cùng một thứ, mặc hai bộ quần áo khác nhau, và không ai dạy chúng ta môn nhận diện trang phục.
Cần nói rõ một điều, trước khi sự hào hứng chạy quá đà. Nhìn ra graph không có nghĩa là mọi thứ vừa kiểm kê ở trên đều nên được nhổ khỏi Postgres đem trồng sang một graph database nào đó ngay quý này. Nhìn ra cấu trúc là một chuyện, quyết định có đáng trả giá để giải theo cấu trúc đó không là chuyện khác hẳn, khó hơn, và phần lớn quyển sách này tồn tại vì chuyện thứ hai. Chương này chỉ làm đúng một việc, đổi vị trí đặt câu hỏi. Trước đây bạn đứng ngoài kho công cụ nhìn vào, hỏi món này dùng làm gì. Từ giờ bạn đứng giữa các bài toán của chính mình, hỏi món nào trong đây vốn không phải hình dạng mà mình vẫn tưởng.
Mọi giáo trình thuật toán, mọi bài giảng, mọi quyển sách phỏng vấn, đều bắt đầu phần đồ thị bằng cùng một nghi thức: cho đồ thị G gồm tập đỉnh V và tập cạnh E.
Ba chữ đầu tiên là ba chữ đắt nhất, và chúng được phát không. Cho đồ thị. Trong suốt những năm đi học, đồ thị luôn được cho: đề bài vẽ sẵn các vòng tròn và mũi tên, hoặc tử tế hơn, phát luôn danh sách kề. Việc của chúng ta bắt đầu từ sau đó, duyệt nó, tô màu nó, tìm đường ngắn nhất trên nó. Chúng ta được chấm điểm ở phần sau, nên chúng ta giỏi phần sau.
Ngoài kia không ai cho ai đồ thị. Ngoài kia chỉ có một bảng account_links
vài chục triệu dòng, một thư mục log, một file lockfile, một câu nói thêm
cuối cuộc họp của đội risk. Đồ thị có mặt trong tất cả những thứ đó, đầy đủ
và chính xác, nhưng nó tồn tại theo kiểu của một bức tranh chưa được căng ra
khỏi cuộn: mọi nét vẽ đều ở đó, chỉ là chưa ai nhìn thấy bức tranh. Khoảng
cách giữa kỹ sư và lời giải, hoá ra, hiếm khi là khoảng cách thuật toán. Nó
là khoảng cách giữa cuộn giấy và bức tranh, giữa dữ liệu đang nằm theo hàng
và cấu trúc đang chờ được nhận ra. Hai bước đứng trước mọi thuật toán, nhận
ra có đồ thị, và quyết định vẽ nó như thế nào, là hai bước không có trong
giáo trình, không có trong vòng phỏng vấn, không có trong bất kỳ kỳ thi nào
chúng ta từng vượt qua.
Sự lệch pha này không phải lỗi của riêng ai, nó là sản phẩm phụ của cách kiến thức được đóng gói để truyền đi. Thuật toán đóng gói được: nó có đầu vào xác định, đầu ra chấm được, độ phức tạp chứng minh được, nên nó vào được giáo trình, vào được kỳ thi, vào được vòng phỏng vấn. Còn cái nhìn, cái khoảnh khắc một người nghe câu "liên hệ trong vòng ba bước" và thấy các đỉnh với cạnh hiện lên sau lưng câu chữ, thì không đóng gói theo cách đó được, nên nó bị để lại cho kinh nghiệm, nghĩa là để lại cho may rủi, cho việc bạn có tình cờ ngồi cạnh đúng người, vấp đúng sự cố, vào đúng lúc còn đủ tò mò để hỏi tại sao hay không.
Mà nếu nhận diện là một kỹ năng, thì nó phải học được. Phải có dấu hiệu, những dấu hiệu nằm ngay trong cách một yêu cầu được phát biểu, kiểm tra được như một cái checklist, không cần đợi trực giác loé lên sau mười năm va vấp. Câu chuyện đầu chương có ít nhất hai dấu hiệu như thế nằm phơi ra từ buổi sáng thứ ba đó, trước cả dòng SQL đầu tiên.
Còn người kỹ sư của chúng ta, họ chưa từng có dịp đọc lại như vậy. Sau lần tối ưu thứ ba, câu query chạy đủ nhanh để đội risk thôi phàn nàn, ticket được đóng, sprint kết thúc, mọi người chuyển sang việc khác. Một kết thúc êm đẹp theo mọi tiêu chuẩn của nghề. Và có lẽ đó là điều đáng tiếc nhất trong cả câu chuyện, cơn đau dừng lại vừa kịp lúc, ngay trước khi nó kịp buộc ai đó đặt đúng câu hỏi.