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.
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
What you’ll learn · Before you start
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
On this page
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
| Criterion | Node.js | Java |
|---|---|---|
| Driver | node:sqlite, built in; pg and mysql2 from npm | JDBC: 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 objects | a ResultSet, read with next() |
| Calls are | synchronous in node:sqlite; Promises in pg and mysql2 | blocking |
| A transaction | BEGIN … COMMIT on one connection | setAutoCommit(false) … commit() |
| Pool for a server | pg.Pool or mysql.createPool(); none for node:sqlite | a 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
PreparedStatementholds 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:
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.
- Your code prepares a statement whose SQL text has a ? where the city goes.
- The driver sends that SQL text to the database once, and the database parses and plans it.
- Your code runs the statement with the value 'Pune'.
- The driver sends only the value.
- The rows for Pune come back.
- Your code runs the same statement with 'Kochi'.
- Again the driver sends only the value.
- 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:
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()andget()return plain objects keyed by column name.get()returnsundefinedwhen there is no row, which is why the second lookup printedundefinedinstead of throwing.run()reports the change aschangesandlastInsertRowid.- Each order commits as a whole. Order 112’s second item broke the
CHECKon 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
sqlite3and thesqlite3shell do not until you runPRAGMA foreign_keys = ON, so the sameINSERTof 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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
-
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 assetString(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. -
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?
-
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?
-
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?
Key takeaways
- Node.js and Java run SQL through drivers that keep values apart from the SQL text:
?in node:sqlite, mysql2 and JDBC,$1in 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
- Node.js v24 documentation: SQLite (node:sqlite) (OpenJS Foundation)
- node-postgres: Queries (node-postgres)
- node-postgres: pg.Pool (node-postgres)
- MySQL2 documentation (MySQL2)
- MySQL2: Prepared Statements (MySQL2)
- Java SE 25 API: java.sql.PreparedStatement (Oracle Corporation)
- Java SE 25 API: java.sql.Connection (Oracle Corporation)
- Java SE 25 API: java.sql.ResultSet (Oracle Corporation)
- Java SE 25 API: java.sql.DriverManager (Oracle Corporation)
- The Java Language Specification, Java SE 25: The try-with-resources Statement (Oracle Corporation)
- JEP 454: Foreign Function & Memory API (OpenJDK)
- JEP 458: Launch Multi-File Source-Code Programs (OpenJDK)
- SQLite: An Introduction To The SQLite C/C++ Interface (SQLite)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress