टेस्टर्स के लिए SQL
टेस्टर को डेटाबेस डिज़ाइन नहीं करना होता, पर उससे सटीक सवाल पूछने होते हैं: क्या यह ऑर्डर सही लिखा गया, क्या totals मेल खाते हैं, क्या कुछ orphan या duplicate है, क्या ज़रूरी values मौजूद हैं। यह पेज इन सवालों को एक छोटे, काल्पनिक schema — customers, orders, order items और payments — पर चलने वाली SQL के रूप में देता है, ताकि आप उन्हें अपनी टेबलों के हिसाब से ढाल सकें।
हर integrity जाँच ऐसे लिखी गई है कि सही डेटाबेस शून्य rows लौटाए, और लौटी हर row bug report का सबूत है; एक बदलाव को key से देखने वाली queries और count-sum summaries इसके बजाय values लौटाती हैं जिनकी तुलना आप expected result से करते हैं। Queries ऐसा syntax इस्तेमाल करती हैं जो PostgreSQL, MySQL 8 और SQLite पर चलता है, जब तक नोट कुछ और न कहे; आख़िरी sections dialect अंतर और ad-hoc queries को दोबारा इस्तेमाल होने वाली suite में बदलने के बारे में हैं।
कुछ भी चलाने से पहले: सुरक्षित तरीका
Shared डेटाबेस से जुड़ा SQL client शक्तिशाली tool है और incident पैदा करने का आसान रास्ता भी। Verification पढ़ना है, इसलिए ज़्यादातर समय आपके पास लिखने की क्षमता होनी ही नहीं चाहिए। जब अपने environment में डेटा बदलना ही हो, तो बदलाव करने से पहले उसका दायरा साबित करें।
- Verification के लिए read-only अकाउंट इस्तेमाल करें। अगर आपका अकाउंट लिख सकता है, तो एक चूक पूरी टीम के लिए outage या test data का नुकसान बन जाती है।
- Shared environment (staging, UAT, shared test database) पर उसके owner की सहमति के बिना कभी UPDATE या DELETE न चलाएँ। इसकी घोषणा करें और हूबहू statement ticket में रखें।
- किसी भी UPDATE या DELETE से पहले वही WHERE clause SELECT COUNT(*) के साथ चलाएँ और संख्या जाँचें। फिर बदलाव transaction के अंदर चलाएँ और COMMIT से पहले rows-affected count की तुलना करें।
- बड़ी टेबल खोजते समय LIMIT (या अपने dialect का समकक्ष) जोड़ें और primary key या तारीख़ सीमा जैसे indexed कॉलम पर filter करें।
- सुविधा के लिए production का personal data न देखें। अगर production access आपकी भूमिका का हिस्सा है, तो उसके साथ आने वाले access और logging नियमों का पालन करें।
-- On your own test database only
BEGIN;
SELECT COUNT(*) AS will_change
FROM orders
WHERE status = 'PENDING' AND created_at < '2026-01-01';
-- expect: 3
UPDATE orders
SET status = 'CANCELLED'
WHERE status = 'PENDING' AND created_at < '2026-01-01';
-- the client reports rows affected: must also be 3
ROLLBACK; -- COMMIT only when every number matchesउदाहरण का schema
उदाहरण चार टेबलें इस्तेमाल करते हैं। एक customer ऑर्डर देता है; हर ऑर्डर में एक या ज़्यादा item lines और शून्य या ज़्यादा payments होते हैं। Schema primary keys, unique email, NOT NULL कॉलम, status values और राशियों के लिए CHECK constraints, और foreign keys घोषित करता है। यह table-level constraint clauses इस्तेमाल करता है, जिन्हें PostgreSQL, MySQL 8 और SQLite तीनों स्वीकार करते हैं; याद रखें कि MySQL CHECK केवल 8.0.16 से लागू करता है और SQLite foreign keys केवल PRAGMA foreign_keys = ON के साथ लागू करता है।
नीचे दिए तीन business rules आगे की जाँचों का oracle हैं। आपके सिस्टम के नियम अलग होंगे; डेटाबेस टेस्टिंग का पहला क़दम हमेशा उन्हें इसी रूप में लिखना है, क्योंकि वाक्य के रूप में लिखा नियम ऐसी query में बदला जा सकता है जो उसके उल्लंघन ढूँढे।
CREATE TABLE customers (
customer_id INTEGER NOT NULL,
email VARCHAR(254) NOT NULL,
full_name VARCHAR(200) NOT NULL,
created_at TIMESTAMP NOT NULL,
CONSTRAINT pk_customers PRIMARY KEY (customer_id),
CONSTRAINT uq_customers_email UNIQUE (email)
);
CREATE TABLE orders (
order_id INTEGER NOT NULL,
customer_id INTEGER NOT NULL,
status VARCHAR(20) NOT NULL,
total_amount NUMERIC(12,2) NOT NULL,
created_at TIMESTAMP NOT NULL,
CONSTRAINT pk_orders PRIMARY KEY (order_id),
CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers (customer_id),
CONSTRAINT ck_orders_status CHECK (status IN ('PENDING', 'PAID', 'SHIPPED', 'CANCELLED')),
CONSTRAINT ck_orders_total CHECK (total_amount >= 0)
);
CREATE TABLE order_items (
order_item_id INTEGER NOT NULL,
order_id INTEGER NOT NULL,
product_sku VARCHAR(40) NOT NULL,
quantity INTEGER NOT NULL,
unit_price NUMERIC(12,2) NOT NULL,
CONSTRAINT pk_order_items PRIMARY KEY (order_item_id),
CONSTRAINT fk_items_order FOREIGN KEY (order_id) REFERENCES orders (order_id),
CONSTRAINT ck_items_quantity CHECK (quantity > 0),
CONSTRAINT ck_items_price CHECK (unit_price >= 0)
);
CREATE TABLE payments (
payment_id INTEGER NOT NULL,
order_id INTEGER NOT NULL,
amount NUMERIC(12,2) NOT NULL,
status VARCHAR(20) NOT NULL,
paid_at TIMESTAMP NULL,
CONSTRAINT pk_payments PRIMARY KEY (payment_id),
CONSTRAINT fk_payments_order FOREIGN KEY (order_id) REFERENCES orders (order_id),
CONSTRAINT ck_payments_status CHECK (status IN ('CAPTURED', 'FAILED', 'REFUNDED'))
);
-- Business rules the checks below verify:
-- R1 orders.total_amount = SUM(quantity * unit_price) of its items (no tax or discount here)
-- R2 a PAID or SHIPPED order has CAPTURED payments that add up to total_amount
-- R3 a CAPTURED payment has paid_at, and paid_at is not earlier than the order's created_atएक बदलाव की पुष्टि: key से खोजें
UI या API में action के बाद प्रभावित rows उस key से खोजें जो ऐप्लिकेशन ने दी — API response में order id, पेज URL में id — न कि “latest row” से, जो हो सकता है किसी दूसरे टेस्टर ने एक सेकंड पहले बनाई हो। SELECT * की जगह स्पष्ट कॉलम चुनें, ताकि नतीजा ठीक वही फ़ील्ड दिखाए जो आप जाँच रहे हैं और कॉलम जुड़ने पर टूटे नहीं।
Parent और children को साथ जाँचें। Create के बाद item lines सही quantity और price के साथ मौजूद हों; delete के बाद सवाल यह है कि क्या बचा, और तीन counts वाली एक row एक ही query में इसका जवाब देती है।
-- The API returned order_id 1042
SELECT o.order_id, o.status, o.total_amount, o.created_at, c.email
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.order_id = 1042;
SELECT order_item_id, product_sku, quantity, unit_price
FROM order_items
WHERE order_id = 1042
ORDER BY order_item_id;
-- After a hard delete of order 1042: nothing left behind anywhere
SELECT
(SELECT COUNT(*) FROM orders WHERE order_id = 1042) AS orders_left,
(SELECT COUNT(*) FROM order_items WHERE order_id = 1042) AS items_left,
(SELECT COUNT(*) FROM payments WHERE order_id = 1042) AS payments_left;Count और sum reconciliation
Reconciliation दो ऐसी संख्याओं की तुलना करता है जिन्हें मेल खाना चाहिए। एक डेटाबेस के भीतर यह header बनाम उसकी lines (R1) या orders बनाम payments (R2) है; सिस्टमों के बीच यह दो जगह निकाला गया वही count और sum है — order database और invoicing सर्विस, डेटाबेस और finance रिपोर्ट। HAVING के साथ GROUP BY सिर्फ़ असहमत groups लौटाता है, इसलिए खाली नतीजा pass है।
दो बातें झूठे alarms रोकती हैं। पैसे की तुलना सिर्फ़ NUMERIC या DECIMAL जैसे exact decimal types में करें; अगर राशियाँ floating point में स्टोर हैं तो यह अपने आप में finding है। और जहाँ एक तरफ़ rows न हों वहाँ COALESCE लगाएँ, क्योंकि शून्य rows पर SUM NULL देता है और NULL से तुलना कभी true नहीं होती, जिससे बिना किसी payment वाला ऑर्डर छिप जाएगा।
-- R1: order header total vs the sum of its lines
SELECT o.order_id, o.total_amount,
SUM(oi.quantity * oi.unit_price) AS lines_total
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
GROUP BY o.order_id, o.total_amount
HAVING o.total_amount <> SUM(oi.quantity * oi.unit_price);
-- R2: paid orders vs captured payments
SELECT o.order_id, o.total_amount,
COALESCE(SUM(p.amount), 0) AS captured
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.order_id AND p.status = 'CAPTURED'
WHERE o.status IN ('PAID', 'SHIPPED')
GROUP BY o.order_id, o.total_amount
HAVING COALESCE(SUM(p.amount), 0) <> o.total_amount;
-- Count and sum per status for one month, to compare with a report or another service
SELECT status, COUNT(*) AS order_count, SUM(total_amount) AS amount_sum
FROM orders
WHERE created_at >= '2026-09-01' AND created_at < '2026-10-01'
GROUP BY status
ORDER BY status;LEFT JOIN … IS NULL से orphan पहचान
Orphan वह child row है जिसका parent मौजूद नहीं। LEFT JOIN हर child row रखता है और parent न मिलने पर parent कॉलमों में NULL भर देता है, इसलिए parent key IS NULL पर filter करने से ठीक orphans की सूची मिलती है। यही ढाँचा उलटी दिशा में चलाने पर ऐसे parents मिलते हैं जिनके ज़रूरी children नहीं हैं, जैसे बिना items वाला ऑर्डर।
| Item row | oi.order_idorder_items से | o.order_idLEFT JOIN के बाद | नतीजाWHERE … IS NULL के बाद |
|---|---|---|---|
1 | 1042 | 1042ऑर्डर मौजूद है | बाहर हो जाती है |
2 | 1042 | 1042ऑर्डर मौजूद है | बाहर हो जाती है |
3 | 1099 | NULLorder 1099 मौजूद नहीं | लौटती है: orphanbug report का सबूतफ़ेल |
उदाहरण की rows: orders में order 1042 है, order 1099 नहीं। सही डेटाबेस शून्य rows लौटाता है।
Foreign keys घोषित और लागू हों तो orphans बन नहीं सकते — फिर भी वे तब दिखते हैं जब bulk load के लिए constraints बंद किए गए हों, जब SQLite foreign-key pragma के बिना चले, जब relationship दो सर्विसों के डेटाबेस में फैला हो, या जब constraint कभी बना ही न हो। इस जाँच में subquery के साथ NOT IN से बचें: अगर subquery एक भी NULL लौटाए तो NOT IN कोई row नहीं लौटाता और जाँच चुपचाप pass हो जाती है। NOT EXISTS और LEFT JOIN … IS NULL में यह समस्या नहीं है।
-- Items whose order does not exist
SELECT oi.order_item_id, oi.order_id
FROM order_items oi
LEFT JOIN orders o ON o.order_id = oi.order_id
WHERE o.order_id IS NULL;
-- Payments whose order does not exist
SELECT p.payment_id, p.order_id
FROM payments p
LEFT JOIN orders o ON o.order_id = p.order_id
WHERE o.order_id IS NULL;
-- The reverse: orders that have no items at all
SELECT o.order_id, o.status, o.created_at
FROM orders o
LEFT JOIN order_items oi ON oi.order_id = o.order_id
WHERE oi.order_item_id IS NULL;
-- The same check written with NOT EXISTS (safe when NULLs are present)
SELECT o.order_id
FROM orders o
WHERE NOT EXISTS (
SELECT 1 FROM order_items oi WHERE oi.order_id = o.order_id
);GROUP BY … HAVING COUNT(*) > 1 से duplicate पहचान
उस value पर group करें जिसे unique होना चाहिए और वे groups रखें जो एक से ज़्यादा बार आते हैं। जब व्यवसाय अलग रूपों को एक ही मानता है तो पहले normalise करें: LOWER और TRIM उन emails को पकड़ते हैं जो सिर्फ़ case या spaces में अलग हैं। Raw कॉलम पर UNIQUE constraint इन्हें नहीं रोकता — हालाँकि MySQL की डिफ़ॉल्ट case-insensitive collations “Layla.H@example.com” और “layla.h@example.com” को बराबर मानती हैं, जबकि PostgreSQL टेक्स्ट की तुलना case-sensitive करता है।
Duplicates idempotency की कमी का लक्षण भी हैं: double-click हुआ बटन या दोबारा भेजा गया request जिसने दो ऑर्डर बना दिए या एक payment दो बार capture कर लिया। अगर आपका डिज़ाइन split payments की अनुमति देता है, तो rows गिनने की जगह captured sum की तुलना order total (R2) से करें।
-- Emails that differ only in letter case or surrounding spaces
SELECT LOWER(TRIM(email)) AS email_key, COUNT(*) AS copies
FROM customers
GROUP BY LOWER(TRIM(email))
HAVING COUNT(*) > 1;
-- List the duplicate rows themselves, grouped together
SELECT c.customer_id, c.email, c.created_at
FROM customers c
JOIN (
SELECT LOWER(TRIM(email)) AS email_key
FROM customers
GROUP BY LOWER(TRIM(email))
HAVING COUNT(*) > 1
) d ON d.email_key = LOWER(TRIM(c.email))
ORDER BY d.email_key, c.customer_id;
-- An order charged more than once
SELECT order_id, COUNT(*) AS captured_payments, SUM(amount) AS captured_total
FROM payments
WHERE status = 'CAPTURED'
GROUP BY order_id
HAVING COUNT(*) > 1;NULL जाँच और date-range जाँच
NULL का अर्थ है “अज्ञात”, और उससे कोई भी तुलना — column = NULL सहित — न true होती है न false, इसलिए ऐसी rows नतीजे से चुपचाप गायब हो जाती हैं। हमेशा IS NULL या IS NOT NULL से जाँचें। यह भी याद रखें कि COUNT(column) NULLs छोड़ देता है जबकि COUNT(*) rows गिनता है, और PostgreSQL, MySQL या SQLite में खाली string NULL नहीं है (Oracle Database खाली string को NULL मानता है)।
तारीख़ों के लिए half-open ranges इस्तेमाल करें: created_at >= पहला दिन AND created_at < अगली अवधि का पहला दिन। कॉलम में समय हो तो 2026-09-30 end date वाला BETWEEN 30 तारीख़ की आधी रात के बाद का सब कुछ छोड़ देता है। जाँचें कि तुलना के दोनों पक्ष एक ही time zone इस्तेमाल करते हैं; एक zone का CURRENT_TIMESTAMP दूसरे zone में स्टोर values से तुलना करने पर झूठी “future” rows बनती हैं या असली छिप जाती हैं।
-- Required text that is NULL or blank
SELECT customer_id, email
FROM customers
WHERE full_name IS NULL OR TRIM(full_name) = '';
-- R3: captured payments without a payment time
SELECT payment_id, order_id
FROM payments
WHERE status = 'CAPTURED' AND paid_at IS NULL;
-- R3: payments recorded before their order existed
SELECT p.payment_id, o.order_id, o.created_at, p.paid_at
FROM payments p
JOIN orders o ON o.order_id = p.order_id
WHERE p.paid_at < o.created_at;
-- Timestamps in the future (clock or time-zone defects)
SELECT order_id, created_at
FROM orders
WHERE created_at > CURRENT_TIMESTAMP;
-- One calendar month as a half-open range
SELECT COUNT(*) AS september_orders
FROM orders
WHERE created_at >= '2026-09-01' AND created_at < '2026-10-01';Dialect अंतर जो portable queries तोड़ते हैं
SQL मानक ISO/IEC 9075 भाषा परिभाषित करता है; हर engine उसे अलग हद तक लागू करता है और अपना syntax जोड़ता है। ऊपर की जाँचें इन अंतरों से बचती हैं, पर एक प्रोजेक्ट से दूसरे में query कॉपी करते समय टेस्टर इन्हीं से सबसे ज़्यादा टकराते हैं:
- Row limits — PostgreSQL, MySQL और SQLite में LIMIT 50; SQL Server में SELECT TOP (50); PostgreSQL और Oracle Database 12c व बाद के वर्ज़न में मानक FETCH FIRST 50 ROWS ONLY।
- String concatenation — PostgreSQL, SQLite और Oracle में ||; MySQL में || डिफ़ॉल्ट रूप से logical OR है, इसलिए CONCAT() इस्तेमाल करें, जो PostgreSQL, MySQL और SQL Server तीनों में है।
- Date arithmetic — PostgreSQL में CURRENT_DATE - INTERVAL '7 days', MySQL में CURRENT_DATE - INTERVAL 7 DAY, SQLite में date('now', '-7 days'), SQL Server में DATEADD(day, -7, CAST(GETDATE() AS date))। '2026-09-01' जैसी ISO literal तारीख़ें सबसे portable विकल्प हैं।
- Timestamp को दिन तक काटना — PostgreSQL, MySQL और SQL Server में CAST(created_at AS DATE); SQLite में date(created_at)।
- Text comparison — PostgreSQL टेक्स्ट की तुलना case-sensitive करता है; MySQL की डिफ़ॉल्ट collations case-insensitive हैं; SQLite का = case-sensitive है और उसका LIKE सिर्फ़ ASCII अक्षरों के लिए case-insensitive है।
- Identifier quoting — मानक, PostgreSQL और SQLite में "double quotes"; MySQL में `backticks`; SQL Server में [brackets]।
- FROM के बिना SELECT — PostgreSQL, MySQL, SQLite और SQL Server स्वीकार करते हैं; Oracle Database को परंपरागत रूप से FROM dual चाहिए।
Ad-hoc queries से reusable check suite तक
जब कोई query defect ढूँढ ले, उसे रख लें। नाम दी गई जाँचों का फ़ोल्डर, जिसमें हर जाँच नियम टिके रहने पर शून्य rows लौटाए, डेटा के लिए regression suite बन जाता है: इसे deployments के बाद, migrations के बाद और staging पर तय समय पर चलाएँ, और automated tests से जोड़ें ताकि हर API call के बाद मेल खाता database assertion चले।
सिस्टमों के बीच तुलना अक्सर किसी दूसरे सिस्टम से export की गई ids की सूची से शुरू होती है। सूची को derived table या IN clause में बदलें और अपने डेटा से join करें, जैसा नीचे है; Mutqan के VALUES और IN-clause generators paste की गई values से सूची का syntax बना देते हैं।
- हर नियम के लिए एक जाँच, नियम के नाम पर — उदाहरण के लिए orders_total_matches_items।
- शून्य rows का मतलब pass; कोई भी rows वह सबूत हैं जो आप query के साथ bug report में लगाते हैं।
- Read-only: जाँच कभी डेटा नहीं बदलती।
- Bounded: तारीख़ या batch से filter करें ताकि बड़ी टेबलों पर जाँच तेज़ रहे।
- Migrations के साथ version control में रखी और कोड की तरह review की गई।
-- Ids exported from the invoicing service that the order database does not have.
-- PostgreSQL and SQLite syntax; MySQL 8.0.19+ writes VALUES ROW(1042), ROW(1043), ...
WITH exported (order_id) AS (
VALUES (1042), (1043), (1044), (1051)
)
SELECT e.order_id
FROM exported e
LEFT JOIN orders o ON o.order_id = e.order_id
WHERE o.order_id IS NULL;आज़माएँ: SQL VALUES जनरेटर आज़माएँ: SQL IN क्लॉज़ जनरेटर आज़माएँ: CSV से SQL आज़माएँ: JSON से SQL
अक्सर पूछे जाने वाले सवाल
टेस्टर को कितनी SQL आनी चाहिए?
WHERE के साथ SELECT, inner और left joins, HAVING के साथ GROUP BY, COUNT, SUM और COALESCE, EXISTS वाली subqueries, और BEGIN/COMMIT/ROLLBACK। इस पेज की लगभग हर verification query इतने में आ जाती है। Window functions और query tuning बाद में काम आते हैं, पहले नहीं।
NOT IN की जगह LEFT JOIN … IS NULL क्यों?
क्योंकि subquery में NULL होने पर NOT IN कोई row नहीं लौटाता, इसलिए NOT IN से लिखी orphan जाँच orphans मौजूद होते हुए भी pass हो सकती है। LEFT JOIN … IS NULL और NOT EXISTS NULLs के साथ सही व्यवहार करते हैं।
क्या verification queries production पर चला सकते हैं?
सिर्फ़ तब जब आपकी भूमिका इसकी अनुमति दे, read-only अकाउंट से, keys या तारीख़ों से सीमित, peak load के बाहर, और personal data export किए बिना। कई टीमें production में aggregate integrity checks का छोटा सेट चलाती हैं और row-level जाँच test environments के लिए रखती हैं।
मेरा count रिपोर्ट से अलग क्यों है?
आम तौर पर इसलिए कि दोनों अलग filters इस्तेमाल करते हैं: दिन की सीमा का time zone, inclusive बनाम half-open date ranges, soft-deleted या cancelled rows, test accounts, या replica या warehouse पर बनी रिपोर्ट जो पीछे चल रही है। Defect कहने से पहले दोनों पक्षों की परिभाषा लिखें।
क्या जाँचों में SELECT * इस्तेमाल करूँ?
सेव की गई जाँचों में नहीं। जिन कॉलमों की पुष्टि कर रहे हैं उन्हें लिखें ताकि output bug report में पढ़ने लायक हो और किसी के कॉलम जोड़ने पर जाँच न बदले। एक row पर जल्दी नज़र डालने के लिए SELECT * ठीक है।
ये queries automated tests में कैसे जुड़ती हैं?
टेस्ट API call करता है, फिर test framework के database connection से मेल खाती query चलाकर नतीजे पर assert करता है — एक बदलाव के लिए ठीक वही row, integrity नियम के लिए शून्य rows। इन्हें CI में ताज़ा seed किए डेटाबेस पर चलाएँ ताकि expected values ज्ञात हों।
संदर्भ
- PostgreSQL Documentation: SQL SyntaxPostgreSQL में SQL की लेक्सिकल संरचना: आइडेंटिफ़ायर, क्वोटिंग, स्ट्रिंग कॉन्स्टेंट और एस्केपिंग।
- SQL As Understood By SQLiteSQLite द्वारा स्वीकृत SQL व्याकरण, स्टेटमेंट दर स्टेटमेंट, सिंटैक्स डायग्राम के साथ।
- ISO/IEC 9075-1:2023 — Information technology — Database languages SQL — Part 1: Framework (SQL/Framework)अंतरराष्ट्रीय SQL मानक का framework भाग, जिसे PostgreSQL, MySQL, SQLite आदि की बोलियाँ अलग-अलग हद तक लागू करती हैं।
- Constraints — PostgreSQL Documentationcheck, not-null, unique, primary key, foreign key और exclusion constraints — वे नियम जो डेटाबेस लागू करता है और टेस्टर जाँचता है।
- Transactions — PostgreSQL Documentationtransaction क्या है: कई क़दम जो BEGIN, COMMIT और ROLLBACK से सब-या-कुछ-नहीं लागू होते हैं, commit होने तक दूसरों को नहीं दिखते।
- OWASP Cheat Sheet Series: SQL Injection Preventionपैरामीटराइज़्ड क्वेरी, सुरक्षित रूप से इस्तेमाल stored procedure, allow-list वैलिडेशन और least privilege — वे बचाव जिन्हें डेटाबेस टेस्ट को पुष्ट करना चाहिए।
अभ्यास
SQL जाँचों से migration rehearsal की पुष्टि करें
आप एक काल्पनिक online home-appliance store को टेस्ट करते हैं, जो अपना order data पुराने platform से नए platform पर ले जा रहा है। कल के पहले migration rehearsal में migration tool ने shared staging database में एक batch load किया — पुराने platform के orders 5001 से 5008, उनके customers, items और payments के साथ — और bulk load के लिए foreign-key checks बंद थे। दूसरे rehearsal से पहले टीम एक data check suite चाहती है जो हर load के बाद चले। आपके साथी हमज़ा ने पाँच जाँचों का draft लिखा है और एक clean-up का प्रस्ताव दिया है। नीचे के material में गाइड के example schema से अंतर, पुराने platform की रिपोर्ट, load हुई rows, draft और clean-up हैं।
गाइड के patterns के साथ काग़ज़ पर काम करें। Batch जान-बूझकर छोटा है, ताकि आप हर query दिखाई गई rows पर हाथ से चला सकें। हर finding ऐसे लिखें कि वह अपनी query और लौटी rows के साथ सीधे defect report में जा सके। एक जाँच में timezone converter काम आएगा। इसके लिए 45–60 मिनट रखें।
काल्पनिक online home-appliance store - order data का नए platform पर migration
पहला migration rehearsal कल staging पर हुआ, जिसे पूरी QA टीम साझा करती है।
Migration tool ने एक batch load किया: पुराने platform के orders 5001 से 5008, उनके
customers, items और payments के साथ, और उसी अवधि के unmatched payments। Bulk load
के लिए foreign-key checks बंद थे। Staging पर QA accounts पढ़ और लिख सकते हैं।
नया platform: PostgreSQL, गाइड का example schema और नियम R1 से R3, और साथ में:
- orders.status सिर्फ़ PENDING, PAID, SHIPPED, CANCELLED स्वीकार करता है (CHECK)
- payments.order_id NULL हो सकता है: ऐसा payment जिसकी gateway ने पुष्टि की, पर
store उसे किसी order से match नहीं कर पाया ("unmatched"); finance इन्हें हाथ से
match करता है
- R4: हर email address के लिए एक customer; जो emails सिर्फ़ letter case या
आगे-पीछे के spaces में अलग हैं, वे एक ही customer के हैं
- timestamps UTC में स्टोर होते हैं
पुराना platform: रिपोर्टें store time, Asia/Riyadh (UTC+3, daylight saving नहीं) में।
Delivery fee ली गई हो तो वह order total में शामिल होती है; fee की कोई item line नहीं।
पुराने platform की रिपोर्ट "Orders by status", सितंबर 2026, store time
status order_count amount_sum
CANCELLED 1 45.00
PAID 3 510.00
REFUNDED 1 75.00
SHIPPED 2 449.00
कुल: 7 orders, 1,079.00
Rehearsal 1 के बाद की rows (सिर्फ़ वे columns जो जाँचें इस्तेमाल करती हैं; समय UTC में)
customers
customer_id email full_name
101 'layla.h@example.com' 'Layla Hassan'
102 'omar.k@example.com' 'Omar Khalil'
103 'Layla.H@example.com ' 'Layla Hassan'
104 'sara.n@example.com' 'Sara Nasser'
orders
order_id customer_id status total_amount created_at
5001 101 SHIPPED 250.00 2026-08-31 22:10:00
5002 102 PAID 120.00 2026-09-05 09:00:00
5003 103 PAID 310.00 2026-09-12 14:30:00
5004 104 PAID 80.00 2026-09-18 11:00:00
5005 101 CANCELLED 45.00 2026-09-22 16:45:00
5006 102 SHIPPED 199.00 2026-09-30 18:20:00
5008 104 PENDING 60.00 2026-09-30 22:40:00
order_items
order_item_id order_id product_sku quantity unit_price
1 5001 KTL-200 1 250.00
2 5002 CUP-010 4 30.00
3 5003 FAN-300 1 260.00
4 5003 FLT-020 1 40.00
5 5004 FLT-020 2 40.00
6 5005 MUG-001 3 15.00
7 5006 TST-150 1 199.00
8 5007 IRN-075 1 75.00
9 5008 CUP-010 2 30.00
payments
payment_id order_id amount status paid_at
71 5001 250.00 CAPTURED 2026-08-31 22:11:00
72 5002 120.00 CAPTURED NULL
73 5003 310.00 CAPTURED 2026-09-12 14:31:00
74 NULL 80.00 CAPTURED 2026-09-18 11:02:00
75 5005 45.00 FAILED NULL
76 5006 199.00 CAPTURED 2026-09-30 18:21:00
77 5006 199.00 CAPTURED 2026-09-30 18:21:30
78 5007 75.00 REFUNDED 2026-09-26 12:00:00
हमज़ा (QA) का draft check suite: नियम टिका हो तो हर जाँच शून्य rows लौटाती है;
Q5 रिपोर्ट से तुलना के लिए आँकड़े लौटाती है।
-- Q1 orders_total_matches_items (R1)
SELECT o.order_id, o.total_amount,
SUM(oi.quantity * oi.unit_price) AS lines_total
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
GROUP BY o.order_id, o.total_amount
HAVING o.total_amount <> SUM(oi.quantity * oi.unit_price);
-- Q2 paid_orders_have_payment (R2)
SELECT order_id, status, total_amount
FROM orders
WHERE status IN ('PAID', 'SHIPPED')
AND order_id NOT IN (SELECT order_id FROM payments WHERE status = 'CAPTURED');
-- Q3 customer_email_unique (R4)
SELECT email, COUNT(*) AS copies
FROM customers
GROUP BY email
HAVING COUNT(*) > 1;
-- Q4 captured_payment_has_time (R3)
SELECT payment_id, order_id
FROM payments
WHERE status = 'CAPTURED' AND paid_at = NULL;
-- Q5 september_matches_old_report
SELECT status, COUNT(*) AS order_count, SUM(total_amount) AS amount_sum
FROM orders
WHERE created_at BETWEEN '2026-09-01' AND '2026-09-30'
GROUP BY status
ORDER BY status;
टीम chat में हमज़ा: "Q1 दिखाती है कि order 5003 में 10.00 का फ़र्क है, और मैंने एक ऐसे
order 5007 की बेकार rows देखीं जो मौजूद ही नहीं। कल की review में suite green रहे,
इसलिए मैं staging साफ़ कर देता हूँ:"
UPDATE orders SET total_amount = 300.00 WHERE order_id = 5003;
DELETE FROM order_items WHERE order_id = 5007;
DELETE FROM payments WHERE order_id = 5007;- Q1 से Q5 तक हर जाँच को उस नियम के सामने review करें जिसे वह verify करने का दावा करती है। तय करें कि जाँच सही है, ग़लत data पर चुपचाप pass हो सकती है, या सही data पर झूठा alarm दे सकती है, और सुधरी हुई query लिखें।
- Load foreign-key checks बंद रखकर चला था। इस स्थिति के लिए suite में जो जाँचें अब भी नहीं हैं वे लिखें, और एक जाँच उस order के लिए जिसका payment दो बार capture हुआ।
- सुधरी हुई और नई जाँचें load हुई rows पर हाथ से चलाएँ और लिखें कि हर जाँच कौन-सी rows लौटाती है।
- सितंबर का पुराने platform की रिपोर्ट से reconciliation करें: इस्तेमाल किया गया date filter बताएँ, हर अंतर समझाएँ, और बताएँ कि कौन-सा अंतर defect है।
- हमज़ा के clean-up प्रस्ताव का जवाब दें। फिर वे findings लिखें जो आप दूसरे rehearsal से पहले report करेंगे, हर एक के सबूत और उससे उठने वाले सवाल के साथ।
मॉडल उत्तर दिखाएँ
Q1 सही है। यह गाइड की R1 जाँच है: order के हिसाब से group करके सिर्फ़ वे orders रखती है जिनका header उनकी lines के sum से मेल नहीं खाता, इसलिए खाली नतीजा pass है। इसे रखें, और ध्यान दें कि यह क्या नहीं देख सकती: inner join बिना किसी item वाले order को छोड़ देता है, इसीलिए suite को नीचे दी गई उलटी orphan जाँच भी चाहिए। इस batch पर यह एक row लौटाती है: order 5003, total_amount 310.00, lines_total 300.00 (260.00 + 40.00)।
Q2 चुपचाप pass होती है। यहाँ payments.order_id NULL हो सकता है, और unmatched payment 74 subquery में एक NULL डाल देता है; order_id NOT IN (…, NULL) कभी true नहीं होता, इसलिए data कैसा भी हो, Q2 शून्य rows लौटाती है। साथ ही यह सिर्फ़ इतना जाँचती है कि कोई captured payment मौजूद है, जबकि R2 कहता है कि captured payments का जोड़ total के बराबर हो। इसकी जगह गाइड का R2 reconciliation लगाएँ: LEFT JOIN payments p ON p.order_id = o.order_id AND p.status = 'CAPTURED', WHERE o.status IN ('PAID', 'SHIPPED'), order के हिसाब से group, और HAVING COALESCE(SUM(p.amount), 0) <> o.total_amount। यह दो rows लौटाती है: order 5004 (total 80.00, captured 0) और order 5006 (total 199.00, captured 398.00)। COALESCE के बिना order 5004 भी ग़ायब हो जाता, क्योंकि शून्य rows पर SUM NULL देता है।
Q3 चुपचाप pass होती है: यह कभी कोई row नहीं लौटा सकती। UNIQUE constraint दो एक जैसे emails को पहले ही रोक देता है, और PostgreSQL टेक्स्ट की तुलना case-sensitive करता है, इसलिए R4 जिन रूपों की बात करता है वे अलग groups में चले जाते हैं। इसकी जगह normalise की गई value पर group करें: SELECT LOWER(TRIM(email)) AS email_key, COUNT(*) AS copies FROM customers GROUP BY LOWER(TRIM(email)) HAVING COUNT(*) > 1। यह layla.h@example.com को 2 copies के साथ लौटाती है, और rows की सूची देने वाली गाइड की query customers 101 और 103 दिखाती है ('Layla.H@example.com ': दो capital अक्षर और आख़िर में एक space)।
Q4 चुपचाप pass होती है: paid_at = NULL कभी true नहीं होता। paid_at IS NULL के साथ यह order 5002 का payment 72 लौटाती है; payment 75 का भी समय नहीं है, लेकिन वह FAILED है, जिसकी R3 अनुमति देता है। Q4 सिर्फ़ R3 का पहला हिस्सा cover करती है, इसलिए अपने order से पहले दर्ज payment के लिए गाइड की जाँच जोड़ें (JOIN orders o ON o.order_id = p.order_id WHERE p.paid_at < o.created_at)। इस batch पर यह शून्य rows लौटाती है: हर payment का समय अपने order से एक-दो मिनट बाद है, और payment 72 का NULL समय तुलना से बस बाहर हो जाता है, इसीलिए उसके साथ IS NULL जाँच ज़रूरी है।
Q5 झूठा alarm देती है। BETWEEN '2026-09-01' AND '2026-09-30' 2026-09-30 00:00:00 पर ख़त्म हो जाता है, और यह महीने को UTC में काटता है जबकि रिपोर्ट store time के दिन गिनती है। इसका नतीजा, CANCELLED 1 / 45.00 और PAID 3 / 510.00, 7 के मुक़ाबले 4 orders देता है और तीन orders ग़ायब होने जैसा लगता है। UTC का half-open महीना (>= '2026-09-01' AND < '2026-10-01') भी इस तुलना के लिए ग़लत है: यह order 5001 को छोड़ देता है (31 अगस्त 22:10 UTC, store time में 1 सितंबर 01:10 है) और order 5008 को ले लेता है (30 सितंबर 22:40 UTC, store time में 1 अक्टूबर 01:40 है)। Store time का महीना UTC में लिखें: created_at >= '2026-08-31 21:00:00' AND created_at < '2026-09-30 21:00:00'। नतीजा: CANCELLED 1 / 45.00, PAID 3 / 510.00, SHIPPED 2 / 449.00, यानी 6 orders और 1,004.00। REFUNDED 1 / 75.00 को छोड़कर हर status रिपोर्ट से मेल खाता है, और REFUNDED की नए database में कोई row नहीं: एक order ग़ायब है। यह जाँच violations नहीं, आँकड़े लौटाती है, इसलिए इसे रिपोर्ट के expected आँकड़ों के साथ सेव करें।
Load foreign keys के बिना चला, इसलिए ये जाँचें जोड़ें, हर एक अपने नियम के नाम पर और बाक़ियों की तरह read-only। items_have_order (LEFT JOIN orders o ON o.order_id = oi.order_id WHERE o.order_id IS NULL) item 8 लौटाती है, जिसका order 5007 मौजूद नहीं। गाइड की तरह लिखी payments_have_order payment 78 (order 5007) लौटाती है और payment 74 भी, क्योंकि NULL order_id किसी parent से match नहीं होता। Unmatched payments डिज़ाइन में allowed हैं, इसलिए AND p.order_id IS NOT NULL जोड़ें; तब यह सिर्फ़ payment 78 लौटाती है, और finance के लिए अलग सूची (WHERE order_id IS NULL) payment 74 दिखाती है। NOT EXISTS से लिखी orders_have_items शून्य rows लौटाती है: pass। payment_captured_once (WHERE status = 'CAPTURED' AND order_id IS NOT NULL GROUP BY order_id HAVING COUNT(*) > 1) order 5006 को 2 captured payments और 398.00 के साथ लौटाती है। यहाँ भी IS NOT NULL ज़रूरी है: GROUP BY सभी NULLs को एक ही group में रखता है, इसलिए दो unmatched payments double charge जैसे दिखेंगे। पूरा data load होने पर हर जाँच को batch से bound करें ताकि वह तेज़ रहे।
Reconciliation और orphan जाँचें एक ही कहानी कहती हैं: order 5007 ग़ायब है, जबकि उसका item 8 और payment 78 उसके बिना load हो गए। Material कारण की ओर इशारा करता है: पुराने platform पर 5007 REFUNDED है, लेकिन नए orders.status का CHECK सिर्फ़ PENDING, PAID, SHIPPED और CANCELLED की अनुमति देता है, इसलिए ज़्यादा संभावना यही है कि order की row reject हुई और उसके children load हो गए, क्योंकि foreign-key checks बंद थे। Migration tool के log से इसकी पुष्टि करें। यह सबसे गंभीर finding है — migration में खोया हुआ order — और इसके लिए refunded orders के migration पर product owner का फ़ैसला, migration में fix, और अगले rehearsal के बाद यही जाँचें चाहिए।
हमज़ा का clean-up नहीं चलना चाहिए। Order 5003 का total migration की ग़लती नहीं है: पुरानी रिपोर्ट का PAID आँकड़ा 510.00 तभी बनता है जब इस order का total 310.00 हो, payment 73 दिखाता है कि customer ने 310.00 दिए, और पुराने platform के totals में delivery fee शामिल होती है, जिसकी कोई item line नहीं। Total को 300.00 करने से असल में लिया गया amount मिट जाएगा, 5003 के लिए R2 की जाँच fail होने लगेगी, और सितंबर का reconciliation टूट जाएगा (PAID 500.00, जबकि रिपोर्ट में 510.00)। Order 5007 की rows delete करने से इस बात का सबूत नष्ट होगा कि एक order खो गया, और अगला load उन्हें फिर बना देगा, क्योंकि defects migration में हैं, staging में नहीं। Staging shared है: उसके owner की सहमति के बिना वहाँ कोई UPDATE या DELETE नहीं चलता। अगर कभी data बदलने पर सहमति हो, तो पहले उसका WHERE clause SELECT COUNT(*) के साथ चलाएँ (यहाँ हर statement के लिए 1 row), फिर बदलाव BEGIN … ROLLBACK के अंदर चलाएँ और COMMIT से पहले rows-affected count की तुलना करें, और हूबहू statements ticket में रखें। Verification के लिए read-only account भी माँगें, क्योंकि QA accounts लिख सकते हैं।
दूसरे rehearsal से पहले report करने लायक findings, हर एक अपनी query और लौटी rows के साथ। (1) Order 5007 migrate नहीं हुआ; item 8 और payment 78 orphans हैं; संभावित कारण: REFUNDED status। (2) Order 5003: total 310.00, जबकि lines का जोड़ 300.00; 10.00 संभवतः delivery fee है, जिसकी नए model में कोई जगह नहीं; product owner से पूछें कि यह item line बनेगी या fee का अलग column, और उसी हिसाब से R1 बदलें। (3) Order 5004 बिना captured payment के PAID है, जबकि unmatched payment 74 का amount वही 80.00 है और वह order के दो मिनट बाद का है; पुराने platform पर देखें कि 74 क्या 5004 का था: अगर हाँ, तो migration ने link खो दिया; अगर नहीं, तो source data ग़लत है और finance तय करेगा। (4) Order 5006 पर 199.00 के दो captures हैं, 30 सेकंड के अंतर पर; पुराने platform के payments और gateway से मिलाएँ: अगर दोनों captures वहाँ हैं, तो customer से सचमुच दो बार पैसा कटा — finance से refund और वहाँ idempotency की कमी की report; अगर सिर्फ़ एक है, तो अतिरिक्त row migration ने बनाई। (5) R4 के हिसाब से customers 101 और 103 एक ही व्यक्ति हैं, और orders दो records में बँटे हैं; पूछें कि पुराने platform पर एक account था या दो, और production से पहले merge का नियम तय करें। (6) Payment 72 बिना paid_at के captured है; पुराने platform से मिलाकर देखें कि mapping में समय खोया या नहीं। जिन जाँचों ने शून्य rows लौटाईं — बिना items वाले orders, order से पहले के payments — वे pass के रूप में दर्ज होती हैं।
अगर आपकी queries अलग दिखती हैं, तो उन्हें शब्दों से नहीं, नियम से मिलाएँ: orphan जाँचों के लिए NOT EXISTS और LEFT JOIN … IS NULL बराबर हैं, और अगर store split payments की अनुमति देता, तो आप double-capture जाँच हटाकर R2 पर भरोसा करते, जो sums की तुलना करता है। ज़रूरी यह है कि हर जाँच सही data पर शून्य rows लौटाए, किसी NULL की वजह से चुपचाप pass न हो, एक जैसी चीज़ों की तुलना करे (एक ही time zone, half-open range), और हर finding अपनी query और लौटी rows के साथ जाए।
इस topic में आपकी प्रगति
प्रगति ट्रैक करने के लिए साइन इन करें
Topics पूर्ण चिह्नित करें, जहाँ छोड़ा था वहीं से जारी रखें और specialisation परीक्षाएँ खोलें।
QA रोडमैप का हिस्सा
इस topic से पहले
अगले topics
इस गाइड में बताए गए टूल्स
SQL स्टेटमेंट में सिंटैक्स की समस्याएँ, असंतुलित कोष्ठक, बिना बंद की स्ट्रिंग और जोखिम भरे पैटर्न जाँचें।
SQL क्वेरी को एकसमान कीवर्ड केसिंग, इंडेंटेशन और लाइन ब्रेक के साथ फ़ॉर्मेट करें (MySQL, PostgreSQL, SQL Server, Oracle, SQLite)।
किसी भी dialect के लिए वास्तविक जैसी टेस्ट पंक्तियों के साथ SQL seed स्क्रिप्ट (CREATE TABLE + INSERT) जनरेट करें।
टेबल परिभाषा, नमूना पंक्तियों, CSV या JSON से INSERT स्टेटमेंट बनाएँ।
वैल्यू की सूची (हर लाइन पर एक या कॉमा से अलग) को कोट किए SQL IN (...) क्लॉज़ में बदलें।
तालिका वाले डेटा को SQL VALUES सूची या VALUES-आधारित derived table में बदलें।
CSV डेटा को अनुमानित कॉलम टाइप के साथ CREATE TABLE और INSERT स्टेटमेंट में बदलें।
ऑब्जेक्ट के JSON ऐरे को SQL INSERT स्टेटमेंट में बदलें (चाहें तो CREATE TABLE के साथ)।