DuckDB

शेड्यूल की गई DuckDB पाइपलाइन, अपने डेटाबेस के आगे एक और डेटाबेस रखे बिना।

DuckDB प्रोसेस के भीतर और बिना सर्वर के चलता है, इसीलिए इसमें शेड्यूलर, रीट्राई और रन इतिहास नहीं है। Dagu यह ऑपरेशनल परत एक ही बाइनरी से जोड़ता है, इसलिए पूरा स्टैक दो एक्ज़ीक्यूटेबल और एक डेटा डायरेक्टरी ही रहता है।

एक बाइनरी दूसरी बाइनरी को ऑर्केस्ट्रेट करती है, संचालित करने योग्य कोई डेटाबेस नहीं
max_active_runs DuckDB के सिंगल-राइटर नियम को लागू करता है
इंक्रीमेंटल लोड के कर्सर प्रोसेस रीस्टार्ट के बाद भी बने रहते हैं
डेटा के पास चलता है, बंद नेटवर्क के भीतर भी
01

एम्बेडेड होने का अर्थ है, डिज़ाइन से ही कोई शेड्यूलर नहीं

DuckDB आपकी प्रोसेस के भीतर चलता है। कोई डेमॉन नहीं, कोई पोर्ट नहीं, और रात तीन बजे क्वेरी चलाने के लिए प्रतीक्षा करता हुआ कुछ भी नहीं। यह डिज़ाइन का उद्देश्य है, चूक नहीं, और इसका अर्थ है कि शेड्यूल बाहर से आना चाहिए।

  • DuckDB पाइपलाइन आमतौर पर एक CLI कॉल होती है, जिससे cron डिफ़ॉल्ट उत्तर बन जाता है और अधिकांश टीमें वहीं रुक जाती हैं
  • cron केवल निष्पादन देता है: S3 टाइमआउट होने पर कोई रीट्राई नहीं, कोई इतिहास नहीं, और जब पिछली रात चुपचाप कुछ नहीं हुआ तो कोई संकेत नहीं
  • समुदाय का cron एक्सटेंशन प्रोसेस के भीतर शेड्यूल करता है, इसलिए प्रोसेस के साथ शेड्यूल भी समाप्त हो जाता है और कोई रन इतिहास नहीं बचता
02

जिस कारण आपने DuckDB चुना, उसे मत मिटाइए

DuckDB का आकर्षण यह है कि न क्लस्टर है, न सर्वर, न वेयरहाउस का बिल। उसके आगे एक भारी ऑर्केस्ट्रेटर रखने से यह सब वापस आ जाता है। Airflow को शेड्यूलर, मेटाडेटा डेटाबेस और Python DAG फ्रेमवर्क चाहिए। Kestra को JDBC डेटाबेस, ऑब्जेक्ट स्टोरेज और चार कंपोनेंट चाहिए। Dagu फ़ाइल-आधारित स्थिति वाली एक बाइनरी है।

  • सर्वरलेस एनालिटिक्स इंजन को शेड्यूल करने के लिए Postgres जोड़ना एक अजीब सौदा है
  • वर्कफ़्लो परिभाषाएँ उसी SQL के पास git में रहती हैं जिसे वे चलाती हैं, किसी दूसरे प्लेटफ़ॉर्म के मेटाडेटा में नहीं
  • पूरा स्टैक एक ही होस्ट पर आ जाता है, और आमतौर पर वही होस्ट है जहाँ डेटा पहले से मौजूद है
03

सिंगल-राइटर एक शेड्यूलिंग समस्या है

DuckDB के लिए मानक प्रोडक्शन सलाह यह है कि ओवरलैपिंग राइट्स को ऑपरेटिंग सिस्टम स्तर के लॉक से रोका जाए, क्योंकि डेटाबेस एक ही राइटर स्वीकार करता है। जब यह सुरक्षा cron और एक लॉकफ़ाइल हो, तो वह गलत होने से केवल एक स्क्रिप्ट दूर है। इसे वर्कफ़्लो पर घोषित करने से बग की पूरी श्रेणी समाप्त हो जाती है।

  • max_active_runs को एक रखने का अर्थ है कि धीमा रन डेटाबेस बिगाड़ने के बजाय अगले रन को टाल देता है
  • resources.limits.memory किसी बड़े जॉइन के होस्ट को साथ ले डूबने से पहले रन को सीमित कर देता है
  • retry_policy उन विफलताओं को संभालता है जो वास्तव में अस्थायी हैं, जैसे स्कैन के बीच ऑब्जेक्ट स्टोर का टाइमआउट

यह इस वर्कफ़्लो के समवर्ती रन को सीमित करता है। इससे DuckDB मल्टी-राइटर नहीं बन जाता: उसी फ़ाइल में लिखने वाली कोई अन्य प्रोसेस रोकना अब भी आपकी ज़िम्मेदारी है।

ऑब्जेक्ट स्टोरेज पर Parquet का रात्रिकालीन एकत्रीकरण
# duckdb-nightly-rollup.yaml
schedule: "0 3 * * *"
max_active_runs: 1

resources:
  limits:
    memory: "8Gi"

steps:
  - id: rollup
    action: duckdb@v1
    with:
      database: /data/analytics.duckdb
      query: |
        INSTALL httpfs; LOAD httpfs;
        CREATE OR REPLACE TABLE daily_sales AS
          SELECT order_date, region, sum(amount) AS amount
          FROM read_parquet('s3://warehouse/raw/orders/*.parquet')
          GROUP BY 1, 2;
    retry_policy:
      limit: 2
      interval_sec: 300

  - id: export
    action: duckdb@v1
    with:
      database: /data/analytics.duckdb
      query: |
        COPY daily_sales TO '/data/export/daily_sales.parquet' (FORMAT parquet);
    depends: rollup

  - id: count_rows
    action: duckdb@v1
    with:
      database: /data/analytics.duckdb
      readonly: true
      query: SELECT count(*) AS row_count FROM daily_sales;
    depends: export

  - id: verify
    env:
      - COUNT_JSON: ${steps.count_rows.outputs.result}
    run: test "$(printf '%s\n' "$COUNT_JSON" | jq -r '.[0].row_count')" -gt 0
    depends: count_rows

handler_on:
  failure:
    run: /opt/analytics/notify-failure.sh

mail_on:
  failure: true
04

इंक्रीमेंटल लोड को ऐसा कर्सर चाहिए जो प्रोसेस से अधिक जिए

हर रन में सब कुछ दोबारा लोड करना ही वह चीज़ है जो एक तेज़ लोकल क्वेरी को धीमा और महँगा बना देती है। इंक्रीमेंटल लोड को याद रखना होता है कि पिछला सफल रन कहाँ समाप्त हुआ, और यह स्मृति बिना किसी डेटाबेस के रीस्टार्ट झेलनी चाहिए।

  • Dagu रन के बीच एक छोटा JSON कर्सर सहेजता है, इसलिए हर रन केवल वही विंडो पढ़ता है जो अभी लोड नहीं हुई
  • कर्सर लोड चरण के सफल होने के बाद सहेजा जाता है, इसलिए विफल रन उसे अछूता छोड़ देता है और अगला रन वही विंडो दोहराता है
  • विंडो को दोनों सिरों पर बाँधने से रन के दौरान आने वाली पंक्तियों के साथ दौड़ नहीं लगती
स्थायी वॉटरमार्क के साथ इंक्रीमेंटल अपेंड
# duckdb-incremental-load.yaml
schedule: "*/15 * * * *"
max_active_runs: 1

steps:
  - id: load_cursor
    action: state.get
    output: CURSOR
    with:
      key: cursors/events-loaded-through
      default:
        loaded_through: "2026-01-01T00:00:00Z"

  - id: window
    run: |
      printf 'since=%s\n' "$(printf '%s\n' "$CURSOR" | jq -r .value.loaded_through)" >> "$DAGU_OUTPUT_FILE"
      printf 'until=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$DAGU_OUTPUT_FILE"
    outputs:
      - name: since
      - name: until
    depends: load_cursor

  - id: append_new_events
    action: duckdb@v1
    with:
      database: /data/analytics.duckdb
      query: |
        INSTALL httpfs; LOAD httpfs;
        INSERT INTO events
          SELECT * FROM read_parquet('s3://warehouse/events/*.parquet')
          WHERE ingested_at >  TIMESTAMP '${steps.window.outputs.since}'
            AND ingested_at <= TIMESTAMP '${steps.window.outputs.until}';
    depends: window
    retry_policy:
      limit: 3
      interval_sec: 60

  - id: save_cursor
    action: state.set
    with:
      key: cursors/events-loaded-through
      value:
        loaded_through: "${steps.window.outputs.until}"
    depends: append_new_events

mail_on:
  failure: true
05

यह संयोजन कहाँ कमज़ोर विकल्प है

Dagu DuckDB को शेड्यूल करता है। यह नहीं बदलता कि DuckDB क्या है, और कुछ वर्कलोड ऐसे हैं जहाँ यह संयोजन गलत उत्तर है।

  • DuckDB सिंगल-नोड और सिंगल-राइटर है। यदि कई सेवाओं को एक साथ लिखना है, तो कोई ऑर्केस्ट्रेटर इसे ठीक नहीं करता
  • यह बार-बार होने वाले छोटे राइट्स के लिए नहीं बना है; वह अब भी PostgreSQL का काम है
  • यदि आप पहले से Airflow या अपने शेड्यूलर वाला वेयरहाउस चला रहे हैं, तो एक पाइपलाइन के लिए दूसरा ऑर्केस्ट्रेटर जोड़ना शायद ही सार्थक है

FAQ

Practical questions before adopting

क्या DuckDB के लिए कोई एक्ज़ीक्यूटर या प्लगइन है?

नहीं, और यह जानबूझकर है। DuckDB एक सिंगल-बाइनरी CLI देता है जो पहले से काम कर देती है, इसलिए Dagu उसे एक सामान्य कमांड चरण के रूप में चलाता है। समर्पित एक्ज़ीक्यूटर केवल एक ऐसी परत जोड़ेगा जिसे DuckDB की रिलीज़ के साथ बनाए रखना पड़ेगा, और वह CLI से कम ही देगा।

दो रन डेटाबेस फ़ाइल को खराब न करें, इसके लिए क्या करूँ?

वर्कफ़्लो पर max_active_runs को एक करें। चल रहा रन अगले निर्धारित रन को रोक देता है, दूसरा राइटर नहीं खोलता — यह उसी लॉकफ़ाइल का घोषणात्मक रूप है जिसे DuckDB का दस्तावेज़ आपसे बनवाता है। यह केवल उसी वर्कफ़्लो के रन पर लागू होता है, इसलिए अन्य प्रोसेस को उसी फ़ाइल से दूर रखें।

बड़ी क्वेरी की मेमोरी समाप्त हो जाए तो क्या होता है?

वर्कफ़्लो पर resources.limits.memory सेट करें ताकि रन होस्ट को साथ ले डूबने से पहले सीमित हो जाए, और चरण को अस्थायी विफलताओं के लिए रीट्राई नीति दें, संरचनात्मक के लिए नहीं। जिस क्वेरी को होस्ट से अधिक मेमोरी चाहिए, उसे दोबारा लिखना चाहिए, दोहराना नहीं।

DuckDB के पास समुदाय का cron एक्सटेंशन है। उसे क्यों न इस्तेमाल करें?

वह DuckDB प्रोसेस के भीतर शेड्यूल करता है, इसलिए शेड्यूल तभी तक रहता है जब तक वह प्रोसेस जीवित है। न रन इतिहास, न रीट्राई, न विफलता पर सूचना, और न अगली सुबह देखने को कुछ। यह लंबे समय चलने वाले एम्बेडेड एप्लिकेशन के लिए ठीक है, सर्वर पर बैच काम के लिए नहीं।

क्या शेड्यूल किए गए रन में DuckDB S3 से पढ़ सकता है?

हाँ, httpfs एक्सटेंशन के ज़रिए, और अधिकांश शेड्यूल किया गया DuckDB काम इसी तरह अलग एक्सट्रैक्ट चरण के बिना Parquet पढ़ता है। चूँकि ऑब्जेक्ट स्टोरेज एक नेटवर्क निर्भरता है, यही वह चरण है जिसे रीट्राई नीति देना सार्थक है।

Next step

Start with one workflow.

Install Dagu, move one script that runs on cron today into YAML, and decide from a real run history.