SQL Reference

SQL Cheat Sheet 2026: Commands, Joins, Window Functions, JSON

A modern SQL reference for 2026 work: every core command, deep window-function coverage with frame clauses, JSON operators across PostgreSQL, MySQL, and SQL Server, CTEs, and the patterns you actually use today — upserts, generated columns, lateral joins, and MERGE. Bookmark this page and stop Googling SQL syntax.

May 8, 2026 22 min read

Basic SQL Commands

These four commands form the foundation of SQL. Every query you write uses at least one of them.

SELECT - Read data

-- Select specific columns
SELECT first_name, last_name, email
FROM users;

-- Select all columns
SELECT * FROM products;

-- Select with alias
SELECT first_name AS name, email AS contact
FROM users;

INSERT - Add new rows

-- Insert a single row
INSERT INTO users (first_name, last_name, email)
VALUES ('Jane', 'Smith', 'jane@example.com');

-- Insert multiple rows
INSERT INTO users (first_name, last_name, email)
VALUES
    ('Alice', 'Johnson', 'alice@example.com'),
    ('Bob', 'Williams', 'bob@example.com');

UPDATE - Modify existing rows

-- Update specific rows
UPDATE users
SET email = 'newemail@example.com'
WHERE id = 42;

-- Update multiple columns
UPDATE products
SET price = price * 1.10, updated_at = NOW()
WHERE category = 'electronics';

DELETE - Remove rows

-- Delete specific rows
DELETE FROM orders
WHERE status = 'cancelled' AND created_at < '2025-01-01';

-- Delete all rows (use with caution)
DELETE FROM temp_logs;

Filtering and Sorting

Control which rows are returned and in what order.

WHERE, AND, OR

SELECT * FROM orders
WHERE status = 'shipped'
  AND total_amount > 100
  AND (region = 'US' OR region = 'EU');

IN and BETWEEN

-- IN: match any value in a list
SELECT * FROM products
WHERE category IN ('electronics', 'books', 'clothing');

-- BETWEEN: inclusive range
SELECT * FROM orders
WHERE order_date BETWEEN '2026-01-01' AND '2026-03-31';

LIKE - Pattern matching

-- % matches any sequence of characters
SELECT * FROM users WHERE email LIKE '%@gmail.com';

-- _ matches exactly one character
SELECT * FROM products WHERE sku LIKE 'SKU-____';

ORDER BY and LIMIT

-- Sort ascending (default) and descending
SELECT product_name, price
FROM products
ORDER BY price DESC;

-- Limit results (pagination)
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;  -- Page 3, 20 per page

Aggregation Functions

Summarize data across multiple rows into a single result.

COUNT, SUM, AVG, MIN, MAX

SELECT
    COUNT(*) AS total_orders,
    SUM(total_amount) AS revenue,
    AVG(total_amount) AS avg_order_value,
    MIN(total_amount) AS smallest_order,
    MAX(total_amount) AS largest_order
FROM orders
WHERE order_date >= '2026-01-01';

GROUP BY

-- Revenue by category
SELECT
    category,
    COUNT(*) AS num_products,
    SUM(price) AS total_value
FROM products
GROUP BY category
ORDER BY total_value DESC;

HAVING - Filter groups

-- Only show categories with more than 10 products
SELECT category, COUNT(*) AS num_products
FROM products
GROUP BY category
HAVING COUNT(*) > 10
ORDER BY num_products DESC;

Remember: WHERE filters rows before grouping. HAVING filters groups after aggregation.

JOINs

Combine rows from two or more tables based on a related column.

INNER JOIN - Only matching rows from both tables

SELECT o.id, o.order_date, c.name, c.email
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id;

LEFT JOIN - All rows from left table, matching from right

-- All customers, even those with no orders
SELECT c.name, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.name;

RIGHT JOIN - All rows from right table, matching from left

-- All products, even those never ordered
SELECT p.product_name, SUM(oi.quantity) AS total_sold
FROM order_items oi
RIGHT JOIN products p ON oi.product_id = p.id
GROUP BY p.product_name;

FULL OUTER JOIN - All rows from both tables

-- Show all customers and all orders, matched where possible
SELECT c.name, o.id AS order_id
FROM customers c
FULL OUTER JOIN orders o ON c.id = o.customer_id;

CROSS JOIN - Every combination of rows

-- Generate all size/color combinations
SELECT s.size_name, c.color_name
FROM sizes s
CROSS JOIN colors c;

Quick reference: INNER = intersection, LEFT = all left + matching right (NULLs for no match), RIGHT = mirror of LEFT, FULL = union of both sides, CROSS = cartesian product.

Subqueries and CTEs

Break complex queries into manageable parts.

Subquery in WHERE

-- Customers who placed orders over $500
SELECT name, email
FROM customers
WHERE id IN (
    SELECT customer_id
    FROM orders
    WHERE total_amount > 500
);

EXISTS

-- Products that have been ordered at least once
SELECT p.product_name
FROM products p
WHERE EXISTS (
    SELECT 1
    FROM order_items oi
    WHERE oi.product_id = p.id
);

CTE (Common Table Expression) with WITH

-- Monthly revenue with running total
WITH monthly_revenue AS (
    SELECT
        DATE_TRUNC('month', order_date) AS month,
        SUM(total_amount) AS revenue
    FROM orders
    GROUP BY DATE_TRUNC('month', order_date)
)
SELECT
    month,
    revenue,
    SUM(revenue) OVER (ORDER BY month) AS running_total
FROM monthly_revenue
ORDER BY month;

Multiple CTEs

WITH active_users AS (
    SELECT user_id, COUNT(*) AS logins
    FROM login_events
    WHERE event_date >= CURRENT_DATE - INTERVAL '30 days'
    GROUP BY user_id
),
user_orders AS (
    SELECT user_id, SUM(total_amount) AS total_spent
    FROM orders
    WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'
    GROUP BY user_id
)
SELECT
    u.name,
    a.logins,
    COALESCE(uo.total_spent, 0) AS total_spent
FROM users u
JOIN active_users a ON u.id = a.user_id
LEFT JOIN user_orders uo ON u.id = uo.user_id
ORDER BY a.logins DESC;

Window Functions

Perform calculations across a set of rows related to the current row, without collapsing them into a single output row.

ROW_NUMBER, RANK, DENSE_RANK

SELECT
    name,
    department,
    salary,
    ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
    RANK() OVER (ORDER BY salary DESC) AS rank,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees;

ROW_NUMBER gives unique sequential numbers. RANK leaves gaps after ties (1,2,2,4). DENSE_RANK has no gaps (1,2,2,3).

PARTITION BY

-- Rank employees within each department
SELECT
    name,
    department,
    salary,
    RANK() OVER (
        PARTITION BY department
        ORDER BY salary DESC
    ) AS dept_rank
FROM employees;

LAG and LEAD

-- Compare each month's revenue to the previous month
SELECT
    month,
    revenue,
    LAG(revenue, 1) OVER (ORDER BY month) AS prev_month,
    revenue - LAG(revenue, 1) OVER (ORDER BY month) AS month_over_month
FROM monthly_revenue;

Frame clauses (ROWS / RANGE BETWEEN)

By default, an OVER (ORDER BY ...) window includes everything from the partition start up through the current row's peer group — which is fine for running totals but bites you on moving averages. Frame clauses let you say exactly which rows go into the calculation. Use ROWS for a positional N-row window and RANGE for a value-based window (great for time series where days may be missing).

-- Running total of daily revenue (positional frame)
SELECT
    order_date,
    daily_revenue,
    SUM(daily_revenue) OVER (
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_total
FROM daily_sales;

-- 7-day moving average (value-based frame, gap-safe)
SELECT
    order_date,
    daily_revenue,
    AVG(daily_revenue) OVER (
        ORDER BY order_date
        RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW
    ) AS moving_avg_7d
FROM daily_sales;

NTILE - bucket rows into N groups

NTILE(n) splits an ordered set into n roughly equal buckets. Use 4 for quartiles, 10 for deciles, 100 for percentiles. It's the fastest way to label "top 25% of customers by revenue" without window-of-window gymnastics.

-- Tag each customer with their revenue quartile (1 = top 25%)
SELECT
    customer_id,
    total_spent,
    NTILE(4) OVER (ORDER BY total_spent DESC) AS revenue_quartile
FROM customer_revenue;

FIRST_VALUE, LAST_VALUE, NTH_VALUE

These pick a specific row out of a window. FIRST_VALUE returns the first row's value in the frame, LAST_VALUE returns the last, and NTH_VALUE(col, n) grabs the n-th. Gotcha: the default frame for an ordered window ends at CURRENT ROW, so LAST_VALUE without an explicit frame returns the current row — almost never what you want. Always pin the frame to UNBOUNDED FOLLOWING when you really mean "last in partition".

-- First and last order amount per customer
SELECT DISTINCT
    customer_id,
    FIRST_VALUE(total_amount) OVER w AS first_order,
    LAST_VALUE(total_amount)  OVER w AS last_order,
    NTH_VALUE(total_amount, 2) OVER w AS second_order
FROM orders
WINDOW w AS (
    PARTITION BY customer_id
    ORDER BY order_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
);

Window function gotcha: no WHERE on the result

Window functions are evaluated after WHERE, GROUP BY, and HAVING, so you cannot filter on a window function in the same SELECT. Wrap the query in a CTE or subquery and filter the outer layer instead.

-- WRONG: "rn" doesn't exist yet when WHERE runs
-- SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) AS rn
-- FROM orders WHERE rn = 1;

-- CORRECT: filter in an outer query
WITH ranked AS (
    SELECT
        *,
        ROW_NUMBER() OVER (
            PARTITION BY customer_id
            ORDER BY order_date DESC
        ) AS rn
    FROM orders
)
SELECT *
FROM ranked
WHERE rn = 1;  -- most recent order per customer

Run any of these against AI2SQL's demo database — no setup.

String Functions

Manipulate and transform text data.

-- CONCAT: combine strings
SELECT CONCAT(first_name, ' ', last_name) AS full_name
FROM users;

-- UPPER / LOWER: change case
SELECT UPPER(email) FROM users;
SELECT LOWER(city) FROM addresses;

-- SUBSTRING: extract part of a string
SELECT SUBSTRING(phone FROM 1 FOR 3) AS area_code
FROM contacts;

-- TRIM: remove whitespace
SELECT TRIM(name) FROM products;

-- REPLACE: substitute text
SELECT REPLACE(url, 'http://', 'https://') FROM links;

-- LENGTH: count characters
SELECT name, LENGTH(name) AS name_length
FROM products
WHERE LENGTH(name) > 50;

Date Functions

Work with dates and timestamps. Syntax varies by database; these examples use PostgreSQL-style syntax.

-- NOW: current timestamp
SELECT NOW();

-- DATE_TRUNC: round down to a unit
SELECT DATE_TRUNC('month', order_date) AS order_month
FROM orders;

-- EXTRACT: pull out a date part
SELECT EXTRACT(YEAR FROM order_date) AS order_year,
       EXTRACT(MONTH FROM order_date) AS order_month
FROM orders;

-- Date arithmetic
SELECT order_date,
       order_date + INTERVAL '7 days' AS one_week_later
FROM orders;

-- DATEDIFF (SQL Server / MySQL style)
SELECT DATEDIFF(day, ship_date, delivery_date) AS transit_days
FROM shipments;

-- AGE (PostgreSQL): difference between dates
SELECT AGE(NOW(), created_at) AS account_age
FROM users;

-- Filter by relative dates
SELECT * FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '30 days';

JSON Operations

Most production schemas now have at least one JSON or JSONB column — event payloads, feature flags, audit metadata, third-party API responses. Here's how to extract and query nested fields without exporting the row to your app code first. Syntax diverges sharply between dialects, so each block calls out which database it's for.

PostgreSQL JSON operators

-> returns JSON, ->> returns text. #> and #>> take a path array for deep nesting. Use jsonb over json for any column you'll query — it's indexed and faster.

-- PostgreSQL: -> (json), ->> (text), #> (json path), #>> (text path)
SELECT
    payload -> 'user'              AS user_obj,        -- jsonb
    payload ->> 'event_name'       AS event_name,      -- text
    payload #> '{user, address}'   AS address_obj,     -- jsonb
    payload #>> '{user, address, city}' AS city        -- text
FROM events
WHERE payload ->> 'event_name' = 'signup';

PostgreSQL: jsonb_path_query and jsonb_array_elements

Use jsonb_path_query for JSONPath-style extraction, and jsonb_array_elements to expand a JSON array into one row per element — the JSON equivalent of UNNEST.

-- PostgreSQL: expand each item in an order's "items" array
SELECT
    o.id AS order_id,
    item ->> 'sku'           AS sku,
    (item ->> 'qty')::int    AS qty,
    (item ->> 'price')::numeric AS price
FROM orders o,
     jsonb_array_elements(o.payload -> 'items') AS item
WHERE o.created_at >= CURRENT_DATE - INTERVAL '7 days';

-- JSONPath: pull every "city" anywhere in the document
SELECT
    id,
    jsonb_path_query(payload, '$.**.city') AS city
FROM events;

MySQL: JSON_EXTRACT, ->>, JSON_TABLE

MySQL 5.7+ supports JSON_EXTRACT (alias ->) and the unquoting shortcut ->>. MySQL 8.0 adds JSON_TABLE, which expands a JSON array into a relational table inline.

-- MySQL 5.7+
SELECT
    JSON_EXTRACT(payload, '$.user.id')  AS user_id_json,
    payload->'$.user.id'                AS user_id_alias,
    payload->>'$.user.email'            AS user_email_text
FROM events;

-- MySQL 8.0+: JSON_TABLE turns a JSON array into rows
SELECT o.id, t.sku, t.qty
FROM orders o,
     JSON_TABLE(
         o.payload,
         '$.items[*]'
         COLUMNS (
             sku VARCHAR(64) PATH '$.sku',
             qty INT         PATH '$.qty'
         )
     ) AS t;

SQL Server: JSON_VALUE and OPENJSON

SQL Server uses JSON_VALUE for scalar extraction, JSON_QUERY for objects/arrays, and OPENJSON with a WITH schema clause to project JSON into rows.

-- SQL Server
SELECT
    JSON_VALUE(payload, '$.user.email') AS user_email,
    JSON_QUERY(payload, '$.items')      AS items_array
FROM events;

-- OPENJSON to expand an array (SQL Server 2016+)
SELECT o.id, t.sku, t.qty
FROM orders o
CROSS APPLY OPENJSON(o.payload, '$.items')
    WITH (
        sku VARCHAR(64) '$.sku',
        qty INT         '$.qty'
    ) AS t;

Unified example: nested fields from an event payload

Same business question — "give me the email and city for every signup event in the last 7 days" — across three dialects. The shape of the query is identical; only the extraction syntax changes.

-- PostgreSQL
SELECT
    id,
    payload ->> 'event_name'              AS event_name,
    payload #>> '{user, email}'           AS email,
    payload #>> '{user, address, city}'   AS city
FROM events
WHERE payload ->> 'event_name' = 'signup'
  AND created_at >= CURRENT_DATE - INTERVAL '7 days';

-- MySQL 5.7+
SELECT
    id,
    payload->>'$.event_name'              AS event_name,
    payload->>'$.user.email'              AS email,
    payload->>'$.user.address.city'      AS city
FROM events
WHERE payload->>'$.event_name' = 'signup'
  AND created_at >= NOW() - INTERVAL 7 DAY;

-- SQL Server
SELECT
    id,
    JSON_VALUE(payload, '$.event_name')           AS event_name,
    JSON_VALUE(payload, '$.user.email')           AS email,
    JSON_VALUE(payload, '$.user.address.city')    AS city
FROM events
WHERE JSON_VALUE(payload, '$.event_name') = 'signup'
  AND created_at >= DATEADD(day, -7, SYSUTCDATETIME());

Table Operations

Define and modify your database structure.

CREATE TABLE

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT NOW(),
    is_active BOOLEAN DEFAULT true
);

ALTER TABLE

-- Add a column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- Change a column type
ALTER TABLE products ALTER COLUMN price TYPE NUMERIC(10,2);

-- Add a constraint
ALTER TABLE orders ADD CONSTRAINT fk_customer
    FOREIGN KEY (customer_id) REFERENCES customers(id);

-- Drop a column
ALTER TABLE users DROP COLUMN phone;

DROP TABLE

-- Drop if exists (safe)
DROP TABLE IF EXISTS temp_imports;

-- Drop with cascade (removes dependent objects)
DROP TABLE IF EXISTS old_orders CASCADE;

Indexes

-- Create an index for faster lookups
CREATE INDEX idx_users_email ON users(email);

-- Composite index
CREATE INDEX idx_orders_customer_date
ON orders(customer_id, order_date);

-- Unique index
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);

-- Drop an index
DROP INDEX IF EXISTS idx_users_email;

Pro Tips

Write faster, more efficient queries.

EXPLAIN - Understand query performance

-- See the query execution plan
EXPLAIN ANALYZE
SELECT c.name, COUNT(o.id)
FROM customers c
JOIN orders o ON c.id = o.customer_id
GROUP BY c.name;

Look for sequential scans on large tables. If you see one, an index on the JOIN or WHERE column usually fixes it.

Query optimization basics

  • Select only the columns you need. Avoid SELECT * in production queries. It reads unnecessary data and breaks when schema changes.
  • Filter early. Put WHERE conditions that eliminate the most rows first. The database optimizer handles this in most cases, but explicit filtering in subqueries and CTEs helps with complex queries.
  • Use EXISTS instead of IN for large subqueries. EXISTS stops at the first match, while IN may evaluate the entire subquery.
  • Index your JOIN and WHERE columns. If you frequently filter by status or join on customer_id, those columns should have indexes.
  • Avoid functions on indexed columns in WHERE. WHERE YEAR(created_at) = 2026 cannot use an index on created_at. Use WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01' instead.
  • Use LIMIT during development. Add LIMIT 100 while building queries to avoid accidentally scanning millions of rows.

Try these queries instantly - AI2SQL generates SQL from plain English so you can skip the syntax and focus on the data you need.

Modern Patterns (2026)

The patterns below are the ones senior engineers reach for in 2026 work but that older cheat sheets still skip. They cover idempotent writes, derived columns the database maintains for you, fast sampling on huge tables, per-row subqueries, and bulk syncs — in that order.

Upserts: ON CONFLICT, ON DUPLICATE KEY, MERGE

The "insert if new, update if exists" pattern is everywhere — webhook handlers, ETL loaders, idempotent retries. Each major database spells it differently, but they all hinge on a unique key.

-- PostgreSQL: ON CONFLICT
INSERT INTO users (email, name, last_seen_at)
VALUES ('jane@example.com', 'Jane Smith', NOW())
ON CONFLICT (email) DO UPDATE
    SET name         = EXCLUDED.name,
        last_seen_at = EXCLUDED.last_seen_at;

-- MySQL: ON DUPLICATE KEY UPDATE
INSERT INTO users (email, name, last_seen_at)
VALUES ('jane@example.com', 'Jane Smith', NOW())
ON DUPLICATE KEY UPDATE
    name         = VALUES(name),
    last_seen_at = VALUES(last_seen_at);

-- SQL Server / Oracle: MERGE
MERGE INTO users AS tgt
USING (SELECT 'jane@example.com' AS email,
              'Jane Smith'        AS name,
              SYSUTCDATETIME()    AS last_seen_at) AS src
ON tgt.email = src.email
WHEN MATCHED THEN
    UPDATE SET name = src.name, last_seen_at = src.last_seen_at
WHEN NOT MATCHED THEN
    INSERT (email, name, last_seen_at)
    VALUES (src.email, src.name, src.last_seen_at);

Generated columns

A generated column is a column whose value the database computes from other columns — you never INSERT or UPDATE it directly. Use STORED when you'll filter or index on it (it's persisted to disk), VIRTUAL when it's cheap and you only read it occasionally. Great for normalized email, full-text concat, computed totals.

-- PostgreSQL / MySQL 8.0 / SQL Server: derived total kept in sync automatically
CREATE TABLE order_items (
    id          SERIAL PRIMARY KEY,
    order_id    INTEGER NOT NULL,
    quantity    INTEGER NOT NULL,
    unit_price  NUMERIC(10,2) NOT NULL,
    line_total  NUMERIC(12,2) GENERATED ALWAYS AS (quantity * unit_price) STORED
);

-- Index the generated column like any other
CREATE INDEX idx_order_items_line_total ON order_items(line_total);

TABLESAMPLE: fast exploratory sampling

Need to eyeball a billion-row table? Don't ORDER BY RANDOM() LIMIT 1000 — that does a full scan. TABLESAMPLE BERNOULLI(p) samples roughly p percent of rows at the storage layer, so it's orders of magnitude faster on large tables. Available in PostgreSQL, SQL Server, and Oracle.

-- Roughly 0.1% of rows, much cheaper than ORDER BY RANDOM()
SELECT *
FROM events TABLESAMPLE BERNOULLI(0.1)
WHERE event_type = 'click'
LIMIT 1000;

-- SYSTEM is even faster (page-level) but less random
SELECT * FROM events TABLESAMPLE SYSTEM(1) LIMIT 100;

Lateral joins (LATERAL / CROSS APPLY)

A normal subquery in the FROM clause cannot reference columns from earlier FROM tables. LATERAL (PostgreSQL, MySQL 8.0.14+) and CROSS APPLY (SQL Server) lift that restriction — you get a per-row subquery that can use the outer row's columns. Perfect for "top N per group" without a window-function gymnastics layer.

-- PostgreSQL: top 3 most recent orders for each customer
SELECT c.id, c.name, o.order_date, o.total_amount
FROM customers c
CROSS JOIN LATERAL (
    SELECT order_date, total_amount
    FROM orders
    WHERE customer_id = c.id
    ORDER BY order_date DESC
    LIMIT 3
) o;

-- SQL Server: same pattern with CROSS APPLY
SELECT c.id, c.name, o.order_date, o.total_amount
FROM customers c
CROSS APPLY (
    SELECT TOP 3 order_date, total_amount
    FROM orders
    WHERE customer_id = c.id
    ORDER BY order_date DESC
) o;

MERGE for batch sync from staging

The classic ETL move: load raw rows into a staging table, then reconcile staging into the production table in one statement — insert new rows, update changed rows, optionally delete missing ones. MERGE is one statement, atomic, and far cleaner than three separate DML calls.

-- SQL Server / Oracle / PostgreSQL 15+ : sync staging_users -> users
MERGE INTO users AS tgt
USING staging_users AS src
ON tgt.external_id = src.external_id
WHEN MATCHED AND (
        tgt.email <> src.email
     OR tgt.name  <> src.name
) THEN
    UPDATE SET
        email      = src.email,
        name       = src.name,
        updated_at = NOW()
WHEN NOT MATCHED BY TARGET THEN
    INSERT (external_id, email, name, created_at)
    VALUES (src.external_id, src.email, src.name, NOW())
WHEN NOT MATCHED BY SOURCE THEN
    UPDATE SET is_active = false;  -- soft-delete rows missing from source

Frequently Asked Questions

What should a SQL cheat sheet include?

A comprehensive SQL cheat sheet should cover SELECT, INSERT, UPDATE, DELETE, JOINs (INNER, LEFT, RIGHT, FULL), aggregation functions (COUNT, SUM, AVG), filtering (WHERE, HAVING), subqueries, CTEs, window functions, string functions, date functions, and table DDL operations. It should also include syntax examples for each command.

Is SQL syntax the same across all databases?

Core SQL syntax (SELECT, INSERT, UPDATE, DELETE, JOINs) is largely the same across databases. However, date functions, string functions, window function syntax, and features like LIMIT vs TOP vary between PostgreSQL, MySQL, SQL Server, Oracle, and SQLite. Always check your specific database dialect for edge cases.

What are the most important SQL commands to memorize?

The essential SQL commands to know are: SELECT (reading data), WHERE (filtering), JOIN (combining tables), GROUP BY with aggregate functions (summarizing data), ORDER BY (sorting), INSERT/UPDATE/DELETE (modifying data), and CREATE TABLE (defining schema). These cover 90% of daily SQL work.

How do I practice SQL commands from a cheat sheet?

The best way to practice is with a real database. Use a tool like AI2SQL with its built-in demo database to run queries instantly. You can also set up a local SQLite or PostgreSQL database, load sample data, and work through each command on the cheat sheet one by one.

Can AI generate SQL commands for me instead of memorizing them?

Yes. Tools like AI2SQL let you describe what you need in plain English and generate the correct SQL automatically. This is especially useful for complex queries involving multiple JOINs, window functions, or nested subqueries. However, understanding the fundamentals from a cheat sheet helps you verify and refine AI-generated queries.

Does this cheat sheet cover SQL window functions?

Yes. The Window Functions section covers ROW_NUMBER, RANK, DENSE_RANK, PARTITION BY, LAG and LEAD, frame clauses (ROWS BETWEEN and RANGE BETWEEN with running totals and 7-day moving averages), NTILE for quartile and decile bucketing, and FIRST_VALUE / LAST_VALUE / NTH_VALUE with the LAST_VALUE frame gotcha. It also explains why you cannot reference a window function in WHERE and how to wrap it in a CTE instead.

What's the difference between SQL syntax across PostgreSQL, MySQL, and SQL Server?

Core SELECT, JOIN, GROUP BY, and aggregation syntax is the same. Divergence shows up in: pagination (LIMIT/OFFSET in PostgreSQL and MySQL vs OFFSET FETCH or TOP in SQL Server), JSON access (-> and ->> in PostgreSQL, JSON_EXTRACT and ->> in MySQL, JSON_VALUE and OPENJSON in SQL Server), upserts (ON CONFLICT vs ON DUPLICATE KEY UPDATE vs MERGE), date arithmetic (INTERVAL vs DATEDIFF vs DATEADD), and identifier quoting (double quotes vs backticks vs square brackets). The cheat sheet calls out the dialect on each example so you can copy the right one.

Generate SQL from Plain English

Stop memorizing syntax. Describe what you need and let AI2SQL generate accurate queries for your database.

Try AI2SQL Free

No credit card required