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 18 – Using SQL from application code

SQL from Node.js and Java

Run SQL from Node.js with node:sqlite, node-postgres and mysql2 and from Java with JDBC: placeholders, prepared statements, transactions and pools.

  • Intermediate
  • 30 minutes
  • Examples run with sql.js 1.14.2, Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8 and Java 25
  • By MySmartCoPilot
Free core, Pro depth: 10 of 30 minutes free
Free: Four drivers, one pattern · The report in Node.js, run. With a pass: 2 in-depth parts and 3 interview answers, about 20 minutes.

What you will learn

  • Query SQLite with node:sqlite and PostgreSQL with node-postgres
  • Use JDBC PreparedStatement in Java
  • Avoid N+1 queries and connection leaks

Before you start

Running SQL from Node.js or from Java takes the same three steps as from any language: open a connection through a driver, run a statement whose values are placeholders and are handed over separately, then read the rows and close what you opened. In Node.js the driver is a module: node:sqlite is built in, and pg (node-postgres) and mysql2 are npm packages. In Java it is JDBC, the java.sql API that comes with the JDK, plus one driver JAR per database. What changes between them is the spelling: ? or $1, synchronous calls or await, plain objects or a ResultSet. The two production bugs are the same everywhere. N+1 queries run one query for a list and then one more for each item in it. A connection leak is a connection your code took from a pool and never gave back.

Four drivers, one pattern

The same parameterised query from Node.js and from Java
Criterion Node.jsJava
Driver node:sqlite, built in; pg and mysql2 from npmJDBC: java.sql in the JDK, plus one driver JAR per database
Placeholders ? in node:sqlite and mysql2; $1, $2 in pg? in every driver, numbered from 1
Run a query stmt.all(a, b), or await client.query(sql, [a, b])ps.setString(1, a), then ps.executeQuery()
Rows come back as plain objectsa ResultSet, read with next()
Calls are synchronous in node:sqlite; Promises in pg and mysql2blocking
A transaction BEGIN … COMMIT on one connectionsetAutoCommit(false) … commit()
Pool for a server pg.Pool or mysql.createPool(); none for node:sqlitea DataSource with a pool
  • node:sqlite runs SQLite inside the Node.js process and answers at once, without await.
  • node-postgres sends your SQL unchanged to PostgreSQL with the values beside it, so its placeholders are PostgreSQL’s own numbered ones.
  • mysql2’s execute() prepares a statement on its first use and reuses it from a cache on that connection.
  • JDBC’s PreparedStatement holds one statement with ? placeholders that you fill with typed setters, numbered from 1.

A prepared statement is parsed and planned once and can then run many times with new values. Each run is still one round trip to the database, and that is where N+1 hurts:

Sequence: code prepares a statement once, the database parses it, then each run sends only a value and gets rows back.Your codeDriverDatabase1. prepare(sql with ?)2. SQL text, once:parse and plan3. run('Pune')4. the value only5. rows for Pune6. run('Kochi')7. the value only8. rows for Kochi

Prepare once, run with new values

Text description of the diagram

The figure is a sequence diagram with three participants: your code, the driver and the database.

  1. Your code prepares a statement whose SQL text has a ? where the city goes.
  2. The driver sends that SQL text to the database once, and the database parses and plans it.
  3. Your code runs the statement with the value 'Pune'.
  4. The driver sends only the value.
  5. The rows for Pune come back.
  6. Your code runs the same statement with 'Kochi'.
  7. Again the driver sends only the value.
  8. The rows for Kochi come back.

Every run is one round trip between your program and the database. A loop that runs a statement once per row of an earlier result makes one round trip per row: the N+1 pattern.

The drivers differ in when that parsing happens. node:sqlite and JDBC compile the statement when you call prepare() or prepareStatement() and keep it until you close it, so a statement prepared before a loop is parsed once. node-postgres sends an unnamed query that PostgreSQL parses on every call; giving the query a name makes the server keep its plan for that connection, which the node-postgres documentation suggests only for queries that run very often. mysql2’s execute() keeps prepared statements in a cache on each connection, 16,000 of them by default.

No driver binds a table or a column name, because a placeholder stands only for a value. Choose such names from a fixed set in your own code; for PostgreSQL the node-postgres documentation suggests escaping them with a library such as pg-format.

The report in Node.js, run

The mart shop is built by a small module, mart.mjs. The report prepares one statement and runs it for two cities, looks up a product by a named parameter, and places three orders inside transactions:

The mart report with node:sqlite

node/report.mjs

import { DatabaseSync } from 'node:sqlite';
import { buildMart } from './mart.mjs';

const db = new DatabaseSync(':memory:');
buildMart(db);

// Prepared once, run for two cities: the values go to all(), never into the SQL text.
const spend = db.prepare(`
  SELECT c.name, count(DISTINCT o.order_id) AS orders,
         sum(i.quantity * p.price_paise) AS spent_paise
  FROM customers c
  JOIN orders o      ON o.customer_id = c.customer_id
  JOIN order_items i ON i.order_id = o.order_id
  JOIN products p    ON p.product_id = i.product_id
  WHERE c.city = ? AND o.status = ?
  GROUP BY c.customer_id
  ORDER BY spent_paise DESC, c.name`);
for (const city of ['Pune', 'Kochi']) {
  for (const row of spend.all(city, 'delivered')) {
    const rupees = (row.spent_paise / 100).toFixed(2).padStart(8);
    console.log(`${city.padEnd(6)} ${row.name.padEnd(14)} ${row.orders} order(s) ${rupees} rupees`);
  }
}

// get() returns the first row, or undefined; a named parameter takes an object.
const product = db.prepare('SELECT name, price_paise FROM products WHERE product_id = :id');
console.log(JSON.stringify(product.get({ id: 3 })), product.get({ id: 99 }));

// run() says what a change did. An order and its items commit together, or not at all.
const addOrder = db.prepare("INSERT INTO orders VALUES (?, ?, 'placed')");
const addItem = db.prepare('INSERT INTO order_items VALUES (?, ?, ?)');
function placeOrder(orderId, customerId, items) {
  db.exec('BEGIN');
  try {
    const { changes } = addOrder.run(orderId, customerId);
    for (const [productId, quantity] of items) addItem.run(orderId, productId, quantity);
    db.exec('COMMIT');
    return `order ${orderId}: committed (${changes} order row, ${items.length} item rows)`;
  } catch (e) {
    db.exec('ROLLBACK');
    return `order ${orderId}: rolled back (${e.message})`;
  }
}
console.log(placeOrder(111, 2, [[3, 2], [5, 1]]));
console.log(placeOrder(112, 2, [[3, 2], [5, 0]]));
console.log(placeOrder(113, 42, [[1, 1]]));
console.log('in a transaction now:', db.isTransaction);
console.log('orders:', db.prepare('SELECT count(*) AS n FROM orders').get().n);
db.close();

Output

Pune   Farah Qureshi  2 order(s)   754.00 rupees
Pune   Asha Kulkarni  1 order(s)   389.48 rupees
Pune   Lakshmi Iyer   1 order(s)   149.99 rupees
Kochi  Vikram Nair    1 order(s)   215.00 rupees
{"name":"Masala chai 250 g","price_paise":12000} undefined
order 111: committed (1 order row, 2 item rows)
order 112: rolled back (CHECK constraint failed: quantity > 0)
order 113: rolled back (FOREIGN KEY constraint failed)
in a transaction now: false
orders: 11

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node report.mjs

node/mart.mjs

// The mart practice shop, built in any node:sqlite database: four tables and their rows.
export function buildMart(db) {
  db.exec(`
    CREATE TABLE customers (customer_id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT NOT NULL);
    CREATE TABLE products (product_id INTEGER PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL,
                           price_paise INTEGER NOT NULL);
    CREATE TABLE orders (order_id INTEGER PRIMARY KEY, customer_id INTEGER NOT NULL REFERENCES customers,
                         status TEXT NOT NULL);
    CREATE TABLE order_items (order_id INTEGER NOT NULL REFERENCES orders,
                              product_id INTEGER NOT NULL REFERENCES products,
                              quantity INTEGER NOT NULL CHECK (quantity > 0), PRIMARY KEY (order_id, product_id));
    INSERT INTO customers VALUES (1, 'Asha Kulkarni', 'Pune'), (2, 'Vikram Nair', 'Kochi'), (3, 'Meera Rathore', 'Jaipur'),
      (4, 'Tenzin Dorjee', 'Gangtok'), (5, 'Farah Qureshi', 'Pune'), (6, 'Rohan Das', 'Kochi'),
      (7, 'Lakshmi Iyer', 'Pune'), (8, 'Arjun Mehta', 'Jaipur');
    INSERT INTO products VALUES (1, 'Basmati rice 1 kg', 'Grains', 14999), (2, 'Toor dal 500 g', 'Pulses', 8950),
      (3, 'Masala chai 250 g', 'Beverages', 12000), (4, 'Coconut oil 1 L', 'Oils', 21500),
      (5, 'Ragi flour 1 kg', 'Grains', 6800);
    INSERT INTO orders VALUES (101, 1, 'delivered'), (102, 2, 'delivered'), (103, 1, 'placed'), (104, 3, 'cancelled'),
      (105, 5, 'delivered'), (106, 7, 'delivered'), (107, 5, 'delivered'), (108, 6, 'placed'),
      (109, 8, 'delivered'), (110, 7, 'cancelled');
    INSERT INTO order_items VALUES (101, 1, 2), (101, 2, 1), (102, 4, 1), (103, 3, 2), (103, 5, 1), (104, 2, 3),
      (105, 3, 1), (105, 4, 2), (106, 1, 1), (107, 5, 3), (108, 2, 2), (109, 1, 1), (109, 3, 1), (110, 4, 1);
  `);
}
  • all() and get() return plain objects keyed by column name. get() returns undefined when there is no row, which is why the second lookup printed undefined instead of throwing.
  • run() reports the change as changes and lastInsertRowid.
  • Each order commits as a whole. Order 112’s second item broke the CHECK on quantity and order 113 named a customer who does not exist, so both rolled back, and 11 orders remain: the 10 of mart and order 111.
  • node:sqlite enforces foreign keys by default. Python’s sqlite3 and the sqlite3 shell do not until you run PRAGMA foreign_keys = ON, so the same INSERT of order 113 would succeed there.

SQL from Node.js through node-postgres looks the same with await: const { rows } = await client.query(sql, [city, 'delivered']) with $1 and $2 in the text. Every query of a transaction must run on one client, so a transaction takes a client from the pool, runs BEGIN, its statements and COMMIT on it, and releases it in a finally block.

In Java, JDBC turns the same report into a PreparedStatement: conn.prepareStatement(sql), then ps.setString(1, "Pune") and ps.executeQuery(), with a while (rs.next()) loop over the rows. Java’s try-with-resources closes the ResultSet, the statement and the connection in reverse order, even when an exception is thrown. A new JDBC connection is in auto-commit mode, which commits every statement on its own.

Check yourself

3 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 3 A query written for mysql2 says WHERE city = ? AND status = ?. What must change to run it with node-postgres?

    Choose one answer.

    Show the answer to question 1

    Answer: The placeholders become $1 and $2

    node-postgres passes the text unchanged to PostgreSQL, whose placeholders are numbered: $1, $2 and so on. mysql2, node:sqlite and JDBC use ?. The values still travel separately in every case.

  2. Question 2 of 3 In JDBC, which call binds the value of the first ? of a PreparedStatement?

    Choose one answer.

    Show the answer to question 2

    Answer: ps.setString(1, city)

    JDBC numbers parameters from 1: the first ? is 1, the second is 2. An index of 0 matches no parameter marker, so the driver raises an SQLException.

  3. Question 3 of 3 With node:sqlite, what does db.prepare("SELECT name FROM products WHERE product_id = ?").get(99) return when no product has id 99?

    Choose one answer.

    Show the answer to question 3

    Answer: undefined

    get() returns the first row as an object, or undefined when there is none, as the report example prints. all() returns an array, empty when nothing matches.

JDBC in Java, run: prepared statements, try-with-resources and transactions Pro: opens with a Pro, Premium or Ultimate pass

JDBC code is short once three habits are in place: prepare once with ?, close everything with try-with-resources, and turn auto-commit off for work that must succeed as a whole. These programs run on Java 25 and show what goes wrong without the third habit.

In this part:

  • The report with a PreparedStatement, two cities, one statement
  • Auto-commit leaves half an order, reproduced
  • NULL from getInt() is 0, reproduced
  • Batches and a statement per loop

2 runnable examples (Mart.java, MiniSqliteDriver.java, NullTrap.java, PlaceOrder.java, Python, Report.java) · 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).

N+1 queries and leaked connections, reproduced and fixed Pro: opens with a Pro, Premium or Ultimate pass

The two most common database bugs in Node.js and Java services do not show in a test on a laptop, where the database answers in microseconds and nothing else runs. This part measures the first in round trips and reproduces the second in a pool, then has you write the detector that finds N+1 loops in a query log.

In this part:

  • N+1 measured in round trips
  • A leaked connection drains the pool, reproduced
  • Catching N+1 before it ships
  • Your turn: find the N+1 shape in a log
  • Find the N+1 pattern in a query log
    • Results of the sample tests

2 runnable examples (JavaScript) · 1 exercise with sample tests · about 5 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

  1. Fresher to mid · Warm-up

    In JDBC, when do you use a Statement and when a PreparedStatement?

    Use a PreparedStatement whenever the SQL contains a value that changes: you write ? where the value goes and fill it with a typed setter such as setString(1, city), numbered from 1. The value never becomes part of the SQL text, so it cannot change what the statement does, and the driver can parse the statement once and run it many times. A plain Statement runs a fixed SQL string, which suits statements without any values, such as a CREATE TABLE in a setup script. Never build a Statement's SQL by joining strings with user input: that is how SQL injection happens. Both are closed with try-with-resources.

  2. Mid to senior

    After a deploy, a Node.js API on node-postgres slowly stops answering: after an hour every request hangs, while the PostgreSQL server shows almost no activity. Restarting the API fixes it for another hour. What do you suspect, how do you confirm it, and what do you change?

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

  3. Fresher to senior

    An order history page takes 40 ms on a developer's laptop and 1.6 seconds in production, where the database is in another zone with a 30 ms round trip. The page lists 50 orders with their items. What is the likely cause, how do you prove it, and how do you fix it?

    Model answer, Pro. First step and follow-ups

  4. Fresher to mid

    A Java service saves an order and then its items with JDBC PreparedStatements. Now and then the reports show orders with no items at all. The code catches SQLException, logs it and returns an error to the client. What is wrong, and what does a correct version look like?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Node.js and Java run SQL through drivers that keep values apart from the SQL text: ? in node:sqlite, mysql2 and JDBC, $1 in node-postgres.
  • node:sqlite is synchronous and runs in your process; node-postgres and mysql2 are asynchronous and talk to a server, usually through a pool.
  • JDBC numbers parameters from 1, closes with try-with-resources and starts every connection in auto-commit mode.

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.