Skip to content

SELECT

In Polars SQL, the SELECT statement is used to retrieve data from a table into a DataFrame. The basic syntax of a SELECT statement in Polars SQL is as follows:

SELECT column1, column2, ...
FROM table_name;

Here, column1, column2, etc. are the columns that you want to select from the table. You can also use the wildcard * to select all columns. table_name is the name of the table or that you want to retrieve data from. In the sections below we will cover some of the more common SELECT variants

register Β· execute

df = pl.DataFrame(
    {
        "country": ["USA", "USA", "USA", "USA", "USA", "Netherlands"],
        "city": [
            "New York",
            "Los Angeles",
            "Chicago",
            "Houston",
            "Phoenix",
            "Amsterdam",
        ],
        "population": [8399000, 3997000, 2705000, 2320000, 1680000, 900000],
    }
)

ctx = pl.SQLContext(population=df, eager=True)

print(ctx.execute("SELECT * FROM population"))

shape: (6, 3)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ country     ┆ city        ┆ population β”‚
β”‚ ---         ┆ ---         ┆ ---        β”‚
β”‚ str         ┆ str         ┆ i64        β”‚
β•žβ•β•β•β•β•β•β•β•β•β•β•β•β•β•ͺ═════════════β•ͺ════════════║
β”‚ USA         ┆ New York    ┆ 8399000    β”‚
β”‚ USA         ┆ Los Angeles ┆ 3997000    β”‚
β”‚ USA         ┆ Chicago     ┆ 2705000    β”‚
β”‚ USA         ┆ Houston     ┆ 2320000    β”‚
β”‚ USA         ┆ Phoenix     ┆ 1680000    β”‚
β”‚ Netherlands ┆ Amsterdam   ┆ 900000     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

GROUP BY

The GROUP BY statement is used to group rows in a table by one or more columns and compute aggregate functions on each group.

execute

result = ctx.execute(
    """
        SELECT country, AVG(population) as avg_population
        FROM population
        GROUP BY country
    """
)
print(result)

shape: (2, 2)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ country     ┆ avg_population β”‚
β”‚ ---         ┆ ---            β”‚
β”‚ str         ┆ f64            β”‚
β•žβ•β•β•β•β•β•β•β•β•β•β•β•β•β•ͺ════════════════║
β”‚ USA         ┆ 3.8202e6       β”‚
β”‚ Netherlands ┆ 900000.0       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

ORDER BY

The ORDER BY statement is used to sort the result set of a query by one or more columns in ascending or descending order.

execute

result = ctx.execute(
    """
        SELECT city, population
        FROM population
        ORDER BY population
    """
)
print(result)

shape: (6, 2)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ city        ┆ population β”‚
β”‚ ---         ┆ ---        β”‚
β”‚ str         ┆ i64        β”‚
β•žβ•β•β•β•β•β•β•β•β•β•β•β•β•β•ͺ════════════║
β”‚ Amsterdam   ┆ 900000     β”‚
β”‚ Phoenix     ┆ 1680000    β”‚
β”‚ Houston     ┆ 2320000    β”‚
β”‚ Chicago     ┆ 2705000    β”‚
β”‚ Los Angeles ┆ 3997000    β”‚
β”‚ New York    ┆ 8399000    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

JOIN

register_many Β· execute

income = pl.DataFrame(
    {
        "country": [
            "USA",
            "USA",
            "USA",
            "USA",
            "Netherlands",
            "Netherlands",
            "Netherlands",
        ],
        "city": [
            "New York",
            "Los Angeles",
            "Chicago",
            "Houston",
            "Amsterdam",
            "Rotterdam",
            "Utrecht",
        ],
        "income": [55000, 62000, 48000, 52000, 42000, 38000, 41000],
    }
)
ctx.register_many(income=income)
result = ctx.execute(
    """
        SELECT income.*, population.population
        FROM population
        LEFT JOIN income ON population.city = income.city
    """
)
print(result)

shape: (6, 4)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ country     ┆ city        ┆ income ┆ population β”‚
β”‚ ---         ┆ ---         ┆ ---    ┆ ---        β”‚
β”‚ str         ┆ str         ┆ i64    ┆ i64        β”‚
β•žβ•β•β•β•β•β•β•β•β•β•β•β•β•β•ͺ═════════════β•ͺ════════β•ͺ════════════║
β”‚ USA         ┆ Los Angeles ┆ 62000  ┆ 3997000    β”‚
β”‚ USA         ┆ New York    ┆ 55000  ┆ 8399000    β”‚
β”‚ USA         ┆ Houston     ┆ 52000  ┆ 2320000    β”‚
β”‚ USA         ┆ Chicago     ┆ 48000  ┆ 2705000    β”‚
β”‚ Netherlands ┆ Amsterdam   ┆ 42000  ┆ 900000     β”‚
β”‚ null        ┆ null        ┆ null   ┆ 1680000    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Functions

Polars provides a wide range of SQL functions, including:

  • Mathematical functions: ABS, EXP, LOG, ASIN, ACOS, ATAN, etc.
  • String functions: LOWER, UPPER, LTRIM, RTRIM, STARTS_WITH,ENDS_WITH.
  • Aggregation functions: SUM, AVG, MIN, MAX, COUNT, STDDEV, FIRST etc.
  • Array functions: EXPLODE, UNNEST,ARRAY_SUM,ARRAY_REVERSE, etc.

For a full list of supported functions go the API documentation. The example below demonstrates how to use a function in a query

execute

result = ctx.execute(
    """
        SELECT city, population
        FROM population
        WHERE STARTS_WITH(country,'U')
    """
)
print(result)

shape: (5, 2)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ city        ┆ population β”‚
β”‚ ---         ┆ ---        β”‚
β”‚ str         ┆ i64        β”‚
β•žβ•β•β•β•β•β•β•β•β•β•β•β•β•β•ͺ════════════║
β”‚ New York    ┆ 8399000    β”‚
β”‚ Los Angeles ┆ 3997000    β”‚
β”‚ Chicago     ┆ 2705000    β”‚
β”‚ Houston     ┆ 2320000    β”‚
β”‚ Phoenix     ┆ 1680000    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Table Functions

In the examples earlier we first generated a DataFrame which we registered in the SQLContext. Polars also support directly reading from CSV, Parquet, JSON and IPC in your SQL query using table functions read_xxx.

execute

result = ctx.execute(
    """
        SELECT *
        FROM read_csv('docs/assets/data/iris.csv')
    """
)
print(result)

shape: (150, 5)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ sepal_length ┆ sepal_width ┆ petal_length ┆ petal_width ┆ species   β”‚
β”‚ ---          ┆ ---         ┆ ---          ┆ ---         ┆ ---       β”‚
β”‚ f64          ┆ f64         ┆ f64          ┆ f64         ┆ str       β”‚
β•žβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•ͺ═════════════β•ͺ══════════════β•ͺ═════════════β•ͺ═══════════║
β”‚ 5.1          ┆ 3.5         ┆ 1.4          ┆ 0.2         ┆ Setosa    β”‚
β”‚ 4.9          ┆ 3.0         ┆ 1.4          ┆ 0.2         ┆ Setosa    β”‚
β”‚ 4.7          ┆ 3.2         ┆ 1.3          ┆ 0.2         ┆ Setosa    β”‚
β”‚ 4.6          ┆ 3.1         ┆ 1.5          ┆ 0.2         ┆ Setosa    β”‚
β”‚ 5.0          ┆ 3.6         ┆ 1.4          ┆ 0.2         ┆ Setosa    β”‚
β”‚ …            ┆ …           ┆ …            ┆ …           ┆ …         β”‚
β”‚ 6.7          ┆ 3.0         ┆ 5.2          ┆ 2.3         ┆ Virginica β”‚
β”‚ 6.3          ┆ 2.5         ┆ 5.0          ┆ 1.9         ┆ Virginica β”‚
β”‚ 6.5          ┆ 3.0         ┆ 5.2          ┆ 2.0         ┆ Virginica β”‚
β”‚ 6.2          ┆ 3.4         ┆ 5.4          ┆ 2.3         ┆ Virginica β”‚
β”‚ 5.9          ┆ 3.0         ┆ 5.1          ┆ 1.8         ┆ Virginica β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The path can also be a glob, or a list of paths or globs, as in read_parquet(['a.parquet', 'b.parquet']).

read_csv also accepts named options after the path, with the same names and meaning as the options of scan_csv. Write them as name => value (or name := value):

execute

result = ctx.execute(
    """
        SELECT *
        FROM read_csv('docs/assets/data/iris.csv', infer_schema => false, skip_rows_after_header => 147)
    """
)
print(result)

shape: (3, 5)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ sepal_length ┆ sepal_width ┆ petal_length ┆ petal_width ┆ species   β”‚
β”‚ ---          ┆ ---         ┆ ---          ┆ ---         ┆ ---       β”‚
β”‚ str          ┆ str         ┆ str          ┆ str         ┆ str       β”‚
β•žβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•ͺ═════════════β•ͺ══════════════β•ͺ═════════════β•ͺ═══════════║
β”‚ 6.5          ┆ 3           ┆ 5.2          ┆ 2           ┆ Virginica β”‚
β”‚ 6.2          ┆ 3.4         ┆ 5.4          ┆ 2.3         ┆ Virginica β”‚
β”‚ 5.9          ┆ 3           ┆ 5.1          ┆ 1.8         ┆ Virginica β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The supported options are:

  • Layout: has_header, separator, quote_char, comment_prefix, skip_rows, skip_rows_after_header, skip_lines, encoding.
  • Names and types: new_columns, schema, schema_overrides, infer_schema, infer_schema_length, infer_schema_files, try_parse_dates, decimal_comma.
  • Values and errors: null_values, ignore_errors.
  • Ragged rows and multiple files: truncate_ragged_lines, missing_columns, extra_columns, include_file_paths.

Values are SQL literals:

  • NULL stands for None, as in infer_schema_length => NULL to infer the schema from the whole file, or quote_char => NULL to turn quoting off.
  • Lists use brackets: new_columns => ['a', 'b'].
  • schema, schema_overrides and per-column null_values take a dictionary, with types written as SQL type names: schema_overrides => {'id': 'INT', 'name': 'VARCHAR'}. schema_overrides also accepts a list of type names, by position.
  • Escape sequences need an E'' string, as in separator => E'\t' for tab-separated files.

Unlike scan_csv, read_csv tries to parse dates by default (try_parse_dates => true).

Some options are unstable: those marked as such in the scan_csv API reference are unstable in read_csv too, and may be changed at any point without it being considered a breaking change.

read_ndjson reads newline-delimited JSON (one object per line) and accepts the options of scan_ndjson in the same way: schema, schema_overrides, infer_schema_length, ignore_errors and include_file_paths.

execute

result = ctx.execute(
    """
        SELECT *
        FROM read_ndjson('docs/assets/data/orders.ndjson', schema_overrides => {'date': 'DATE'})
    """
)
print(result)

shape: (3, 4)
β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id  ┆ date       ┆ customer        ┆ items     β”‚
β”‚ --- ┆ ---        ┆ ---             ┆ ---       β”‚
β”‚ i64 ┆ date       ┆ struct[2]       ┆ list[i64] β”‚
β•žβ•β•β•β•β•β•ͺ════════════β•ͺ═════════════════β•ͺ═══════════║
β”‚ 1   ┆ 2024-03-01 ┆ {"Ann","Ghent"} ┆ [3, 7]    β”‚
β”‚ 2   ┆ 2024-03-04 ┆ {"Bo","Leuven"} ┆ [1]       β”‚
β”‚ 3   ┆ 2024-03-09 ┆ {"Cy","Ghent"}  ┆ [2, 5, 8] β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • The schema is inferred from the first 100 rows, so a key that first appears later is dropped. Use infer_schema_length => NULL to infer it from the whole file.
  • A nested object read as 'VARCHAR' keeps its JSON text.
  • read_json reads the same files, but takes only the path. For a JSON array or a pretty-printed JSON document, use pl.read_json instead.

read_parquet accepts the options of scan_parquet in the same way: schema, missing_columns, extra_columns, hive_partitioning, hive_schema, try_parse_hive_dates and include_file_paths.

execute

result = ctx.execute(
    """
        SELECT id, bool_col, path
        FROM read_parquet('docs/assets/data/alltypes_plain.parquet', include_file_paths => 'path')
        LIMIT 3
    """
)
print(result)

shape: (3, 3)
β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id  ┆ bool_col ┆ path                            β”‚
β”‚ --- ┆ ---      ┆ ---                             β”‚
β”‚ i32 ┆ bool     ┆ str                             β”‚
β•žβ•β•β•β•β•β•ͺ══════════β•ͺ═════════════════════════════════║
β”‚ 4   ┆ true     ┆ docs/assets/data/alltypes_plai… β”‚
β”‚ 5   ┆ false    ┆ docs/assets/data/alltypes_plai… β”‚
β”‚ 6   ┆ true     ┆ docs/assets/data/alltypes_plai… β”‚
β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • Unlike scan_parquet, read_parquet reads hive partitions by default when given one path, for a glob or a single file too: a path such as data/k=1/part.parquet adds a column k. Every key=value folder in such a path counts, so an absolute path can also pick up columns from its parent folders. A list of paths reads them only when it is a single directory, as in scan_parquet. Set hive_partitioning to true or false to choose either way.
  • When reading several files, the first one defines the columns, unless schema is given. Use missing_columns => 'insert' and extra_columns => 'ignore' for files that differ from it.
  • As for read_csv, options marked unstable in the scan_parquet API reference are unstable in read_parquet too.

read_ipc accepts the options of scan_ipc in the same way: hive_partitioning, hive_schema, try_parse_hive_dates and include_file_paths.

execute

pl.read_csv("docs/assets/data/iris.csv").write_ipc("docs/assets/data/iris.feather")

result = ctx.execute(
    """
        SELECT species, sepal_length, path
        FROM read_ipc('docs/assets/data/iris.feather', include_file_paths => 'path')
        LIMIT 3
    """
)
print(result)

shape: (3, 3)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ species ┆ sepal_length ┆ path                          β”‚
β”‚ ---     ┆ ---          ┆ ---                           β”‚
β”‚ str     ┆ f64          ┆ str                           β”‚
β•žβ•β•β•β•β•β•β•β•β•β•ͺ══════════════β•ͺ═══════════════════════════════║
β”‚ Setosa  ┆ 5.1          ┆ docs/assets/data/iris.feather β”‚
β”‚ Setosa  ┆ 4.9          ┆ docs/assets/data/iris.feather β”‚
β”‚ Setosa  ┆ 4.7          ┆ docs/assets/data/iris.feather β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • Hive partitions are read as for read_parquet: by default for one path, and for a list only when it is a single directory.
  • As for read_parquet, options marked unstable in the scan_ipc API reference are unstable in read_ipc too.