Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

SQL (PostgreSQL, MySQL, SQLite)  Module 20 – Dialects and portability

Other dialects: SQL Server, Oracle, DuckDB

How T-SQL, Oracle SQL and DuckDB differ from PostgreSQL, MySQL and SQLite: row limits, keys, MERGE, QUALIFY and GROUP BY ALL, with DuckDB queries run.

  • Intermediate
  • 22 minutes
  • Examples run with sql.js 1.14.2, Python 3.14.8 and duckdb 1.5.6
  • By MySmartCoPilot
Free core, Pro depth: 8 of 22 minutes free
Free: What transfers, and what each dialect spells its own way · DuckDB: QUALIFY, and the query it replaces · The other shorthands, named · Your turn. With a pass: 2 in-depth parts and 3 interview answers, about 14 minutes.

What you will learn

  • Recognise the syntax of T-SQL, Oracle SQL and DuckDB that differs from this track's engines
  • Apply PostgreSQL, MySQL and SQLite skills to those three dialects
  • Write a QUALIFY query for an engine that has no QUALIFY
  • Check a dialect claim against its official documentation and your own server

Before you start

What you’ll learn · Before you start

What you will learn

  • Recognise the syntax of T-SQL, Oracle SQL and DuckDB that differs from this track's engines
  • Apply PostgreSQL, MySQL and SQLite skills to those three dialects
  • Write a QUALIFY query for an engine that has no QUALIFY
  • Check a dialect claim against its official documentation and your own server

Before you start

On this page

Beyond PostgreSQL, MySQL and SQLite, three dialects turn up most often in job descriptions and data work: T-SQL, the SQL of Microsoft SQL Server; Oracle SQL, which adds its own row limits, sequences and DUAL table; and DuckDB, a small analytical engine that reads files as if they were tables and adds shorthands such as QUALIFY and GROUP BY ALL. The good news is that most of what you know transfers: the relational model, SELECT, joins, GROUP BY, subqueries, common table expressions and window functions are the same language in all of them. What differs is the edges, and the edges are short enough to list.

DuckDB runs in this lesson, with its own recorded output, because it is pinned here as DuckDB 1.5.6. T-SQL and Oracle SQL are described from their official documentation only, with no outputs: the next module takes those two engines side by side with results recorded on their own servers.

What transfers, and what each dialect spells its own way

In this track’s three engines you write LIMIT … OFFSET for a row limit, an identity column for a generated key, ON CONFLICT … DO UPDATE for an upsert, || to join two strings, a common table expression to filter on a window result, and a bare SELECT 1 when no table is needed. Here is what the other three want instead:

Six edges, as T-SQL, Oracle SQL and DuckDB spell them
Criterion T-SQLOracle SQLDuckDB
Row limit TOP (n), or OFFSET … FETCHOFFSET … FETCH FIRSTLIMIT … OFFSET
Generated key IDENTITY(1, 1)Identity or a SEQUENCEA SEQUENCE
Insert or update MERGEMERGEON CONFLICT or MERGE
Join two strings +, and || from 2025||||
Filter on a window A subquery or CTEQUALIFYQUALIFY
No table needed SELECT 1SELECT 1 FROM DUALSELECT 1

Everything in that table is one line of code. The differences that cost a day are the ones in behaviour, and three of them are worth memorising. Oracle stores text of length zero as a null, and its manual warns that this may change in a future release, so code should not lean on it. T-SQL sorts nulls as the lowest value, which puts them first in an ascending sort, as in MySQL and SQLite but not as in PostgreSQL. And DuckDB divides with two operators, / for floating point and // for whole numbers, so the division trap of this track’s three engines has a third answer.

DuckDB: QUALIFY, and the query it replaces

QUALIFY keeps or drops rows by what a window function returned, as HAVING does for an aggregate. DuckDB’s documentation describes it as a way to do that filtering without a subquery or a WITH clause, and places it after the WINDOW clause and before ORDER BY.

The biggest order of each city in DuckDB 1.5.6, four ways Python · servers/duckdb_qualify.py
"""The statements of ../duckdb/qualify.sql on DuckDB 1.5.6, as its shell prints them."""

from duckdb_server import duckdb_file

duckdb_file("../duckdb/qualify.sql")

Output

┌────────┬──────────┬────────┐
│  city  │ customer │ amount │
├────────┼──────────┼────────┤
│ Jaipur │ Tara     │ 600    │
│ Kochi  │ Meena    │ 700    │
│ Pune   │ Kabir    │ 900    │
└────────┴──────────┴────────┘
┌────────┬──────────┬────────┐
│  city  │ customer │ amount │
├────────┼──────────┼────────┤
│ Jaipur │ Tara     │ 600    │
│ Kochi  │ Meena    │ 700    │
│ Pune   │ Kabir    │ 900    │
└────────┴──────────┴────────┘
┌────────┬──────────┬────────┐
│  city  │ customer │ amount │
├────────┼──────────┼────────┤
│ Jaipur │ Tara     │ 600    │
│ Kochi  │ Meena    │ 700    │
│ Pune   │ Ravi     │ 900    │
└────────┴──────────┴────────┘
┌────────┬──────────┬────────┐
│  city  │ customer │ amount │
├────────┼──────────┼────────┤
│ Jaipur │ Tara     │ 600    │
│ Kochi  │ Meena    │ 700    │
│ Pune   │ Kabir    │ 900    │
│ Pune   │ Ravi     │ 900    │
└────────┴──────────┴────────┘

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 duckdb_qualify.py

The SQL those four answers came from SQL · duckdb/qualify.sql
-- DuckDB 1.5.6: the biggest order of each city, three ways, and what a tie does.
CREATE TABLE orders (id INTEGER PRIMARY KEY, city VARCHAR NOT NULL, customer VARCHAR NOT NULL, amount INTEGER NOT NULL);
INSERT INTO orders VALUES
  (1, 'Pune', 'Asha', 400), (2, 'Pune', 'Ravi', 900), (3, 'Kochi', 'Meena', 700),
  (4, 'Kochi', 'Arjun', 250), (5, 'Jaipur', 'Tara', 600), (6, 'Pune', 'Kabir', 900);

-- 1. QUALIFY filters on the window function itself, with a named window and a tie-breaker.
SELECT city, customer, amount
FROM orders
WINDOW w AS (PARTITION BY city ORDER BY amount DESC, customer)
QUALIFY row_number() OVER w = 1
ORDER BY city;

-- 2. The same answer as a common table expression, which PostgreSQL, MySQL and SQLite also run.
WITH ranked AS (
  SELECT city, customer, amount,
         row_number() OVER (PARTITION BY city ORDER BY amount DESC, customer) AS rn
  FROM orders
)
SELECT city, customer, amount FROM ranked WHERE rn = 1 ORDER BY city;

-- 3. The same QUALIFY without the tie-breaker: Pune has two orders of 900, and one of them wins by chance.
SELECT city, customer, amount
FROM orders
QUALIFY row_number() OVER (PARTITION BY city ORDER BY amount DESC) = 1
ORDER BY city;

-- 4. Every tied row kept, by ranking instead of numbering.
SELECT city, customer, amount
FROM orders
QUALIFY rank() OVER (PARTITION BY city ORDER BY amount DESC) = 1
ORDER BY city, customer;

The first two queries return the same three rows, so QUALIFY is a shorthand, not a new ability. The third shows why the tie-breaker matters: Pune has two orders of 900, and without , customer in the window’s ORDER BY the engine picks one of them, here Ravi rather than Kabir. The fourth keeps both tied rows by ranking instead of numbering.

On an engine without QUALIFY, the common table expression is the answer, because a window function may not appear in WHERE at all:

The same question in SQLite 3.49.1, and what WHERE says about it SQL · same-sql/top_per_city.sql
-- The same question in SQLite 3.49.1, which has no QUALIFY: the biggest order of each city.
CREATE TABLE orders (id INTEGER PRIMARY KEY, city TEXT NOT NULL, customer TEXT NOT NULL, amount INTEGER NOT NULL);
INSERT INTO orders VALUES
  (1, 'Pune', 'Asha', 400), (2, 'Pune', 'Ravi', 900), (3, 'Kochi', 'Meena', 700),
  (4, 'Kochi', 'Arjun', 250), (5, 'Jaipur', 'Tara', 600), (6, 'Pune', 'Kabir', 900);

-- 1. The common table expression that stands in for QUALIFY, with a tie-breaker.
WITH ranked AS (
  SELECT city, customer, amount,
         row_number() OVER (PARTITION BY city ORDER BY amount DESC, customer) AS rn
  FROM orders
)
SELECT city, customer, amount FROM ranked WHERE rn = 1 ORDER BY city;

-- 2. A window function in WHERE is refused: that is the rule QUALIFY exists to work around.
SELECT city, customer FROM orders WHERE row_number() OVER (PARTITION BY city ORDER BY amount DESC) = 1;

Output (exit status 1)

┌────────┬──────────┬────────┐
│  city  │ customer │ amount │
├────────┼──────────┼────────┤
│ Jaipur │ Tara     │ 600    │
│ Kochi  │ Meena    │ 700    │
│ Pune   │ Kabir    │ 900    │
└────────┴──────────┴────────┘

Printed as an error (standard error)

Parse error near line 16: misuse of window function row_number()

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < top_per_city.sql

SQLite answers the second query with misuse of window function row_number(). PostgreSQL and MySQL refuse it too: WHERE is evaluated before window functions are computed, which is the rule QUALIFY exists to work around. Oracle has QUALIFY, and its manual pins the order of operations: FROM, WHERE, GROUP BY, HAVING, WINDOW, QUALIFY, DISTINCT, ORDER BY, FETCH FIRST.

The other shorthands, named

DuckDB adds five more conveniences, each of which stands for a longer form you already know. GROUP BY ALL groups by every column of the select list that is not inside an aggregate. ORDER BY ALL sorts by all the selected columns, left to right. SELECT * EXCLUDE (a, b) returns every column except the ones named. COLUMNS('…') picks the columns whose names match a pattern and applies one function to each. A file as a table lets SELECT … FROM 'orders.csv' read a CSV or Parquet file with no CREATE TABLE at all.

The part below runs all five and gives the plain form beside each, then turns to T-SQL and Oracle SQL: what maps to what, which traps cost a day, and how to confirm each claim on a server of your own.

Your turn

Exercise · Medium · SQL

Write the QUALIFY query for an engine without it

The table orders has id, city, customer and amount.

In DuckDB this query answers "the biggest order of each city":

SELECT city, customer, amount FROM orders
QUALIFY row_number() OVER (PARTITION BY city ORDER BY amount DESC, customer) = 1
ORDER BY city;

SQLite, PostgreSQL and MySQL have no QUALIFY. Write the query that returns the same three columns and rows here, sorted by city. Where two orders of a city tie on amount, the one whose customer name comes first alphabetically wins, exactly as the window above says.

The starter code numbers the orders inside each city, which is the first half of the answer. Each test runs your SQL on a fresh table and compares the rows it returns, in order.

Starter code · top_per_city.sql

-- A start: every order with its position inside its city. Return only the first of each city.
SELECT city, customer, amount,
       row_number() OVER (PARTITION BY city ORDER BY amount DESC, customer) AS rn
FROM orders
ORDER BY city, rn;
The sample tests · tests.yaml
# Sample tests: each one builds a small orders table, runs your SQL on it and compares the rows it returns, in order.
tests:
  - name: three cities, one tie broken by the customer name
    ordered: true
    setup: |
      CREATE TABLE orders (id INTEGER PRIMARY KEY, city TEXT NOT NULL, customer TEXT NOT NULL, amount INTEGER NOT NULL);
      INSERT INTO orders VALUES (1, 'Pune', 'Asha', 400), (2, 'Pune', 'Ravi', 900), (3, 'Kochi', 'Meena', 700),
        (4, 'Kochi', 'Arjun', 250), (5, 'Jaipur', 'Tara', 600), (6, 'Pune', 'Kabir', 900);
    expected:
      columns: [city, customer, amount]
      rows:
        - [Jaipur, Tara, 600]
        - [Kochi, Meena, 700]
        - [Pune, Kabir, 900]
  - name: one city with one order
    ordered: true
    setup: |
      CREATE TABLE orders (id INTEGER PRIMARY KEY, city TEXT NOT NULL, customer TEXT NOT NULL, amount INTEGER NOT NULL);
      INSERT INTO orders VALUES (1, 'Patna', 'Zoya', 1000);
    expected:
      columns: [city, customer, amount]
      rows:
        - [Patna, Zoya, 1000]
  - name: every order of a city has the same amount
    ordered: true
    setup: |
      CREATE TABLE orders (id INTEGER PRIMARY KEY, city TEXT NOT NULL, customer TEXT NOT NULL, amount INTEGER NOT NULL);
      INSERT INTO orders VALUES (1, 'Surat', 'Dev', 500), (2, 'Surat', 'Bhavna', 500), (3, 'Surat', 'Chetan', 500);
    expected:
      columns: [city, customer, amount]
      rows:
        - [Surat, Bhavna, 500]
  - name: cities sorted by name, not by amount
    ordered: true
    setup: |
      CREATE TABLE orders (id INTEGER PRIMARY KEY, city TEXT NOT NULL, customer TEXT NOT NULL, amount INTEGER NOT NULL);
      INSERT INTO orders VALUES (1, 'Mysuru', 'Latha', 100), (2, 'Bhopal', 'Imran', 900), (3, 'Agra', 'Nisha', 450),
        (4, 'Agra', 'Omar', 450);
    expected:
      columns: [city, customer, amount]
      rows:
        - [Agra, Nisha, 450]
        - [Bhopal, Imran, 900]
        - [Mysuru, Latha, 100]
A hint

A window function cannot go in WHERE, because WHERE is evaluated before the window functions are computed. Put the numbering query in a WITH block (or a subquery in FROM), give the number a name such as rn, and then filter rn = 1 in the outer query.

The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 Why can WHERE row_number() OVER (…) = 1 not be used in PostgreSQL, MySQL or SQLite?

    Choose one answer.

    Show the answer to question 1

    Answer: WHERE is evaluated before window functions are computed, so the number does not exist yet

    Window functions are computed after WHERE and GROUP BY have chosen the rows, so a filter on their result needs a second step: a subquery or a common table expression. QUALIFY, which Oracle SQL and DuckDB have, is that second step written as one clause.

  2. Question 2 of 4 In DuckDB, what do 7 / 2 and 7 // 2 return?

    Choose one answer.

    Show the answer to question 2

    Answer: 3.5 and 3

    DuckDB has two division operators: / always divides as floating point, and // divides two integers to an integer. PostgreSQL and SQLite give 3 for / between whole numbers, and MySQL gives 3.5000, so this is a third answer to the same expression.

  3. Question 3 of 4 A row in Oracle was inserted with name = '' (two quotes). What does WHERE name IS NULL return for it?

    Choose one answer.

    Show the answer to question 3

    Answer: True: Oracle treats a character value of length zero as null

    Oracle stores text of length zero as a null, and its manual warns that this may change in a future release, so it advises against depending on the behaviour. A NOT NULL column in Oracle therefore rejects an empty string.

  4. Question 4 of 4 Which statements about T-SQL's TOP are documented by Microsoft?

    Choose every answer that is right.

    Show the answer to question 4

    Answer:

    • Without ORDER BY, TOP returns the first n rows in an undefined order
    • WITH TIES may be used only in SELECT and only with ORDER BY
    • OFFSET and FETCH are recommended over TOP for paging

    The TOP page says the result is limited to the first n ordered rows when ORDER BY is used and otherwise returns rows in an undefined order, that WITH TIES needs ORDER BY and works only in SELECT, that OFFSET and FETCH are the easier paging solution, and that TOP cannot be combined with OFFSET and FETCH in the same query scope.

The five DuckDB shorthands run, with the plain form beside each Pro: opens with a Pro, Premium or Ultimate pass

Shorthands are worth learning only if you know what they stand for, because the plain form is what you will write on every other engine. This part runs all five on DuckDB 1.5.6 and names the cost of each.

In this part:

  • Five shorthands, one run, and what each one stands for
  • What DuckDB is for, and when it is the wrong choice
  • Reading a DuckDB query as a PostgreSQL user

2 runnable examples (Python, SQL) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

T-SQL and Oracle SQL mapped from what you already write, with the traps named Pro: opens with a Pro, Premium or Ultimate pass

What follows is a mapping, not a tour: for each thing you write in PostgreSQL, MySQL or SQLite, the T-SQL and Oracle forms, taken from their vendor documentation, and the behaviour that catches people. No output is shown here, because neither engine runs in this lesson; the last section gives the statements that let you confirm every row on a server of your own.

In this part:

  • The mapping table
  • Missing values and empty text, side by side
  • The four traps that cost a day
  • How to confirm every row yourself, in ten minutes

about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What does QUALIFY filter, and how do you write the same query on an engine without it?

    QUALIFY keeps or drops rows by what a window function returned, as HAVING does for an aggregate. With a window that partitions by city and orders by amount, QUALIFY row_number() OVER w = 1 keeps each city's biggest order. Oracle SQL and DuckDB have it; PostgreSQL, MySQL, SQLite and SQL Server do not, and they refuse a window function in WHERE, because WHERE is evaluated before window functions are computed. The same query there computes the window in a common table expression or a subquery, names its result, and filters that name in the outer query. Oracle's manual puts QUALIFY after WINDOW and before DISTINCT and ORDER BY.

  2. Mid to senior

    An analyst's DuckDB report uses QUALIFY, GROUP BY ALL and SELECT * EXCLUDE, and it now has to run nightly against PostgreSQL 18. Rewrite each construct, and say which parts of the report you would not trust after the rewrite.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    A SQL Server screen pages through orders with TOP (20) and an ORDER BY inside a view. Users report seeing the same order on two pages and never seeing others. Diagnose it from what Microsoft documents, and write the paging query you would ship.

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    A product keeps its records in PostgreSQL. The analytics screen runs eight reports over three years of orders, 40 million rows, and each takes 20 to 40 seconds. Someone proposes DuckDB. How would you decide, and what would you measure?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • The relational core transfers: SELECT, joins, GROUP BY, subqueries, common table expressions and window functions are the same in T-SQL, Oracle SQL and DuckDB.
  • Row limits, generated keys, upserts, string concatenation and “no table needed” are the five edges spelled differently in each dialect.
  • QUALIFY filters on a window result in Oracle and DuckDB; everywhere else the same query is a common table expression, because WHERE runs before window functions.
  • DuckDB’s shorthands each stand for a plain form, and reading files as tables guesses the types.
  • Oracle treats an empty string as NULL, and T-SQL sorts NULLs first with no NULLS LAST to change it.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.