DeepSeek can help turn an analytical question into a SQL draft, explain an unfamiliar join, and suggest cases that deserve a test. The difficult part is deciding whether the query answers the question you actually meant. A statement can execute successfully, return a tidy number, and still double-count sales, exclude customers without refunds, or compare different reporting periods. The database checks syntax and execution rules. It does not know your business definition of a good answer.

This guide builds a practical review workflow around a small, completely synthetic shop database. We will define a metric, show how a plausible join produces the wrong total, repair the query, and test the result without sending any data to a model. The examples are educational SQL and local Python tests, not outputs collected from DeepSeek and not a benchmark of its accuracy. The goal is a repeatable method you can use before trusting AI-assisted analysis.

The short version: give DeepSeek a minimal schema and an explicit metric contract; ask it to expose assumptions; keep execution under application and database controls; and compare proposed queries with independently established results on fixtures designed to reveal mistakes. Treat the final written explanation as another output to check. A correct number can still be described with the wrong date range, currency, population, or causal claim.

1. Define the metric before the query

Start with a question precise enough that two competent analysts could implement it independently. “What was revenue last month?” leaves important choices open. Revenue might mean ordered merchandise, captured payments, recognized accounting revenue, or cash received after refunds. “Last month” needs a time zone. A refund may belong to the month when it occurred or to the order cohort that generated it. These interpretations can all produce legitimate but different queries.

For our example, the metric is September completed-order cohort net merchandise sales, measured in USD using integer cents in an entirely synthetic dataset. An order belongs to the cohort when its completion timestamp is on or after September 1, 2026 at midnight UTC and before October 1 at midnight UTC. Merchandise sales are the sum of quantity multiplied by unit price, minus each whole-line discount. A NULL discount means zero under this fixture’s explicit policy. Successful refunds linked to those orders are deducted when their refund timestamp is earlier than the reporting cutoff of October 3 at midnight UTC.

This is deliberately a cohort metric, not September cash flow or accounting revenue. A successful October 2 refund for a September order belongs in this report because its event timestamp falls before the chosen cutoff. A September refund for an August order does not belong. Tax, shipping, exchange rates, chargebacks, and accounting recognition are outside this toy definition. A production metric would need its own explicit treatment of each applicable component.

Write down the expected output grain as well. Here the final result is one summary row containing the order count, merchandise cents after line discounts, successful-refund cents, and net cents. Intermediate calculations should have one row per order before we combine them. That sentence is a powerful review tool: if an intermediate join has several rows per order, you need to understand why and which quantities will be repeated before any aggregate is trusted.

Keep the approved definition outside the model response. The model can point out ambiguity, but it should not quietly redefine completed orders as created orders or replace the refund cutoff with the order cutoff. Version the definition with the report. If a stakeholder later changes the rule, update the expected results and tests as a deliberate metric revision rather than treating the changed number as unexplained model inconsistency.

2. Give DeepSeek useful schema context without unnecessary data

A useful schema description includes table names, column types, keys, relationships, and the meaning of a row. In our fixture, orders have one row per order; order_items have one row per line item; refunds have one row per refund event. Each child references an order. There may be several item rows and several refund events for the same order. This one-to-many information matters more than a long sample of realistic customer records.

Also describe constraints that an analyst could otherwise misread. A completion timestamp can be NULL; the fixture’s cancelled order has no completion time, and the query does not impute one. Whole-line discounts can be NULL, meaning no discount in this dataset. A refund has a status, so an attempted or failed refund does not reduce the metric. Unit price, discount, and refund amount use USD cents; the fixture constrains its currency to USD. The example uses uniformly formatted UTC timestamps whose lexical ordering matches chronological ordering. State which guarantees are enforced by the database and which merely describe the current data pipeline.

Do not dump your entire production schema into a prompt by default. A schema may expose business relationships, internal project names, or sensitive column names even when it contains no rows. Prefer an approved analytical view with only the fields needed for the question. Invented examples are often enough to explain cardinality. If real samples are necessary, follow your organization’s data-sharing rules and minimize them using the approach in our DeepSeek data-minimization guide.

Tell the model the actual SQL dialect and version constraints. SQLite, PostgreSQL, BigQuery, and other systems differ in date functions, interval syntax, casting, placeholders, and execution controls. A query that reads naturally may still use the wrong dialect. For this tutorial, the executable examples use Python’s standard-library SQLite interface. The broader review method transfers, but the exact SQL and operational restrictions require verification on your own engine.

3. Ask for a proposal that a person can review

A good prompt should make missing information visible rather than reward a confident guess. Ask for the interpretation first, then the proposed SQL, the bound parameter names, and a short list of cases that could change the answer. Require the model to flag ambiguity that would change the population or arithmetic. You do not need a long private reasoning transcript. A concise statement of assumptions and the grain of each subquery gives a reviewer more useful evidence.

You are helping draft an analytical query, not executing it.
Dialect: SQLite. All data described below is synthetic.

Question: September 2026 completed-order cohort net merchandise sales.
Include completed orders in [start, end).
Subtract refunds with status succeeded for those orders and refunded_at < as_of.
Use USD cents. Subtract whole-line discounts; NULL discount means zero.
Exclude tax, shipping, and other currencies.
Return one summary row: order_count, sales_cents, refund_cents, net_cents.

Schema grain:
orders: one row per order; order_id, completed_at, status, currency.
order_items: one row per line; item_id, order_id, quantity,
             unit_price_cents, discount_cents.
refunds: one row per event; refund_id, order_id, refunded_at,
         status, amount_cents.
An order can have multiple item rows and multiple refund rows.

Before SQL, state any unresolved assumption that changes the result.
Do not invent columns. Use named value placeholders.
Explain the output grain of each subquery and propose three edge cases.
Do not claim the query has been executed or validated.

If an application requests machine-readable proposals, validate their structure before using them. As checked on October 3, 2026, DeepSeek’s Chat Completions documentation lists the current model identifiers deepseek-flash and deepseek-v4-pro. Its JSON Output mode requires an explicit JSON instruction and can still produce incomplete content when generation is truncated. Valid JSON does not establish that a SQL string has the correct joins, permissions, or metric definition. Endpoint-specific structured-output behavior should be checked in the current documentation.

A narrow response object might contain sql, parameter_names, assumptions, and test_cases. Those fields make the proposal easier to inspect, but they are untrusted content. A malicious or mistaken proposal must not decide which credentials to use, which tables it may read, or whether a reviewer has approved it. Our structured-output validation guide explains the distinction between parseable responses and application-valid results in more detail.

Do not paste a database connection string, password, or unrestricted query tool into this drafting step. You can get useful help with a schema and synthetic data alone. If the model asks to run a query, the application should either use a separately authorized, restricted workflow or keep the result as a draft. The prompt’s statement that it must not execute is useful communication; actual execution authority must be enforced outside the prompt.

4. Build a small fixture whose answer you can establish by hand

A fixture should be small enough to inspect but rich enough to distinguish a correct query from an attractive wrong one. Three completed orders in the September cohort are sufficient here. Together they contain 12,000 cents of merchandise and 4,500 cents of qualifying refunds, leaving 7,500 cents net. The complete companion fixture also includes orders and refunds outside the relevant boundaries so that a missing filter cannot silently pass.

Keep the fixture answer independent of the generated query. If the same query produces both the expected result and the actual result, the test only proves consistency with itself. For a small case, calculate order-level totals by hand, write them down, and verify them with a separate straightforward implementation. Make the expected numbers part of the test data before comparing candidate queries. A model can suggest test cases, but it should not be the only judge of its own answer.

  • Order 1001: 6,500 sales cents after a 500-cent line discount; 1,500 qualifying refund cents; 5,000 net cents
  • Order 1002: 2,500 sales cents; no refund; 2,500 net cents
  • Order 1003: 3,000 sales cents; 3,000 refund cents; zero net cents, still included in the order count

To reproduce the two-query demonstration, save the following setup as setup.sql, the two queries below as naive_join.sql and correct.sql, and the later Python runner as article_demo.py in one folder. These files require no packages beyond Python’s standard library. The setup contains only invented records and creates tables in the runner’s disposable in-memory database.

Show the complete synthetic schema and fixture (setup.sql)
-- Synthetic teaching fixture only. All timestamps are normalized UTC to seconds.
PRAGMA foreign_keys = ON;
CREATE TABLE orders (
    order_id INTEGER PRIMARY KEY,
    completed_at TEXT,
    status TEXT NOT NULL CHECK (status IN ('completed', 'cancelled')),
    currency TEXT NOT NULL CHECK (currency = 'USD')
);
CREATE TABLE order_items (
    item_id INTEGER PRIMARY KEY,
    order_id INTEGER NOT NULL REFERENCES orders(order_id),
    quantity INTEGER NOT NULL CHECK (typeof(quantity) = 'integer' AND quantity > 0),
    unit_price_cents INTEGER NOT NULL
        CHECK (typeof(unit_price_cents) = 'integer' AND unit_price_cents >= 0),
    -- Discount applies to the entire line, not each unit. NULL means zero here.
    discount_cents INTEGER CHECK (
        discount_cents IS NULL OR (
            typeof(discount_cents) = 'integer'
            AND discount_cents BETWEEN 0 AND quantity * unit_price_cents
        )
    )
);
CREATE TABLE refunds (
    refund_id INTEGER PRIMARY KEY,
    order_id INTEGER NOT NULL REFERENCES orders(order_id),
    refunded_at TEXT NOT NULL,
    status TEXT NOT NULL CHECK (status IN ('succeeded', 'pending', 'failed')),
    amount_cents INTEGER NOT NULL
        CHECK (typeof(amount_cents) = 'integer' AND amount_cents > 0)
);

-- Every record is invented. Amounts are USD cents, not floating-point dollars.
INSERT INTO orders VALUES
    (1001, '2026-09-01T00:00:00Z', 'completed', 'USD'),
    (1002, '2026-09-15T12:00:00Z', 'completed', 'USD'),
    (1003, '2026-09-30T23:59:59Z', 'completed', 'USD'),
    (1004, NULL, 'cancelled', 'USD'),
    (1005, '2026-08-31T23:59:59Z', 'completed', 'USD'),
    (1006, '2026-10-01T00:00:00Z', 'completed', 'USD');

INSERT INTO order_items VALUES
    (1, 1001, 2, 1500, NULL), -- 3,000
    (2, 1001, 1, 4000, 500),  -- 3,500: sales for order 1001 = 6,500
    (3, 1002, 1, 2500, NULL), -- 2,500: no refund
    (4, 1003, 3, 1000, 0),    -- 3,000: fully refunded, still counted
    (5, 1004, 1, 5000, 0),    -- excluded: cancelled
    (6, 1005, 1, 4000, 0),    -- excluded: before cohort start
    (7, 1006, 1, 6000, 0);    -- excluded: exactly at cohort end

INSERT INTO refunds VALUES
    (1, 1001, '2026-09-03T10:00:00Z', 'succeeded', 1000),
    (2, 1001, '2026-10-02T10:00:00Z', 'succeeded', 500), -- after cohort end, before as_of
    (3, 1001, '2026-09-05T10:00:00Z', 'pending', 900),
    (4, 1001, '2026-09-06T10:00:00Z', 'failed', 700),
    (5, 1001, '2026-10-03T00:00:00Z', 'succeeded', 200), -- cutoff excluded
    (6, 1003, '2026-09-30T23:59:59Z', 'succeeded', 3000),
    (7, 1005, '2026-09-02T10:00:00Z', 'succeeded', 1000); -- wrong order cohort

The most revealing fixture contains at least one order with multiple item rows and multiple successful refunds, one completed order with no refund, and one boundary timestamp. Add a failed refund to distinguish status filtering from summing every event. Add an out-of-cohort order with a qualifying refund date to expose accidental cash-flow logic. These examples are much more valuable than a hundred nearly identical ordinary orders that all happen to fit the model’s assumptions.

A larger production sample can reveal performance or distribution problems later, but it is not a replacement for a deliberately adversarial tiny example. Small fixtures explain the failure. When a net figure is wrong, you can inspect the few joined rows and show exactly which amount was counted twice. That makes review faster, makes fixes teachable, and leaves a regression test that continues to protect the definition after the original analyst has moved on.

5. Watch for join fan-out: correct syntax, incorrect money

The tempting query joins orders to their items, joins refunds to the same orders, then sums item amounts and refund amounts. The problem is the row count. For an order with two item rows and two matching refund events, the combined join contains four rows. Each item appears beside both refunds, and each refund appears beside both items. The join is following relational semantics exactly; the aggregation is measuring duplicated quantities.

For order 1001, two item subtotals and two refunds create four joined rows. Summing after the join gives 10,000 net cents instead of the correct 5,000.
The four-row join repeats both item and refund amounts. Aggregate each child table to one row per order before combining them. Values are from the synthetic fixture.
Show the intentionally wrong query (naive_join.sql)
-- INTENTIONALLY WRONG: two independent one-to-many joins multiply rows.
-- Even COUNT(DISTINCT order_id) does not fix the duplicated monetary sums.
SELECT COUNT(DISTINCT o.order_id) AS order_count,
       COALESCE(SUM(i.quantity * i.unit_price_cents
                    - COALESCE(i.discount_cents, 0)), 0) AS sales_cents,
       COALESCE(SUM(r.amount_cents), 0) AS refund_cents,
       COALESCE(SUM(i.quantity * i.unit_price_cents
                    - COALESCE(i.discount_cents, 0)
                    - COALESCE(r.amount_cents, 0)), 0) AS net_cents
FROM orders AS o
LEFT JOIN order_items AS i ON i.order_id = o.order_id
LEFT JOIN refunds AS r
       ON r.order_id = o.order_id
      AND r.status = 'succeeded' AND r.refunded_at < :as_of
WHERE o.status = 'completed'
  AND o.completed_at >= :start
  AND o.completed_at < :end;

Our intentionally wrong naive-join query executes successfully. On the complete synthetic fixture it reports 18,500 merchandise cents and 6,000 refund cents, giving 12,500 net cents instead of 7,500. This is not a claim that DeepSeek generated that query. It is a handcrafted counterexample showing why successful execution and plausible-looking totals do not establish analytical correctness. The failure is visible only when the fixture contains the relevant relationships.

Counting distinct order identifiers can repair an inflated order count without repairing the money. That can make the result look more convincing: the order count matches the business report while the summed amounts remain wrong. Similarly, adding DISTINCT inside a sum is not a general fix. Two genuine orders or line items may have equal monetary values. Deduplicating equal numbers can remove real contributions while leaving the underlying relationship mistake unresolved.

Ask a reviewer to describe each relation in plain English before looking at the final SUM. “One row per order-item-refund combination” is different from “one row per order.” If the quantity being summed belongs to an item, consider how many times that item is represented after every join. If it belongs to a refund event, perform the same check for refunds. This simple habit finds mistakes that polished SQL formatting can hide.

6. Aggregate each child relation to the required grain

The repair is to aggregate item amounts by order and aggregate qualifying refund amounts by order before joining those results to the selected orders. Each intermediate result then has at most one row for each order identifier. The final join can preserve one row per order, making the summary arithmetic straightforward. Common table expressions give those stages names, which helps reviewers inspect the intended grain without mentally untangling a single dense statement.

-- One row per order in each child aggregate prevents join fan-out.
WITH item_totals AS (
    SELECT order_id,
           SUM(quantity * unit_price_cents - COALESCE(discount_cents, 0)) AS sales_cents
    FROM order_items
    GROUP BY order_id
), refund_totals AS (
    SELECT order_id, SUM(amount_cents) AS refund_cents
    FROM refunds
    WHERE status = 'succeeded' AND refunded_at < :as_of
    GROUP BY order_id
)
SELECT COUNT(*) AS order_count,
       COALESCE(SUM(i.sales_cents), 0) AS sales_cents,
       COALESCE(SUM(r.refund_cents), 0) AS refund_cents,
       COALESCE(SUM(COALESCE(i.sales_cents, 0)
                  - COALESCE(r.refund_cents, 0)), 0) AS net_cents
FROM orders AS o
LEFT JOIN item_totals AS i ON i.order_id = o.order_id
LEFT JOIN refund_totals AS r ON r.order_id = o.order_id
WHERE o.status = 'completed'
  AND o.completed_at >= :start
  AND o.completed_at < :end;

The item subtotal does not need refund columns, and the refund subtotal does not need item columns. Keeping these concerns separate reduces the chance of accidental multiplication. The cohort selection owns order status and completion-time filtering. The refund stage owns refund status and reporting-cutoff filtering. The final stage combines those independently defined components. This separation makes it easier to change one business rule without quietly altering the others.

Review missing-child behavior explicitly. The example preserves completed orders even when the child aggregate has no matching row, and substitutes zero at the point where the metric defines absence as no recorded amount. In a real warehouse, a completed order with no items may indicate broken ingestion rather than a legitimate zero-value sale. A separate data-quality check should surface that anomaly. Query arithmetic and pipeline health are related responsibilities, but one should not silently stand in for the other.

Use named placeholders for the time boundaries, and bind their values through the database driver. Parameters handle values, not arbitrary table names, column names, or SQL fragments. If users can select a grouping dimension, map their choice to a small set of reviewed identifiers in application code. Never interpolate a free-form “group by” expression just because the date parameters are bound safely. A partially parameterized statement can still expose uncontrolled SQL structure.

7. Treat NULLs and denominators as business decisions

NULL handling is a frequent source of quiet analytical errors. In SQLite, COUNT(*) counts rows, while COUNT(column) counts rows where that expression is not NULL. SUM ignores NULL inputs and yields NULL if there are no non-NULL inputs. An outer join can introduce NULLs even when the original child column is constrained to be non-null. Decide what each absence means before applying a blanket COALESCE throughout a query.

For a completed order with no successful refunds, a missing refund subtotal can reasonably become zero in this metric. A missing completion timestamp cannot reasonably become a convenient reporting date. An unknown product price should not automatically become free merchandise. These cases may look similar as SQL values while representing different business facts. A model that normalizes every NULL to zero can produce a clean report that erases the very anomalies an analyst needed to investigate.

Denominators deserve the same care. Average order value could divide merchandise sales by all selected orders, by paid orders, or by orders with at least one item. A refund rate could count refunded orders or divide refunded cents by merchandise cents. Both may be useful, but they answer different questions. State the numerator, denominator, and treatment of an empty population in the metric contract. Prefer an explicit unavailable result to an invented percentage when the denominator is zero.

Be careful where filters are placed around outer joins. A condition on the right-hand table in WHERE can remove rows whose join produced NULL, effectively excluding unmatched orders. In our fixture, a wrong version that keeps only refunded orders can still run and return credible numbers. Include an order without a refund in the tests so that the population loss is detected. Reviewing only the refunded orders would miss the problem.

8. Make time windows, currencies, and snapshots explicit

Half-open intervals are easy to reason about for adjacent reporting periods: include the start boundary and exclude the end boundary. September and October can then meet at exactly midnight without sharing the same order. A condition using an inclusive upper bound at October 1 would include an event that belongs to the next month. Writing “through September 30” without a timestamp convention can also lose late-night events or mishandle fractional seconds.

The fixture uses UTC strings with a uniform representation, which makes textual comparisons appropriate for this example. That is not a universal rule for timestamp text. Mixed offsets, missing zero padding, inconsistent precision, or local timestamps can break the assumption. In production, use the engine’s native timestamp semantics and establish how a user’s local reporting boundaries are converted. Daylight-saving transitions and time-zone changes deserve explicit tests where they affect your business.

A reporting cutoff is also a reproducibility choice. This example includes qualifying refund events before October 3, even though the order cohort is September. Rerunning the report after additional refunds arrive may change the result unless the query also preserves an appropriate data snapshot or as-of representation. A timestamp filter on event time alone does not reproduce what the warehouse knew earlier if late-arriving records are later backfilled into that interval.

Integer cents keep our small USD fixture arithmetic exact and easy to inspect. Real financial datasets may use decimal types, several currencies, non-cent minor units, or amounts requiring more precision. Do not mix currencies in one total, assume every currency has two decimal places, or convert a monetary aggregate to binary floating point without considering rounding. The example is an analytical demonstration, not accounting advice or a universal money-storage design.

9. Separate drafting, validation, and authorized execution

The safest useful starting point is a draft-only assistant. It proposes a query; a reviewer checks the metric and SQL; a controlled runner executes an approved statement against an appropriate analytical dataset. For repeated questions, a reviewed query template with bound parameters often provides a simpler and more predictable interface than unrestricted text-to-SQL. The model can help choose a supported report or clarify a question while the application retains the executable structure.

Five-stage AI-assisted SQL workflow: define the metric, draft from minimal schema, verify fixtures and joins, execute approved SQL with limited privileges, and report traceable results.
The model contributes a draft. Metric approval, execution authority, correctness checks, and report context remain explicit steps.

Read-only access reduces one category of risk, but it does not make every query harmless. A SELECT can expose rows the requester should not see, read confidential columns, consume substantial compute, or invoke database functionality whose behavior needs separate review. Limiting displayed rows does not necessarily limit the work required to compute them. Database permissions, available functions, tenant isolation, resource controls, and output handling all matter alongside the SQL text.

Use credentials that already have the minimum approved access, restricted views where appropriate, and engine-specific execution budgets. Keep the query runner separate from production administration. This article does not ask you to create credentials or change security settings, and the companion code contains no production connection or arbitrary-SQL service. A prompt rule such as “only read data” cannot replace database-enforced permissions. A regular expression checking whether a string starts with SELECT cannot establish safety either.

SQLite’s query_only pragma illustrates why labels need careful interpretation: it prevents specified database-content changes while enabled, but it is not a complete read-only or confidentiality sandbox. Do not elevate the tutorial’s disposable in-memory database into a production security design. If you need an automated SQL service, have its actual engine, drivers, extensions, allowed operations, resource limits, and data exposure reviewed as a system. The prompt-injection prevention guide covers why text encountered during a task must not gain authority over those controls.

10. Test the meaning of the query, not just whether it runs

The local companion runs only reviewed, fixed example SQL against a fresh synthetic database. It does not call DeepSeek, inspect private data, or claim to sandbox arbitrary SQL supplied by a user. The tests compare known totals, confirm boundary behavior, and demonstrate that intentionally wrong statements can execute without raising a syntax error. This is a correctness exercise for the analytical workflow, not evidence of any model’s success rate.

Run python article_demo.py from the folder containing the four files. The printed tuple is ordered as order count, sales cents, refund cents, and net cents. The correct query matches (3, 12000, 4500, 7500); the naive query returns (3, 18500, 6000, 12500). The full local suite also tests missing discounts, inclusive endpoints, excluded statuses, an incorrect refund cutoff, and independently calculated totals.

"""Run fixed examples on synthetic data; no model or network access."""
from contextlib import closing
from pathlib import Path
import sqlite3

folder = Path(__file__).resolve().parent
parameters = {
    "start": "2026-09-01T00:00:00Z",
    "end": "2026-10-01T00:00:00Z",
    "as_of": "2026-10-03T00:00:00Z",
}
expected = (3, 12000, 4500, 7500)
for filename in ("correct.sql", "naive_join.sql"):
    with closing(sqlite3.connect(":memory:")) as db:
        # Only the locally authored setup uses executescript.
        db.executescript((folder / "setup.sql").read_text(encoding="utf-8"))
        sql = (folder / filename).read_text(encoding="utf-8")
        actual = db.execute(sql, parameters).fetchone()
    print(filename, actual, "matches expected:", actual == expected)
    if filename == "correct.sql":
        assert actual == expected
    else:
        assert actual == (3, 18500, 6000, 12500)

Keep several layers of checks. A schema check catches missing or renamed columns. A grain check verifies uniqueness where one row per order is expected. Fixture assertions verify the metric under selected edge cases. Reconciliation compares the result with a trusted independent report or calculation. Finally, a domain owner reviews whether the agreed definition still matches the business question. Passing an earlier layer does not make later layers unnecessary.

Useful tests are often transformations with predictable outcomes. Adding an out-of-cohort order should leave the September result unchanged. Adding a failed refund should not reduce net merchandise sales. Adding a successful pre-cutoff refund for a cohort order should reduce net by exactly that refund amount. Moving an order to the exclusive end boundary should remove it from September. These properties expose errors that a single expected total can accidentally conceal.

Test permutations and multiplicity as well. Reordering inserted rows should not change aggregate results. Two equal-priced items should still contribute twice. Multiple refunds should all contribute if they qualify. Splitting one item quantity across two equivalent item rows should preserve merchandise sales. A query that only works because each order happens to have one child row is not ready for a dataset where the relationship permits more.

When a query fails a test, inspect the intermediate rows before asking the model to repair it. Provide the failing case, the expected result, and the observed result with no unnecessary private data. Ask for a localized explanation and revision, then rerun the complete suite. Repeatedly saying “try again” without preserving the failure can replace one mistake with another. Keep prior cases as regression tests, using the discipline described in our prompt regression-testing guide.

11. Check the explanation after checking the numbers

A useful analytical answer should name the metric, population, reporting window, cutoff, units, and important exclusions. For our fixture, the result is three completed September orders, 12,000 merchandise cents, 4,500 qualifying refund cents, and 7,500 net cents under the stated cohort definition. Calling that “September cash revenue” would be wrong even though the arithmetic is correct. The label is part of the result and deserves the same review as the SQL.

Give the writer only the approved result and the context needed to explain it. If the model receives a truncated table, tell it that the table is partial and prevent claims about the full population. If a query failed, return an execution error rather than inviting a guess at the missing answer. If the result is empty, distinguish an actual empty population from a permissions issue, missing ingestion, or an incorrect date parameter.

Do not let narrative fluency invent causes. A fall in net sales does not establish that customers disliked a product, that a campaign failed, or that refunds were fraudulent. Those interpretations require additional evidence and often a different analysis. Ask the model to separate observed values, supported comparisons, plausible hypotheses, and unanswered questions. For a report used in decisions, the reader should be able to trace every numerical claim back to an approved query result.

Preserve enough provenance to reproduce the work: the metric-definition version, reviewed SQL, parameter values, schema or view version, execution timestamp, and relevant data-snapshot identifier. Record the model and prompt version if they helped produce the draft, without treating that information as proof of correctness. Store only what your organization permits and avoid retaining sensitive raw result rows merely because logging is convenient.

12. A practical release checklist for AI-assisted SQL

Before releasing a report, ask whether the definition is settled, the grain of every join is understood, and the required data is authorized for both execution and any model processing. Confirm that the SQL dialect matches the target engine, time boundaries are bound as values, and the report’s currency and units are explicit. Make a reviewer demonstrate at least one fixture that would fail the most tempting wrong query.

Then inspect execution and presentation separately. The approved query must run within the intended permissions and resource constraints, and its results must reconcile with the expected evidence. The explanation must preserve the metric’s scope and limitations. A chart must use the same units and population as the query. A copied number in a slide or email should carry enough context that it does not become misleading when separated from the original report.

Start automation with the narrowest valuable question set. A small catalog of reviewed reports can deliver useful natural-language access while keeping definitions stable. Expand only when you can identify the new tables, relationships, permissions, and tests required. If users routinely ask questions outside the supported catalog, collect those requests for deliberate development rather than silently giving the model a more powerful query tool.

The core habit is straightforward: use DeepSeek to accelerate drafting and explanation, then make analytical correctness observable. Clear definitions, intentionally difficult fixtures, controlled execution, and traceable results do more for trust than a confident response ever can. Once those foundations exist, a model becomes a helpful collaborator in the workflow because you have a way to discover when its proposal is useful, incomplete, or wrong.

Frequently asked questions

Can DeepSeek write a correct SQL query from a schema alone?

It can propose a query, but a schema rarely contains the complete metric definition. Keys and column types do not explain whether refunds belong to a cash-flow period or an order cohort, which time zone defines a month, or how missing records should be treated. Supply that context and test the proposal. This guide makes no claim about the accuracy of any particular DeepSeek model on your schema.

Does JSON output make generated SQL safe?

No. It can help an application parse a response, but a well-formed object can contain a wrong or unauthorized query. Structural validation, analytical review, and execution controls solve different problems. Keep database access and approval decisions outside the model-generated object, and use only the fields your application explicitly supports. A string labelled “validated” is still a claim that needs independent evidence.

Should I use SELECT DISTINCT to fix duplicate totals?

First determine why rows are repeated and which entity each amount belongs to. DISTINCT can be appropriate for a genuinely distinct set, but it is not a general repair for joining multiple one-to-many relationships. Equal prices can represent separate real items. In the worked example, aggregating each child table to one row per order before joining preserves legitimate amounts and removes the accidental multiplication.

What did the local tests prove?

They checked the behavior of fixed SQL examples on synthetic data, including known totals and selected edge cases. They did not evaluate live DeepSeek responses, measure production query performance, certify a database security boundary, or prove that the toy metric matches your business. Use the fixture as a pattern for building your own independent evidence, then validate the adapted queries on your actual engine and approved data.

Sources & References

Primary documentation checked October 3, 2026. The worked fixture, illustrative prompt, diagrams, and review workflow are original educational examples. No live or paid model calls were made.

  1. DeepSeek Chat Completions API: current model IDs and response handling
  2. DeepSeek JSON Output: required instructions and limitations
  3. SQLite SELECT: joins, outer joins, filters and grouping
  4. SQLite aggregate functions: COUNT, SUM and DISTINCT
  5. SQLite expressions: NULL semantics
  6. Python sqlite3: parameter binding and driver behavior
  7. SQLite query_only: documented limits
  8. SQLite security guidance for untrusted input
  9. PostgreSQL privileges: object and function permissions
Shares:
1 Comment
Leave a Reply

Your email address will not be published. Required fields are marked *