SQL vs Python: Choosing an Analysis Workflow

SQL Published 12 mins read By Leon Wei
SQL vs Python: Choosing an Analysis Workflow cover image

Quick summary

Summarize this blog with AI

Knowing both SQL and Python does not mean every analysis should use both. The useful question is where each step can run correctly, at a reasonable cost, with someone able to maintain it after you hand it over.

Start with the data's current location and the result you need. Keep relational work near the database when that fits the workload. Add Python when a specific library, external integration, or analytical method makes the next step clearer. Then define exactly what crosses the boundary between them.

This guide walks through that decision for a support-team analysis, including cases where adding Python would help and cases where it would add work without improving the answer.

Choose the execution environment before the language

SQL is a language, not a promise that computation happens on a large warehouse. It can run in a small local database. Python can operate on a local Pandas DataFrame, submit work to a database, or use a distributed engine. A Python script that submits SQL to a warehouse still does the heavy query processing in the warehouse.

Ask four questions before comparing syntax: Where is the source? Where will computation happen? What data will move? Who owns the result? “Python versus SQL” becomes much easier once those questions have concrete answers.

SituationReasonable starting pointWhat to check
Shared tables feed a recurring dashboard.SQL transformations in the existing data platform.Metric ownership, query cost, freshness, and downstream reuse.
An approved API supplies records absent from the warehouse.An existing ingestion tool or a Python integration.Pagination, retries, rate limits, and how updates are detected.
A specialist statistical or modeling library is required.SQL for the eligible population; Python for the method.The observation grain, required history, and supported runtime.
A small, one-time file needs inspection.The supported tool that makes inspection easiest.Data types, reproducibility, and whether the task will recur.
A query is long but consists of relational transformations.Readable SQL stages with checks.Whether complexity comes from the business rule or the implementation.

A long query is not, by itself, a reason to rewrite in Python. Joins, windows, aggregations, and multi-stage business rules can be expressed in SQL. Conversely, access to a database does not make SQL the clearest tool for every task.

Work through one real decision

Suppose a support manager wants to decide how many people to schedule next week. Tickets already live in the warehouse. Each ticket has an ID, a creation timestamp, a queue, and a status. The team also maintains a separate calendar of planned product launches.

The first request is descriptive: show ticket arrivals by local calendar day and queue for the last eight complete weeks. SQL is a sensible starting point. Filter the agreed population, assign each ticket to its reporting date, join an approved queue mapping, and count at day-and-queue grain. Preserve zero-activity days where the source is confirmed complete.

A dashboard can read that table directly. If the manager only needs a historical comparison, there is no missing Python step. Adding a notebook merely to repeat the same grouping would introduce another implementation of the metric.

Now the request changes: compare forecasting methods using historical holdout periods and evaluate how planned launches affect the staffing range. If the team already supports a suitable Python forecasting library and runtime, Python may earn its place. SQL still defines eligible arrivals; Python evaluates the modeling choices. The daily counts become an input contract between those stages.

The scope also matters. Daily queue totals may support a forecast of arrivals. They are insufficient for a detailed staffing model if you also need handling-time distributions, shift coverage, service targets, or simultaneous workload. Do not claim that predicting daily volume solves staffing. Agree which additional inputs are needed before choosing or implementing a model.

Define the handoff as a data contract

The boundary should be a named dataset with a defined grain, not “whatever the query returned.” For the illustrative daily-arrivals forecast, use a contract like this:

FieldAgreed meaning
One rowOne complete local calendar date and one supported queue.
Keyreport_date plus queue_id, unique and non-null.
Measurearrivals: eligible tickets first created that day; a nonnegative integer.
CalendarAmerica/Los_Angeles; the current partial day is excluded.
Zero versus missingZero means a completed source contained no arrivals. Missing or incomplete sources block the affected period.
LineageRetained source batch or snapshot, definition version, and extraction time recorded with the dataset.
ExclusionsTest tickets excluded under an approved rule; unknown queue mappings reported as exceptions.

These are example decisions, not universal support metrics. In another organization, reassigned tickets or reopened cases may be separate workload events. A stakeholder must confirm that definition before it becomes a shared input.

The receiving step checks the contract again: expected columns, types, key uniqueness, date coverage, nonnegative counts, and source readiness. Reconcile the arrival total with the SQL control total for the same population. A successful file read proves only that the file could be read.

Preserve the information the next method needs. A customer-level model needs customer-level observations; a median cannot generally be reconstructed from subgroup medians; an overall rate needs its component counts. Aggregation is a decision about what information to discard.

Run a small SQL-to-Python handoff

Here is a reproducible version of the support example for one queue. SQL defines the daily arrival series. Python compares two simple forecasting baselines on a later, held-out week. The division keeps population rules in one query while making the evaluation loop easy to extend with a supported modeling library.

The small baseline calculations could also be written in SQL. Python is justified here by the intended next step of evaluating several models in the team's Python environment, not by a claim that SQL cannot calculate an average.

1. Produce the agreed daily input

Run the following in PostgreSQL. It uses only generated fictional tickets and creates no tables. The fixture includes a test ticket, an event just before the reporting period, and an event exactly at its exclusive end. The first eligible ticket is exactly at the opening boundary. The other valid events occur at local noon.

WITH daily_fixture(day_offset, arrivals) AS (
    VALUES (0,4), (1,6), (2,8), (3,5), (4,7), (5,3), (6,2),
           (7,5), (8,7), (9,9), (10,6), (11,8), (12,4), (13,3)
),
tickets(ticket_id, created_at, is_test) AS (
    SELECT 100 * f.day_offset + s.n,
           ((DATE '2026-08-03' + f.day_offset)::timestamp
             + CASE WHEN f.day_offset = 0 AND s.n = 1
                    THEN INTERVAL '0 hours' ELSE INTERVAL '12 hours' END)
               AT TIME ZONE 'America/Los_Angeles',
           false
    FROM daily_fixture AS f
    CROSS JOIN LATERAL generate_series(1, f.arrivals) AS s(n)
    UNION ALL
    VALUES (9001, TIMESTAMPTZ '2026-08-03 07:00:00+00', true),
           (9002, TIMESTAMPTZ '2026-08-03 06:59:59+00', false),
           (9003, TIMESTAMPTZ '2026-08-17 07:00:00+00', false)
),
days AS (
    SELECT DATE '2026-08-03' + n AS report_date
    FROM generate_series(0, 13) AS g(n)
)
SELECT d.report_date, COUNT(t.ticket_id) AS arrivals
FROM days AS d
LEFT JOIN tickets AS t
  ON t.created_at >= d.report_date::timestamp
                        AT TIME ZONE 'America/Los_Angeles'
 AND t.created_at < (d.report_date + 1)::timestamp
                        AT TIME ZONE 'America/Los_Angeles'
 AND t.is_test = false
GROUP BY d.report_date
ORDER BY d.report_date;

The tickets CTE is the source stand-in; the calendar and final SELECT are the extraction stage. In a real workflow, use the approved source and bound period parameters, validate unique ticket IDs, and confirm source completeness before treating an absent day's events as zero. PostgreSQL documents generate_series, used here to create the fixture and calendar.

Export the result with headers to daily_arrivals.csv. It must contain exactly these 14 rows, totaling 77 arrivals:

report_date,arrivals
2026-08-03,4
2026-08-04,6
2026-08-05,8
2026-08-06,5
2026-08-07,7
2026-08-08,3
2026-08-09,2
2026-08-10,5
2026-08-11,7
2026-08-12,9
2026-08-13,6
2026-08-14,8
2026-08-15,4
2026-08-16,3

2. Validate the handoff and evaluate the next week

Save this as compare_baselines.py beside the CSV and run python3 compare_baselines.py. It uses Python's standard library. The file reader rejects an unexpected schema; the date check also catches missing, duplicated, or out-of-order days. The fixed total is a test-fixture control, not a number to hard-code into a live report.

import csv
from datetime import date, timedelta
from statistics import mean

with open('daily_arrivals.csv', newline='', encoding='utf-8') as source:
    reader = csv.DictReader(source)
    if reader.fieldnames != ['report_date', 'arrivals']:
        raise ValueError('Expected report_date,arrivals columns')
    rows = list(reader)

dates = [date.fromisoformat(row['report_date']) for row in rows]
counts = [int(row['arrivals']) for row in rows]
expected_dates = [date(2026, 8, 3) + timedelta(days=i) for i in range(14)]
if dates != expected_dates or any(value < 0 for value in counts):
    raise ValueError('Expected 14 ordered, unique, consecutive days and nonnegative counts')
if sum(counts) != 77:
    raise ValueError('Fixture control total should be 77')

training = counts[:7]
actual = counts[7:]
predictions = {
    'training_week_mean': [mean(training)] * 7,
    'previous_week_same_day': training.copy(),
}

for name, predicted in predictions.items():
    error = mean(abs(observed - forecast)
                 for observed, forecast in zip(actual, predicted))
    print(f'{name}: MAE={error:.3f} tickets/day')

Expected output:

training_week_mean: MAE=1.857 tickets/day
previous_week_same_day: MAE=1.000 tickets/day

The training-week mean predicts five tickets on every holdout day. The other baseline repeats the previous week's count for the same weekday. Mean absolute error (MAE) averages the absolute difference between prediction and observation, so lower is better on this fixed comparison. Neither baseline reads the second week's counts when making predictions.

The fictional second week was deliberately constructed as the first week plus one ticket per day. Its result illustrates the evaluation mechanics; it is not evidence that weekday forecasting wins on real support data. For model selection, use substantially more history and repeated time-ordered validation; reserve a separate final test period if tuning based on validation scores. The authors of Forecasting: Principles and Practice explain evaluating forecasts on observations outside the training sample.

The handoff decision is now inspectable: SQL sends 14 daily observations instead of raw ticket records; Python validates the contract and evaluates a method. The example does not benchmark either language or produce a staffing recommendation. Those require representative performance measurements and the handling-time, coverage, and service-target inputs described earlier.

Keep numerators and denominators when moving rates

A small example shows why the handoff matters. Queue A handled 10 eligible tickets, of which 9 met the response target. Queue B handled 90, of which 45 met it. Their rates are 90% and 50%.

The combined rate is (9 + 45) / (10 + 90) = 54%. Averaging the two displayed percentages gives 70%, which answers a different question: the unweighted average of queue rates.

Send met_target and eligible_tickets alongside any display rate. The next tool can then calculate the intended aggregate. When the denominator is zero, return an undefined or missing rate with an explanation; do not silently convert it into zero performance.

Other boundaries need similar care. Keep identifiers as identifiers, including leading zeros. Agree on time-zone handling. For exact monetary values, define a representation such as integer minor units or decimals and verify that the receiving tool preserves it.

Compare total cost, including moving the data

Time the complete workflow on representative data. Include query execution, transfer, parsing, transformation, and delivery. A fast local transformation can still be a poor choice if retrieving its raw input is the slowest and most expensive stage.

For each candidate, record the input size, columns returned, elapsed time, peak memory, platform usage, and whether the result matches the expected output. Keep filters and metric definitions identical. Note caching and concurrency conditions so a warm-cache result is not compared casually with a cold run.

Pandas primarily provides in-memory analysis. Its scaling guide recommends loading fewer columns and rows and considering intermediate memory use. A file's size on disk is not a safe estimate of peak working memory.

Chunking can help when a calculation decomposes cleanly. Counts and sums can be combined across chunks; an average needs the corresponding counts. Global deduplication, sorting, joins across chunks, and exact quantiles require additional coordination or a different execution strategy. Fetching chunks does not automatically make the whole analysis memory-bounded.

Avoid a universal row-count threshold for switching tools. Wide text fields, join expansion, available memory, engine configuration, and concurrency all matter. Measure the actual workload before moving a stable shared calculation.

Separate extraction, analysis, and orchestration

A useful division for the forecasting example is: SQL produces validated daily counts; a supported Python job performs the model evaluation; the existing scheduler runs the stages and reports failures; the dashboard displays an approved result. One tool may perform more than one role, but the responsibilities should remain clear.

When Python submits SQL, bind parameter values through the supported driver or query interface. Do not interpolate user-supplied filters into SQL strings. The Pandas SQL-query documentation describes parameter passing and notes that parameter syntax depends on the database driver.

Multiple extractions can observe different source states if records change between reads. Use a retained source version or an appropriate database snapshot when consistent inputs are required. PostgreSQL's transaction-isolation documentation explains why separate reads under the default Read Committed level need not see the same snapshot. Follow your platform's supported approach rather than keeping an ad hoc production transaction open indefinitely.

Record the query version, input identity, parameters, library environment, and any random seed needed by the method. A notebook with a saved chart but an unknown execution order is difficult to reproduce. Validate a clean run before handing it over.

Choose an owner, then write a short decision record

For the support example, the decision record could read:

Daily arrival definitions stay in SQL because they feed both reporting and forecasting. The analytics team owns the day-and-queue dataset. Python is used only for the forecast evaluation in the supported job environment. The input is a validated, versioned daily extract; incomplete periods stop the run. The support manager reviews the forecast's operational usefulness. Historical reporting remains available if the forecast fails, and its freshness is shown explicitly.

Record what would change the decision: the required method becomes available in the existing platform, inputs outgrow the approved runtime, latency requirements change, or the maintaining team changes. That gives future readers a reasoned choice instead of a preference disguised as an architectural rule.

Moving data also moves responsibility. Use approved storage and access controls, select only necessary fields, and keep credentials outside notebooks and query files. An aggregate may still contain sensitive information, especially for small groups. Confirm the destination is appropriate for the data.

FAQ

Am I falling behind if most of my work uses SQL?

Tool frequency alone does not measure analytical ability. Judge whether you can define the problem, produce trustworthy results, and maintain the workflow. Develop Python skills around a concrete need in your target work rather than adding it to every existing report.

Should all business logic live in SQL?

No universal rule fits every system. Shared relational metric definitions often benefit from living near shared data. Application behavior, external integrations, and specialist methods may belong elsewhere. Assign one authoritative implementation for each shared definition and document its consumers.

Should I rewrite a working notebook in SQL?

Consider it when stable relational transformations need shared ownership or repeated execution in the existing data platform. First preserve expected outputs and edge cases, compare both implementations on the same inputs, and migrate only if the operational benefit justifies the change.

Where should I go next?

Use the Pandas translation guide for syntax, expected-result tests for correctness, and the recurring-report guide when the analysis needs an operating schedule and recovery process.

Interview Prep

Begin Your SQL, Python, and R Journey

Master 230 interview-style coding questions and build the data skills needed for analyst, scientist, and engineering roles.

Related Articles

All Articles