Skip to content

ClickHouse PIPE_* Table Duplicate Row Analysis

Generated: 2026-06-21
Database: thebicycle (localhost:8123)

STATUS UPDATE (2026-06-21): All duplicate rows have been cleaned and the root cause has been fixed. See files/DDL/ch_migration_repartition_scenario_id.sql for the partition key migration, and files/scripts/cleanup_pipe_duplicates.py for the one-time cleanup script. All 27 PIPE_ tables now have 0 exact duplicates*. The remaining "inexact duplicates" (same ORDER BY key, different values) in some tables are either legitimate business data (e.g., multiple exception types per item/week) or stochastic method samples — these are documented per-table below.

Summary

# Table Engine Total Rows Distinct Keys Exact Dups Inexact Dups Max Dup Factor Severity
1 PIPE_supply_pegging MergeTree 5,578,753 716,662 2,455,471 2,406,620 360 CRITICAL
2 PIPE_backtest_forecast MergeTree 5,879,016 3,163,680 2,240,306 475,030 5 CRITICAL
3 PIPE_demand_corrected MergeTree 669,460 59,416 610,044 0 50 CRITICAL
4 PIPE_series_exceptions MergeTree 326,577 40,691 238,416 47,470 39 HIGH
5 PIPE_forecast_point_values MergeTree 1,402,428 895,678 136,706 370,044 12 HIGH
6 PIPE_forecast_netting MergeTree 897,780 511,732 321,397 64,651 10 HIGH
7 PIPE_indirect_demand MergeTree 900,786 731,286 136,072 33,428 6 MEDIUM
8 PIPE_supply_exceptions MergeTree 41,863 21,635 102 20,126 40 MEDIUM
9 PIPE_supply_orders MergeTree 58,933 54,276 0 4,657 38 MEDIUM
10 PIPE_segment_membership MergeTree 80,568 67,573 12,995 0 3 MEDIUM
11 PIPE_series_backtest_metrics MergeTree 39,326 18,814 20,480 32 5 MEDIUM
12 PIPE_on_hand_snapshot MergeTree 9,599 4,769 575 4,255 MEDIUM
13 PIPE_causal_forecast MergeTree 96,347 25,737 0 70,610 6 MEDIUM
14 PIPE_meio_group_results MergeTree 5,506 513 3,006 1,987 48 MEDIUM
15 PIPE_abc_results MergeTree 20,700 17,250 3,450 0 3 LOW
16 PIPE_meio_results MergeTree 16,956 14,140 723 2,093 3 LOW
17 PIPE_series_best_methods MergeTree 4,126 3,116 721 289 3 LOW
18 PIPE_fitted_distributions MergeTree 25,661 24,737 917 7 12 LOW
19 PIPE_series_characteristics MergeTree 5,174 5,173 1 0 2 TRIVIAL

Clean Tables (no duplicates)

Table Engine Rows
PIPE_backtest_fingerprints ReplacingMergeTree 575
PIPE_lead_time_actuals ReplacingMergeTree 20
PIPE_maintenance_forecast MergeTree 6,102
PIPE_order_arrival_actuals ReplacingMergeTree 20
PIPE_preprocessed ReplacingMergeTree 8,661
PIPE_series_hashes ReplacingMergeTree 575
PIPE_service_actuals ReplacingMergeTree 20
PIPE_supply_inventory_projection MergeTree 111,228
  • PIPE_forecast_fingerprints — pipeline-scoped forecast skip cache (pipeline_id, scenario_id, item_id, site_id, fingerprint, computed_at); ReplacingMergeTree(computed_at).

Duplicate Types Explained

  • Exact duplicates: Rows where ALL columns are identical — same key, same values. Caused by re-running pipelines without first dropping the old partition (missing drop_ch_partition before INSERT).
  • Inexact duplicates: Same ORDER BY key but different non-key column values. Caused by:
  • Multiple scenario_ids producing different results for the same key (scenario_id not in ORDER BY key)
  • Multiple runs producing slightly different numerical results (e.g., stochastic methods)
  • ORDER BY key granularity too coarse (missing order_id, supply_week, etc. from the sort key)

Detailed Analysis Per Table

1. PIPE_supply_pegging — CRITICAL

  • ORDER BY: pipeline_id, scenario_id, archive_date, item_id, site_id, demand_week
  • Total: 5,578,753 rows → 716,662 distinct keys (87% are duplicates)
  • Exact dups: 2,455,471 | Inexact dups: 2,406,620
  • Max dup factor: 360×
  • Root cause: ORDER BY key excludes order_id and supply_week. Same item/site/demand_week can legitimately have multiple pegging records (one per supply order). These are NOT true duplicates — the ORDER BY key is too coarse. Additionally, exact duplicates suggest pipeline re-runs without partition cleanup.

Sample inexact duplicate (different order_id, supply_week, qty):

pipeline=4  scenario=0  archive=2026-04-13  item=15  site=302  demand_week=33 → 9 rows, 5 distinct versions
pipeline=4  scenario=0  archive=2026-06-08  item=18  site=301  demand_week=0  → 6 rows, 6 distinct versions

Sample exact duplicate (same all columns):

pipeline=4  scenario=0  archive=2026-03-09  → 323,694 total vs 25,056 distinct (2,986,388 exact dups per archive_date)

Action: 1. Add order_id, supply_week to ORDER BY key to make inexact duplicates distinct rows 2. Add drop_ch_partition before INSERT to eliminate exact duplicates


2. PIPE_backtest_forecast — CRITICAL

  • ORDER BY: pipeline_id, archive_date, item_id, site_id, method, forecast_origin, horizon_step
  • Total: 5,879,016 rows → 3,163,680 distinct keys (46% duplicates)
  • Exact dups: 2,240,306 | Inexact dups: 475,030
  • Max dup factor: 5×

Archive distribution: | archive_date | pipeline | total | distinct | dups | |-------------|----------|-------|----------|------| | 2026-03-09 | 4 | 1,050,504 | 545,376 | 505,128 | | 2026-04-13 | 4 | 1,050,504 | 545,376 | 505,128 | | 2026-05-11 | 4 | 1,050,504 | 545,376 | 505,128 | | 2026-05-11 | 85 | 546,624 | 546,624 | 0 | | 2026-06-01 | 85 | 546,624 | 546,624 | 0 | | 2026-06-08 | 4 | 134,316 | 134,316 | 0 | | 2026-06-15 | 4 | 1,499,940 | 299,988 | 1,199,952 |

Sample exact duplicate (different point_forecast):

pipeline=4  archive=2026-05-11  unique_id=3_301  method=AutoARIMA  origin=2025-10-12  step=29
  → row1: point_forecast=5.8383
  → row2: point_forecast=7.3432

Action: Ensure drop_ch_partition is called before INSERT; investigate why scenario_id is not in ORDER BY key (different scenarios produce different forecasts for same key)


3. PIPE_demand_corrected — CRITICAL

  • ORDER BY: pipeline_id, archive_date, item_id, site_id, date
  • Total: 669,460 rows → 59,416 distinct keys (91% are exact duplicates!)
  • All duplicates are EXACT (all columns identical including scenario_id=4)
  • Max dup factor: 50×

Archive distribution: | archive_date | total | distinct | dups | |-------------|-------|----------|------| | 2026-03-09 | 15,844 | 3,961 | 11,883 | | 2026-03-16 | 15,844 | 3,961 | 11,883 | | 2026-03-23 | 15,844 | 3,961 | 11,883 | | ... | ... | ... | ... | | 2026-05-11 | 118,830 | 7,922 | 110,908 | | 2026-06-01 | 221,836 | 7,922 | 213,914 | | 2026-06-08 | 134,674 | 3,961 | 130,713 |

Sample exact duplicate (all 50 rows identical):

pipeline=85  archive=2026-06-01  item=21  site=303  date=2024-03-03
  original_value=4.0  corrected_value=3.5  detection_method=iqr  z_score=20.235
  → 50 identical copies, scenario_id=4 for all

Root cause: Pipeline runs are inserting without dropping the partition first. The outlier correction step is being re-run and appending duplicates. The deduplication factor varies (4→50) suggesting different numbers of re-runs.

Action: Add drop_ch_partition(archive_date, pipeline_id) before INSERT in the outlier correction pipeline step.


4. PIPE_series_exceptions — HIGH

  • ORDER BY: pipeline_id, archive_date, item_id, site_id, exc_id
  • Total: 326,577 rows → 40,691 distinct keys
  • Exact dups: 238,416 | Inexact dups: 47,470
  • Max dup factor: 39×
  • Note: scenario_id is NOT in ORDER BY key, so different scenarios produce different exception rows for the same key

Sample inexact duplicate (different scenario_id, metric_value, item_name):

pipeline=4  archive=2026-04-27  unique_id=9_334  exc_id=supply_shortage
  → row1: scenario=0, metric_value=217.25, item_name='', exc_type=supply
  → row2: scenario=4, metric_value=104.00, item_name='HB-RC', exc_type=supply

Sample exact duplicate (scenario=4, identical values):

  → row2 and row3 are exact copies (scenario=4, metric_value=104.0)

Action: 1. Add scenario_id to ORDER BY key (exceptions differ per scenario) 2. Add drop_ch_partition before INSERT


5. PIPE_forecast_point_values — HIGH

  • ORDER BY: pipeline_id, scenario_id, archive_date, item_id, site_id, method, forecast_week
  • Total: 1,402,428 rows → 895,678 distinct keys
  • Exact dups: 136,706 | Inexact dups: 370,044
  • Max dup factor: 12×

Sample inexact duplicate (different point_forecast for causal method):

pipeline=1  scenario=1  archive=2026-06-15  unique_id=21_100  method=causal  week=2025-03-07
  → 5 rows with point_forecast: 0.578, 0.480, 0.518, 0.032, 0.018

Root cause: Causal forecast with stochastic sampling produces different point_forecast values on each run. Multiple runs without partition cleanup.

Action: Deduplicate by keeping only the latest run per archive_date (partition drop before INSERT).


6. PIPE_forecast_netting — HIGH

  • ORDER BY: pipeline_id, archive_date, item_id, site_id, forecast_week
  • Total: 897,780 rows → 511,732 distinct keys
  • Exact dups: 321,397 | Inexact dups: 64,651
  • Max dup factor: 10×

Sample inexact duplicate (different stat_forecast_qty):

pipeline=4  archive=2026-06-01  unique_id=14_331  forecast_week=2026-10-11
  → row1: stat_forecast_qty=1.146, scenario_id=0
  → row2: stat_forecast_qty=1.309, scenario_id=0
  → row3: stat_forecast_qty=1.309, scenario_id=0  (exact dup of row2)

Action: scenario_id not in ORDER BY; add partition drop + consider adding scenario_id to key.


7. PIPE_indirect_demand — MEDIUM

  • ORDER BY: pipeline_id, scenario_id, archive_date, item_id, site_id, date
  • Total: 900,786 rows → 731,286 distinct keys
  • Exact dups: 136,072 | Inexact dups: 33,428
  • Max dup factor: 6×

Sample exact duplicate:

pipeline=4  scenario=0  archive=2026-06-01  unique_id=WS-ZP_CW-NL  date=2024-10-06
  → 2 identical rows: qty=1.0, demand_type=network

Action: Add drop_ch_partition before INSERT.


8. PIPE_supply_exceptions — MEDIUM

  • ORDER BY: pipeline_id, scenario_id, archive_date, item_id, site_id, week
  • Total: 41,863 rows → 21,635 distinct keys
  • Exact dups: 102 | Inexact dups: 20,126
  • Max dup factor: 40×
  • Note: exception_type and severity are NOT in ORDER BY. Same item/site/week can have multiple exception types (e.g., UnmetDemand vs Shortage).

Sample inexact duplicate:

pipeline=4  scenario=1  archive=2026-06-15  item=11  site=201  week=6
  → row1: exception_type=UnmetDemand, severity=Critical, qty=6.56
  → row2: exception_type=Shortage, severity=Warning, qty=6.56

Action: Add exception_type to ORDER BY key (legitimate business duplicates, not data errors).


9. PIPE_supply_orders — MEDIUM

  • ORDER BY: pipeline_id, scenario_id, archive_date, item_id, site_id, arrival_week
  • Total: 58,933 rows → 54,276 distinct keys
  • Exact dups: 0 | Inexact dups: 4,657
  • Max dup factor: 38×
  • Note: id (order ID) is NOT in ORDER BY. Same item/site/arrival_week can have multiple orders (different order_type, source_site_id).

Sample inexact duplicate (different orders for same key):

pipeline=85  scenario=1  archive=2026-06-08  item=7  site=201  arrival_week=47
  → order_id=1025: REPAIR, qty=8, source=0
  → order_id=1001015: TRANSFER, qty=9, source=100

Action: Add id (order_id) to ORDER BY key (these are NOT true duplicates).


10. PIPE_segment_membership — MEDIUM

  • ORDER BY: pipeline_id, archive_date, segment_id, item_id, site_id
  • Total: 80,568 rows → 67,573 distinct keys
  • All duplicates are EXACT (12,995 exact, 0 inexact)
  • Max dup factor: 3×

Sample exact duplicate:

pipeline=4  archive=2026-06-01  segment=1249  unique_id=16_305
  → 2 identical rows: participation_weight=1.0

Action: Add drop_ch_partition before INSERT.


11. PIPE_series_backtest_metrics — MEDIUM

  • ORDER BY: pipeline_id, archive_date, item_id, site_id, method, forecast_origin
  • Total: 39,326 rows → 18,814 distinct keys
  • Exact dups: 20,480 | Inexact dups: 32
  • Max dup factor: 5×

Sample exact duplicate:

pipeline=4  archive=2026-06-15  unique_id=6_323  method=AutoETS  origin=__AGG__
  → 3 identical rows: mae=0, smape=0, scenario_id=4

Action: Add drop_ch_partition before INSERT.


12. PIPE_on_hand_snapshot — MEDIUM

  • ORDER BY: archive_date, item_id, site_id, type_id
  • Total: 9,599 rows → 4,769 distinct keys
  • Exact dups: 575 | Inexact dups: 4,255
  • No pipeline_id column — uses snapshot_ts instead

Sample inexact duplicate (different snapshot_ts):

archive=2026-06-15  item=6  site=345  type=1
  → snapshot_ts=2026-06-16 20:38:22  qty=5.6
  → snapshot_ts=2026-06-16 20:41:48  qty=5.6
  → snapshot_ts=2026-06-17 15:53:32  qty=5.6
  → snapshot_ts=2026-06-18 10:21:57  qty=5.6
  → snapshot_ts=2026-06-18 10:37:25  qty=5.6

Root cause: Multiple snapshot runs within the same archive_date, each inserting without clearing previous snapshots. Same data but different timestamps.

Action: Add snapshot_ts to ORDER BY key, or drop existing rows for the same archive_date before inserting new snapshots.


13. PIPE_causal_forecast — MEDIUM

  • ORDER BY: pipeline_id, scenario_id, archive_date, item_id, site_id, forecast_week
  • Total: 96,347 rows → 25,737 distinct keys
  • All duplicates are INEXACT (0 exact, 70,610 inexact)
  • Max dup factor: 6×

Sample inexact duplicate (stochastic causal forecast):

pipeline=1  scenario=1  archive=2026-06-15  unique_id=4_201  forecast_week=2026-07-10
  → 3 rows with qty: 0.021, 0.033, 0.042

Root cause: Causal forecast engine uses stochastic sampling, producing different qty values on each run. Multiple runs accumulate without cleanup.

Action: Add drop_ch_partition before INSERT to ensure only the latest run's results are kept.


14. PIPE_meio_group_results — MEDIUM

  • ORDER BY: pipeline_id, scenario_id, archive_date, group_name
  • Total: 5,506 rows → 513 distinct keys (91% duplicates!)
  • Exact dups: 3,006 | Inexact dups: 1,987
  • Max dup factor: 48×

Sample duplicate (slightly different achieved_fill_rate due to iterative optimization):

pipeline=4  scenario=10  archive=2026-04-06  group_name=A items
  → achieved_fill_rate=0.9501337, achieved_budget=26695
  → achieved_fill_rate=0.9501975, achieved_budget=26707
  → achieved_fill_rate=0.9501975, achieved_budget=26707  (exact dup)

Root cause: MEIO optimization writes intermediate iterations as separate rows instead of only the final result.

Action: Ensure only the final MEIO result is written (drop partition before final INSERT, or add iteration counter + keep only last).


15. PIPE_abc_results — LOW

  • ORDER BY: pipeline_id, archive_date, config_id, item_id, site_id
  • Total: 20,700 rows → 17,250 distinct keys
  • All duplicates are EXACT (3,450 exact, 0 inexact)
  • Max dup factor: 3×

Sample exact duplicate:

pipeline=4  archive=2026-06-01  config=23  unique_id=3_331
  → 2 identical rows: class_label=Y, metric_value=53.0, rank=541, scenario_id=4

Action: Add drop_ch_partition before INSERT.


16. PIPE_meio_results — LOW

  • ORDER BY: pipeline_id, scenario_id, archive_date, item_id, site_id
  • Total: 16,956 rows → 14,140 distinct keys
  • Exact dups: 723 | Inexact dups: 2,093
  • Max dup factor: 3×

Sample inexact duplicate (different optimization results):

pipeline=4  scenario=10  archive=2026-06-01  item=9  site=201
  → row1: committed_buffer=1.0, fill_rate=0.9954
  → row2: committed_buffer=0.0, fill_rate=0.9054

Action: Same as PIPE_meio_group_results — keep only final optimization result.


17. PIPE_series_best_methods — LOW

  • ORDER BY: pipeline_id, scenario_id, archive_date, item_id, site_id
  • Total: 4,126 rows → 3,116 distinct keys
  • Exact dups: 721 | Inexact dups: 289
  • Max dup factor: 3×

Sample exact duplicate:

pipeline=4  scenario=4  archive=2026-05-11  unique_id=9_304
  → 3 identical rows: best_method=AutoTheta, best_score=0.15

Action: Add drop_ch_partition before INSERT.


18. PIPE_fitted_distributions — LOW

  • ORDER BY: pipeline_id, scenario_id, archive_date, item_id, site_id, method
  • Total: 25,661 rows → 24,737 distinct keys
  • Exact dups: 917 | Inexact dups: 7
  • Max dup factor: 12×

Sample exact duplicate:

pipeline=4  scenario=4  archive=2026-04-20  unique_id=7_301  method=AutoTheta
  → 3 identical rows: distribution_type=weibull, mean=2.460, std=1.123

Action: Add drop_ch_partition before INSERT.


19. PIPE_series_characteristics — TRIVIAL

  • ORDER BY: pipeline_id, scenario_id, archive_date, item_id, site_id
  • Total: 5,174 rows → 5,173 distinct keys (1 duplicate key)
  • Exact dup: 1 | Inexact dup: 0

The single duplicate:

pipeline=4  scenario=4  archive=2026-05-11  unique_id=7_301 → 2 identical rows
  has_seasonality=1, has_trend=1, is_intermittent=0, adi=1.0, complexity_score=0.328

Action: Trivial — can be cleaned manually or with partition drop fix.


Root Causes Summary

1. Missing drop_ch_partition before INSERT (biggest issue)

Most exact duplicates are caused by pipeline steps that INSERT without first dropping the existing partition for (archive_date, pipeline_id). When a pipeline re-runs (e.g., after parameter changes), new rows are appended rather than replacing old ones.

Affected tables: PIPE_demand_corrected, PIPE_abc_results, PIPE_segment_membership, PIPE_series_backtest_metrics, PIPE_series_best_methods, PIPE_fitted_distributions, PIPE_series_characteristics, PIPE_indirect_demand, PIPE_backtest_forecast, PIPE_forecast_netting

2. ORDER BY key too coarse (inexact "duplicates" that are legitimate rows)

Some tables have ORDER BY keys that exclude columns needed to distinguish legitimate different records: - PIPE_supply_pegging: missing order_id, supply_week - PIPE_supply_exceptions: missing exception_type - PIPE_supply_orders: missing id (order_id) - PIPE_series_exceptions: missing scenario_id - PIPE_forecast_netting: missing scenario_id

3. Stochastic/iterative methods writing multiple results

  • PIPE_causal_forecast: stochastic sampling produces different qty on each run
  • PIPE_forecast_point_values: causal method same issue
  • PIPE_meio_group_results / PIPE_meio_results: iterative optimization writes intermediate results

4. Snapshot accumulation

  • PIPE_on_hand_snapshot: snapshot_ts not in ORDER BY; multiple snapshots per archive_date accumulate

  1. Add drop_ch_partition calls before all PIPE_* INSERT operations — this alone would eliminate all exact duplicates (~3.5M rows across all tables)
  2. Fix ORDER BY keys for tables where the key is too coarse (PIPE_supply_pegging, PIPE_supply_exceptions, PIPE_supply_orders, PIPE_series_exceptions, PIPE_forecast_netting) — this requires ALTER TABLE and is a schema change
  3. Ensure MEIO writes only final results — drop partition before final insert, or add a is_final flag
  4. Fix on_hand_snapshot to either clear previous snapshots for the same archive_date or add snapshot_ts to ORDER BY
  5. One-time cleanup: Run INSERT INTO ... SELECT DISTINCT ... or OPTIMIZE TABLE FINAL to clean existing duplicates (note: OPTIMIZE on large tables is expensive)

Resolution (2026-06-21)

All actions have been implemented:

Root cause fix

  • 5 tables repartitioned to add scenario_id to PARTITION BY + ORDER BY, matching ch_schema.sql: PIPE_demand_corrected, PIPE_backtest_forecast, PIPE_forecast_netting, PIPE_series_backtest_metrics, PIPE_series_exceptions — Migration: files/DDL/ch_migration_repartition_scenario_id.sql
  • _PARTITION_COL_NAMES fixed for PIPE_on_hand_snapshot in files/db/db.py (("pipeline_id",)("archive_date",))
  • freeze.py _PIPE_TABLES updated to move the 5 repartitioned tables to has_scenario=True

Preventive code fixes

  • supply_runner.py: Added dedup before bulk_insert_ch("PIPE_supply_pegging") — the Rust engine merges pass_a + pass_c pegging without dedup
  • meio_runner.py: Added dedup before both bulk_insert_ch calls for PIPE_meio_results and PIPE_meio_group_results — keeps last iteration per key

One-time cleanup

  • files/scripts/cleanup_pipe_duplicates.py: Recreated 12 tables with row_number() OVER (PARTITION BY <key>) = 1 dedup
  • Total rows removed: ~6,160,000 across all tables

Final state

All 27 PIPE_ tables have 0 exact duplicates. Remaining "inexact duplicates" (same ORDER BY key, different values) in some tables are either: - Legitimate business data: multiple exception types per item/week (PIPE_supply_exceptions), multiple orders per item/site/arrival_week (PIPE_supply_orders), multiple pegging links per item/site/demand_week (PIPE_supply_pegging) - Stochastic method samples*: causal forecast produces different point_forecast values per run (PIPE_forecast_point_values, PIPE_causal_forecast) — these are lower priority (Step 8 in the plan)

Backup tables

17 *_pre_repart and *_pre_clean backup tables are kept for manual verification. Drop after confirming pipeline runs correctly:

-- Run this after verifying pipelines work:
DROP TABLE IF EXISTS thebicycle.PIPE_demand_corrected_pre_repart;
DROP TABLE IF EXISTS thebicycle.PIPE_backtest_forecast_pre_repart;
DROP TABLE IF EXISTS thebicycle.PIPE_forecast_netting_pre_repart;
DROP TABLE IF EXISTS thebicycle.PIPE_series_backtest_metrics_pre_repart;
DROP TABLE IF EXISTS thebicycle.PIPE_series_exceptions_pre_repart;
DROP TABLE IF EXISTS thebicycle.PIPE_abc_results_pre_clean;
DROP TABLE IF EXISTS thebicycle.PIPE_segment_membership_pre_clean;
DROP TABLE IF EXISTS thebicycle.PIPE_indirect_demand_pre_clean;
DROP TABLE IF EXISTS thebicycle.PIPE_series_best_methods_pre_clean;
DROP TABLE IF EXISTS thebicycle.PIPE_fitted_distributions_pre_clean;
DROP TABLE IF EXISTS thebicycle.PIPE_series_characteristics_pre_clean;
DROP TABLE IF EXISTS thebicycle.PIPE_on_hand_snapshot_pre_clean;
DROP TABLE IF EXISTS thebicycle.PIPE_supply_pegging_pre_clean;
DROP TABLE IF EXISTS thebicycle.PIPE_meio_results_pre_clean;
DROP TABLE IF EXISTS thebicycle.PIPE_meio_group_results_pre_clean;
DROP TABLE IF EXISTS thebicycle.PIPE_forecast_point_values_pre_clean;
DROP TABLE IF EXISTS thebicycle.PIPE_supply_exceptions_pre_clean;