Mở đầu — vì sao bài này quan trọng
Ở Bài 8, bạn đã làm quen với nền tảng JQL (Jira Query Language): cú pháp cơ bản, các toán tử =, !=, IN, AND, OR, và cách lọc theo project, status, assignee. Đó là bộ khung xương. Nhưng khi bạn bước vào công việc PM thực chiến — quản lý sprint đang chạy, theo dõi bug tồn đọng, chuẩn bị báo cáo cho ban lãnh đạo — thì JQL cơ bản nhanh chóng trở nên chật chội. Bạn sẽ liên tục gặp những câu hỏi mà một điều kiện lọc tĩnh không trả lời được: "Những issue nào trong sprint hiện tại vẫn chưa xong?", "Có bug nào đã nằm im quá 14 ngày mà không ai đụng đến?", "Story nào assign cho team frontend nhưng đến hạn tuần này mà chưa được review?".
Đây chính là địa hạt của JQL nâng cao. Bài 9 sẽ dạy bạn ba nhóm vũ khí mà một PM giỏi phải nằm lòng: hàm động (functions) như openSprints(), currentUser(), startOfWeek(); toán tử thời gian tương đối để đo "tuổi" của issue; và các toán tử quan hệ như WAS, CHANGED, IN với sub-query để truy vào lịch sử thay đổi. Khác biệt cốt lõi giữa JQL cơ bản và nâng cao nằm ở chữ động: thay vì gõ tay "sprint = Sprint 23", bạn viết một truy vấn tự cập nhật theo thời gian và ngữ cảnh, để bộ lọc bạn lưu hôm nay vẫn đúng sau ba tháng.
Với PM, kỹ năng này không phải để khoe mẽ kỹ thuật. Nó trực tiếp tiết kiệm cho bạn hàng giờ mỗi tuần: filter chuẩn nuôi dashboard chuẩn, dashboard chuẩn cho ra báo cáo chuẩn, và báo cáo chuẩn giúp bạn ra quyết định nhanh hơn cả team. Hãy xem đây là bài học biến bạn từ người "dùng Jira" thành người "điều khiển Jira".
Khái niệm cốt lõi
Hàm động (JQL Functions) — bộ lọc tự cập nhật theo thời gian
Hàm trong JQL là những từ khóa có dấu ngoặc đơn () trả về một tập giá trị được tính lại mỗi lần truy vấn chạy. Đây là điểm khiến chúng "sống động" hơn giá trị tĩnh.
Nhóm hàm về sprint quan trọng nhất với PM:
openSprints()— trả về tất cả các sprint đang ở trạng thái active (đã start, chưa complete). Rất hữu ích khi bạn quản lý nhiều board và không muốn sửa tên sprint mỗi hai tuần.closedSprints()— các sprint đã đóng, dùng để phân tích lịch sử.futureSprints()— các sprint đã tạo trong backlog nhưng chưa start.
project = "MOB" AND sprint IN openSprints() AND status != Done
Câu này đọc là: "Trong dự án MOB, lấy mọi issue thuộc các sprint đang chạy mà chưa ở trạng thái Done." Bạn lưu filter này một lần, và nó đúng vĩnh viễn — sprint 23, 24, 25 trôi qua, câu truy vấn vẫn tự bám theo sprint hiện hành.
Nhóm hàm về người dùng:
currentUser()— trả về chính người đang chạy truy vấn.assignee = currentUser()cho ra một filter "việc của tôi" dùng chung cho cả team mà không ai phải sửa tên.membersOf("tên-group")— trả về mọi thành viên của một group.assignee IN membersOf("team-frontend")lọc việc của cả nhóm.
Toán tử thời gian tương đối — đo "tuổi" và mốc thời gian
Đây là phần PM hay bỏ sót nhưng lại giá trị nhất. Jira cho phép so sánh ngày tháng bằng biểu thức tương đối thay vì ngày cứng.
- Đơn vị:
w(tuần),d(ngày),h(giờ),m(phút). Ví dụ-14dnghĩa là "14 ngày trước". - Dùng với các trường ngày như
created,updated,duedate,resolved.
project = "WEB" AND issuetype = Bug AND status = Open AND updated <= -14d
Câu này tìm mọi bug đang mở nhưng không được cập nhật trong 14 ngày qua — chính xác là bài toán "bug aging" (bug bị bỏ quên) trong ghi chú gốc. Lưu ý logic: updated <= -14d nghĩa là thời điểm cập nhật gần nhất nằm ở mốc 14 ngày trước hoặc xa hơn nữa, tức issue đã "im lặng" ít nhất hai tuần.
Bộ hàm mốc thời gian giúp câu truy vấn đọc tự nhiên hơn:
startOfDay(),endOfDay(),startOfWeek(),endOfWeek(),startOfMonth(),endOfMonth().- Chúng nhận cả tham số offset:
endOfWeek()là cuối tuần này,endOfWeek("1w")hoặcendOfWeek(1)là cuối tuần sau.
assignee = currentUser() AND duedate <= endOfWeek() AND resolution = Unresolved
"Việc của tôi, đến hạn trong tuần này, chưa xong." Đây là câu bạn nên gắn vào dashboard cá nhân.
Toán tử lịch sử — WAS, CHANGED, WAS IN
JQL cơ bản chỉ nhìn trạng thái hiện tại của issue. JQL nâng cao cho phép truy vào lịch sử thay đổi — một sức mạnh riêng biệt.
WAS— issue từng ở một giá trị nào đó.status WAS "In Progress"lấy mọi issue đã từng In Progress, kể cả nay đã Done.CHANGED— một trường đã thay đổi.status CHANGEDkết hợp với mệnh đề bổ nghĩaFROM,TO,BY,AFTER,BEFORE,DURING.WAS IN/WAS NOT IN— từng (không) ở trong một tập giá trị.
project = "MOB" AND status CHANGED FROM "In Progress" TO "Reopened" AFTER -30d
Câu này phát hiện các issue bị reopen trong 30 ngày qua — tín hiệu cảnh báo chất lượng hoặc acceptance criteria chưa rõ. Không có toán tử lịch sử, bạn không thể trả lời câu hỏi này bằng JQL.
Sub-query với IN và trường liên kết
Bạn có thể lồng một truy vấn để lọc theo issue liên quan:
issueFunction(từ plugin ScriptRunner, phổ biến ở nhiều tổ chức) hoặc các trườngparent,"Epic Link".
"Epic Link" = MOB-100 AND status != Done AND sprint IN openSprints()
"Các story con của Epic MOB-100, chưa xong, trong sprint hiện tại." Rất tiện khi bạn theo dõi tiến độ một epic lớn xuyên nhiều sprint.
Sắp xếp và thu hẹp kết quả — ORDER BY
Đừng quên ORDER BY để kết quả có thể đọc được. ORDER BY priority DESC, updated ASC đưa việc ưu tiên cao và cũ nhất lên đầu — trật tự tự nhiên để một PM quét mắt xử lý.
Tình huống thực tế
Tình huống 1 — Tiki: dọn bug tồn đọng trước mùa sale 11/11
Một PM tại đội Payment của Tiki (giả định hợp lý theo bối cảnh thương mại điện tử Việt Nam) chuẩn bị cho đợt sale 11/11. Board của đội có gần 400 issue, trong đó bug lẫn lộn giữa cũ và mới. Ban đầu cô lọc thủ công issuetype = Bug AND status = Open, ra 87 bug — quá nhiều để xử lý trong hai tuần còn lại.
Cô chuyển sang câu truy vấn nâng cao đo tuổi bug:
project = "PAY" AND issuetype = Bug AND resolution = Unresolved
AND updated <= -14d AND priority IN (High, Highest)
ORDER BY created ASC
Kết quả rút xuống còn 19 bug: ưu tiên cao, đã im lặng hơn hai tuần, cũ nhất lên đầu. Cô gắn câu này vào một filter tên "Bug Aging - Payment" và tạo dashboard theo dõi số này mỗi sáng. Sau một tuần, con số về 4.
Bài học: Toán tử thời gian tương đối updated <= -14d biến một đống việc mơ hồ thành danh sách hành động cụ thể. Con số "tuổi bug" là chỉ báo sức khỏe backlog mà lãnh đạo hiểu ngay, không cần giải thích kỹ thuật.
Tình huống 2 — Grab: phát hiện issue "nhảy sprint" liên tục
Một PM ở đội tài xế của Grab (bối cảnh Đông Nam Á) nghi ngờ team đang cam kết quá tay mỗi sprint: nhiều story cứ bị đẩy từ sprint này sang sprint khác mà không xong. Trực giác thì có, nhưng cần bằng chứng để nói chuyện với team trong retro.
Anh dùng toán tử lịch sử để bắt các issue từng nằm trong nhiều sprint:
project = "DRV" AND sprint IN openSprints()
AND status WAS "To Do" AFTER -28d
AND "Story Points" >= 5
ORDER BY "Story Points" DESC
Câu này lọc ra story điểm lớn (>= 5) trong sprint hiện tại nhưng vẫn còn dấu vết "To Do" trong 28 ngày qua — dấu hiệu chúng đã bị kéo qua ít nhất hai sprint mà chưa thực sự khởi động. Anh tìm ra 6 story, tổng 41 điểm, phần lớn là các task phụ thuộc vào team khác. Với bằng chứng cụ thể, cuộc retro chuyển từ "cảm giác" sang thảo luận về việc chia nhỏ story và làm rõ dependency.
Bài học: WAS và các mệnh đề AFTER/BEFORE cho phép PM nhìn vào lịch sử, không chỉ hiện tại. Đây là công cụ định lượng cho những vấn đề trước đây chỉ nói bằng cảm tính.
Tình huống 3 — VNG: dashboard "việc của tôi tuần này" dùng chung cho cả PM team
Tại VNG, một nhóm 5 PM quản lý nhiều dự án game chia sẻ chung vài board. Trước đây mỗi người tự tạo filter cá nhân bằng cách gõ tên mình vào assignee, dẫn đến hàng chục filter trùng lặp, khó bảo trì. Trưởng nhóm PM quyết định chuẩn hóa bằng một filter động duy nhất:
assignee = currentUser() AND resolution = Unresolved
AND (duedate <= endOfWeek() OR sprint IN openSprints())
ORDER BY duedate ASC, priority DESC
Vì dùng currentUser(), cùng một filter cho ra kết quả khác nhau tùy người đăng nhập — mỗi PM thấy đúng việc của mình. Filter được lưu ở cấp shared, gắn vào một dashboard chung. Số lượng filter cá nhân giảm từ hơn 30 xuống còn 1, và khi cần đổi logic (ví dụ thêm điều kiện lọc theo priority), họ chỉ sửa một chỗ.
Bài học: currentUser() biến một filter thành template cá nhân hóa. Nó vừa giảm gánh nặng bảo trì, vừa đảm bảo cả team dùng chung một định nghĩa "việc cần làm".
Hướng dẫn từng bước
Cách xây dựng và triển khai một filter JQL nâng cao một cách bài bản:
- Xác định câu hỏi nghiệp vụ trước, JQL sau. Viết ra bằng tiếng Việt: "Tôi muốn thấy bug ưu tiên cao bị bỏ quên hơn 2 tuần." Câu hỏi rõ thì truy vấn mới đúng.
- Bắt đầu từ bộ lọc tĩnh, chạy thử. Vào Filters → Advanced issue search, gõ điều kiện cơ bản trước:
project = "PAY" AND issuetype = Bug. Xác nhận có kết quả hợp lý.
- Thêm dần từng điều kiện nâng cao và kiểm tra sau mỗi bước. Thêm
AND resolution = Unresolved, chạy lại. Rồi thêmAND updated <= -14d, chạy lại. Cách "cộng dồn" này giúp bạn biết ngay điều kiện nào làm sai kết quả.
- Dùng chế độ gợi ý của Jira. Khi gõ ở chế độ Advanced (JQL), Jira tự động gợi ý tên trường, hàm và giá trị hợp lệ. Với trường có dấu cách như Story Points hay Epic Link, luôn bọc trong ngoặc kép:
"Story Points".
- Thêm ORDER BY để kết quả đọc được. Không có sắp xếp, danh sách chỉ là đống dữ liệu.
ORDER BY priority DESC, created ASClà điểm khởi đầu tốt cho hầu hết filter theo dõi công việc.
- Lưu filter và đặt tên rõ ràng. Nhấn Save as, đặt tên theo công thức [Chủ đề] - [Phạm vi], ví dụ "Bug Aging - Payment". Đặt quyền chia sẻ (share) cho đúng nhóm cần dùng.
- Gắn filter vào dashboard hoặc board. Filter đã lưu có thể nuôi các gadget dashboard (Filter Results, Two Dimensional Filter Statistics) — nội dung bạn sẽ đào sâu ở Bài 19. Ở đây, chỉ cần nhớ: filter tốt là nền móng cho mọi báo cáo về sau.
Lỗi thường gặp & mẹo
- Nhầm
WASvới=.status = "In Progress"là "hiện đang In Progress".status WAS "In Progress"là "đã từng In Progress". Dùng nhầm sẽ ra tập kết quả sai hoàn toàn. Khi cần nhìn lịch sử, luôn nghĩ đếnWAS/CHANGED.
- Nhầm dấu của thời gian tương đối.
updated >= -7dlà "cập nhật trong 7 ngày gần đây" (issue mới động).updated <= -7dlà "im lặng hơn 7 ngày" (issue cũ). PM hay gõ ngược dấu khi tìm bug tồn đọng. Đọc to câu truy vấn để kiểm tra.
- Quên
resolution = Unresolved. Rất nhiều PM lọcstatus != Donenhưng vẫn dính issue đã Closed/Cancelled. Trong nhiều cấu hình Jira, cách chuẩn để lọc "việc chưa xong thực sự" làresolution = Unresolved(hoặcresolution IS EMPTY), vì đây là trường phản ánh việc đã có kết luận hay chưa, độc lập với tên status.
- Gõ giá trị tĩnh vào chỗ nên dùng hàm. Viết
sprint = "Sprint 24"khiến filter chết ngay khi sang sprint mới. Dùngsprint IN openSprints()để filter tự sống.
- Quên ngoặc kép cho trường có dấu cách.
Story Points > 5báo lỗi;"Story Points" > 5mới chạy. Áp dụng cho mọi tên trường và giá trị chứa dấu cách.
- Không dùng ngoặc đơn khi trộn AND với OR.
A AND B OR Cdễ bị hiểu sai thứ tự ưu tiên. Luôn nhóm rõ:A AND (B OR C).
- Mẹo — lưu filter theo tầng. Tạo một filter "gốc" cho phạm vi chung (ví dụ
project = "PAY" AND sprint IN openSprints()) rồi các filter con tham chiếu tới nó bằngfilter = "Tên filter gốc" AND .... Điều này giúp bảo trì tập trung.
- Mẹo — kiểm chứng bằng cách đảo ngược. Nếu nghi câu truy vấn bỏ sót, chạy phiên bản đảo (
!=thành=) và cộng hai kết quả lại xem có khớp tổng số issue không.
Bài tập thực hành
Chọn một project thật bạn có quyền truy cập trong Jira (hoặc dùng site jira miễn phí để thử) và viết các filter sau. Với mỗi câu, hãy viết ra câu hỏi nghiệp vụ bằng tiếng Việt trước, rồi mới dịch sang JQL, và cuối cùng kiểm tra kết quả có hợp lý không.
- Sprint chưa xong: Liệt kê mọi issue trong các sprint đang chạy của project bạn chọn mà chưa Done. Gợi ý dùng
openSprints()vàresolution = Unresolved.
- Bug tồn đọng: Tìm bug ưu tiên High trở lên, chưa xong, không được cập nhật trong 10 ngày. Sắp xếp cũ nhất lên đầu.
- Việc của tôi tuần này: Viết filter động cho "việc assign cho tôi, chưa xong, đến hạn trước cuối tuần này" bằng
currentUser()vàendOfWeek().
- Reopen gần đây: Tìm mọi issue đã bị chuyển sang trạng thái Reopened (hoặc trở lại "In Progress" từ "Done") trong 30 ngày qua. Dùng
status CHANGED ... AFTER -30d.
- Thử thách: Kết hợp — story điểm >= 3, thuộc một Epic cụ thể, trong sprint hiện tại, chưa được assign cho ai (
assignee IS EMPTY). Đây là danh sách "việc trong epic còn chưa có chủ" để bạn phân công.
Tóm tắt
JQL nâng cao là bước nhảy từ "lọc tĩnh" sang "lọc động" — và đó chính là thứ biến JQL thành công cụ vận hành hằng ngày của PM. Ba nhóm vũ khí cần nhớ: hàm động (openSprints(), currentUser(), endOfWeek()) giúp filter tự cập nhật theo thời gian và ngữ cảnh; toán tử thời gian tương đối (-14d, updated <= -14d) giúp đo tuổi issue và bắt việc bị bỏ quên; toán tử lịch sử (WAS, CHANGED, AFTER) giúp nhìn vào quá khứ chứ không chỉ hiện tại.
Nguyên tắc vàng: luôn xuất phát từ câu hỏi nghiệp vụ, xây truy vấn theo kiểu cộng dồn từng điều kiện, và đừng quên resolution = Unresolved cùng ORDER BY. Qua ba tình huống ở Tiki, Grab và VNG, bạn thấy rõ mỗi câu truy vấn tốt đều gắn với một quyết định thật: dọn bug trước mùa sale, đưa bằng chứng vào retro, hay chuẩn hóa dashboard cho cả team.
Những filter bạn xây hôm nay chính là nguyên liệu cho các bài sắp tới về custom fields (Bài 18), dashboard và gadget (Bài 19), cũng như báo cáo built-in (Bài 20). Hãy luyện đến khi viết một câu JQL nâng cao trở thành phản xạ — lúc đó bạn không còn chỉ dùng Jira, mà thật sự điều khiển nó.