All articles

KajoData / python en

SQL or Python: which should a data analyst learn first?

I start with where the data lives and the result I need. Using the same orders, I demonstrate SQL, Python and their cooperation before suggesting a learning sequence.

Kajo RudzińskiPublished Updated 13 min read

A database passes organised blocks to a general purpose processing workshop.

SQL or Python? When you begin learning data analysis, the choice can feel like a decision about your entire professional future. In practice, you are mainly choosing a tool for your first tasks. You can begin with one, learn a useful scope and add the other later. Learning an entire ecosystem at once is unnecessary for producing a worthwhile result.

My default suggestion for someone aiming to analyse data in relational databases is SQL. It introduces questions about tables: which rows to select, how to group them and what to join. If your immediate problem is combining files or automating a repetitive operation, Python may be a better starting point. The task, the data and the available environment should guide the decision.

Below, I compare the approaches using one example. My purpose is to show which decisions stay the same and what changes when you express them in code. There is no universal winner to announce. All the data is synthetic, and you can run the code locally without access to a company system or any private records.

SQL and Python address different needs

SQL lets me describe the result of an operation on data. A query specifies columns, filtering conditions and grouping rules. The database engine determines how to execute it. I still need to understand the meaning of the data and the consequences of the operations. Valid syntax cannot repair a wrong denominator or orders multiplied by an inappropriate join.

Python is a general purpose programming language. I can express sequences of operations, functions and responses to different conditions. The official Python tutorial introduces its language constructs. Understanding the syntax does not automatically mean understanding analytical libraries. However, quite a lot is possible before installing any additional packages, especially for a small learning exercise.

The tools can also cooperate. Python can open a connection, submit a SQL query and use the returned result. I use the sqlite3 module below to access SQLite. An in-memory database lets me inspect the complete process without running a separate server. This is a convenient learning arrangement rather than a universal architecture for applications.

Start with where your data lives

When data already exists in a database, learning its tables and the available SQL client is a natural first step. You can restrict retrieval to the columns, conditions and aggregates you need. This does not mean every calculation should always happen inside the database. It means examining the size of the required result before exporting an entire table without a clear purpose.

When you receive a directory of similarly structured files, Python can help iterate through them, check their columns and produce a combined output. That work also requires attention to paths, encoding, data types and reading errors. The flexibility is useful, but it comes with decisions that need to be made explicitly. A working loop alone does not establish that all files were interpreted correctly.

Access matters too. You might be allowed to read a database view but unable to install Python on a work computer. Alternatively, you may receive a file without any connection to its source system. Choose an exercise you can complete in an authorised environment. Your first project should develop data skills instead of becoming stuck on access that you cannot obtain yourself.

One question and six orders

Suppose I want to summarise paid orders by category. I need the number of orders, the number with a known quantity and the sum of known units. Each record represents one order. A real ordering system could have multiple items per order, but I deliberately use a simpler model here and state that assumption before calculating anything.

A missing quantity matters. I will not silently turn it into zero, because zero is a known value while missing means unknown. Instead, I report completeness separately. A summary can show the sum of available values while making clear that a category has an incomplete measurement. This analytical decision is necessary whichever language you choose to implement the calculation.

Create a fresh, empty SQLite database and execute:

-- example: fixture
CREATE TABLE orders (
    id INTEGER PRIMARY KEY,
    category TEXT NOT NULL,
    units INTEGER,
    status TEXT NOT NULL
);
INSERT INTO orders VALUES
    (1, 'books', 2, 'paid'),
    (2, 'games', 3, 'paid'),
    (3, 'books', 1, 'paid'),
    (4, 'games', NULL, 'paid'),
    (5, 'books', 9, 'cancelled'),
    (6, 'books', 0, 'paid');

For books, there are three paid orders and three known quantities: two, one and zero. The total is three units. For games, there are two paid orders, but only one has a known quantity. The sum of available values is also three. The cancelled order containing nine units is outside the report’s scope.

The SQL solution

The query expresses the rules together: a filter for paid orders, grouping by category and three measurements. I include explicit sorting so that the output order is predictable. The SQLite SELECT documentation describes these query elements. Without an appropriate ORDER BY, row order is not a promise on which I would base the expected output of an exercise.

-- example: summary
SELECT category,
       COUNT(*) AS paid_orders,
       COUNT(units) AS known_units,
       SUM(units) AS total_units
FROM orders
WHERE status = 'paid'
GROUP BY category
ORDER BY category;
category paid_orders known_units total_units
books 3 3 3
games 2 1 3

COUNT(*) counts rows after filtering, while COUNT(units) counts non-NULL values in that column. SUM(units) adds the available quantities; a group containing no known values produces NULL. The SQLite aggregate documentation describes that behaviour. Separating the measurements lets the output show a total and expose a gap in the underlying information at the same time.

I would not describe the two categories as identical just because their final columns match. Books have a complete measurement within this example, while games do not. If the reader needs a complete unit total, the missing quantity must be resolved or the interpretation restricted. Replacing NULL with zero without agreeing that rule would hide information needed to assess the report.

The same result in Python, connected to SQL

The program below is self-contained. It creates a list containing the same orders, calculates the result in Python, then runs the SQL query in SQLite and compares the outputs. It uses the standard library and does not require pandas. Save it in a file and run it with Python 3 and an available sqlite3 module; the example was checked using Python 3.14.

# example: compare
import sqlite3

orders = [
    (1, "books", 2, "paid"),
    (2, "games", 3, "paid"),
    (3, "books", 1, "paid"),
    (4, "games", None, "paid"),
    (5, "books", 9, "cancelled"),
    (6, "books", 0, "paid"),
]

groups = {}
for order_id, category, units, status in orders:
    if status != "paid":
        continue
    stats = groups.setdefault(category, [0, 0, 0])
    stats[0] += 1
    if units is not None:
        stats[1] += 1
        stats[2] += units

python_result = [
    (category, count, known, total if known else None)
    for category, (count, known, total) in sorted(groups.items())
]

connection = sqlite3.connect(":memory:")
try:
    connection.execute("""
        CREATE TABLE orders (
            id INTEGER PRIMARY KEY,
            category TEXT NOT NULL,
            units INTEGER,
            status TEXT NOT NULL
        )
    """)
    connection.executemany(
        "INSERT INTO orders VALUES (?, ?, ?, ?)", orders
    )
    sql_result = connection.execute("""
        SELECT category,
               COUNT(*) AS paid_orders,
               COUNT(units) AS known_units,
               SUM(units) AS total_units
        FROM orders
        WHERE status = 'paid'
        GROUP BY category
        ORDER BY category
    """).fetchall()
finally:
    connection.close()

assert python_result == sql_result
print(sql_result)

Output:

[('books', 3, 3, 3), ('games', 2, 1, 3)]

The Python loop skips cancelled orders and maintains three counters for each category. The condition units is not None preserves the distinction between a missing quantity and zero. Using just if units would treat zero as false and produce an incorrect count of known values. This is why I check apparently uninteresting inputs instead of stopping as soon as a program runs successfully.

Comparing the outputs checks agreement in this small example. It does not automatically establish that the business rules are correct. Two implementations can repeat the same mistaken assumption. That is why I first worked out the expected answer manually and explained the missing value. Code can check the implementation, while deciding what belongs in the calculation remains a separate responsibility.

The insertion uses placeholders and passes the values separately. There is no need to construct a different SQL string for each record. The connection also closes in a finally block, including when an operation fails. These details keep the example explicit about its resources and inputs without introducing a framework before you have understood the basic flow.

Two different processing lanes produce matching outputs that are inspected with a magnifying lens.

What the comparison shows

SQL is shorter here because grouping and aggregation are built into the language. The Python loop exposes individual operations and the state being maintained. That can be useful for learning, but it does not mean you should manually build aggregation dictionaries in every project. Analytical libraries provide their own operations, which you can learn when a task gives you a reason to use them.

I am not benchmarking performance on six rows. Speed depends on the data volume, storage location, query plan, transfer and structures being used. A shorter expression is not a measurement. If performance becomes a problem, measure the actual process and identify its cost. Counting lines of code cannot tell you which implementation is appropriate for a large production dataset.

The program is also not a complete reporting system. It does not provide scheduling, schema evolution or multi-user access. Its purpose is to demonstrate filtering, aggregation, missing quantities and exchanging results between tools. Before expanding it, make sure you can explain its output and reconstruct the calculation after a small input change. A clear boundary makes the example easier to learn from.

How I would begin learning SQL

I would initially choose one database and one way to execute queries. Then I would practise selecting columns, filtering, sorting and aggregating data I understand. For each query, I would write down what one output row represents. That question quickly exposes mistakes such as comparing order counts with item counts, or treating a total amount as a count of customers.

Next, I would introduce a second table and examine relationships between identifiers. A join can increase the number of rows and change totals. Recognising that consequence matters more than remembering every keyword. Subqueries, CTEs and window functions can follow, but they need not prevent you from finishing a first report with a clearly defined scope and basic checks.

Using our database, change the filter to cancelled orders and predict the output before executing the query. Then make every quantity in one category missing. Inspect all three measurements and explain the difference. Small variations like these develop the ability to read results. Another tutorial is useful when it helps answer a question that arose while you were actually doing the work.

Keep the question visible while you experiment. If you add DISTINCT merely because a result contains repeated values, first ask whether those repetitions are valid observations. If you limit output to a few rows, explain how those rows were chosen. Learning a keyword includes learning what it leaves unresolved. You can explore this further in the SELECT DISTINCT knowledge-base entry.

How I would begin learning Python

I would start with values, types, lists, dictionaries, conditions and loops. Then I would put one operation into a function and call it using different inputs. Understanding the input and output is essential. A function whose result you cannot predict for three records simply places uncertainty inside a neater-looking container; the function definition itself does not resolve it.

The next step would be reading a small file, checking its structure and producing a useful error message. Distinguishing text from numbers and missing values from zero is part of that work. I would add a library for a specific task afterwards. This gives a later pandas error message a chance to mean something beyond being text that needs to be pasted into a search engine.

Keep a working example and change one thing at a time. Replacing the environment, library, data source and question simultaneously makes it difficult to identify the cause of a failure. Early on, a small independent revision tells you more about progress than a large script copied without understanding. Extend the program after checking its basic behaviour and recording what you expect to remain unchanged.

For this exercise, inspect the three values stored in each dictionary entry and explain their meaning without running the code. Then trace one book order, the missing game quantity and the cancelled order. If those steps are clear, move the calculation into a function that accepts a list. That is a manageable extension with a visible purpose and a result you can still check manually.

Check the boundaries before calling the task finished

After obtaining matching results, I would test an empty order list. Both implementations should return an empty list of categories. Having no orders differs from having a category whose quantities are all unknown. In the latter case, the group still exists, its order count is positive, its known-quantity count is zero and its total remains unknown. That explains the condition on the known-value counter in the Python result construction.

Next, check an order with zero units. Our fictional model permits zero and treats it as known. In another analysis, zero could indicate a data error or a special operation. A revised rule should appear in the task description and both implementations. Choosing a language does not decide what a record means; the tool expresses a decision about the data.

A repeated identifier exposes another boundary. A Python list can contain two records with the same ID, and our loop would count both. The SQLite table has a primary key, so inserting a duplicate is rejected. The calculation comparison assumes valid input with unique identifiers. When data arrives from a file, check that rule before reporting and decide how conflicting records should be investigated.

Write the expected outcome of each check before changing the code. Otherwise, it is easy to accept whatever happens to appear on the screen. An expectation can need correction, but the reason should follow from the task’s rules. This habit remains useful when you move to other libraries and engines, even when the syntax looks completely different.

When to add the second tool

I would add Python to SQL when I need to connect a query with file processing, result checks or subsequent operations. I would add SQL to Python when the data lives in a relational database and I want deliberate control over retrieval. In both cases, a concrete problem limits the material to learn and gives you a way to assess whether the new tool actually helped.

You do not need complete mastery first. You do need a foothold: the ability to run a solution, explain it and check basic mistakes. If every task still requires copying an entire answer, adding another language may create more confusion. Return to a smaller example and identify the decision that is difficult. It might concern the data rather than the language you selected.

Data understanding remains a shared foundation. Identifiers, duplicates, missing values and the unit of observation return in both tools. You can change the syntax and still count an order twice. I would therefore connect language practice with simple checks: how many records existed, how many remain, whether totals make sense and which cases fall outside the stated assumptions.

Do not assign an entire profession to a language

Programmers may use SQL, and analysts may use Python. There are also applications whose authors do not write SQL directly, for example when using other storage systems or existing access layers. The earlier Polish article’s suggestion that building an application without SQL was practically impossible was too broad. SQL’s usefulness in analysis does not depend on defending that claim.

A Python library and a SQL dialect are different concepts too. A library supplies tools available to a program, while a dialect describes SQL syntax and behaviour in a particular engine. A query executed in SQLite does not validate every SQL Server function. This article executes SQLite. Using another environment requires checking its documentation and running the relevant code there.

If your unresolved question is the role itself, return to choosing between analysis and programming through practical tasks. For the next learning stage, I suggest a smaller decision: choose one problem, run a complete solution, check its output and then expand your toolkit. The order of learning should make practice possible instead of becoming another subject to postpone.

Key takeaways

Should a beginner data analyst start with SQL?

I recommend it when the first tasks involve relational data: filtering, grouping and joining tables. If your immediate need is automating file processing or working with an API, Python may be a better starting point.

Do I need Python to work with SQL?

No. You can execute queries in a database client. Python is useful when you want to connect data retrieval with other process steps, but it is a separate skill.

Does the example require pandas or a database server?

No. It uses Python 3 with the standard sqlite3 module and an in-memory SQLite database. The data is synthetic and the example does not connect to a company system.

Kajo Rudziński

Newsletter KajoData

Let’s keep in touch.

I write about data analysis, learning and changing careers. My newsletter and the sign-up form are in Polish. If that works for you, I’d be glad to see you there.