Skip to main content
Workforce LibreTexts

4.9: Cleaning Data with SQL

  • Page ID
    48054
  • \( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \)

    \( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)

    \( \newcommand{\dsum}{\displaystyle\sum\limits} \)

    \( \newcommand{\dint}{\displaystyle\int\limits} \)

    \( \newcommand{\dlim}{\displaystyle\lim\limits} \)

    \( \newcommand{\id}{\mathrm{id}}\) \( \newcommand{\Span}{\mathrm{span}}\)

    ( \newcommand{\kernel}{\mathrm{null}\,}\) \( \newcommand{\range}{\mathrm{range}\,}\)

    \( \newcommand{\RealPart}{\mathrm{Re}}\) \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\)

    \( \newcommand{\Argument}{\mathrm{Arg}}\) \( \newcommand{\norm}[1]{\| #1 \|}\)

    \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\)

    \( \newcommand{\Span}{\mathrm{span}}\)

    \( \newcommand{\id}{\mathrm{id}}\)

    \( \newcommand{\Span}{\mathrm{span}}\)

    \( \newcommand{\kernel}{\mathrm{null}\,}\)

    \( \newcommand{\range}{\mathrm{range}\,}\)

    \( \newcommand{\RealPart}{\mathrm{Re}}\)

    \( \newcommand{\ImaginaryPart}{\mathrm{Im}}\)

    \( \newcommand{\Argument}{\mathrm{Arg}}\)

    \( \newcommand{\norm}[1]{\| #1 \|}\)

    \( \newcommand{\inner}[2]{\langle #1, #2 \rangle}\)

    \( \newcommand{\Span}{\mathrm{span}}\) \( \newcommand{\AA}{\unicode[.8,0]{x212B}}\)

    \( \newcommand{\vectorA}[1]{\vec{#1}}      % arrow\)

    \( \newcommand{\vectorAt}[1]{\vec{\text{#1}}}      % arrow\)

    \( \newcommand{\vectorB}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \)

    \( \newcommand{\vectorC}[1]{\textbf{#1}} \)

    \( \newcommand{\vectorD}[1]{\overrightarrow{#1}} \)

    \( \newcommand{\vectorDt}[1]{\overrightarrow{\text{#1}}} \)

    \( \newcommand{\vectE}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash{\mathbf {#1}}}} \)

    \( \newcommand{\vecs}[1]{\overset { \scriptstyle \rightharpoonup} {\mathbf{#1}} } \)

    \(\newcommand{\longvect}{\overrightarrow}\)

    \( \newcommand{\vecd}[1]{\overset{-\!-\!\rightharpoonup}{\vphantom{a}\smash {#1}}} \)

    \(\newcommand{\ket}[1]{\left| #1 \right>}\)
    \(\newcommand{\bra}[1]{\left< #1 \right|}\)
    \(\newcommand{\braket}[2]{\left< #1 \vphantom{#2} \right| \left. #2 \vphantom{#1} \right>}\)
    \(\newcommand{\braopket}[3]{\left< #1 \vphantom{#2}\vphantom{#3} \right| #2 \vphantom{#1}\vphantom{#3} \left| #3 \vphantom{#1}\vphantom{#2} \right>}\)
    \(\newcommand{\qmvec}[1]{\mathbf{\vec{#1}}}\)
    \(\newcommand{\op}[1]{\hat{\mathbf{#1}}}\)
    \(\newcommand{\expect}[1]{\langle #1 \rangle}\)
    \(\newcommand{\dfn}[1]{\emph{\textbf{#1}}}\)

    \(\newcommand{\avec}{\mathbf a}\) \(\newcommand{\bvec}{\mathbf b}\) \(\newcommand{\cvec}{\mathbf c}\) \(\newcommand{\dvec}{\mathbf d}\) \(\newcommand{\dtil}{\widetilde{\mathbf d}}\) \(\newcommand{\evec}{\mathbf e}\) \(\newcommand{\fvec}{\mathbf f}\) \(\newcommand{\nvec}{\mathbf n}\) \(\newcommand{\pvec}{\mathbf p}\) \(\newcommand{\qvec}{\mathbf q}\) \(\newcommand{\svec}{\mathbf s}\) \(\newcommand{\tvec}{\mathbf t}\) \(\newcommand{\uvec}{\mathbf u}\) \(\newcommand{\vvec}{\mathbf v}\) \(\newcommand{\wvec}{\mathbf w}\) \(\newcommand{\xvec}{\mathbf x}\) \(\newcommand{\yvec}{\mathbf y}\) \(\newcommand{\zvec}{\mathbf z}\) \(\newcommand{\rvec}{\mathbf r}\) \(\newcommand{\mvec}{\mathbf m}\) \(\newcommand{\zerovec}{\mathbf 0}\) \(\newcommand{\onevec}{\mathbf 1}\) \(\newcommand{\real}{\mathbb R}\) \(\newcommand{\twovec}[2]{\left[\begin{array}{r}#1 \\ #2 \end{array}\right]}\) \(\newcommand{\ctwovec}[2]{\left[\begin{array}{c}#1 \\ #2 \end{array}\right]}\) \(\newcommand{\threevec}[3]{\left[\begin{array}{r}#1 \\ #2 \\ #3 \end{array}\right]}\) \(\newcommand{\cthreevec}[3]{\left[\begin{array}{c}#1 \\ #2 \\ #3 \end{array}\right]}\) \(\newcommand{\fourvec}[4]{\left[\begin{array}{r}#1 \\ #2 \\ #3 \\ #4 \end{array}\right]}\) \(\newcommand{\cfourvec}[4]{\left[\begin{array}{c}#1 \\ #2 \\ #3 \\ #4 \end{array}\right]}\) \(\newcommand{\fivevec}[5]{\left[\begin{array}{r}#1 \\ #2 \\ #3 \\ #4 \\ #5 \\ \end{array}\right]}\) \(\newcommand{\cfivevec}[5]{\left[\begin{array}{c}#1 \\ #2 \\ #3 \\ #4 \\ #5 \\ \end{array}\right]}\) \(\newcommand{\mattwo}[4]{\left[\begin{array}{rr}#1 \amp #2 \\ #3 \amp #4 \\ \end{array}\right]}\) \(\newcommand{\laspan}[1]{\text{Span}\{#1\}}\) \(\newcommand{\bcal}{\cal B}\) \(\newcommand{\ccal}{\cal C}\) \(\newcommand{\scal}{\cal S}\) \(\newcommand{\wcal}{\cal W}\) \(\newcommand{\ecal}{\cal E}\) \(\newcommand{\coords}[2]{\left\{#1\right\}_{#2}}\) \(\newcommand{\gray}[1]{\color{gray}{#1}}\) \(\newcommand{\lgray}[1]{\color{lightgray}{#1}}\) \(\newcommand{\rank}{\operatorname{rank}}\) \(\newcommand{\row}{\text{Row}}\) \(\newcommand{\col}{\text{Col}}\) \(\renewcommand{\row}{\text{Row}}\) \(\newcommand{\nul}{\text{Nul}}\) \(\newcommand{\var}{\text{Var}}\) \(\newcommand{\corr}{\text{corr}}\) \(\newcommand{\len}[1]{\left|#1\right|}\) \(\newcommand{\bbar}{\overline{\bvec}}\) \(\newcommand{\bhat}{\widehat{\bvec}}\) \(\newcommand{\bperp}{\bvec^\perp}\) \(\newcommand{\xhat}{\widehat{\xvec}}\) \(\newcommand{\vhat}{\widehat{\vvec}}\) \(\newcommand{\uhat}{\widehat{\uvec}}\) \(\newcommand{\what}{\widehat{\wvec}}\) \(\newcommand{\Sighat}{\widehat{\Sigma}}\) \(\newcommand{\lt}{<}\) \(\newcommand{\gt}{>}\) \(\newcommand{\amp}{&}\) \(\definecolor{fillinmathshade}{gray}{0.9}\)

    Cleaning Data with SQL

    SQL (Structured Query Language) is the backbone of data manipulation in relational databases. It might not have fancy graphical interfaces, but it is extremely powerful for cleaning operations. Many data analysts and engineers rely on SQL scripts or queries as part of their Extract-Transform-Load (ETL) processes for data cleaning.

    SQL for filtering and detection:

    The first step in cleaning is often detection of issues, and SQL excels at querying data to find anomalies. We have already given many examples: using WHERE clauses to find NULLs or out-of-range values, using GROUP BY ... HAVING COUNT(*)>1 to find duplicates, joins to find referential breaks, etc. SQL allows you to slice and dice the data to isolate problematic records. For example:

    -- Find products with negative prices (invalid business rule)
    SELECT ProductID, Price FROM Products
    WHERE Price < 0;

    Or:

    -- Find potential duplicate customers by same email
    SELECT Email, COUNT(*) FROM Customers
    GROUP BY Email
    HAVING COUNT(*) > 1;

    Such queries don’t fix data by themselves, but they produce result sets that highlight what needs fixing.

    SQL for transformation:

    Once issues are identified, SQL can also correct or transform data via UPDATE statements or creating new cleaned tables with INSERT SELECT statements. For instance, if we want to fix inconsistent text cases:

    UPDATE Customers
    SET State = UPPER(State);

    This would standardize all state abbreviations to uppercase. Another example, replacing empty strings with NULL:

    UPDATE Customers
    SET Phone = NULL
    WHERE Phone = '';

    In some workflows, instead of updating the original data, you might create a cleaned version. For example:

    CREATE TABLE Customers_Cleaned AS
    SELECT
      CustomerID,
      UPPER(TRIM(State)) AS State,
      COALESCE(NULLIF(Phone, ''), 'Unknown') AS Phone,
      -- ... other fields with transformations ...
    FROM Customers;

    Here, we create a new table applying transformations: TRIM to remove whitespace, UPPER to normalize case, and using a combination of NULLIF and COALESCE to turn empty strings into "Unknown" (COALESCE returns the first non-null, so if Phone becomes NULL for empty string, it then gives "Unknown"). This approach is common in ETL: reading from a raw table and writing to a cleaned table.

    Casting and converting in SQL: We already talked about CAST() for data types. SQL also provides functions like TO_DATE or PARSE functions in some dialects for complex conversion (e.g., parsing a string with a specific date format into a date). Ensuring all conversions succeed is part of cleaning. Often, you might use a staging table where all columns are text (just to load the raw data), and then a SQL step to convert those to their proper types, pushing any bad records into an “error” table for manual review. This can be done with case expressions:

    INSERT INTO Errors_Table
    SELECT *
    FROM Staging
    WHERE TRY_CAST(price AS DECIMAL(10,2)) IS NULL
      AND price IS NOT NULL;

    (This captures rows where price is not null but couldn’t convert to decimal, meaning it contained non-numeric text.)

    SQL for deduplication: Suppose we identify duplicate entries. How to remove them with SQL? One way is using window functions:

    DELETE FROM Customers
    WHERE CustomerID NOT IN (
        SELECT MIN(CustomerID)
        FROM Customers
        GROUP BY Email
    );

    This would keep the lowest ID for each email and delete others, under the assumption duplicates share email. Or using window functions:

    WITH Ranked AS (
      SELECT *, ROW_NUMBER() OVER (PARTITION BY Email ORDER BY CustomerID) as rn
      FROM Customers
    )
    DELETE FROM Ranked WHERE rn > 1;

    This marks duplicates (rn > 1) for deletion, leaving one record per email. Caution: You need to decide the criteria for which duplicate to keep (here we arbitrarily kept the lowest ID).

    Validating with constraints in SQL: One neat trick during cleaning is to leverage the database’s constraint enforcement to check your work. For example, after cleaning, you might add a temporary UNIQUE constraint on a field that should be unique to see if any duplicates remain (the DB will throw an error if they do). Or use ALTER TABLE ... ADD CHECK to test a rule – if it fails, the DB will complain indicating some row violates it. If you use a transaction and roll it back, you can do this testing without permanently altering the schema, just to verify integrity.

    Stored procedures and scripts:

    In complex cleaning, you may wrap SQL steps in a stored procedure or script that does multiple passes: e.g., first fill default values, then fix formats, then remove duplicates, etc., logging how many rows were affected at each step. This becomes a repeatable cleaning pipeline that can be run whenever new raw data comes in. For example, a stored procedure might take all new daily records, apply a series of UPDATE/DELETE statements for cleaning, and then insert them into the main table.

    Example of a cleaning workflow in SQL:

    Imagine we have a table Sales_Staging with raw sales data loaded from CSVs. We want to clean it and append to Sales table. Steps might be: 1. Remove entirely blank rows (if any got in) – DELETE FROM Sales_Staging WHERE OrderID IS NULL AND CustomerID IS NULL ...;. 2. Fix data types: perhaps ALTER TABLE Sales_Staging ALTER COLUMN SaleAmount TYPE DECIMAL(10,2) if the staging was text – but more often, we’d do INSERT ... SELECT CAST(...) into the final. 3. Handle missing values: fill ShipDate with OrderDate if missing (assuming same-day shipping as a business rule) – UPDATE Sales_Staging SET ShipDate = OrderDate WHERE ShipDate IS NULL;. 4. Enforce constraints manually: ensure all CustomerIDs exist in Customers (remove or mark those that don’t, or fix IDs if possible). 5. Finally, insert into Sales (which might have constraints turned on).

    Each of these can be done with plain SQL. It might be more code to write compared to using a specialized tool, but SQL’s advantage is that it is very transparent, auditable, and often efficient on large datasets since the work is pushed to the database engine.

    In short, SQL remains one of the most important tools for data cleaning. It provides granular control and is the lingua franca for data in relational stores. Mastering a set of SQL queries for cleaning common issues is a valuable skill for any data analyst. It’s no surprise that you can explore and clean data through SQL or via UI tools – oftentimes, even the UI tools will generate SQL under the hood for the transformations you specify.


    This page titled 4.9: Cleaning Data with SQL was last modified on Tue, 04 Aug 2026 22:40:46 GMT and is shared under a CC BY 4.0 license and was authored, remixed, and/or curated by Felix Amoruwa.

    • Was this article helpful?