Utilrix

Online SQL Editor & Compiler

Run SQL against a real SQLite database in your browser — sample data already loaded, schema browser, nothing to install.

TABLES0

No tables yet. Run a CREATE TABLE, or reset to the sample database.

query.sql1:1 · SQLite
RESULTS

Press Run. The sample database is already loaded — double-click a table on the left to drop its name into the query.

Real SQLite compiled to WebAssembly, running in this tab — about 1.5MB on first load, then cached. Your data and queries never leave the device, and INSERTs persist until you reset.

SQL examples you can run

Every one of these is loaded from the Examples menu in the editor above — press Run and it works. They are printed here so you can read the code before you run it.

GROUP BY + HAVING

GROUP BY with an aggregate and HAVING to filter the groups — the difference between WHERE and HAVING made concrete.

-- The sample database is already loaded. Press Run, or Ctrl + Enter.
-- Ctrl + Enter with text selected runs only the selection.

SELECT
  d.name        AS department,
  COUNT(*)      AS headcount,
  ROUND(AVG(e.salary)) AS avg_salary
FROM employees e
JOIN departments d ON d.id = e.department_id
WHERE e.active = 1
GROUP BY d.name
HAVING COUNT(*) > 1
ORDER BY avg_salary DESC;

Multi-table JOIN

A four-table join summing line items per order, which is the shape of most reporting queries.

-- Revenue per order, joining four tables.
SELECT
  o.id                              AS order_id,
  c.name                            AS customer,
  e.name                            AS sold_by,
  o.ordered_on,
  SUM(oi.quantity * oi.unit_price)  AS order_total
FROM orders o
JOIN customers   c  ON c.id = o.customer_id
JOIN employees   e  ON e.id = o.employee_id
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status = 'paid'
GROUP BY o.id
ORDER BY order_total DESC
LIMIT 10;

Self-join (who reports to whom)

A table joined to itself through manager_id, so every employee is listed beside their manager.

-- employees.manager_id points back at employees.id.
SELECT
  e.name              AS employee,
  e.role,
  COALESCE(m.name, '— none —') AS manager,
  d.name              AS department
FROM employees e
LEFT JOIN employees   m ON m.id = e.manager_id
LEFT JOIN departments d ON d.id = e.department_id
ORDER BY d.name, m.name NULLS FIRST, e.name;

Window functions

RANK and AVG as window functions with PARTITION BY — ranking inside each group without collapsing the rows.

-- Rank salaries inside each department, and compare to the dept average.
SELECT
  d.name AS department,
  e.name AS employee,
  e.salary,
  RANK()       OVER (PARTITION BY e.department_id ORDER BY e.salary DESC) AS rank_in_dept,
  ROUND(AVG(e.salary) OVER (PARTITION BY e.department_id))                AS dept_avg,
  ROUND(e.salary - AVG(e.salary) OVER (PARTITION BY e.department_id))     AS vs_avg
FROM employees e
JOIN departments d ON d.id = e.department_id
WHERE e.active = 1
ORDER BY department, rank_in_dept;

Subqueries & EXISTS

NOT EXISTS to find rows with no match, and a correlated subquery comparing each row to its own group's average.

-- Customers who have never placed a paid order.
SELECT c.name, c.city, c.joined_on
FROM customers c
WHERE NOT EXISTS (
  SELECT 1 FROM orders o
  WHERE o.customer_id = c.id AND o.status = 'paid'
);

-- Employees earning more than their department's average.
SELECT e.name, e.role, e.salary
FROM employees e
WHERE e.salary > (
  SELECT AVG(e2.salary) FROM employees e2
  WHERE e2.department_id = e.department_id
)
ORDER BY e.salary DESC;

CTEs (WITH ...)

WITH to build the answer in named steps instead of nesting subqueries.

-- Build up an answer in readable steps instead of one nested query.
WITH order_totals AS (
  SELECT o.id, o.customer_id, o.ordered_on,
         SUM(oi.quantity * oi.unit_price) AS total
  FROM orders o
  JOIN order_items oi ON oi.order_id = o.id
  WHERE o.status = 'paid'
  GROUP BY o.id
),
customer_spend AS (
  SELECT c.name AS customer, c.state,
         COUNT(*)    AS orders,
         SUM(t.total) AS lifetime_value
  FROM order_totals t
  JOIN customers c ON c.id = t.customer_id
  GROUP BY c.id
)
SELECT * FROM customer_spend
ORDER BY lifetime_value DESC;

Dates & CASE

strftime to group by month, and CASE to bucket the result.

-- SQLite stores dates as text; strftime does the work.
SELECT
  strftime('%Y-%m', o.ordered_on) AS month,
  COUNT(DISTINCT o.id)            AS orders,
  SUM(oi.quantity * oi.unit_price) AS revenue,
  CASE
    WHEN SUM(oi.quantity * oi.unit_price) > 150000 THEN 'strong'
    WHEN SUM(oi.quantity * oi.unit_price) > 60000  THEN 'steady'
    ELSE 'slow'
  END AS verdict
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status = 'paid'
GROUP BY month
ORDER BY month;

INSERT / UPDATE / DELETE

INSERT, UPDATE and DELETE against the live database, then reading back what changed.

-- Changes are real, and last until you reset the database.
INSERT INTO products (name, category, price, stock)
VALUES ('Analytics Add-on', 'Add-on', 7500, 999);

UPDATE employees
SET salary = ROUND(salary * 1.10)
WHERE department_id = 3 AND active = 1;

DELETE FROM orders WHERE status = 'cancelled';

-- Then check what changed:
SELECT name, category, price FROM products WHERE category = 'Add-on';
SELECT name, salary FROM employees WHERE department_id = 3;
SELECT COUNT(*) AS remaining_orders FROM orders;

Create your own table

CREATE TABLE with a primary key and a CHECK constraint, then querying your own table.

DROP TABLE IF EXISTS students;

CREATE TABLE students (
  id      INTEGER PRIMARY KEY AUTOINCREMENT,
  name    TEXT    NOT NULL,
  branch  TEXT    NOT NULL,
  marks   INTEGER CHECK (marks BETWEEN 0 AND 100)
);

INSERT INTO students (name, branch, marks) VALUES
  ('Aisha',  'CSE', 91),
  ('Rohan',  'ECE', 78),
  ('Meera',  'CSE', 84),
  ('Kabir',  'ME',  66),
  ('Tanvi',  'ECE', 89);

SELECT branch,
       COUNT(*)        AS students,
       MAX(marks)      AS top,
       ROUND(AVG(marks), 1) AS average
FROM students
GROUP BY branch
ORDER BY average DESC;

SQL quick reference

The syntax people look up most often. Copy any line into the editor above to see what it does.

Clause order — written, and how it actually runs

WhatSyntaxNotes
1. FROM / JOINFROM orders o JOIN customers c ON c.id = o.customer_idRows are assembled first.
2. WHEREWHERE o.status = 'paid'Filters individual rows, before grouping.
3. GROUP BYGROUP BY c.idCollapses rows into groups.
4. HAVINGHAVING COUNT(*) > 1Filters groups — this is why WHERE can't use COUNT.
5. SELECTSELECT c.name, COUNT(*) AS ordersColumn aliases exist only from here on.
6. ORDER BYORDER BY orders DESCCan use SELECT aliases, because it runs after.
7. LIMITLIMIT 10 OFFSET 20Applied last of all.

Join types

WhatSyntaxNotes
INNER JOINFROM a JOIN b ON b.a_id = a.idOnly rows that match on both sides.
LEFT JOINFROM a LEFT JOIN b ON ...Every row from a; NULLs where b has no match.
Anti-joinLEFT JOIN b ... WHERE b.id IS NULLRows in a with nothing in b.
CROSS JOINFROM a, bEvery combination — usually a mistake.
Self joinFROM employees e JOIN employees m ON m.id = e.manager_idOne table, two roles.

Aggregates and window functions

WhatSyntaxNotes
COUNTCOUNT(*) vs COUNT(column)COUNT(column) skips NULLs; COUNT(*) doesn't.
SUM / AVGROUND(AVG(salary), 2)NULLs are ignored, not treated as zero.
GROUP_CONCATGROUP_CONCAT(name, ', ')Collapses a group into one string.
ROW_NUMBERROW_NUMBER() OVER (ORDER BY salary DESC)1, 2, 3 with no ties.
RANKRANK() OVER (PARTITION BY dept ORDER BY salary DESC)Ties share a rank and leave a gap.
Running totalSUM(x) OVER (ORDER BY d ROWS UNBOUNDED PRECEDING)Accumulates down the rows.
LAG / LEADLAG(total) OVER (ORDER BY month)The previous or next row's value.

Other languages you can run here

Processed 100% in your browser — nothing you enter here is ever uploaded.

Common use cases

Practising joins and aggregates

The sample database has foreign keys in every direction and a self-referencing manager column, so JOINs, GROUP BY, HAVING, subqueries and self-joins all have somewhere real to point.

Interview and exam preparation

Window functions, CTEs, EXISTS and CASE are all supported and each has a ready example. Small tables mean you can verify an answer by eye instead of trusting it.

Trying a query before running it at work

Create the same table shape, load a few rows, and check your query does what you expect — without touching a production database or waiting for a DBA.

Exploring a .sqlite file you were sent

Open the file, browse the schema in the side panel and query it — without installing a database tool. The file never leaves your computer.

How to use the SQL Editor

A sample database of six related tables — departments, employees, customers, products, orders and order_items — is already loaded. Nothing to install, nothing to seed. Press Run, or Ctrl + Enter.

The Tables panel on the left shows every table with its row count; expand one to see its columns, primary keys and types. Double-click a table name or click a column to drop it into your query.

Select part of the query and press Run to execute just the selection — the way you would in a real client when a file holds a dozen queries.

Autocomplete knows your schema: table and column names come from the database itself, alongside SQL keywords, aggregate functions and window-function snippets. Ctrl + Space asks for them.

INSERT, UPDATE, DELETE and CREATE TABLE all really run and the changes persist, so you can build up a schema across several queries. Reset DB puts the sample data back.

Save DB downloads a real .sqlite file you can open in DB Browser or the sqlite3 CLI, and Open .sqlite loads your own database in. Results export as CSV, and Share copies a link with the query inside it.

Full screen gives the grid the whole window — press Esc to come back.

Frequently asked questions

Related tools