基于BigQuery递归SQL计算项目进度的开始与结束时间
Alright, let's solve this recursive project scheduling problem in BigQuery. You need to calculate start and finish dates for each activity based on their predecessors, following specific rules—here's a complete solution tailored to your requirements.
Input Activity Data
First, let's structure your raw input into a clear table for reference:
| ID | NAME | DURATION | P1 | P2 | P3 |
|---|---|---|---|---|---|
| 1 | A | 14 | |||
| 2 | B | 15 | 1 | ||
| 3 | C | 15 | 1 | ||
| 4 | D | 12 | 1 | ||
| 5 | E | 22 | 2 | 3 | |
| 6 | F | 14 | 4 | 1 | |
| 7 | G | 9 | 5 | 6 |
Expected Calculated Schedule
The final output you're aiming for (with dates formatted for readability):
| ID | NAME | DURATION | START | FINISH | P1 | P2 | P3 |
|---|---|---|---|---|---|---|---|
| 1 | A | 14 | 1-Jan-17 | 14-Jan-17 | |||
| 2 | B | 15 | 15-Jan-17 | 29-Jan-17 | 1 | ||
| 3 | C | 15 | 15-Jan-17 | 29-Jan-17 | 1 | ||
| 4 | D | 12 | 15-Jan-17 | 26-Jan-17 | 1 | ||
| 5 | E | 22 | 30-Jan-17 | 20-Feb-17 | 2 | 3 | |
| 6 | F | 14 | 27-Jan-17 | 9-Feb-17 | 4 | 1 | |
| 7 | G | 9 | 21-Feb-17 | 1-Mar-17 | 5 | 6 |
Recursive BigQuery SQL Solution
This query uses BigQuery's WITH RECURSIVE syntax to iterate through dependencies, resolve dates, and stop once all activities have finalized start/finish values:
WITH RECURSIVE activities AS ( -- Step 1: Prep raw data, convert P columns to an array for flexible predecessor handling SELECT id, name, duration, -- Combine non-null predecessor IDs into a single array ARRAY_CONCAT( IF(p1 IS NOT NULL, [p1], []), IF(p2 IS NOT NULL, [p2], []), IF(p3 IS NOT NULL, [p3], []) ) AS predecessors, -- Initialize dates for activities with no predecessors CASE WHEN p1 IS NULL AND p2 IS NULL AND p3 IS NULL THEN DATE('2017-01-01') ELSE NULL END AS start_date, CASE WHEN p1 IS NULL AND p2 IS NULL AND p3 IS NULL THEN DATE_ADD(DATE('2017-01-01'), INTERVAL (duration - 1) DAY) ELSE NULL END AS finish_date FROM -- Replace this with your actual BigQuery table reference `your-project.your-dataset.your-table` ), recursive_schedule AS ( -- Base case: Start with activities that have no dependencies (dates already known) SELECT * FROM activities WHERE start_date IS NOT NULL UNION ALL -- Recursive step: Calculate dates for activities where all predecessors are processed SELECT a.id, a.name, a.duration, a.predecessors, -- Start date = max of 2017-01-01 and (latest predecessor finish + 1 day) GREATEST( DATE('2017-01-01'), DATE_ADD(MAX(rs.finish_date), INTERVAL 1 DAY) ) AS start_date, -- Finish date = start date + duration - 1 days DATE_ADD( GREATEST( DATE('2017-01-01'), DATE_ADD(MAX(rs.finish_date), INTERVAL 1 DAY) ), INTERVAL (a.duration - 1) DAY ) AS finish_date FROM activities a JOIN recursive_schedule rs ON rs.id IN UNNEST(a.predecessors) -- Only process activities where all predecessors are already in the recursive schedule WHERE a.start_date IS NULL AND (SELECT COUNT(*) FROM UNNEST(a.predecessors) p WHERE p NOT IN (SELECT id FROM recursive_schedule)) = 0 GROUP BY a.id, a.name, a.duration, a.predecessors ) -- Final output: Format dates to match your expected style, and include original P columns SELECT rs.id, rs.name, rs.duration, FORMAT_DATE('%d-%b-%y', rs.start_date) AS START, FORMAT_DATE('%d-%b-%y', rs.finish_date) AS FINISH, a.p1, a.p2, a.p3 FROM recursive_schedule rs JOIN `your-project.your-dataset.your-table` a ON rs.id = a.id ORDER BY rs.id;
How This Works
Let’s break down the logic to make it clear:
- Base CTE (
activities): We transform the raw input to store predecessors as an array (this handles variable numbers of dependencies, not just 1-3). We also set initial dates for activities with no predecessors, since their start is fixed to2017-01-01. - Recursive CTE (
recursive_schedule):- Base Case: Starts with all activities that have no dependencies—their dates are already finalized.
- Recursive Step: For each activity with unknown dates, we check if all its predecessors have been processed. If yes, we calculate the start date as the later of
2017-01-01or the latest predecessor's finish date plus one day. The finish date is derived by adding the duration (minus one day) to the start date.
- Final Output: We join back to the original table to include the original P1/P2/P3 columns, format dates to match your desired readability, and sort by activity ID.
Key Notes
- Replace
your-project.your-dataset.your-tablewith your actual BigQuery table path. - This solution scales if you add more predecessor columns later—just update the
ARRAY_CONCATsection to include the new columns. - The recursion stops automatically once all activities have calculated dates, as there will be no more unprocessed activities left.
内容的提问来源于stack exchange,提问作者Baha Hussein
相关产品推荐
相关产品推荐

