For AI agents: the complete documentation index is available at https://docs.dataplatform.ovh.net/llms.txt, the full documentation bundle is available at https://docs.dataplatform.ovh.net/llms-full.txt, and this page is available as Markdown at https://docs.dataplatform.ovh.net/tutorials-sql-transformation.md.
  • πŸ‡¬πŸ‡§ English
  • SQL Transformations within the OVHcloud Data Platform

    SQL (Structured Query Language) remains the most widely used and efficient language for querying and transforming structured data

    Objective

    Welcome to the guide on performing SQL Transformations within the OVHcloud Data Platform. SQL (Structured Query Language) remains the most widely used and efficient language for querying and transforming structured data. This document will introduce you to the core concepts of using SQL for data manipulation and aggregation directly within our platform, leveraging its powerful capabilities for data processing.

    Whether you're looking to clean, reshape, filter, or aggregate large datasets, SQL provides a robust and intuitive way to achieve your data transformation goals. Its declarative nature allows you to focus on what you want to achieve with your data, rather than how the operations are performed, making it accessible to a wide range of data professionals.

    Why SQL for Data Transformation?

    SQL is an indispensable tool in the data transformation pipeline for several key reasons:

    • Universality: It's a standard language, highly adopted across various databases and data platforms, making skills easily transferable.
    • Readability & Simplicity: Its English-like syntax makes complex operations relatively easy to understand and write.
    • Performance: SQL engines are highly optimized for relational operations, often outperforming custom code for large-scale data manipulation.
    • Declarative Nature: You specify the desired end-state of your data, and the engine determines the most efficient way to achieve it.
    • Integration: Seamlessly integrates with data warehousing, business intelligence, and reporting tools.

    Core Concepts of SQL Transformations

    At its heart, SQL transformation involves using standard SQL commands to manipulate data. This can include:

    1. Data Cleaning and Preparation

    • Filtering: Using WHERE clauses to select specific rows based on conditions.
    • Selecting/Projecting: Using SELECT statements to choose specific columns and rename them (AS).
    • Type Conversion: Using CAST or TRY_CAST functions to change data types. TRY_CAST is particularly useful in Trino for gracefully handling conversion errors (returning NULL instead of crashing).
    • Handling Missing Values: Using COALESCE or CASE statements to replace NULL values.
    • String Manipulation: Functions like SUBSTRING, LENGTH, UPPER, LOWER, TRIM to clean and standardize text data.

    2. Data Aggregation and Summarization

    • Grouping: Using GROUP BY to aggregate rows that have the same values in specified columns.
    • Aggregate Functions: Applying functions like COUNT, SUM, AVG, MIN, MAX to summarized groups.
    • Filtering Aggregations: Using HAVING clauses to filter results of GROUP BY operations.

    3. Data Reshaping and Restructuring

    • Joins: Combining data from two or more tables based on related columns (INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN).
    • Unions: Combining the result sets of two or more SELECT statements (UNION, UNION ALL).
    • Pivoting/Unpivoting: Transforming rows into columns (pivot) or columns into rows (unpivot) to change the data's structure. In Trino SQL (commonly used in DPE), pivoting is often achieved using SUM with FILTER or CASE statements, and unpivoting with CROSS JOIN UNNEST.
    • Window Functions: Performing calculations across a set of table rows that are related to the current row, without collapsing rows (ROW_NUMBER(), RANK(), LEAD(), LAG(), SUM() OVER(), AVG() OVER()).

    SQL Transformations on OVHcloud Data Platform (Practical Guide)

    This section provides a practical, end-to-end example of performing SQL transformations within the OVHcloud Data Platform's DPE (Data Processing Environment) Notebooks, specifically using the SDK and focusing on the dirty_cafe_sales dataset.

    Prerequisites

    1. Download the dirty_cafe_sales.csv
    2. Upload it to the Connectors and extract the metadata using the Analyzer.
    3. Create a new Table from source in the Lakehouse Manager

    Dataset Information:

    We will be working with a simulated cafe sales dataset named dirty_cafe_sales. It contains the following columns and known data quality issues:

    Column NameDescriptionKnown Issues
    transaction_idUnique identifier for each transactionNone
    itemName of the item soldNone
    quantityNumber of units sold in the transactionContains 'UNKNOWN' strings
    price_per_unitPrice of a single unit of the itemNone
    total_spentTotal amount spent for the item line in transactionContains 'ERROR' strings
    payment_methodMethod used for payment (e.g., Credit Card, Cash)None
    locationSales location (e.g., In-store, Takeaway)Contains 'ERROR' strings and empty strings ''
    transaction_dateDate of the transactionContains non-date strings (e.g., 'ERROR'), needs casting and error handling

    1. Create a New DPE Notebook

    • Navigate to Data Processing Environment (DPE) β†’ Notebooks.
    • Click + New Notebook and for the purpose of this guide we will continue with the Base Notebook.
    • Give your notebook a meaningful name (e.g., Cafe_Sales_SQL_Transformations).
    • Click on Create.
    • Once JupyterLab is open, click on the Python3 Notebook to create a new .ipynb file. This is where we are going to run all the following steps in cells.

    2. Connect to the SDK

    We'll use the SDK provided in your DPE environment. This SDK provides methods to connect to the Lakehouse Manager and interact with tables.

    # Import necessary modules from SDK
    from forepaas.dwh import connect, bulk_insert
    from forepaas.core.settings import CONFIG
    from forepaas.dwh.logical import LogicalObject
    import pandas as pd #useful for displaying dataframes
    
    print("SDK modules imported successfully.")

    3. List Tables from the Dataset

    Before connecting to dirty_cafe_sales, it's good practice to list available tables to confirm its presence and exact name within the specified data path.

    # Connect to the Lakehouse Manager
    # Connect to the default Lakehouse Manager dataset
    connector = connect("dwh/default_dataset/")
    
    print("Listing tables in 'dwh/default_dataset/':")
    available_tables = connector.list()
    for table_name in available_tables:
        print(f"- {table_name}")
    
    if "dirty_cafe_sales" in available_tables:
        print("\n'dirty_cafe_sales' table found!")
    else:
        print("\nWARNING: 'dirty_cafe_sales' table not found. Please check the table name or path.")

    4. Connect to the Table and Inspect Data

    Now, let's connect to the dirty_cafe_sales table using connector.select() and print its information and descriptive statistics. This step visually confirms the data, including the 'ERROR' and 'UNKNOWN' values we need to clean.

    # Connect to the 'dirty_cafe_sales' table
    df_raw_sales = connector.select("dirty_cafe_sales")
    
    print("\nDataset information:")
    df_raw_sales.info()
    
    print("\nDataset details:")
    df_raw_sales.describe()
    
    print("\nSample of Raw Data (first 5 rows):")
    display(df_raw_sales.head())

    5. Run SQL Commands (Transformations)

    This is the core of our transformation. We'll define two SQL queries: a simple one for basic exploration and a more complex one for thorough cleaning and detailed aggregation.

    Info

    Important Note on Semicolons: When executing SQL through an SDK or API in a programmatic environment like a notebook, do not include a trailing semicolon (;) at the very end of your SQL query string. The API typically expects a single SQL statement without an explicit delimiter at the end. Including it can lead to common errors like mismatched input ';' or syntax error near ';'.

    5.1 Simple SQL Query: Daily Total Sales per Location (Initial Exploration)

    This query demonstrates a basic aggregation, initially showing how raw data issues can lead to errors, and then fixing them using TRY_CAST for robustness. It also cleans the location field.

    print("--- Running Simple SQL Query ---")
    
    SIMPLE_SQL_QUERY = """
    SELECT
        CAST(valid_transaction_date AS DATE) AS sale_date,
        -- Handle 'ERROR' and empty strings in location
        CASE
            WHEN location = 'ERROR' THEN 'Unknown'
            WHEN TRIM(location) = '' THEN 'Unknown'
            ELSE location
        END AS clean_location,
        SUM(TRY_CAST(total_spent AS DOUBLE)) AS gross_revenue_dirty
    FROM
        (
            SELECT
                TRY_CAST(transaction_date AS DATE) AS valid_transaction_date,
                location,
                total_spent
            FROM
                dirty_cafe_sales
        ) AS subquery_sales
    WHERE
        valid_transaction_date IS NOT NULL
    GROUP BY
        CAST(valid_transaction_date AS DATE),
        CASE
            WHEN location = 'ERROR' THEN 'Unknown'
            WHEN TRIM(location) = '' THEN 'Unknown'
            ELSE location
        END
    ORDER BY
        sale_date DESC, clean_location
    """ # No semicolon at the end here!
    
    try:
        # Execute the query using your connector's method.
        df_simple_result = connector.query(SIMPLE_SQL_QUERY)
        
        # --- Pandas Post-Processing for Data Types ---
        # Convert 'sale_date' to datetime objects for proper date operations
        df_simple_result['sale_date'] = pd.to_datetime(df_simple_result['sale_date'])
        # --- End Pandas Post-Processing ---
    
        print("\nSimple Query Results (first 10 rows):")
        display(df_simple_result.head(10))
        print("\nSimple Query Results Schema:")
        df_simple_result.info()
    
    except Exception as e:
        print(f"Error executing Simple SQL query: {e}")
        print("\nFailed SQL Query:\n", SIMPLE_SQL_QUERY)

    5.2 Complex SQL Query: Detailed Cleaned Daily Item Performance

    This query will perform robust data cleaning, calculate accurate sales metrics, and aggregate them by date, item, and location. It directly addresses the data quality issues identified (UNKNOWN in quantity, ERROR in total_spent, bad transaction_date strings, and problematic location entries).

    print("\n--- Running Complex SQL Transformation Query ---")
    
    COMPLEX_SQL_TRANSFORMATION_QUERY = """
    WITH cleaned_and_corrected_sales AS (
        SELECT
            transaction_id,
            item,
            -- Clean and cast quantity: 'UNKNOWN' becomes NULL, then cast to INTEGER.
            -- TRY_CAST handles non-numeric strings safely by returning NULL.
            TRY_CAST(NULLIF(quantity, 'UNKNOWN') AS INTEGER) AS quantity_cleaned,
            -- Ensure price_per_unit is numeric, handling potential non-numeric entries safely.
            TRY_CAST(price_per_unit AS DOUBLE) AS price_per_unit_cleaned,
            payment_method,
            -- Clean location: 'ERROR' and empty strings become 'Unknown'.
            CASE
                WHEN location = 'ERROR' THEN 'Unknown'
                WHEN TRIM(location) = '' THEN 'Unknown'
                ELSE location
            END AS clean_location,
            -- Use TRY_CAST for transaction_date to handle bad date strings safely, then filter later.
            TRY_CAST(transaction_date AS DATE) AS sale_date_raw
        FROM
            dirty_cafe_sales
    ),
    final_calculated_sales AS (
        SELECT
            transaction_id,
            item,
            quantity_cleaned AS final_quantity,
            price_per_unit_cleaned AS final_price_per_unit,
            -- Recalculate total_spent based on cleaned quantity and price_per_unit.
            quantity_cleaned * price_per_unit_cleaned AS calculated_total_spent,
            payment_method,
            clean_location,
            sale_date_raw AS sale_date
        FROM
            cleaned_and_corrected_sales
        -- Filter out rows where crucial values (quantity, price_per_unit, or sale_date)
        -- couldn't be cleanly converted, ensuring only valid data proceeds.
        WHERE
            quantity_cleaned IS NOT NULL 
            AND price_per_unit_cleaned IS NOT NULL
            AND sale_date_raw IS NOT NULL 
    )
    SELECT
        sale_date,
        item,
        clean_location AS location,
        COUNT(DISTINCT transaction_id) AS number_of_transactions,
        SUM(final_quantity) AS total_items_sold,
        SUM(calculated_total_spent) AS total_revenue_cleaned,
        AVG(final_price_per_unit) AS average_item_price_per_unit,
        -- Pivot revenue by payment method using Trino's FILTER clause
        SUM(calculated_total_spent) FILTER (WHERE payment_method = 'Credit Card') AS revenue_credit_card,
        SUM(calculated_total_spent) FILTER (WHERE payment_method = 'Cash') AS revenue_cash,
        SUM(calculated_total_spent) FILTER (WHERE payment_method = 'Digital Wallet') AS revenue_digital_wallet,
        -- Calculate average quantity per transaction for this group
        CAST(SUM(final_quantity) AS DOUBLE) / CAST(COUNT(DISTINCT transaction_id) AS DOUBLE) AS avg_quantity_per_transaction
    FROM
        final_calculated_sales
    GROUP BY
        sale_date,
        item,
        clean_location
    ORDER BY
        sale_date DESC, total_revenue_cleaned DESC
    """ # No semicolon at the end here!
    
    try:
        # Execute the complex transformation query
        df_transformed = connector.query(COMPLEX_SQL_TRANSFORMATION_QUERY)
    
        # --- Pandas Post-Processing for Data Types ---
        # Convert 'sale_date' to datetime objects for proper date operations
        df_transformed['sale_date'] = pd.to_datetime(df_transformed['sale_date'])
        # --- End Pandas Post-Processing ---
    
        print("\nTransformed Cafe Sales Data (Sample - first 5 rows):")
        display(df_transformed.head())
        print(f"\nTransformed Data Schema:")
        df_transformed.info()
    
        print(f"\nTotal rows in transformed data: {len(df_transformed)}")
    
    except Exception as e:
        print(f"Error during Complex SQL transformation: {e}")
        print("\nFailed SQL Query:\n", COMPLEX_SQL_TRANSFORMATION_QUERY)

    6. Create a Physical Table from the Transformed Data (CTAS)

    After successfully performing the complex transformation and verifying the results, the next logical step is to persist this cleaned and aggregated data into a new physical table in your database. This is typically done using a CREATE TABLE AS SELECT (CTAS) statement. This new table can then be used for reporting, further analysis, or as a source for other data processes, without needing to re-run the complex cleaning logic every time.

    Info

    Important Note on SQL Execution: Some database connectors or APIs expect only one SQL statement per query() call. To execute DROP TABLE and CREATE TABLE AS SELECT, we'll send them as separate commands. Also, ensure there are no trailing semicolons at the very end of each query string.

    print("\n--- Creating Physical Table from Complex SQL Transformation Query ---")
    
    # Define the name of your new cleaned table. This variable can be reused across cells.
    NEW_CLEANED_TABLE_NAME = "cleaned_cafe_sales_daily_summary"
    
    # 1. DROP TABLE statement (removes the table if it already exists, for idempotent runs)
    # Note: No trailing semicolon at the very end of the string.
    DROP_TABLE_QUERY = f"DROP TABLE IF EXISTS {NEW_CLEANED_TABLE_NAME}"
    
    # 2. CREATE TABLE AS SELECT statement
    # This uses the same logic from the COMPLEX_SQL_TRANSFORMATION_QUERY
    # Note: No trailing semicolon at the very end of the string.
    CTAS_CORE_QUERY = f"""
    CREATE TABLE {NEW_CLEANED_TABLE_NAME} AS
    WITH cleaned_and_corrected_sales AS (
        SELECT
            transaction_id,
            item,
            TRY_CAST(NULLIF(quantity, 'UNKNOWN') AS INTEGER) AS quantity_cleaned,
            TRY_CAST(price_per_unit AS DOUBLE) AS price_per_unit_cleaned,
            payment_method,
            CASE
                WHEN location = 'ERROR' THEN 'Unknown'
                WHEN TRIM(location) = '' THEN 'Unknown'
                ELSE location
            END AS clean_location,
            TRY_CAST(transaction_date AS DATE) AS sale_date_raw
        FROM
            dirty_cafe_sales
    ),
    final_calculated_sales AS (
        SELECT
            transaction_id,
            item,
            quantity_cleaned AS final_quantity,
            price_per_unit_cleaned AS final_price_per_unit,
            quantity_cleaned * price_per_unit_cleaned AS calculated_total_spent,
            payment_method,
            clean_location,
            sale_date_raw AS sale_date
        FROM
            cleaned_and_corrected_sales
        WHERE
            quantity_cleaned IS NOT NULL 
            AND price_per_unit_cleaned IS NOT NULL
            AND sale_date_raw IS NOT NULL 
    )
    SELECT
        sale_date,
        item,
        clean_location AS location,
        COUNT(DISTINCT transaction_id) AS number_of_transactions,
        SUM(final_quantity) AS total_items_sold,
        SUM(calculated_total_spent) AS total_revenue_cleaned,
        AVG(final_price_per_unit) AS average_item_price_per_unit,
        SUM(calculated_total_spent) FILTER (WHERE payment_method = 'Credit Card') AS revenue_credit_card,
        SUM(calculated_total_spent) FILTER (WHERE payment_method = 'Cash') AS revenue_cash,
        SUM(calculated_total_spent) FILTER (WHERE payment_method = 'Digital Wallet') AS revenue_digital_wallet,
        CAST(SUM(final_quantity) AS DOUBLE) / CAST(COUNT(DISTINCT transaction_id) AS DOUBLE) AS avg_quantity_per_transaction
    FROM
        final_calculated_sales
    GROUP BY
        sale_date,
        item,
        clean_location
    ORDER BY
        sale_date DESC, total_revenue_cleaned DESC
    """
    
    try:
        # Execute DROP TABLE first
        print(f"Dropping table {NEW_CLEANED_TABLE_NAME} if it exists...")
        connector.query(DROP_TABLE_QUERY)
        print("Drop table command executed.")
    
        # Then execute CREATE TABLE AS SELECT
        print(f"Creating table {NEW_CLEANED_TABLE_NAME}...")
        # For DDL operations like CREATE TABLE, connector.query() might return an empty DataFrame or None.
        connector.query(CTAS_CORE_QUERY)
        
        print(f"\nSuccessfully created table: {NEW_CLEANED_TABLE_NAME}")
        
        # Optional: Verify the table was created by querying its schema or a few rows
        print(f"\nVerifying schema of new table: {NEW_CLEANED_TABLE_NAME}")
        df_verify = connector.query(f"SELECT * FROM {NEW_CLEANED_TABLE_NAME} LIMIT 5")
        display(df_verify)
        df_verify.info()
    
    except Exception as e:
        print(f"Error during CTAS operation for {NEW_CLEANED_TABLE_NAME}: {e}")
        if "DROP TABLE" in str(e) and DROP_TABLE_QUERY in str(e):
            print("\nFailed SQL Query (DROP TABLE):\n", DROP_TABLE_QUERY)
        elif "CREATE TABLE" in str(e) and CTAS_CORE_QUERY in str(e):
            print("\nFailed SQL Query (CREATE TABLE AS SELECT):\n", CTAS_CORE_QUERY)
        else:
            print("\nFailed SQL Query:\n", e)

    7. Explore the New Physical Table

    You can use standard SQL metadata commands to explore your newly created physical table. Remember to replace <your_catalog_name> and <your_schema_name> with your actual values.

    # You'll need to know your catalog and schema names.
    # Example: your_catalog_name = "default_dataset", your_schema_name = "sales_data"
    # To retrieve catalog and schema name you can execute the following commands in the Lakehouse Manager Explorer
    # CATALOG LIST: show catalogs
    # SCHEMA LIST: show schemas from {catalog_name}
    
    your_catalog_name = "your_main_catalog" # <<< IMPORTANT: Replace with your actual catalog name!
    your_schema_name = "your_schema_name"   # <<< IMPORTANT: Replace with your actual schema name!
    your_table_name = "cleaned_cafe_sales_daily_summary"
    
    print(f"\n--- Exploring the New Table: {your_table_name} ---")
    
    try:
        # Describe your new table
        df_describe = connector.query(f"DESCRIBE {your_catalog_name}.{your_schema_name}.{your_table_name}")
        print(f"\nDescription of table '{your_table_name}':")
        display(df_describe)
        
        # Select some data from your new table
        df_sample_data = connector.query(f"SELECT * FROM {your_catalog_name}.{your_schema_name}.{your_table_name} LIMIT 5")
        print(f"\nSample data from '{your_table_name}':")
        display(df_sample_data)
    
    except Exception as e:
        print(f"Error exploring table '{your_table_name}': {e}")
        print("Please check the full table path (catalog.schema.table) and permissions.")

    8. Create a Logical Object from the Physical Table

    When you create a new table through SQL, it exists only as a physical table in the database. However, the Tables section in the UI operates at a logical level. It displays tables that are registered as logical objects within the platform.

    To make your newly created physical table visible and usable in the UI, you’ll need to create a corresponding logical object. This logical representation acts as a bridge between the database and the platform interface, allowing you to interact with the table’s schema and data directly from the UI.

    print(f"\n--- Creating LogicalObject for '{NEW_CLEANED_TABLE_NAME}' ---")
    
    try:
            
        logical_cleaned_sales = LogicalObject().create_from_physical(NEW_CLEANED_TABLE_NAME)
        print(f"Successfully created logical object for: {NEW_CLEANED_TABLE_NAME}")
    
    except NameError:
        print("Error: 'LogicalObject' is not defined. Please ensure you have imported the correct library/class or that it's globally accessible.")
    except Exception as e:
        print(f"Error creating logical object: {e}")
    Info

    if you want to remove a LogicalObject that you have created, you can directly delete the table from the UI or else use the method - LogicalObject().remove("table_name") - this will remove the table at both logical and physical levels.

    Conclusion

    You have successfully navigated through an end-to-end SQL transformation process on the OVHcloud Data Platform. Starting from a dirty raw dataset, you have applied various SQL cleaning, aggregation, and reshaping techniques to produce a clean, summarized, and highly usable dataset. This transformed data was then persisted into a new physical table and represented as a logical object for seamless integration into your Python workflows. This foundational knowledge empowers you to tackle more complex data preparation challenges and build robust data pipelines.

    Go further

    If you need training or technical assistance to implement our solutions, contact your sales representative or click on this link to get a quote and ask our Professional Services experts for a custom analysis of your project.

    Ask questions, give your feedback and interact directly with the team building the Data Platform on the dedicated Discord channel.

    If you need support with your OVHcloud services, create a request in our Help Centre.

    Join our community of users.