HTML Web SQL Retrieve

Beginner
⏱️ ~10–12 min
📚 Updated: Aug 2026
🎯 5 Examples
🚀 5 Try-it labs
SELECT · rows.item · WHERE

What You’ll Learn

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.

SELECT

Core statement

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

ResultSet

Query answer

The success callback receives a ResultSet with a rows collection.

WHERE

Filter rows

Narrow results to matching rows instead of pulling the whole table.

? bindings

Safe params

Bind filter values with ? placeholders instead of concatenating SQL.

LIMIT

Paginate

Cap how many rows return, and skip pages with OFFSET.

Transactions

Atomic ops

Every executeSql call runs inside db.transaction(), even reads.

Introduction

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.

Why it matters?

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.

Key Highlights

Reads Rows

Fetches existing rows without changing them—the counterpart to INSERT.

Returns a ResultSet

rows.length and rows.item(i) give you the count and each row object.

Bound Parameters

? placeholders in WHERE keep dynamic filters safe from SQL injection.

Deprecated API

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.

Setting Up the Database

Before retrieving data, open the database and seed a table so your SELECT queries have something to return.

Step 1 — Open the database

openDatabase(name, version, displayName, estimatedSize) opens an existing database or creates a new one.

js
const db = openDatabase('myDatabase', '1.0', 'Test DB', 2 * 1024 * 1024);

Step 2 — Create a table and seed rows

Run CREATE TABLE IF NOT EXISTS and insert a few sample rows so every example below has data to fetch.

js
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.

📝 SELECT Syntax

Standard SQL syntax for reading rows:

SQL
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:

js
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 */ }
);

executeSql arguments

ArgumentDescription
sqlStatementThe SELECT ... FROM ... WHERE ... string, with ? placeholders for dynamic values.
argumentsArray 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.

ResultSet properties

  • 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.

⚡ Quick Reference

OperationCode pattern
All rowsSELECT * FROM users
FilterSELECT * FROM users WHERE age > ?
One row by idSELECT * FROM users WHERE id = ?
Row countresults.rows.length
Read rowresults.rows.item(i)
PaginateORDER BY id LIMIT ? OFFSET ?
Query
SELECT * FROM t

Read all

Safe
WHERE col = ?

? binding

Loop
rows.item(i)

Each row

Empty
length === 0

No matches

📋 SELECT * vs SELECT Columns vs SELECT WHERE

All three read rows—but they disagree on how much data comes back and how targeted the query is.

SELECT *
all columns

Convenient for quick reads, but wastes bandwidth on tables with many columns you don’t need.

SELECT cols
SELECT id, name

Names only the columns you use—smaller row objects and clearer intent.

SELECT WHERE
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.

Context

When to Retrieve Rows

Reach for SELECT whenever the UI needs to show data that already lives in the database.

  1. Render a saved list

    Load a cart, reading list, or todo list on page load with SELECT * FROM table.

  2. Power a search or filter

    Turn a search box into a bound WHERE clause instead of filtering everything in JavaScript.

  3. Fetch a single record

    WHERE id = ? pulls exactly one row when you already know its primary key.

  4. Paginate a large table

    LIMIT/OFFSET loads one page at a time instead of every row at once.

  5. Not for writes

    SELECT never changes data—use INSERT, UPDATE, or DELETE to modify rows.

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.

Executing SELECT Queries

Retrieve all records from a table and log each name to the console:

js
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.

Processing the ResultSet

Filter with WHERE and read column values from each row object with rows.item(i):

js
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.

Handling Success, Errors, and Empty Results

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.

js
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;
  }
);
  • Error callback — fires on SQL or schema errors; return true to roll back.
  • Empty resultrows.length === 0 means no matches, not a failed query.
  • Null columns — missing values may come back as null; guard before displaying them.

Examples Gallery

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.

📚 Getting Started

Run a basic SELECT, then filter it with a bound WHERE clause.

Example 1 — SELECT All Rows

Fetch every user in the users table and log their names.

js
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);
  });
});
Try It Yourself

How It Works

SELECT * returns all columns for every row. The loop reads each row’s name property with rows.item(i).

Example 2 — SELECT with WHERE

Filter users older than 30 with a bound parameter.

js
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;
    }
  );
});
Try It Yourself

How It Works

WHERE age > ? with binding [30] keeps the query safe from injection. ORDER BY age sorts results before you display them.

📈 Practical Patterns

Fetch one row, paginate large tables, and render data in HTML.

Example 3 — Fetch One Row by ID

Retrieve a single user when you already know the primary key.

js
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');
});
Try It Yourself

How It Works

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.

Example 4 — Paginate with LIMIT and OFFSET

Load a fixed number of rows per page for large tables.

js
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;
    }
  );
});
Try It Yourself

How It Works

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.

Example 5 — Complete HTML Page with Row List

Set up the database, seed rows, and render every result in an on-page list.

html
<!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>
Try It Yourself

How It Works

The page seeds data, runs SELECT *, and builds <li> elements from each row. Status text confirms exactly how many rows loaded.

Use Cases

Real-world scenarios where reading rows back out is exactly what you need.

1. Show Cart Contents

List every item in the shopper’s cart with quantities and prices.

Example: SELECT * FROM cart WHERE user_id = ?.

2. Search and Filter UI

Turn a search box into a parameterized WHERE clause.

Example: SELECT * FROM notes WHERE title LIKE ?.

3. Restore a Form Draft

Reload an autosaved draft by id when the user returns to the page.

Example: a draft editor pre-filled from a saved row.

4. Settings Screens

Read saved preference rows and populate form fields on load.

Example: SELECT theme, notifications FROM settings.

5. Paginated Tables

Load one page of rows at a time from a large offline dataset.

Example: LIMIT 20 OFFSET 40 for page three.

6. Legacy Maintenance

Inspect and debug data inside older Web SQL–based apps still running in the wild.

Example: a support script auditing stored rows.

Pro Tip: run the same WHERE clause as a SELECT before an UPDATE or DELETE to preview exactly which rows will be affected.

Advantages

Why the standard SQL SELECT pattern is worth learning, even in a deprecated API.

  1. 1. Read-Only Safety

    SELECT never modifies data—it is safe to run repeatedly while you debug.

  2. 2. Flexible Filtering

    WHERE, ORDER BY, and LIMIT shape exactly which rows come back, in what order.

  3. 3. Structured Rows

    Each rows.item(i) is a plain object with column names as properties—easy to consume.

  4. 4. Pairs With Full CRUD

    The same bindings and transaction pattern used here works for INSERT, UPDATE, and DELETE.

Pro Tip: this exact pattern—bound WHERE, transactions, and looping a ResultSet—transfers directly to IndexedDB, SQLite, and server-side SQL.

Usage Tips

Follow these practices to write safe, predictable SELECT queries.

  1. 1. Select Only What You Need

    Prefer SELECT id, name over SELECT * when you only use a few columns.

  2. 2. Always Check rows.length

    Zero rows is success, not failure—branch your UI to an empty state, not an error message.

  3. 3. Bind Every WHERE Value

    Use ? placeholders for ids, search terms, and filters—never build SQL with string concatenation.

  4. 4. Paginate Large Tables

    Use ORDER BY ... LIMIT ? OFFSET ? instead of loading thousands of rows in one query.

  5. 5. Plan Your Migration to IndexedDB

    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.

Common Pitfalls

Avoid these mistakes when reading rows with Web SQL.

  1. 1. Treating an Empty ResultSet as an Error

    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.

  2. 2. Concatenating SQL Strings

    Building a WHERE clause with string concatenation opens the door to SQL injection.

    → Bind every dynamic value with ? placeholders instead.

  3. 3. Forgetting rows.item(i)

    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).

  4. 4. SELECT * Blindly on Large Tables

    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.

  5. 5. Assuming Web SQL Works in Modern Chrome

    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.

🧠 How Web SQL SELECT Works

1

Open & transact

openDatabase() then db.transaction() starts an atomic unit of work.

Setup
2

Run SELECT

executeSql sends the query, optionally bound with ? values.

Query
3

Loop the ResultSet

rows.item(i) gives each row as a plain object.

Read
=

Data displayed

Rows render in the DOM, console, or app state.

Notes

  • Web SQL is deprecated and removed from modern Chrome—use IndexedDB for any new project.
  • SELECT is read-only—it never changes data, no matter how it is written.
  • Zero rows is success, not failure—check rows.length and show an empty state.
  • Bind every dynamic WHERE value with ? placeholders—never string-concatenate SQL.
  • Use LIMIT/OFFSET for large tables instead of loading every row at once.
  • Pair with UPDATE and DELETE for full CRUD on the same table.

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.

Browser Support

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.

Deprecated

Web SQL API

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.

Legacy Not in modern browsers
Google Chrome Removed in Chrome 97+
Removed
Mozilla Firefox Never implemented
Not supported
Apple Safari Legacy versions only
Legacy
Microsoft Edge Chromium-based · removed
Removed
Internet Explorer Never implemented
Not supported
Opera Chromium-based · removed
Removed
Web SQL API Deprecated

Bottom line: Do not build new features on Web SQL. For cross-browser client-side storage today, use IndexedDB.

Wrap Up

🎉 Conclusion

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.

💡 Best Practices

✅ Do

  • Use parameterized WHERE clauses for every filter
  • Check rows.length before assuming a match
  • Use LIMIT and OFFSET for pagination
  • Select only the columns you actually need
  • Handle error callbacks on every query
  • Prefer IndexedDB for new projects

❌ Don’t

  • Concatenate user input into SQL strings
  • Assume SELECT * on huge tables is fast
  • Treat zero rows as a SQL error
  • Forget to handle null column values
  • Run executeSql outside a transaction
  • Build new features on Web SQL in 2026

Key Takeaways

Knowledge Unlocked

Five things to remember about Web SQL SELECT

Use these points when reading data from a client-side database.

5
Core concepts
📄 02

ResultSet

results.rows

Structure
🔄 03

rows.item()

Each row.

Loop
🎯 04

WHERE ?

Filter safe.

Query
🔄 05

Empty OK

length === 0

UI

❓ Frequently Asked Questions

Open the database with openDatabase(), then call db.transaction(). Inside the transaction, use tx.executeSql() with a SELECT statement. In the success callback, loop results.rows and read each row with results.rows.item(i).
Standard SQL: SELECT column1, column2 FROM table_name WHERE condition. Use SELECT * to fetch all columns. Add ORDER BY, LIMIT, and OFFSET for sorting and pagination.
The success callback receives a result object. result.rows.length is the row count. result.rows.item(index) returns one row as an object with column names as properties (e.g. row.name, row.age).
Yes. Use ? placeholders and pass values in the bindings array: SELECT * FROM users WHERE age > ? with [30]. This prevents SQL injection.
Web SQL is deprecated and removed from most modern browsers (including current Chrome). Treat this tutorial as educational for legacy code; use IndexedDB for new projects.
results.rows.length will be 0. That is not an error—the query succeeded but found no matches. Show an empty state in your UI instead of treating it as a failure.

Did you Know? 🔊

A 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.

Continue to Web SQL UPDATE

Now that you can read rows back out, learn how to modify them in place with UPDATE.

UPDATE tutorial →

About the author

Mari Selvan M P
Mari Selvan M P 🔗

Developer, cloud engineer, and technical writer

  • Experience 12 years building web and cloud systems
  • Focus Full Stack Development, AWS, and Developer Education

I write practical tutorials so students and working developers can learn by doing—from databases and APIs to deployment on AWS.

9 people found this page helpful