
फाइनेंशियल डेटा वेयरहाउस के लिए SQL
स्ट्रक्चर्ड फाइनेंशियल डेटा को क्वेरी, एग्रीगेट और एनालाइज़ करना
SQL फाइनेंशियल डेटा की भाषा क्यों है
हर ट्रेड, हर टिक, ऑर्डर बुक का हर अपडेट — फाइनेंशियल मार्केट्स स्ट्रक्चर्ड डेटा की हैरतअंगेज़ मात्रा पैदा करते हैं। और चार दशकों से भी ज़्यादा समय से, SQL (Structured Query Language) उस डेटा को क्वेरी, ट्रांसफॉर्म, और एनालाइज़ करने का सबसे प्रमुख टूल रहा है। यह चमकदार नहीं है। यह ट्रेंडी नहीं है। लेकिन जब कोई पोर्टफोलियो मैनेजर पूछता है “पिछले मंगलवार दोपहर 2 बजे से 3 बजे UTC के बीच ETH ट्रेड्स पर मेरी औसत फिल कीमत क्या थी?”, तो जवाब SQL से ही आता है।
फाइनेंशियल डेटा स्वभाव से ही relational होता है। एक trade किसी order का संदर्भ लेता है, जो किसी account का संदर्भ लेता है, जो किसी user का है। पोज़िशंस इंस्ट्रूमेंट्स से जुड़ी होती हैं; इंस्ट्रूमेंट्स मार्केट्स से जुड़े होते हैं। relational मॉडल — पंक्तियों और कॉलम वाली टेबल्स, foreign key से जुड़ी हुई — इन रिश्तों पर स्वाभाविक रूप से फिट होता है। इसीलिए PostgreSQL, MySQL, और SQL Server जैसे relational डेटाबेस Goldman Sachs से लेकर GaiaEx तक फाइनेंशियल इंफ्रास्ट्रक्चर की रीढ़ बने हुए हैं।
SQL की ताकत इसकी declarative प्रकृति में है: आप बताते हैं क्या चाहिए, कैसे पाना है नहीं। डेटाबेस इंजन सबसे बेहतर एग्ज़ीक्यूशन प्लान खुद तय करता है। यह बहुत मायने रखता है जब आपकी trade history टेबल में 50 करोड़ पंक्तियाँ हों और आपको एक सेकंड से भी कम में नतीजे चाहिए हों। सही क्वेरी लिखें, सही इंडेक्स बनाएँ, और SQL नतीजे दे देता है — चाहे आप उसे किसी लोकल PostgreSQL इंस्टेंस पर चला रहे हों या क्लाउड-स्केल डेटा वेयरहाउस पर।
SELECT, JOIN, और WHERE: Trade डेटा को क्वेरी करना
एक ठोस schema से शुरुआत करते हैं। GaiaEx जैसे किसी प्लेटफॉर्म पर एक trades टेबल की कल्पना करें:
CREATE TABLE trades (
trade_id BIGINT PRIMARY KEY,
user_id INT NOT NULL,
symbol VARCHAR(20) NOT NULL,
side VARCHAR(4) NOT NULL, -- 'buy' or 'sell'
price NUMERIC(18,8) NOT NULL,
quantity NUMERIC(18,8) NOT NULL,
fee NUMERIC(18,8) DEFAULT 0,
executed_at TIMESTAMPTZ NOT NULL
);
सबसे बुनियादी ऑपरेशन है फिल्टरिंग के साथ SELECT स्टेटमेंट। पिछले 24 घंटों में $3,000 से ऊपर के सभी ETH-USD buy trades ढूँढने के लिए:
SELECT trade_id, price, quantity, executed_at
FROM trades
WHERE symbol = 'ETH-USD'
AND side = 'buy'
AND price > 3000
AND executed_at > NOW() - INTERVAL '24 hours'
ORDER BY executed_at DESC;
JOIN संबंधित टेबल्स को जोड़ते हैं। मान लें आपके पास एक accounts टेबल है और आप हर account tier के हिसाब से ट्रेडिंग वॉल्यूम देखना चाहते हैं:
SELECT a.tier, COUNT(*) AS trade_count,
SUM(t.price * t.quantity) AS total_volume
FROM trades t
JOIN accounts a ON t.user_id = a.user_id
WHERE t.executed_at > NOW() - INTERVAL '30 days'
GROUP BY a.tier
ORDER BY total_volume DESC;
ये क्वेरी फाइनेंशियल एनालिस्ट, रिस्क टीम, और कंप्लायंस अधिकारियों की रोज़मर्रा की रोटी हैं। इन्हें अच्छे से समझ लें और आप अपने डेटा में मौजूद किसी भी सवाल का जवाब दे सकते हैं।
Window Functions: एनालिस्ट का गुप्त हथियार
Window functions ही वह चीज़ है जो फाइनेंस में SQL के शुरुआती लोगों को अनुभवी प्रैक्टिशनर से अलग करती है। ये आपको current row से संबंधित पंक्तियों के एक समूह पर कैलकुलेशन करने देते हैं — बिना नतीजे को एक ही aggregated वैल्यू में समेटे। इन्हें अपने डेटा पर चलने वाले “running computations” की तरह सोचें।
फाइनेंशियल एनालिसिस में सबसे ज़्यादा इस्तेमाल होने वाले window functions:
- ROW_NUMBER() — हर partition के भीतर हर row को क्रमबद्ध integer देता है। deduplication या हर symbol का Nवाँ trade चुनने के लिए उपयोगी।
- LAG() और LEAD() — पिछली या अगली row की वैल्यू तक पहुँच देते हैं। trade-to-trade returns निकालने या टाइम-सीरीज़ डेटा में गैप पहचानने के लिए ज़रूरी।
- SUM() OVER () — Running totals। cumulative वॉल्यूम, cumulative P&L, या running position size निकालें।
- AVG() OVER (ROWS BETWEEN) — SQL में सीधे moving averages। Python की ज़रूरत नहीं।
यहाँ एक व्यावहारिक उदाहरण है — BTC के लिए trade-to-trade price change और running position निकालना:
SELECT executed_at, price, quantity, side,
price - LAG(price) OVER (ORDER BY executed_at) AS price_change,
SUM(CASE WHEN side = 'buy' THEN quantity ELSE -quantity END)
OVER (ORDER BY executed_at) AS running_position
FROM trades
WHERE symbol = 'BTC-USD'
ORDER BY executed_at;
Window functions आपकी row count कम नहीं करते — GROUP BY के उलट, हर input row एक output row बनाता है। इससे ये विस्तृत trade logs में जानकारी की बारीकी खोए बिना एनालिटिकल कॉलम जोड़ने के लिए एकदम सही होते हैं। इन्हें अपने डेटा को “summarize” करने के बजाय “enrich” करने के तरीके के रूप में सोचें।
OHLCV एग्रीगेशन के लिए — जो कैंडलस्टिक चार्ट का आधार है — आप GROUP BY को टाइम-बकेटिंग और स्टैंडर्ड एग्रीगेट्स के साथ जोड़ सकते हैं:
SELECT date_trunc('hour', executed_at) AS bucket,
(ARRAY_AGG(price ORDER BY executed_at))[1] AS open,
MAX(price) AS high,
MIN(price) AS low,
(ARRAY_AGG(price ORDER BY executed_at DESC))[1] AS close,
SUM(quantity) AS volume
FROM trades
WHERE symbol = 'ETH-USD'
GROUP BY bucket
ORDER BY bucket;CTE: जटिल एनालिटिकल क्वेरी को जोड़-जोड़कर बनाना
Common Table Expressions (CTE) आपको एक जटिल क्वेरी को नाम वाले, पढ़ने में आसान चरणों में तोड़ने देते हैं — जैसे प्रोग्रामिंग में फंक्शन। ये WITH कीवर्ड के साथ शुरू होते हैं और एक-दूसरे को क्रम में संदर्भित कर सकते हैं।
मान लें आप अपने 10 सबसे मुनाफे वाले ट्रेडिंग दिन जानना चाहते हैं, लेकिन मुनाफे के लिए फीस शामिल करते हुए हर दिन का net P&L निकालना ज़रूरी है। CTE के साथ, आप इसे चरण-दर-चरण बना सकते हैं:
WITH daily_trades AS (
SELECT DATE(executed_at) AS trade_date,
SUM(CASE WHEN side = 'sell' THEN price * quantity
ELSE -price * quantity END) AS gross_pnl,
SUM(fee) AS total_fees
FROM trades
WHERE user_id = 42
GROUP BY DATE(executed_at)
),
daily_pnl AS (
SELECT trade_date,
gross_pnl - total_fees AS net_pnl,
SUM(gross_pnl - total_fees) OVER (ORDER BY trade_date) AS cumulative_pnl
FROM daily_trades
)
SELECT trade_date, net_pnl, cumulative_pnl
FROM daily_pnl
ORDER BY net_pnl DESC
LIMIT 10;
हर CTE एक पैराग्राफ की तरह पढ़ा जाता है। daily_trades कच्चे trades को दैनिक सारांशों में एग्रीगेट करता है। daily_pnl net P&L और running total निकालता है। आख़िरी SELECT टॉप 10 दिन चुनता है। इसे एक अकेली विशाल subquery से तुलना करें — CTE वर्शन मेंटेनेबल है, टेस्ट करने योग्य है, और खुद ही अपनी व्याख्या करता है।
प्रोडक्शन फाइनेंशियल सिस्टम्स में, CTE का इस्तेमाल रेगुलेटरी रिपोर्टिंग (सभी accounts में margin requirement एग्रीगेट करना) से लेकर रीयल-टाइम डैशबोर्ड क्वेरी (GaiaEx पर rolling 24-घंटे का वॉल्यूम निकालना) तक हर चीज़ में होता है। ये उसी तरह जुड़ते हैं जैसे अच्छे से बनाया गया कोड जुड़ता है — हर लेयर पिछली लेयर पर बनती है।
टाइम-सीरीज़ परफॉर्मेंस के लिए Indexing और Partitioning
कोई क्वेरी तभी तेज़ होती है जब उसे सपोर्ट करने वाला इंडेक्स सही हो। सही इंडेक्स के बिना, एक सादा WHERE क्लॉज़ भी sequential scan करने पर मजबूर करता है — टेबल की हर row पढ़ना। 50 करोड़ रो पर, यह मिनटों में लगता है, मिलीसेकंड में नहीं।
फाइनेंशियल टाइम-सीरीज़ डेटा के लिए सबसे अहम इंडेक्स पैटर्न है (symbol, executed_at) पर एक composite B-tree इंडेक्स:
CREATE INDEX idx_trades_symbol_time
ON trades (symbol, executed_at DESC);
यह एक ही इंडेक्स अधिकतर एनालिटिकल क्वेरी को तेज़ करता है: “पिछले एक घंटे के सभी ETH trades,” “दो timestamps के बीच के BTC trades,” या “हर symbol का सबसे नया trade।” कॉलम का क्रम अहम है — पहले symbol होने से equality फिल्टरिंग संभव होती है, फिर executed_at इंडेक्स के उस हिस्से के भीतर कुशल range scan संभव बनाता है।
करोड़ों रो से भी बड़ी हो जाने वाली टेबल्स के लिए, table partitioning ज़रूरी है। PostgreSQL रेंज के हिसाब से declarative partitioning सपोर्ट करता है — जो टाइम-सीरीज़ के लिए एकदम फिट है:
CREATE TABLE trades (
trade_id BIGINT, symbol VARCHAR(20),
price NUMERIC(18,8), executed_at TIMESTAMPTZ
) PARTITION BY RANGE (executed_at);
CREATE TABLE trades_2026_q1 PARTITION OF trades
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
CREATE TABLE trades_2026_q2 PARTITION OF trades
FOR VALUES FROM ('2026-04-01') TO ('2026-07-01');
Partitioning के साथ, मार्च 2026 के डेटा के लिए एक क्वेरी सिर्फ Q1 पार्टिशन को स्कैन करती है — इंजन Q2, Q3, और Q4 को पूरी तरह छोड़ देता है। इस तकनीक को partition pruning कहा जाता है, और यह बड़े हिस्टॉरिकल डेटासेट पर कई गुना स्पीडअप देती है।
PostgreSQL बनाम ClickHouse बनाम BigQuery: सही इंजन चुनना
सभी डेटाबेस फाइनेंशियल एनालिटिक्स को एक जैसी दक्षता से नहीं संभालते। चुनाव आपके क्वेरी पैटर्न, डेटा वॉल्यूम, और लेटेंसी की ज़रूरतों पर निर्भर करता है।
PostgreSQL मुख्य कार्यभार उठाने वाला (workhorse) है। यह transactional वर्कलोड (रीयल-टाइम में trades दर्ज करना) में बेहतरीन है, ACID गारंटी सपोर्ट करता है, जटिल JOIN को अच्छे से संभालता है, और सही इंडेक्सिंग और पार्टिशनिंग के साथ करोड़ों रो तक स्केल करता है। यह operational डेटाबेस — system of record — के लिए सही चुनाव है। उदाहरण के लिए, GaiaEx अपने मुख्य ट्रेडिंग डेटा के लिए PostgreSQL-कम्पैटिबल इंफ्रास्ट्रक्चर पर उन reliability गारंटियों के साथ निर्भर करता है जो फाइनेंशियल सिस्टम्स से अपेक्षित होती हैं।
ClickHouse अरबों रो पर एनालिटिकल क्वेरी के लिए डिज़ाइन किया गया एक columnar डेटाबेस है। जहाँ PostgreSQL डेटा को row-by-row स्टोर करता है (individual trades डालने के लिए बढ़िया), ClickHouse डेटा को column-by-column स्टोर करता है (लाखों रो में एक ही कॉलम को एग्रीगेट करने के लिए बढ़िया)। जैसे “3 सालों में BTC के लिए औसत घंटे-वार वॉल्यूम” जैसी क्वेरी PostgreSQL में 30 सेकंड ले सकती है, वह ClickHouse में 200 मिलीसेकंड में पूरी हो सकती है। ट्रेड-ऑफ: ClickHouse UPDATE या DELETE को कुशलता से सपोर्ट नहीं करता — यह डिज़ाइन से ही append-only है।
BigQuery (Google Cloud) एक सर्वरलेस columnar वेयरहाउस है। मैनेज करने के लिए कोई इंफ्रास्ट्रक्चर नहीं, pay-per-query प्राइसिंग, और पेटाबाइट-स्केल क्षमता। यह ad-hoc एनालिटिक्स, रिसर्च, और एक्सप्लोरेशन के लिए आदर्श है — लेकिन क्वेरी लेटेंसी सेकंड में मापी जाती है, मिलीसेकंड में नहीं, जिससे यह रीयल-टाइम एप्लिकेशन के लिए उपयुक्त नहीं है।
- रीयल-टाइम trade रिकॉर्डिंग और ऑर्डर मैनेजमेंट → PostgreSQL
- अरबों रो पर हिस्टॉरिकल एनालिटिक्स और बैकटेस्टिंग → ClickHouse
- Ad-hoc एक्सप्लोरेशन और क्रॉस-टीम डेटा शेयरिंग → BigQuery
कई प्रोफेशनल ट्रेडिंग फर्म तीनों का इस्तेमाल एक tiered आर्किटेक्चर में करती हैं: PostgreSQL को hot operational स्टोर के रूप में, ClickHouse को warm एनालिटिकल लेयर के रूप में, और BigQuery को cold आर्काइव के रूप में। अच्छा SQL लिखने का हुनर इन सभी में ट्रांसफर होता है — सिंटैक्स 90% एक जैसा है, और एनालिटिकल सोच 100% एक जैसी है।