SELECT
Core statement
Read one or more rows from a table with a standard SQL SELECT statement.

Web SQL Database is a deprecated browser API that stores structured data in a local SQLite database. The SQL SELECT statement reads rows back out—a saved list, a search result, or a single record by id. This tutorial covers SELECT syntax, looping the ResultSet with rows.item(), filtering safely with WHERE, paginating with LIMIT/OFFSET, handling empty results, five worked examples, and why Web SQL is deprecated in favor of IndexedDB.
Core statement
Read one or more rows from a table with a standard SQL SELECT statement.
Query answer
The success callback receives a ResultSet with a rows collection.
Filter rows
Narrow results to matching rows instead of pulling the whole table.
Safe params
Bind filter values with ? placeholders instead of concatenating SQL.
Paginate
Cap how many rows return, and skip pages with OFFSET.
Atomic ops
Every executeSql call runs inside db.transaction(), even reads.
Web SQL Database is a deprecated browser API for client-side relational storage. It uses SQLite under the hood, so standard SQL statements—including SELECT—work just like they do on server-side databases.
The W3C deprecated Web SQL in favor of IndexedDB, and Chrome removed it in version 97. Although the API is no longer recommended for new projects, understanding SELECT still helps you maintain legacy applications and grasp the query fundamentals that carry over to other databases.
Saving data is only half the job—every app also needs to read it back: a cart to display, a list to render, a record to edit. SELECT is how a SQL database answers “what do you have?”, and reading its ResultSet correctly is what turns stored rows into a working UI.
Fetches existing rows without changing them—the counterpart to INSERT.
rows.length and rows.item(i) give you the count and each row object.
? placeholders in WHERE keep dynamic filters safe from SQL injection.
Web SQL is educational for legacy code—use IndexedDB for new work.
In short: SELECT ... FROM table WHERE condition, bound with ?, inside a transaction, read back with rows.item(i)—that pattern retrieves rows safely in Web SQL and in most SQL databases.
Before retrieving data, open the database and seed a table so your SELECT queries have something to return.
openDatabase(name, version, displayName, estimatedSize) opens an existing database or creates a new one.
const db = openDatabase('myDatabase', '1.0', 'Test DB', 2 * 1024 * 1024); Run CREATE TABLE IF NOT EXISTS and insert a few sample rows so every example below has data to fetch.
db.transaction(function (tx) {
tx.executeSql(
'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)'
);
tx.executeSql(
'INSERT OR IGNORE INTO users (id, name, age) VALUES (?, ?, ?)',
[1, 'John Doe', 25]
);
tx.executeSql(
'INSERT OR IGNORE INTO users (id, name, age) VALUES (?, ?, ?)',
[2, 'Jane Smith', 32]
);
}); Here users has id, name, and age columns. Every example in this tutorial reads from this same table.
Standard SQL syntax for reading rows:
SELECT column1, column2 FROM table_name WHERE condition;
SELECT * FROM table_name ORDER BY column LIMIT 10 OFFSET 0; In Web SQL, call it through executeSql and loop the returned ResultSet:
tx.executeSql(
'SELECT * FROM users WHERE id = ?',
[userId],
function (tx, results) {
for (var i = 0; i < results.rows.length; i++) {
var row = results.rows.item(i);
console.log(row.name, row.age);
}
},
function (tx, error) { /* failure */ }
); | Argument | Description |
|---|---|
sqlStatement | The SELECT ... FROM ... WHERE ... string, with ? placeholders for dynamic values. |
arguments | Array of values bound to each ?, in order—e.g. [userId]. Use [] when there is no WHERE. |
successCallback (optional) | Called as (tx, results); loop results.rows with rows.item(i). |
errorCallback (optional) | Called as (tx, error); return true to roll back the transaction. |
results.rows.length — number of rows returned by the query.results.rows.item(i) — the row at index i, returned as a plain object with column names as properties.results.insertId — meaningful for INSERT, not relevant to SELECT.results.rowsAffected — meaningful for INSERT/UPDATE/DELETE, not relevant to SELECT.| Operation | Code pattern |
|---|---|
| All rows | SELECT * FROM users |
| Filter | SELECT * FROM users WHERE age > ? |
| One row by id | SELECT * FROM users WHERE id = ? |
| Row count | results.rows.length |
| Read row | results.rows.item(i) |
| Paginate | ORDER BY id LIMIT ? OFFSET ? |
SELECT * FROM tRead all
WHERE col = ?? binding
rows.item(i)Each row
length === 0No matches
All three read rows—but they disagree on how much data comes back and how targeted the query is.
all columnsConvenient for quick reads, but wastes bandwidth on tables with many columns you don’t need.
SELECT id, nameNames only the columns you use—smaller row objects and clearer intent.
WHERE age > ?Filters which rows come back at all—fewer rows to loop and render.
Reach for the full UPDATE tutorial when the goal is changing a fetched row instead of just reading it.
Reach for SELECT whenever the UI needs to show data that already lives in the database.
Load a cart, reading list, or todo list on page load with SELECT * FROM table.
Turn a search box into a bound WHERE clause instead of filtering everything in JavaScript.
WHERE id = ? pulls exactly one row when you already know its primary key.
LIMIT/OFFSET loads one page at a time instead of every row at once.
Key benefit: a well-scoped SELECT—the right columns, the right WHERE, the right LIMIT—returns exactly the data your UI needs, no more and no less.
Retrieve all records from a table and log each name to the console:
db.transaction(function (tx) {
tx.executeSql(
'SELECT * FROM users',
[],
function (tx, results) {
for (var i = 0; i < results.rows.length; i++) {
console.log(results.rows.item(i).name);
}
},
function (tx, error) {
console.error('Query error:', error.message);
return true;
}
);
}); openDatabase — opens or creates the Web SQL database.transaction — wraps the read in an atomic unit, same as writes.executeSql — runs SELECT ... and hands back a ResultSet in the success callback.The empty array [] is the bindings list—no ? placeholders are needed for SELECT * without a WHERE clause.
Filter with WHERE and read column values from each row object with rows.item(i):
db.transaction(function (tx) {
tx.executeSql(
'SELECT * FROM users WHERE age > ? ORDER BY age',
[30],
function (tx, results) {
for (var i = 0; i < results.rows.length; i++) {
var row = results.rows.item(i);
console.log('ID:', row.id, 'Name:', row.name, 'Age:', row.age);
}
}
);
}); Only users with age > 30 come back, sorted by ORDER BY age. Each call to rows.item(i) returns a fresh object—rows itself is array-like, not a real array, so loop it with a plain for instead of array methods.
Errors come from invalid SQL, a missing table, or a database access failure. A query that finds nothing is not an error—check rows.length separately and render an empty state instead of an error message.
tx.executeSql(
'SELECT * FROM users WHERE id = ?',
[id],
function (tx, results) {
if (results.rows.length === 0) {
console.warn('No user with id', id);
} else {
console.log('Found:', results.rows.item(0).name);
}
},
function (tx, error) {
console.error('Query error:', error.message);
return true;
}
); true to roll back.rows.length === 0 means no matches, not a failed query.null; guard before displaying them.These examples use the Web SQL API pattern. Use the Try It Yourself links to run live demos. Web SQL works only in browsers that still expose openDatabase; otherwise the demo shows a clear status message.
Run a basic SELECT, then filter it with a bound WHERE clause.
Fetch every user in the users table and log their names.
const db = openDatabase('myDatabase', '1.0', 'Test DB', 2 * 1024 * 1024);
db.transaction(function (tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');
tx.executeSql('SELECT * FROM users', [], function (tx, results) {
var len = results.rows.length;
for (var i = 0; i < len; i++) {
console.log(results.rows.item(i).name);
}
}, function (tx, error) {
console.error('Error:', error.message);
});
}); SELECT * returns all columns for every row. The loop reads each row’s name property with rows.item(i).
Filter users older than 30 with a bound parameter.
db.transaction(function (tx) {
tx.executeSql(
'SELECT * FROM users WHERE age > ? ORDER BY age',
[30],
function (tx, results) {
for (var i = 0; i < results.rows.length; i++) {
var row = results.rows.item(i);
console.log(row.name + ' — age ' + row.age);
}
},
function (tx, error) {
console.error('Error:', error.message);
return true;
}
);
}); WHERE age > ? with binding [30] keeps the query safe from injection. ORDER BY age sorts results before you display them.
Fetch one row, paginate large tables, and render data in HTML.
Retrieve a single user when you already know the primary key.
function getUserById(id, callback) {
db.transaction(function (tx) {
tx.executeSql(
'SELECT * FROM users WHERE id = ?',
[id],
function (tx, results) {
if (results.rows.length === 0) {
callback(null);
} else {
callback(results.rows.item(0));
}
},
function (tx, error) {
console.error('Error:', error.message);
return true;
}
);
});
}
getUserById(2, function (user) {
if (user) console.log(user.name, user.age);
else console.log('User not found');
}); rows.item(0) is the first (and only) matching row when id is a primary key. Return null when length is zero instead of throwing.
Load a fixed number of rows per page for large tables.
var pageSize = 10;
var page = 0; /* 0-based page index */
db.transaction(function (tx) {
tx.executeSql(
'SELECT * FROM users ORDER BY id LIMIT ? OFFSET ?',
[pageSize, page * pageSize],
function (tx, results) {
console.log('Page', page, '—', results.rows.length, 'rows');
},
function (tx, error) {
console.error('Error:', error.message);
return true;
}
);
}); LIMIT caps how many rows return; OFFSET skips earlier pages. Increment page and re-run the query for a “Load more” button or a pager.
Set up the database, seed rows, and render every result in an on-page list.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Web SQL Retrieve Example</title>
</head>
<body>
<h1>Data from users table</h1>
<ul id="dataList"></ul>
<p id="status"></p>
<script>
if (!window.openDatabase) {
document.getElementById('status').textContent =
'Web SQL is not supported in this browser.';
} else {
const db = openDatabase('myDatabase', '1.0', 'Test DB', 2 * 1024 * 1024);
db.transaction(function (tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');
tx.executeSql('INSERT OR IGNORE INTO users (id, name, age) VALUES (?, ?, ?)', [1, 'John Doe', 25]);
tx.executeSql('INSERT OR IGNORE INTO users (id, name, age) VALUES (?, ?, ?)', [2, 'Jane Smith', 32]);
});
db.transaction(function (tx) {
tx.executeSql('SELECT * FROM users', [], function (tx, results) {
const list = document.getElementById('dataList');
for (let i = 0; i < results.rows.length; i++) {
const row = results.rows.item(i);
const li = document.createElement('li');
li.textContent = 'ID: ' + row.id + ', Name: ' + row.name + ', Age: ' + row.age;
list.appendChild(li);
}
document.getElementById('status').textContent =
'Loaded ' + results.rows.length + ' row(s).';
}, function (tx, error) {
document.getElementById('status').textContent = 'Error: ' + error.message;
return true;
});
});
}
</script>
</body>
</html> The page seeds data, runs SELECT *, and builds <li> elements from each row. Status text confirms exactly how many rows loaded.
Real-world scenarios where reading rows back out is exactly what you need.
List every item in the shopper’s cart with quantities and prices.
Example: SELECT * FROM cart WHERE user_id = ?.
Turn a search box into a parameterized WHERE clause.
Example: SELECT * FROM notes WHERE title LIKE ?.
Reload an autosaved draft by id when the user returns to the page.
Example: a draft editor pre-filled from a saved row.
Read saved preference rows and populate form fields on load.
Example: SELECT theme, notifications FROM settings.
Load one page of rows at a time from a large offline dataset.
Example: LIMIT 20 OFFSET 40 for page three.
Inspect and debug data inside older Web SQL–based apps still running in the wild.
Example: a support script auditing stored rows.
Why the standard SQL SELECT pattern is worth learning, even in a deprecated API.
SELECT never modifies data—it is safe to run repeatedly while you debug.
WHERE, ORDER BY, and LIMIT shape exactly which rows come back, in what order.
Each rows.item(i) is a plain object with column names as properties—easy to consume.
Pro Tip: this exact pattern—bound WHERE, transactions, and looping a ResultSet—transfers directly to IndexedDB, SQLite, and server-side SQL.
Follow these practices to write safe, predictable SELECT queries.
Prefer SELECT id, name over SELECT * when you only use a few columns.
Zero rows is success, not failure—branch your UI to an empty state, not an error message.
Use ? placeholders for ids, search terms, and filters—never build SQL with string concatenation.
Use ORDER BY ... LIMIT ? OFFSET ? instead of loading thousands of rows in one query.
Web SQL is deprecated—treat new features as an opportunity to move to IndexedDB.
Pro Tip: log every query’s SQL, bound values, and rows.length during development—it turns “why is my list empty?” bugs into a quick log lookup.
Avoid these mistakes when reading rows with Web SQL.
A query that runs fine but matches nothing still calls the success callback—rows.length is simply 0.
→ Check rows.length === 0 and show an empty state, not an error message.
Building a WHERE clause with string concatenation opens the door to SQL injection.
→ Bind every dynamic value with ? placeholders instead.
results.rows is array-like but not a real array—forEach or bracket indexing is not portable.
→ Loop with a plain for and read each row via rows.item(i).
Pulling every column and every row wastes memory and slows down rendering on big datasets.
→ Name the columns you need, and add WHERE/LIMIT to scope the query.
Chrome removed Web SQL in version 97—code that assumes openDatabase exists will crash.
→ Feature-detect with if (window.openDatabase) before running any Web SQL code.
Pro Tip: when a list looks wrong, log the exact SQL and bound values first—most “missing data” bugs turn out to be a mistyped WHERE condition.
openDatabase() then db.transaction() starts an atomic unit of work.
executeSql sends the query, optionally bound with ? values.
rows.item(i) gives each row as a plain object.
Rows render in the DOM, console, or app state.
SELECT is read-only—it never changes data, no matter how it is written.rows.length and show an empty state.WHERE value with ? placeholders—never string-concatenate SQL.LIMIT/OFFSET for large tables instead of loading every row at once.Quick Takeaway: Web SQL is deprecated—practice SELECT here to learn the pattern, then apply the same bindings + ResultSet habits in IndexedDB and other SQL databases.
Web SQL was never implemented in Firefox or Internet Explorer. Chrome and other Chromium browsers removed it in Chrome 97. Only older Safari versions historically supported it. Feature-detect with window.openDatabase before running any demo or legacy code.
Web SQL is not part of any current web standard. Modern Chrome, Edge, and Opera have removed it entirely; Firefox never implemented it. Treat this tutorial as educational for legacy maintenance only.
Bottom line: Do not build new features on Web SQL. For cross-browser client-side storage today, use IndexedDB.
Retrieving records with Web SQL means running SELECT ... FROM ... WHERE ... inside db.transaction, looping the returned ResultSet with rows.item(i), and treating zero matches as a normal, empty result—not an error.
Although Web SQL is deprecated, understanding SELECT helps you maintain older systems. For new projects, use IndexedDB—but the query habits you learned here (bound parameters, pagination, and empty-state handling) apply everywhere.
Practice the five examples above, then continue to UPDATE so you can modify the rows you just retrieved.
WHERE clauses for every filterrows.length before assuming a matchLIMIT and OFFSET for paginationSELECT * on huge tables is fastnull column valuesexecuteSql outside a transactionUse these points when reading data from a client-side database.
Read rows.
Basicsresults.rows
StructureEach row.
LoopFilter safe.
Querylength === 0
UIA Web SQL ResultSet’s rows collection is array-like but not a real JavaScript array—that’s why the API gives you rows.item(i) instead of letting you call forEach or index it directly. Because Web SQL is deprecated, treat every SELECT you write here as a rehearsal for the same pattern in IndexedDB.
Now that you can read rows back out, learn how to modify them in place with UPDATE.
9 people found this page helpful