JDBC architecture, drivers, statements, ResultSet, SQL injection, batching, transactions and isolation, HikariCP pooling, streaming, LOBs, locking, retries and production debugging.
Theory
Q1
What is JDBC and how is its architecture layered?
basic
JDBC is the standard Java API (java.sql, javax.sql) for talking to relational databases. Your code depends only on interfaces (Connection, Statement, ResultSet); a vendor-supplied driver implements them and speaks the database wire protocol.
JDBC is blocking and synchronous: a thread is parked on socket I/O for every call.
ORMs, JdbcTemplate and jOOQ are all built on top of it.
⚠ Follow-up traps
Is JDBC an ORM? No. It returns rows and columns; mapping to objects is your job or a library's.
Is JDBC non-blocking? No. R2DBC is the reactive alternative.
#jdbc#architecture
Q2
Explain the four JDBC driver types.
basic
Type 1 is the JDBC-ODBC bridge (removed in Java 8). Type 2 is native-API (uses vendor C libraries via JNI). Type 3 is network-protocol (middleware translates). Type 4 is pure Java and speaks the database protocol directly.
Type 4 is what you use today: PostgreSQL, MySQL, Oracle thin, SQL Server.
Type 2 (Oracle OCI) is still used for features like TAF or special auth, at the cost of native installs.
⚠ Follow-up traps
Which type is the most portable? Type 4, since it needs no native library.
Does Java 17 still ship the ODBC bridge? No, it was removed in Java 8.
#drivers#types
Q3
How does a driver get registered, and do I still need Class.forName?
basic
Since JDBC 4.0 (Java 6), drivers are discovered through ServiceLoader via META-INF/services/java.sql.Driver, so Class.forName("org.postgresql.Driver") is unnecessary. DriverManager.getConnection(url) asks each registered driver whether it accepts the URL.
Class.forName is still needed for very old drivers or inside some custom classloader setups (app servers, plugin containers).
If the driver jar is not on the classpath you get No suitable driver found for jdbc:....
⚠ Follow-up traps
Why "No suitable driver" even though the jar is present? Wrong URL prefix, or the jar is in a different classloader than the caller.
Can several driver versions coexist? Yes in separate classloaders, but DriverManager checks caller classloader visibility.
#driver#serviceloader
Q4
How should you manage Connection, Statement and ResultSet lifecycle?
basic
Use try-with-resources for all three; they close in reverse order of declaration, even on exceptions. Closing a Connection closes its statements, and closing a Statement closes its ResultSets, but relying on that leaves resources open longer than needed.
String sql = "select id, name from users where status = ?";try (Connection c = ds.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) { ps.setString(1, "ACTIVE"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { System.out.println(rs.getString("name")); } }}
With a pool, close() returns the connection to the pool instead of closing the socket.
⚠ Follow-up traps
Does closing the Connection close the ResultSet? Yes, but you should not depend on it, especially with pools that may not close underlying statements promptly.
Is closing a ResultSet twice an error? No, close() is idempotent.
#try-with-resources#resources
Q5
Compare Statement, PreparedStatement and CallableStatement.
basic
Statement runs a complete SQL string; PreparedStatement runs parameterized SQL with ? placeholders; CallableStatement extends PreparedStatement to invoke stored procedures/functions and read OUT parameters.
Prefer PreparedStatement for anything that has input values: safer and often faster.
Statement is acceptable for static DDL or constant SQL.
⚠ Follow-up traps
Does CallableStatement extend Statement? Indirectly: CallableStatement -> PreparedStatement -> Statement.
Can a PreparedStatement be reused? Yes, rebind parameters and execute again, as long as the connection is open.
#statement#preparedstatement#callablestatement
Q6
Why is PreparedStatement better than Statement beyond security?
intermediate
Parameterized SQL gives the database a stable statement text, so it can parse and plan once and reuse the plan, and the driver can reuse the prepared handle. Parameters travel separately from SQL text, so no escaping or quoting bugs occur.
PostgreSQL's driver switches to a server-side prepared statement after prepareThreshold executions (default 5).
MySQL Connector/J does client-side preparation by default (useServerPrepStmts=false); set useServerPrepStmts=true plus cachePrepStmts=true for server-side caching.
Concatenating literals creates a distinct SQL text per value, polluting the plan/statement cache.
⚠ Follow-up traps
Is a PreparedStatement always server-side prepared? No, it depends on the driver and its settings.
Is it faster for a single execution? Not necessarily; the benefit shows on repeated execution or via the cache.
#preparedstatement#performance
Q7
How do you prevent SQL injection, and what cannot be parameterized?
basic
Bind every value with ? placeholders. Placeholders only stand in for values, never for identifiers (table/column names), keywords or ORDER BY direction, so those must be validated against an allow-list.
Map<String, String> sortable = Map.of("name", "name", "created", "created_at");String col = sortable.getOrDefault(req.sort(), "created_at");String dir = "desc".equalsIgnoreCase(req.dir()) ? "desc" : "asc";String sql = "select id, name from users where status = ? order by " + col + " " + dir;
Escaping by hand is fragile (charset and quoting differences); do not rely on it.
Least privilege DB accounts limit the damage if injection slips through.
⚠ Follow-up traps
Can I bind a table name with setString? No, it would be sent as a string literal, not an identifier.
Does a PreparedStatement built from concatenated input protect you? No; the vulnerability is in the concatenation.
#sql-injection#security
Q8
How do you use CallableStatement with IN and OUT parameters?
intermediate
Use the escape syntax {call proc(?, ?)} or {? = call func(?)}, bind IN values with setXxx, register OUT types with registerOutParameter, execute, then read with getXxx.
Procedures returning result sets: call getResultSet() / getMoreResults(). PostgreSQL functions returning refcursor need autoCommit=false.
Parameters can be accessed by name (getString("out")) where the driver supports it.
⚠ Follow-up traps
Do I need registerOutParameter for INOUT params? Yes, and also bind the IN side.
Is execute() or executeQuery() right for procedures? Usually execute(), since the shape of results is not fixed.
#callablestatement#stored-procedure
Q9
What are the ResultSet types and concurrency modes?
intermediate
Type: TYPE_FORWARD_ONLY (default, cursor moves forward), TYPE_SCROLL_INSENSITIVE (scrollable, snapshot), TYPE_SCROLL_SENSITIVE (scrollable and reflects changes). Concurrency: CONCUR_READ_ONLY (default) or CONCUR_UPDATABLE.
Scrollable result sets often make the driver buffer the whole result client-side, costing memory.
Holdability: HOLD_CURSORS_OVER_COMMIT vs CLOSE_CURSORS_AT_COMMIT; the default is driver-specific.
Many drivers silently downgrade a requested type they do not support; check rs.getType().
⚠ Follow-up traps
Is TYPE_SCROLL_SENSITIVE guaranteed to see other sessions' changes? No; support is driver dependent and often downgraded.
Why is forward-only recommended? It is cheapest and is the only mode that streams well.
#resultset#scrollable
Q10
How do you read nullable columns correctly from a ResultSet?
basic
Primitive getters (getInt, getLong, getBoolean) return 0/false for SQL NULL, so call rs.wasNull() right after, or use getObject(col, Integer.class) which returns null.
Integer age = rs.getObject("age", Integer.class); // null-safelong id = rs.getLong("id");if (rs.wasNull()) { /* id was NULL */ }
getString returns null for NULL; getBigDecimal, getDate, getTimestamp also.
Column indexes start at 1, not 0.
⚠ Follow-up traps
Does getInt throw on NULL? No, it returns 0, which is the bug.
What does wasNull() refer to? The last column read only.
#resultset#null
Q11
How do batch updates work and what does executeBatch return?
intermediate
addBatch() queues parameter sets client-side; executeBatch() sends them together, cutting round trips. It returns an int[] of update counts per statement, where Statement.SUCCESS_NO_INFO (-2) means success with unknown count.
try (PreparedStatement ps = c.prepareStatement("insert into t(a, b) values (?, ?)")) { for (Row r : rows) { ps.setInt(1, r.a()); ps.setString(2, r.b()); ps.addBatch(); } int[] counts = ps.executeBatch();}
Flush in chunks (e.g. 500-5000) to bound memory.
On failure a BatchUpdateException is thrown; getUpdateCounts() shows what completed. Behaviour (stop vs continue) is driver-dependent.
Java 8+ offers executeLargeBatch() for counts above int.
⚠ Follow-up traps
Does the batch run atomically? Only if you wrap it in a transaction and roll back on failure.
Can a batch hold SELECTs? No; batches are for DML/DDL returning update counts.
#batch#performance
Q12
Why can batch inserts still be slow, and which driver flags help?
advanced
Drivers may still send each statement as a separate round trip. MySQL Connector/J needs rewriteBatchedStatements=true to rewrite into multi-row INSERT ... VALUES (...),(...); PostgreSQL JDBC needs reWriteBatchedInserts=true to do the same.
Oracle batches natively via the array interface; SQL Server uses useBulkCopyForBatchInsert for bulk copy.
With MySQL rewriting, getGeneratedKeys behaviour and update counts (SUCCESS_NO_INFO) change.
Is addBatch alone enough for speed on MySQL? No, without rewriteBatchedStatements it is still one statement per round trip.
Why did Hibernate not batch my inserts? Likely GenerationType.IDENTITY, or ordering of mixed entity inserts.
#batch#mysql#postgresql
Q13
What is auto-commit and why does it matter?
basic
By default a JDBC connection is in auto-commit mode: each statement is its own transaction, committed upon completion. To group statements atomically, call setAutoCommit(false) and then commit() or rollback().
Turning auto-commit off starts a transaction implicitly on first statement.
Calling commit() while auto-commit is on throws SQLException in most drivers.
Pools reset auto-commit when the connection is returned (HikariCP does).
⚠ Follow-up traps
What happens if you close a connection with pending changes and no commit? Driver dependent; Oracle commits, most others roll back. Always be explicit.
Does enabling auto-commit mid-transaction commit? Yes, per the JDBC spec setAutoCommit(true) commits the current transaction.
#transactions#autocommit
Q14
Show the standard commit/rollback pattern.
basic
Disable auto-commit, run work, commit at the end, and roll back in a catch before rethrowing. Restore auto-commit if you own the connection.
Rollback itself can throw (connection lost); add the original as primary and use addSuppressed.
In Spring, @Transactional does this for you via DataSourceTransactionManager.
⚠ Follow-up traps
Is rollback needed if the connection is closed anyway? Pooled connections are not closed, so uncommitted work could leak to the next borrower unless the pool rolls back.
Do DDL statements roll back? In MySQL and Oracle DDL implicitly commits; PostgreSQL DDL is transactional.
#transactions#rollback
Q15
What are savepoints and when are they useful?
intermediate
A Savepoint marks a point inside a transaction to which you can roll back partially without aborting the entire transaction. Create with c.setSavepoint("name"), roll back with c.rollback(sp), free with c.releaseSavepoint(sp).
Savepoint sp = c.setSavepoint();try { insertOptionalAudit(c);} catch (SQLException e) { c.rollback(sp); // keep the rest of the transaction}c.commit();
In PostgreSQL a failed statement aborts the whole transaction ("current transaction is aborted") unless you used a savepoint; the driver option autosave=conservative automates this.
Spring's PROPAGATION_NESTED is built on savepoints.
⚠ Follow-up traps
Does rolling back to a savepoint end the transaction? No, work before the savepoint stays pending.
Are savepoints supported with auto-commit on? No.
#savepoint#transactions
Q16
Explain ACID in the JDBC context.
basic
Atomicity: all or nothing (commit/rollback). Consistency: constraints hold before and after. Isolation: concurrent transactions do not see each other's partial work, to a degree set by the isolation level. Durability: committed data survives crashes (WAL/redo log flushed).
JDBC only exposes controls (commit, rollback, isolation); the guarantees are implemented by the database.
Durability depends on DB settings like synchronous_commit or innodb_flush_log_at_trx_commit.
⚠ Follow-up traps
Does JDBC guarantee atomicity across two databases? No, that needs XA/two-phase commit or saga patterns.
Is "consistency" in ACID the same as in CAP? No, different meanings.
#acid#transactions
Q17
List the JDBC isolation levels and which anomalies each prevents.
intermediate
TRANSACTION_READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE (and NONE, rarely supported). Set via c.setTransactionIsolation(level) before the transaction starts.
Level
Dirty read
Non-repeatable read
Phantom
READ_UNCOMMITTED
possible
possible
possible
READ_COMMITTED
prevented
possible
possible
REPEATABLE_READ
prevented
prevented
possible (by the standard)
SERIALIZABLE
prevented
prevented
prevented
The table is the ANSI definition; real engines differ (snapshot isolation does not suffer phantoms in MySQL InnoDB for plain reads, and has write skew in PostgreSQL REPEATABLE READ).
⚠ Follow-up traps
Does PostgreSQL honor READ_UNCOMMITTED? It treats it as READ COMMITTED; dirty reads never occur.
Can you change isolation mid-transaction? Generally not safely; driver/DB specific.
#isolation#anomalies
Q18
What are the default isolation levels of common databases?
intermediate
PostgreSQL, Oracle and SQL Server default to READ COMMITTED; MySQL InnoDB defaults to REPEATABLE READ. H2 uses READ COMMITTED.
MySQL InnoDB REPEATABLE READ uses MVCC snapshots for plain reads and gap/next-key locks for locking reads.
Oracle only offers READ COMMITTED, SERIALIZABLE and read-only.
Moving code between MySQL and PostgreSQL can change behaviour of the same read-then-write logic.
⚠ Follow-up traps
Why can the same code behave differently across MySQL and Postgres? Different default isolation and different locking implementations.
Does the JDBC driver set isolation for you? No, it uses the database default unless configured (pool transactionIsolation).
#isolation#defaults
Q19
What does Connection.setReadOnly(true) do?
intermediate
It is a hint to the driver/database that the transaction will not modify data. The effect varies: PostgreSQL issues SET SESSION CHARACTERISTICS ... READ ONLY so writes fail; MySQL Connector/J can route to replicas in replication mode; Oracle starts a read-only transaction.
Must generally be called before the transaction begins.
Spring's @Transactional(readOnly = true) calls it, and Hibernate also skips dirty checking.
Pools reset it on return (HikariCP does).
⚠ Follow-up traps
Is it enforced everywhere? No, it is a hint for some drivers.
Does it make queries faster? It can enable optimizations or replica routing, but not guaranteed.
#readonly#connection
Q20
Why use connection pooling?
basic
Opening a physical connection costs TCP + TLS + authentication + session setup (often 5-100 ms) and the database can only hold a limited number of connections. A pool keeps a bounded set of connections open and lends them out.
Bounds concurrency to the DB, giving back-pressure instead of overload.
close() on a pooled connection is a proxy method that returns it to the pool and resets state.
HikariCP recommends a fixed-size pool (do not set minimumIdle lower unless needed).
Setting connectionTimeout to a few seconds fails fast under overload.
⚠ Follow-up traps
What is the Hikari default pool size? 10.
Is idleTimeout honored with a fixed-size pool? No; it only retires connections above minimumIdle.
#hikaricp#configuration
Q22
How do you size a connection pool?
advanced
Start small. A common formula from the PostgreSQL wiki and Hikari docs is connections = (core_count * 2) + effective_spindle_count, applied to the database server, not the app. Then measure and tune against throughput and wait time.
A 4-core DB serving 20 pool connections can already be oversubscribed; extra connections queue inside the DB.
Total across instances matters: pods * maximumPoolSize must stay below the DB max_connections minus admin/headroom.
Little's Law: needed connections ~ requests/sec x average connection-hold time.
Reduce hold time (short transactions, no remote calls while holding a connection) before increasing size.
⚠ Follow-up traps
Is "one connection per request thread" right? No; 200 threads can share 10-20 connections if the hold time is short.
Do I size per app instance or cluster-wide? Cluster-wide against the DB capacity.
#hikaricp#sizing
Q23
What do maxLifetime, idleTimeout and keepaliveTime do?
intermediate
maxLifetime retires a connection after a fixed age (with random variance), so it is replaced before server or firewall kills it. idleTimeout shrinks idle connections above the minimum. keepaliveTime pings idle connections periodically so network devices do not drop them.
Set maxLifetime several seconds to minutes below the database/proxy limit (wait_timeout in MySQL, load balancer idle timeout, RDS Proxy limits).
keepaliveTime must be less than maxLifetime (minimum 30 s).
Retiring is done only for connections not in use; an in-use connection is evicted on return.
⚠ Follow-up traps
If a connection is borrowed, does maxLifetime kill it mid-query? No, it is retired after being returned.
Why should lifetime be shorter than the server timeout? Otherwise the server closes it first and the pool hands out a dead connection.
#hikaricp#lifetime#keepalive
Q24
What do connectionTimeout and validationTimeout mean in HikariCP?
intermediate
connectionTimeout is how long getConnection() waits for a connection from the pool before throwing SQLTransientConnectionException. validationTimeout is how long a liveness check of a borrowed connection may take (must be less than connectionTimeout).
Hikari validates with Connection.isValid() (JDBC4) if the driver supports it, so connectionTestQuery is rarely needed.
It skips validation if the connection was used in the last 500 ms.
Other timeouts are separate: socket/read timeout, statement queryTimeout, and DB-side statement_timeout/lock_timeout.
⚠ Follow-up traps
Is connectionTimeout the time to run a query? No, only the wait for a pool slot (or to establish a new connection).
Should connectionTimeout be 30 s in a web service? Usually too long; it causes request pile-up. Choose below your request timeout.
#hikaricp#timeouts
Q25
How does HikariCP leak detection work?
intermediate
With leakDetectionThreshold > 0 (minimum 2000 ms), Hikari starts a timer when a connection is borrowed and logs a warning with the borrowing stack trace if it has not been returned in time. It does not forcibly close or reclaim the connection.
Set the threshold above your slowest legitimate transaction.
A late return after warning logs "Previously reported leaked connection ... was returned to the pool".
The stack trace points to where the connection was acquired, not where it is stuck.
⚠ Follow-up traps
Does leak detection fix leaks? No, it only reports.
Is a leak warning always a leak? No; a slow long transaction triggers it too.
#hikaricp#leak
Q26
What is the relationship between pool size, DB max_connections and a connection proxy?
advanced
Each pool connection is one DB session (a backend process in PostgreSQL, a thread in MySQL). The sum over all app instances must stay below max_connections. An external pooler (PgBouncer, ProxySQL, RDS Proxy) multiplexes many client connections onto fewer server connections.
PgBouncer transaction mode breaks session features: session-level SET, advisory locks, LISTEN, and server-side prepared statements (use prepareThreshold=0 or a recent PgBouncer with prepared-statement support).
Autoscaling apps can multiply connections unexpectedly; cap pool sizes per instance.
Idle connections still consume memory on the server (PostgreSQL ~ several MB each).
⚠ Follow-up traps
Does a pooler remove the need for an app-side pool? No, the app-side pool still avoids connect overhead and bounds concurrency.
Why do prepared statements fail behind PgBouncer transaction mode? Statements prepared on one server connection are not present on the next one.
#pooling#pgbouncer#max-connections
Q27
What does fetch size do?
intermediate
setFetchSize(n) hints how many rows the driver should retrieve per network round trip. Too small means many round trips; too large means high memory.
Oracle default is 10 (increase to 100-1000 for bulk reads).
PostgreSQL fetches all rows at once by default; fetch size only applies when autoCommit=false and the ResultSet is forward-only.
MySQL Connector/J loads the whole result unless useCursorFetch=true with fetch size set, or fetch size is Integer.MIN_VALUE (row-by-row streaming).
SQL Server buffers adaptively by default.
⚠ Follow-up traps
Why does setFetchSize(1000) not stream on PostgreSQL in my code? Auto-commit is on, so the driver loads everything.
Does fetch size limit rows returned? No, that is setMaxRows or LIMIT.
#fetchsize#performance
Q28
How do you stream a large result set without running out of memory?
advanced
Use a forward-only, read-only ResultSet, set a fetch size, and use the driver's cursor/streaming mode; process rows as you read and never collect them all.
MySQL: setFetchSize(Integer.MIN_VALUE) or useCursorFetch=true + fetch size.
The connection and often a transaction stay open for the entire read, so keep processing quick and do not run other queries on the same MySQL connection while streaming.
For huge exports, consider keyset pagination or COPY (PostgreSQL CopyManager).
⚠ Follow-up traps
Is the memory of a streamed query constant? Roughly one fetch batch, but the server may still hold a cursor/temp results.
Is LIMIT/OFFSET pagination a streaming replacement? It gets slower with a deep offset; keyset pagination is better.
#streaming#fetchsize#memory
Q29
How do you retrieve generated keys?
basic
Pass Statement.RETURN_GENERATED_KEYS (or column names) when preparing, then call getGeneratedKeys() after executing.
try (PreparedStatement ps = c.prepareStatement( "insert into orders(customer_id) values (?)", Statement.RETURN_GENERATED_KEYS)) { ps.setLong(1, 42L); ps.executeUpdate(); try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { long id = keys.getLong(1); } }}
Oracle requires column names (new String[]{"ID"}) because ROWID is returned otherwise.
PostgreSQL implements this by appending RETURNING * (or the listed columns).
Works with batches on most drivers (one key row per statement).
⚠ Follow-up traps
Is there a portable way to get the key in a separate query?SELECT LAST_INSERT_ID() or currval are vendor-specific and unsafe across connections; use the API.
What if several columns are generated?getGeneratedKeys() returns them all by name or position, driver permitting.
#generated-keys#insert
Q30
What is a DataSource and why prefer it over DriverManager?
basic
javax.sql.DataSource is a factory for connections, configured externally (URL, credentials, pool settings) and can be pooled, wrapped or looked up through JNDI. DriverManager is a static, unpooled, global registry.
Pooling, distributed transactions (XADataSource) and monitoring implementations all plug in via DataSource.
Application code receives a DataSource (injection), so credentials and URLs stay in config.
Spring Boot auto-configures a HikariDataSource from spring.datasource.*.
⚠ Follow-up traps
Is DataSource itself a pool? Not necessarily; the interface says nothing about pooling, HikariDataSource implements it with a pool.
Where does JNDI fit? Servers like Tomcat/WildFly publish a managed DataSource under a JNDI name.
#datasource#drivermanager
Q31
What can DatabaseMetaData and ResultSetMetaData tell you?
DatabaseMetaData md = c.getMetaData();try (ResultSet t = md.getTables(null, "public", "%", new String[]{"TABLE"})) { while (t.next()) { System.out.println(t.getString("TABLE_NAME")); }}
Used by schema migration tools, generic query UIs and ORMs for dialect detection.
Metadata calls can be slow on big catalogs; cache results.
⚠ Follow-up traps
getColumnName vs getColumnLabel? Label is the alias (select a as x), name is the underlying column; prefer label.
Is the schema/catalog meaning the same across databases? No: MySQL uses catalog = database, PostgreSQL uses schema.
#metadata
Q32
How do you work with BLOB and CLOB values?
intermediate
Use streaming setters/getters (setBinaryStream, getBinaryStream, setCharacterStream, getCharacterStream) or Blob/Clob locators (createBlob(), createClob()), instead of loading whole values as byte[]/String.
try (InputStream in = Files.newInputStream(path); PreparedStatement ps = c.prepareStatement("insert into docs(name, body) values (?, ?)")) { ps.setString(1, "report.pdf"); ps.setBinaryStream(2, in, Files.size(path)); ps.executeUpdate();}
Read streams while the ResultSet and connection are still open and the transaction is active (Oracle and PostgreSQL large objects require it).
PostgreSQL has bytea (inline, up to 1 GB) and Large Objects (oid, needs a transaction).
Often better: store files in object storage and keep a URL/key in the row.
⚠ Follow-up traps
Can I read a Blob after closing the connection? Generally no; the locator becomes invalid.
Does getBytes suit multi-hundred-MB files? No; it loads everything into heap.
#lob#blob#clob
Q33
What is the N+1 problem at the JDBC level and how do you fix it?
intermediate
Running one query to fetch N parents and then one query per parent for its children produces N+1 round trips. Fix with a join, a single IN/ANY query for all children, or a batch.
// 1 query for all children instead of NString sql = "select order_id, sku, qty from order_items where order_id = any (?)";Array ids = c.createArrayOf("bigint", orderIds.toArray());try (PreparedStatement ps = c.prepareStatement(sql)) { ps.setArray(1, ids); // group rows by order_id into a Map<Long, List<Item>>}
Latency per round trip (0.5-2 ms same AZ) multiplies by N.
A join duplicates parent columns per child row; a two-query approach avoids that and pagination of parents stays simple.
Chunk large IN lists (e.g. 500-1000).
⚠ Follow-up traps
Is a JOIN always better? No; joins on to-many produce row multiplication and make pagination awkward.
How do you spot N+1 without an ORM? Count statements per request using datasource-proxy or p6spy, or look at DB pg_stat_statements.
#n+1#performance
Q34
What is the difference between a deadlock and a lock wait timeout?
intermediate
A deadlock is a cycle: transaction A waits for B while B waits for A; the database detects it and aborts one victim immediately. A lock wait is a plain wait for a lock held by a still-running transaction (no cycle), which ends when it commits or the wait times out.
PostgreSQL: SQLState 40P01 for deadlock, 55P03 for lock_not_available, 57014 for statement timeout.
Oracle: ORA-00060 deadlock; ORA-30006/ORA-00054 for waits with NOWAIT.
Avoid deadlocks by acquiring locks in a consistent order and keeping transactions short.
⚠ Follow-up traps
Is a deadlock a bug to eliminate completely? Not always; rare ones are normal, so code must be able to retry.
Does MySQL roll back the entire transaction on lock wait timeout? No, by default only the statement (InnoDB), unless innodb_rollback_on_timeout is on. Deadlock rolls back the whole transaction.
#deadlock#locking
Q35
How do you set lock timeouts and statement timeouts?
intermediate
JDBC provides Statement.setQueryTimeout(seconds), which the driver implements by cancelling the query (often via a timer thread and a second connection). Databases have their own settings: PostgreSQL lock_timeout, statement_timeout; MySQL innodb_lock_wait_timeout, max_execution_time; Oracle DDL_LOCK_TIMEOUT, resource limits.
Network socketTimeout/oracle.net.READ_TIMEOUT is a last resort for hung connections; it should exceed the longest legitimate query.
Set DB-side timeouts via connection init SQL or URL: jdbc:postgresql://h/db?options=-c%20statement_timeout=5000.
A timeout produces SQLTimeoutException (or a vendor SQLState such as 57014).
⚠ Follow-up traps
Does setQueryTimeout always stop the server-side work? Usually it sends cancel, but a blocked network or unresponsive server may not respond promptly.
Is setQueryTimeout(0) unlimited? Yes, it means no timeout.
#timeouts#locking
Q36
How do SELECT FOR UPDATE, NOWAIT and SKIP LOCKED work?
intermediate
SELECT ... FOR UPDATE locks the selected rows until the transaction ends. NOWAIT fails immediately if a row is locked; SKIP LOCKED ignores locked rows, which makes it a good fit for work queues. Available in PostgreSQL 9.5+, MySQL 8.0+, Oracle.
select id, payload from jobswhere status = 'NEW'order by idlimit 10for update skip locked;
Needs autoCommit=false; the lock is released at commit/rollback, so keep the transaction short.
Multiple workers can poll the same table without blocking each other.
⚠ Follow-up traps
Does auto-commit hold the lock after the statement? No; it commits immediately, releasing the lock.
Is SKIP LOCKED consistent for reporting? No; it deliberately returns an inconsistent subset.
#locking#for-update#skip-locked
Q37
How does optimistic locking work with plain JDBC?
intermediate
Add a version column; read it with the row, then update with WHERE id = ? AND version = ? and SET version = version + 1. If executeUpdate() returns 0, someone else changed the row first: reload and retry, or fail.
String sql = "update account set balance = ?, version = version + 1 where id = ? and version = ?";// ps.executeUpdate() == 0 -> concurrent modification
No row locks are held during user think time, so it scales well with low contention.
Pessimistic locking (FOR UPDATE) is better when conflicts are frequent.
⚠ Follow-up traps
What does executeUpdate returning 0 mean here? Zero rows matched: version changed or the row was deleted.
Does MySQL report matched or changed rows? Connector/J reports matched ("found") rows by default; with useAffectedRows=true it reports only rows actually changed, so an update that sets identical values returns 0.
#optimistic-locking#concurrency
Q38
How should you classify SQLExceptions for retries?
advanced
Use the JDBC 4 hierarchy: SQLTransientException subclasses (SQLTransientConnectionException, SQLTimeoutException, SQLTransactionRollbackException) may succeed on retry; SQLNonTransientException (syntax error, SQLIntegrityConstraintViolationException) will not; SQLRecoverableException needs a new connection. Use getSQLState() (portable, 5 chars) and getErrorCode() (vendor) for finer detail.
Check getNextException()/getCause() and the chain; batch errors nest.
Drivers do not all populate the subclass; SQLState is more reliable.
⚠ Follow-up traps
Should I retry on unique violation? No, it is deterministic (unless you are doing an upsert-race fix-up read).
What is getSQLState for? A standard code; first two chars indicate the class.
#sqlexception#retry#sqlstate
Q39
How do you make database retries safe (idempotency)?
advanced
Retry the entire transaction on a fresh connection, only for transient errors, with bounded attempts and jittered backoff. Make the operation idempotent so a retry after an unknown outcome (timeout during commit) does not duplicate effects.
Use an idempotency key with a unique constraint: insert ... on conflict do nothing (PostgreSQL) / insert ignore / merge.
Prefer set-based updates (set status='PAID' where id=? and status='NEW') over increments computed in app code.
If commit() throws a connection error, the outcome is unknown: check state before retrying.
Do not retry inside an outer transaction; retry at the outermost boundary.
⚠ Follow-up traps
Is retrying a failed INSERT safe? Only with a natural or idempotency key; otherwise you may duplicate.
Why retry the whole transaction rather than the statement? The failed transaction has been rolled back (or is aborted), so earlier statements are lost.
#retry#idempotency
Q40
How are Java date/time types mapped in JDBC?
intermediate
Since JDBC 4.2 (Java 8) use setObject/getObject with java.time types: LocalDate <-> DATE, LocalTime <-> TIME, LocalDateTime <-> TIMESTAMP, OffsetDateTime <-> TIMESTAMP WITH TIME ZONE. The legacy java.sql.Date/Time/Timestamp are mutable and timezone-sensitive.
ps.setObject(1, LocalDate.of(2026, 1, 31));LocalDateTime t = rs.getObject("created_at", LocalDateTime.class);OffsetDateTime o = rs.getObject("paid_at", OffsetDateTime.class);
Instant is not required to be supported by JDBC; use OffsetDateTime at UTC (PostgreSQL rejects setObject(Instant)).
Store instants as UTC timestamptz/TIMESTAMP and convert at the edges.
java.sql.Timestamp.toLocalDateTime() uses the JVM default zone.
⚠ Follow-up traps
TIMESTAMP vs TIMESTAMP WITH TIME ZONE in PostgreSQL?timestamptz stores an absolute instant (normalized to UTC); timestamp has no zone.
Does MySQL TIMESTAMP store the zone? No, it converts to UTC using the session time zone; DATETIME stores it as is.
#datetime#java-time#jdbc42
Q41
What goes wrong with time zones between the JVM, driver and database?
advanced
Legacy Timestamp/Calendar mapping interprets values in the JVM default zone, and the driver may also apply a session or server zone, so the same value can shift by hours when the JVM zone differs from the DB's, or at DST boundaries.
Run JVMs in UTC (-Duser.timezone=UTC) and keep timestamptz/UTC columns.
MySQL Connector/J 8: connectionTimeZone, forceConnectionTimeZoneToSession (older: serverTimezone, useLegacyDatetimeCode).
PostgreSQL JDBC sets session TimeZone to the JVM zone.
Use LocalDateTime only for wall-clock values without meaning of instant (e.g. store opening time).
⚠ Follow-up traps
Why does a date appear one day earlier in the JSON? A java.sql.Date (midnight local) converted across zones; use LocalDate.
Is it safe to use LocalDateTime for event times? No, the instant is ambiguous across zones and during DST overlaps.
#datetime#timezone
Q42
How do you bind NULL values and typed parameters?
basic
Use setNull(index, Types.X) for explicit NULL, or setObject(index, value, Types.X). setString(i, null) also works for most drivers, but a typeless setObject(i, null) can fail (PostgreSQL, Oracle) because the driver cannot infer the type.
Use setObject(i, x, Types.TIMESTAMP_WITH_TIMEZONE) to be explicit with java.time.
Comparing to NULL with = ? never matches; use IS NULL or IS NOT DISTINCT FROM (PostgreSQL).
Primitive wrappers require a null check before setInt(i, wrapper) (NPE on unboxing).
⚠ Follow-up traps
Does where col = ? with NULL return rows with NULL? No, = NULL is unknown.
Do the setter indexes start at 0? No, they start at 1.
#preparedstatement#null#types
Q43
How do you handle variable-length IN lists?
intermediate
Generate one ? per element (IN (?,?,?)) and bind each, or use an array parameter (PostgreSQL = ANY(?) with createArrayOf). Cap the list size and chunk beyond it, because of driver/DB limits (Oracle 1000 expressions, PostgreSQL/SQL Server ~32k/2100 bind parameters).
String marks = String.join(",", Collections.nCopies(ids.size(), "?"));String sql = "select id, name from users where id in (" + marks + ")";
An empty list yields invalid SQL IN (): short-circuit and return empty.
Varying SQL text per list size pollutes the statement cache; pad to power-of-two sizes if needed.
⚠ Follow-up traps
Can setString(1, "1,2,3") fill an IN list? No, it is one string value.
Is IN with 5000 values fine? Usually inefficient or over limits; use a temp table, join or chunking.
#in-clause#preparedstatement
Q44
What is the difference between execute, executeQuery and executeUpdate?
basic
executeQuery returns a ResultSet and is for SELECT; executeUpdate returns the affected row count (int) for INSERT/UPDATE/DELETE/DDL; execute returns boolean (true if first result is a ResultSet) for unknown or multiple results.
Calling executeQuery on DML throws SQLException (and vice versa on most drivers).
With execute, iterate: getResultSet(), getUpdateCount(), getMoreResults().
executeLargeUpdate returns long.
⚠ Follow-up traps
What does executeUpdate return for DDL? 0.
What does executeQuery return for a query with no rows? An empty ResultSet, never null.
#statement#execute
Q45
How do queryTimeout, socket timeout and pool timeouts interact?
advanced
Layered timeouts: pool connectionTimeout (wait for a connection) -> queryTimeout (per statement cancel) -> lock/statement timeout on the server -> socketTimeout (no bytes from server) -> transaction-level timeouts (Spring @Transactional(timeout=)). Each protects against a different failure.
Order them so inner ones fire first: queryTimeout < socketTimeout; request deadline > pool wait + query time.
Without a socket timeout, a silently dropped TCP connection can hang a thread for the OS timeout (minutes).
Spring's transaction timeout only applies to the next statement execution; it does not interrupt running SQL.
⚠ Follow-up traps
If the socket times out, is the connection reusable? No; the driver should mark it broken and the pool evicts it.
Does @Transactional(timeout=5) kill a 30 s query? No, it only sets the query timeout for later statements.
#timeouts#hikaricp
Q46
How do distributed (XA) transactions work in JDBC?
advanced
An XADataSource yields XAConnections whose XAResource participates in two-phase commit (prepare, then commit/rollback) coordinated by a transaction manager (Atomikos, Narayana, JTA in app servers) across several resources.
Costs: extra log writes and locks held longer; in-doubt transactions need recovery.
Many microservice designs prefer sagas, outbox and idempotent consumers instead.
Not every pool supports XA; HikariCP does not.
⚠ Follow-up traps
Does setAutoCommit(false) on two connections make a transaction across both? No, commits are independent and one may fail after the other succeeded.
Is XA free of failure windows? No; crashes between prepare and commit leave in-doubt branches.
#xa#distributed-transactions
Q47
What does Spring do around JDBC (JdbcTemplate and transaction binding)?
intermediate
JdbcTemplate handles acquisition/release of connections, statements and exceptions, translating SQLException into the unchecked DataAccessException hierarchy. DataSourceTransactionManager binds a connection to the current thread (TransactionSynchronizationManager) so all JdbcTemplate calls in the transaction reuse it.
Without a transaction, each JdbcTemplate call borrows and returns its own connection (auto-commit).
DataSourceUtils.getConnection(ds) participates in the thread-bound connection; ds.getConnection() directly does not.
Wrapping with TransactionAwareDataSourceProxy makes legacy code transaction aware.
⚠ Follow-up traps
Two JdbcTemplate calls in a method without @Transactional - one connection? Not guaranteed; usually two separate borrows.
What happens when bypassing DataSourceUtils inside a transaction? You get a second connection, outside the transaction.
#spring#jdbctemplate#transactions
Q48
Which Java types map to which SQL numeric types, and why is BigDecimal used for money?
basic
SMALLINT/INTEGER/BIGINT -> short/int/long; REAL/DOUBLE -> float/double; NUMERIC/DECIMAL -> BigDecimal. Money must use NUMERIC/BigDecimal since binary floating point cannot represent decimals like 0.1 exactly.
Use rs.getBigDecimal("amount") and ps.setBigDecimal.
Watch scale: NUMERIC(10,2) rounds on insert; Oracle NUMBER without scale may produce large or odd scales.
Do not do new BigDecimal(0.1); use BigDecimal.valueOf(0.1) or a string.
Unsigned BIGINT in MySQL can overflow long: it maps to BigInteger.
⚠ Follow-up traps
Why not double for amounts? Rounding errors accumulate.
Does getInt on a BIGINT column fail? It throws or truncates when out of range, depending on the driver.
#types#bigdecimal
Q49
How does keyset (seek) pagination differ from OFFSET at the JDBC level?
intermediate
OFFSET n LIMIT m makes the database walk and discard n rows each page, so deep pages get slow and unstable under concurrent inserts. Keyset pagination remembers the last seen key and queries WHERE (created_at, id) > (?, ?) ORDER BY created_at, id LIMIT m, using an index seek.
Needs a deterministic, unique sort key (add id as tie-breaker) and a matching composite index.
Cannot jump to arbitrary page numbers; suits infinite scroll, exports and batch jobs.
Row-value comparison works in PostgreSQL and MySQL 8; elsewhere expand to a > ? OR (a = ? AND id > ?).
⚠ Follow-up traps
Why include id in the order by? Ties on the timestamp would skip or repeat rows.
Is total count cheap with keyset? No, count(*) is a separate (often expensive) query.
#pagination#performance
Q50
How does the ResultSet interact with the Statement that created it?
intermediate
Each Statement has at most one current ResultSet (per result of execute). Re-executing the statement or closing it closes the previous ResultSet; reading afterwards throws SQLException: ResultSet closed (or "Operation not allowed after ResultSet closed").
Use separate PreparedStatements when interleaving nested loops over two result sets.
MySQL streaming forbids another statement on the same connection until the result is fully read or closed.
Closing the Statement before consuming rows is a common bug when returning a ResultSet from a method.
⚠ Follow-up traps
Can you return a ResultSet from a try-with-resources method? No, it is closed on exit; map to objects inside.
Is ResultSet thread-safe? No; it is bound to a connection and not to be shared.
#resultset#statement
Q51
What is a RowSet and when would you use CachedRowSet?
advanced
javax.sql.rowset.RowSet extends ResultSet with JavaBean properties and events. CachedRowSet is a disconnected, serializable, scrollable copy of data that can be modified offline and synced back with acceptChanges(), which uses optimistic conflict detection.
Created with RowSetProvider.newFactory().createCachedRowSet().
Holds all rows in memory, so unsuitable for large results.
Rarely used in modern services; DTO mapping with JdbcTemplate/jOOQ/ORM is the norm.
⚠ Follow-up traps
Does a CachedRowSet keep the connection open? No, only briefly while populating and syncing.
Is a RowSet memory efficient? No; it caches everything.
#rowset#disconnected
Q52
What are prepared statement caches and how can they cause trouble?
advanced
Drivers (PostgreSQL preparedStatementCacheQueries, Connector/J cachePrepStmts, Oracle implicit cache) cache prepared statement handles per connection, so repeated SQL skips the parse step. Pools like Hikari deliberately do not cache statements, leaving that to the driver.
Schema changes can invalidate cached plans: PostgreSQL raises cached plan must not change result type after ALTER TABLE changing columns (the driver retries in autocommit only).
PostgreSQL generic-plan switching after 5 executions can pick a bad plan for skewed data (plan_cache_mode).
Memory: each connection caches separately, so pool size multiplies cache size.
⚠ Follow-up traps
Why does an ALTER TABLE break a running service? Cached plans with the old row shape raise errors until connections reset.
Does HikariCP cache statements? No.
#statement-cache#preparedstatement
Scenarios
Q53
Production logs show `Connection is not available, request timed out after 30000ms`. How do you debug it?
intermediate
The pool had no free connection for connectionTimeout. Either connections are leaked, held too long, or demand exceeds pool capacity. Determine which before touching the pool size.
Enable Hikari metrics (hikaricp_connections_active/pending/usage_seconds) and log leakDetectionThreshold.
Take thread dumps: threads parked in HikariPool.getConnection are waiters; threads holding connections show where they are stuck (HTTP call, lock wait, slow query).
On the DB (pg_stat_activity, SHOW PROCESSLIST) look for idle in transaction sessions or long queries from this app.
Active == max with idle-in-transaction sessions means leaked or long-held connections; active near max with real queries means DB is slow or pool is too small.
⚠ Follow-up traps
Is raising maximumPoolSize the fix? Often it just moves the bottleneck to the database; find the cause first.
What does idle in transaction imply? The app opened a transaction and is not sending statements (leak, remote call inside transaction).
#pool-exhausted#hikaricp#debugging
Q54
A method forgets to close a Connection on an error path. What are the symptoms and the fix?
basic
Each failing call permanently removes one connection from the pool; after maximumPoolSize failures every caller times out waiting. The service works until the error rate accumulates, then hangs entirely.
// buggyConnection c = ds.getConnection();PreparedStatement ps = c.prepareStatement(sql);ps.executeUpdate(); // throws -> connection never closedc.close();
Fix with try-with-resources so close runs on every path.
Leak detection logs the borrowing stack trace, pointing at the method.
⚠ Follow-up traps
Does GC reclaim an abandoned pooled connection? No, the pool still holds a strong reference to it.
Does the problem go away on restart? Temporarily; it returns whenever the failure path is hit.
#leak#try-with-resources
Q55
A service holds a connection while calling a remote HTTP API inside a transaction. What happens under load?
intermediate
Connection hold time becomes database time plus network latency of the remote call. With 20 connections and a 500 ms remote call, throughput caps near 40 requests/second, and a slow dependency exhausts the pool, taking down unrelated endpoints.
Do the remote call before opening the transaction or after commit.
If atomicity with a message is required, use the outbox pattern: write the event in the same DB transaction and publish asynchronously.
Spring Boot's spring.jpa.open-in-view has a similar effect by holding a connection through view rendering.
⚠ Follow-up traps
Will a bigger pool fix it? It postpones saturation and shifts load to the DB.
Does @Transactional hold the connection from method start? With Spring DataSourceTransactionManager yes, from the transaction begin, though lazy-acquisition proxies can delay it.
#transactions#pool-exhausted#design
Q56
Code opens a second connection inside a transaction that already holds one. What can go wrong?
advanced
Under load every thread holds one connection and waits for a second, so when all connections are taken, no thread can progress: a pool-level deadlock until connectionTimeout expires. The second connection is also a different transaction: it cannot see uncommitted rows of the first and may block on its locks.
Pass the same Connection (or use Spring-managed access) to participate in one transaction.
If a separate transaction is intentional (audit log), keep that pool separate or use REQUIRES_NEW knowingly with enough pool headroom (pool >= threads x 2).
⚠ Follow-up traps
Why is it fine in dev and fails in production? Dev concurrency is lower than pool size, so the cycle never forms.
Can the second connection see rows inserted by the first, uncommitted? No, not at READ COMMITTED or above.
#pool-exhausted#deadlock#transactions
Q57
A query was fast in dev but takes 30 s in production. How do you investigate?
intermediate
Capture the exact SQL and parameter values, run EXPLAIN (ANALYZE, BUFFERS) (PostgreSQL) or EXPLAIN ANALYZE (MySQL 8) on production-sized data, and look for sequential scans, wrong join order, misestimated rows and lock waits.
Check data volume and statistics (ANALYZE), missing or unused indexes, implicit casts that defeat indexes (where varchar_col = ? bound as number, or setString on a numeric column).
A function on an indexed column (where lower(email) = ?) needs a functional index.
Compare plan with first parameters: PostgreSQL generic plans after 5 executions can differ from custom plans.
Look at the DB's pg_stat_statements/slow query log to see whether it is CPU, I/O or waiting on locks.
⚠ Follow-up traps
The same SQL is fast in psql but slow via JDBC. Why? Generic prepared plan, different parameter types, different fetchSize/network, or session settings.
Does adding an index always help? Not for low-selectivity predicates or small tables; it also slows writes.
#slow-query#explain#debugging
Q58
After a database failover or restart, the app throws `Connection reset` / `This connection has been closed` for a while. Why, and how do you reduce it?
intermediate
The pool still holds sockets opened to the old server. Hikari validates on borrow only if the connection was idle longer than 500 ms and detects death via isValid, but in-flight and recently-used connections fail on first use.
Set maxLifetime and keepaliveTime below infrastructure idle limits; Hikari evicts broken connections on fatal SQLExceptions.
Set socketTimeout/TCP keepalive so half-open connections fail fast.
Use DNS/cluster endpoints with short TTL (JVM networkaddress.cache.ttl), or a driver with failover support (AWS Advanced JDBC Wrapper, MariaDB failover).
Retry idempotent transactions once on SQLTransientConnectionException/SQLState 08xxx.
⚠ Follow-up traps
Does the pool know the DB failed over? Only when a connection fails or validation fails.
Is JVM DNS caching relevant? Yes; with a security manager the default TTL is infinite, otherwise 30 s.
#stale-connection#failover#hikaricp
Q59
MySQL connections die with `Communications link failure` after hours of idleness. Why?
intermediate
MySQL closes idle connections after wait_timeout (default 8 hours), and firewalls/NAT/load balancers often drop idle TCP flows even sooner (e.g. 350 s on AWS NLB, 4 min on Azure). The pool then hands out dead connections.
Set Hikari maxLifetime below the smallest of wait_timeout and network idle timeouts (e.g. 25 min vs DB 30 min).
Use keepaliveTime (e.g. 5 min) to keep flows alive.
Hikari warns when maxLifetime exceeds the server's timeout.
⚠ Follow-up traps
Should you rely on autoReconnect=true? No; it can silently lose transaction state and session variables, and is discouraged.
Is a connectionTestQuery needed? Not with JDBC 4 drivers; isValid() is used.
#stale-connection#mysql#wait_timeout
Q60
200 request threads share a pool of 10 connections. Is that a problem?
intermediate
Not necessarily. If each request holds a connection for 10 ms, 10 connections serve about 1000 requests/second. Threads wait briefly in the pool queue; this acts as useful back-pressure for the DB.
It becomes a problem when the hold time rises (slow queries, remote calls in transactions) or most requests need a connection at once.
Check hikaricp_connections_pending and acquire time; if pending is regularly above zero for long periods, investigate hold time first.
Tuning sequence: shorten transactions, add indexes, then raise the pool within DB capacity.
⚠ Follow-up traps
Should the pool match the thread count? No; that would overload the database with concurrent queries.
What is a healthy acquire wait? Low single-digit milliseconds on average.
#sizing#hikaricp#concurrency
Q61
40 pods each use `maximumPoolSize=20`. The DB `max_connections` is 500. What happens at deployment time?
advanced
40 x 20 = 800 potential connections exceeds 500; surplus attempts fail with FATAL: sorry, too many clients already (PostgreSQL) or Too many connections (MySQL). During rolling deploys old and new pods overlap, so the peak is even higher.
Cap by total_connections = pods_max * pool; shrink per-pod pool (e.g. 8), add a pooler (PgBouncer/RDS Proxy), or reduce replicas.
Reserve connections for admin, migrations and monitoring (superuser_reserved_connections).
Make Hikari minimumIdle = maximumPoolSize only if you can afford that always-open count; otherwise let it shrink.
⚠ Follow-up traps
Does the pool open all 20 connections at startup? With default minimumIdle = max, yes, it fills the pool in the background.
Does HPA autoscaling change the math? Yes, use the maximum replica count.
#sizing#max-connections#deployment
Q62
A batch of 10,000 inserts fails at row 5,000 due to a constraint violation. What happens to the earlier rows?
intermediate
It depends on the transaction. With auto-commit on, drivers may commit chunks or each statement; with autoCommit=false and a rollback on failure, none persist. BatchUpdateException.getUpdateCounts() shows which succeeded: PostgreSQL stops at the first failure, MySQL may continue or stop (continueBatchOnError), Oracle continues.
For partial success semantics, split into smaller chunks with per-chunk commits and record failed chunks.
Inspect e.getNextException() for the real cause (PostgreSQL wraps it).
⚠ Follow-up traps
Is getUpdateCounts().length always the failing index? Only on drivers that stop at the first error; others return one entry per statement.
Is the batch atomic by default? No; only the transaction makes it atomic.
#batch#transactions#error-handling
Q63
Would 1 million batch inserts in a single transaction be a good idea?
advanced
Risky: long transactions hold locks and bloat undo/WAL, fail all-or-nothing, and one error loses everything. Commit in chunks (e.g. 1000-10000 rows) and use the fastest bulk path available.
Fastest options: PostgreSQL COPY via CopyManager, MySQL LOAD DATA LOCAL INFILE, Oracle direct-path/array insert, SQL Server bulk copy.
Drop or defer secondary indexes and constraints for one-time loads when allowed.
Make the job restartable using a checkpoint/idempotent upsert.
⚠ Follow-up traps
Does a larger batch always speed things up? Gains flatten after a few thousand; memory and lock duration grow.
Is executeBatch faster than COPY? No, COPY is typically several times faster.
#batch#bulk-load#performance
Q64
After an exception is caught and logged, the code continues using the connection and later commits. What is the risk in PostgreSQL?
advanced
In PostgreSQL a failed statement marks the transaction aborted: every subsequent statement fails with current transaction is aborted, commands ignored until end of transaction block, and commit() effectively performs a rollback (no exception from the driver in some versions).
Swallowing the exception hides data loss: the code believes it committed.
Use a savepoint around statements whose failure you tolerate, or autosave=conservative in the URL.
MySQL/Oracle keep the transaction usable after a statement-level error.
⚠ Follow-up traps
Does commit() throw after an aborted transaction? The PostgreSQL JDBC driver may silently roll back instead; verify with the driver version, and never swallow errors.
Is this behaviour in MySQL too? No, a failed statement there rolls back only that statement.
#postgresql#transactions#savepoint
Q65
A pooled connection comes back with the wrong isolation level or read-only flag. Why?
advanced
Session state set on a physical connection (isolation, read-only, catalog, schema, auto-commit, session variables) persists after close() if the pool does not reset it. HikariCP tracks and resets the standard JDBC properties (autoCommit, readOnly, transactionIsolation, catalog, schema) on return, but not arbitrary SET statements or SET ROLE.
If you run SET search_path, SET TIME ZONE or temp-table creation manually, reset it in finally or use connectionInitSql for constants.
Use DISCARD ALL or pooler reset queries for PostgreSQL in poolers.
With transaction-mode poolers, session state is unreliable by design.
⚠ Follow-up traps
Does Hikari reset session variables set with raw SQL? No.
Is it OK to change isolation per transaction? Yes, if the pool restores it (Hikari does) and you set it before starting work.
#pooling#isolation#state-leak
Q66
Reading a 5 GB table with `select *` throws OutOfMemoryError. What do you do?
intermediate
The driver buffered the entire ResultSet in the heap (default for PostgreSQL with auto-commit and for MySQL). Switch to streaming/cursor mode and process row by row, or page by key.
c.setAutoCommit(false); // PostgreSQL requirementtry (PreparedStatement ps = c.prepareStatement("select id, data from big", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) { ps.setFetchSize(2000); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { handle(rs.getLong(1), rs.getString(2)); } }}
Select only needed columns; avoid collecting rows to a list.
Heap sizing is not the fix; memory use must be bounded.
⚠ Follow-up traps
Why did setFetchSize do nothing? Auto-commit on (PostgreSQL), scrollable type used, or MySQL without useCursorFetch.
Can a streaming read be combined with writes on the same connection? Not on MySQL streaming; use another connection.
#oom#fetchsize#streaming
Q67
While streaming a MySQL result set you execute another query on the same connection. What happens?
advanced
Connector/J throws Streaming result set ... is still active. No statements may be issued when any streaming result sets are open and in use on a given connection. The unread rows still occupy the protocol stream.
Use a second connection for the nested query, or read the first result set fully (cache keys) before issuing nested queries.
useCursorFetch=true with a fetch size uses server-side cursors and permits interleaving.
Closing a streaming result set before the end makes the driver read and discard all the remaining rows, which may take long.
⚠ Follow-up traps
Does rs.close() early abort the stream instantly? No; the driver drains remaining rows (or can use Statement.cancel()).
Is this issue in PostgreSQL too? No, cursors allow interleaving on the same connection.
#mysql#streaming#resultset
Q68
A search endpoint concatenates `sort` and `direction` request parameters into ORDER BY. Is it safe if the data values use `PreparedStatement`?
basic
No. Identifiers and keywords cannot be bound, so order by + sort is injectable (e.g. sort=(case when (select ...) then id else name end) leaks data via side channels). Allow-list column names and directions.
Set<String> allowed = Set.of("id", "name", "created_at");if (!allowed.contains(sort)) { throw new IllegalArgumentException("bad sort"); }String dir = "desc".equalsIgnoreCase(direction) ? "DESC" : "ASC";
Map public names to real column names, so internal schema is not exposed.
Never quote-escape as a substitute.
⚠ Follow-up traps
Would wrapping the column in quotes make it safe? No, an attacker can include quotes.
Would setString for the sort column work? It becomes a constant expression and the sort has no effect.
#sql-injection#order-by
Q69
A LIKE search uses `"%" + term + "%"` bound via `setString`. Is there still any risk?
intermediate
No injection, since it is bound as a value, but % and _ in user input act as wildcards, so a term of % matches everything and a pattern like %_%_% can be slow (DoS). Escape wildcards and declare an escape character.
String escaped = term.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");PreparedStatement ps = c.prepareStatement("select id from items where name like ? escape '\\'");ps.setString(1, "%" + escaped + "%");
A leading % prevents B-tree index use; consider trigram (pg_trgm) or full-text indexes.
The default escape character differs by database (MySQL \, PostgreSQL \, SQL Server none).
⚠ Follow-up traps
Does binding stop wildcard abuse? No, wildcards are semantic, not syntax.
Is like 'abc%' indexable? Yes, with a B-tree and suitable collation/operator class.
#like#sql-injection#performance
Q70
`where id in ()` with an empty list throws a syntax error. How do you handle it?
basic
An empty IN () is invalid SQL in most databases. Check the list first and return an empty result without querying (or substitute a predicate such as 1 = 0).
if (ids.isEmpty()) { return List.of(); }
For NOT IN with an empty list the semantics are "true for all rows": skip the predicate instead.
NOT IN (subquery) returns nothing if the subquery contains a NULL; prefer NOT EXISTS.
⚠ Follow-up traps
What does x NOT IN (1, NULL) return? Unknown for every row, so no rows.
Should the SQL text change with list size? It must (one placeholder per element), unless you use an array parameter.
#in-clause#edge-case
Q71
A date saved as 2026-03-01 reads back as 2026-02-28 in another service. What happened?
intermediate
A java.sql.Date/Timestamp represents midnight in the JVM's zone. When writer and reader (or driver and server) use different zones, converting to UTC shifts the instant across midnight and the calendar date moves by a day.
Use LocalDate with setObject/getObject(col, LocalDate.class) for date-only fields; no zone is involved.
Align all JVMs to UTC and check driver zone options (connectionTimeZone, serverTimezone).
Check for DATETIME vs TIMESTAMP column choice and API layer serialization (Date in JSON).
⚠ Follow-up traps
Would storing varchar dates be better? No; you lose validation, ordering semantic and index efficiency.
Is the DB value wrong? Possibly right; the shift often happens when reading with a different zone.
#date#timezone#debugging
Q72
A deadlock error (SQLState 40001/40P01) appears occasionally in production. What do you do?
intermediate
Treat it as retryable: roll back, then re-run the entire transaction a bounded number of times with jittered backoff. Meanwhile find the cause and reduce the frequency.
PostgreSQL logs both queries involved (log_lock_waits, deadlock details in the server log); MySQL SHOW ENGINE INNODB STATUS shows "LATEST DETECTED DEADLOCK".
Typical causes: different row update order in two code paths, updating parent and child in opposite order, gap locks under REPEATABLE READ, missing index causing wider locks.
Fixes: lock in consistent order (e.g. sort ids before updating), shorten transactions, add the index, lower isolation where acceptable.
⚠ Follow-up traps
Which transaction does the DB abort? Generally the cheapest to roll back (InnoDB) or the one that detected the cycle (PostgreSQL).
Can you retry only the failed statement? No, the entire transaction was rolled back.
#deadlock#retry#debugging
Q73
Two services transfer money A->B and B->A concurrently and deadlock. How do you prevent it?
intermediate
Each transaction locks its own debit account first and then the credit one, forming a cycle. Always acquire locks in a global order, e.g. ascending account id, regardless of direction.
long first = Math.min(from, to), second = Math.max(from, to);lock(c, first); // select ... from account where id = ? for updatelock(c, second);
Alternatively use a single atomic UPDATE per account with conditions, or a single statement updating both rows ordered by id.
Retry on deadlock as a safety net.
⚠ Follow-up traps
Does SERIALIZABLE remove deadlocks? No, it can add serialization failures.
Does lowering transaction time remove the risk? It reduces probability but not the possibility.
#deadlock#lock-ordering
Q74
Two requests read a balance, both subtract 10 and write back. The final balance is only reduced by 10. How do you fix this lost update?
intermediate
Read-modify-write in application code races. Make the write atomic in the DB, or lock/validate.
// atomic and safeString sql = "update account set balance = balance - ? where id = ? and balance >= ?";int n = ps.executeUpdate(); // 0 -> insufficient funds
Alternatives: SELECT ... FOR UPDATE before reading, optimistic version column, or SERIALIZABLE with retry.
READ COMMITTED does not stop the lost update for the read-then-write pattern; PostgreSQL REPEATABLE READ aborts the second writer with a serialization error.
⚠ Follow-up traps
Does wrapping both steps in a transaction fix it at READ COMMITTED? No, both read the same old value.
Does MySQL REPEATABLE READ prevent it? Not with a plain read followed by update; locking read or atomic update is needed.
#lost-update#concurrency#locking
Q75
A report run twice in one transaction returns different totals. Which isolation level problem is it, and how do you fix it?
basic
This is a non-repeatable read (or phantoms for newly inserted rows) at READ COMMITTED: each statement sees the latest committed data. Use REPEATABLE READ (snapshot per transaction) or run the report on a consistent snapshot.
Keep such snapshot transactions short; long ones hold old versions, delaying vacuum/purge (PostgreSQL bloat, InnoDB history list growth).
Better: compute all numbers in one SQL statement, which is consistent at any level.
⚠ Follow-up traps
Does a single statement see a consistent view at READ COMMITTED? Yes in PostgreSQL/Oracle (statement-level snapshot).
Does MySQL REPEATABLE READ show phantoms for plain SELECTs? No, only for locking reads or after own writes.
#isolation#non-repeatable-read
Q76
Under SERIALIZABLE in PostgreSQL, requests fail with `could not serialize access` sporadically. What does that mean?
advanced
SQLState 40001: the SSI (serializable snapshot isolation) engine detected a dependency cycle that would break serializable behaviour and aborted one transaction. It is expected and the application must retry the whole transaction.
Keep transactions small and read-only ones flagged (setReadOnly(true) with DEFERRABLE avoids aborts).
Retry with backoff; keep the count of retries bounded.
MySQL SERIALIZABLE instead turns plain reads into locking reads, increasing lock waits and deadlocks.
⚠ Follow-up traps
Is the failure a bug in the app? No, it is the contract of serializable isolation.
Does retrying the commit alone help? No, rerun the full transaction body.
#serializable#postgresql#retry
Q77
Two transactions each check `if not exists` then insert the same unique key. What happens?
intermediate
Both checks pass, then one insert fails with a unique violation (SQLState 23505 / MySQL 1062). The check-then-insert is racy; rely on the unique constraint as the single arbiter and handle the violation.
insert into users(email, name) values (?, ?)on conflict (email) do nothing; -- PostgreSQL-- MySQL: insert ignore ... / on duplicate key update ...
executeUpdate() returning 0 means the row already existed; then select it.
Catch SQLIntegrityConstraintViolationException and translate it to a domain "already exists" error.
In PostgreSQL a failed insert aborts the transaction unless using ON CONFLICT or a savepoint.
⚠ Follow-up traps
Does SERIALIZABLE solve it without a constraint? It can abort one, but the constraint is simpler and cheaper.
Is insert ignore safe? It also ignores other errors (truncation), so prefer on duplicate key update.
#race-condition#unique-constraint#upsert
Q78
A client times out calling `commit()` and retries the payment, causing duplicates. What went wrong and how do you fix it?
advanced
A timeout or connection loss during commit() leaves the outcome unknown: the transaction may have committed. Blind retry duplicates the effect. Make the operation idempotent with a client-supplied idempotency key stored under a unique constraint within the same transaction.
insert into payments(idempotency_key, order_id, amount)values (?, ?, ?) on conflict (idempotency_key) do nothing;
On retry, if the key exists, return the previously stored result.
After an unknown commit outcome, query by key before repeating.
⚠ Follow-up traps
Can the driver tell you if the commit happened? No; the connection is gone or the response was lost.
Is a random UUID generated per attempt idempotent? No; the key must be stable across retries.
#idempotency#commit#retry
Q79
An UPDATE through JDBC returns 0 rows affected, but no exception is thrown. How should the code react?
basic
0 means no rows matched: wrong id, a stale version, or a precondition (status = 'NEW') no longer true. Zero is not an error for JDBC, so you must check it and decide: raise not-found, concurrent-modification, or an idempotent no-op.
For MySQL, useAffectedRows=true changes the semantic to rows actually changed, so a same-value update returns 0.
Check array counts of a batch for SUCCESS_NO_INFO (-2) and EXECUTE_FAILED (-3).
⚠ Follow-up traps
Why does a same-value update return 0 on MySQL sometimes? With useAffectedRows=true unchanged rows are not counted.
Does the update count include rows updated by triggers? Generally no.
#executeupdate#error-handling
Q80
`getGeneratedKeys()` returns an empty ResultSet or the wrong column on Oracle. Why?
intermediate
The statement was prepared without requesting the key column, so Oracle returns the ROWID (or nothing). Prepare with the column names array so the driver adds RETURNING id INTO ?.
PreparedStatement ps = c.prepareStatement( "insert into orders(customer_id) values (?)", new String[] {"ID"});
Identity columns (12c+) and sequences with triggers both work this way.
PostgreSQL and MySQL work with RETURN_GENERATED_KEYS; with MySQL batch rewriting the keys may be partially available.
Using select max(id) afterward is unsafe under concurrency.
⚠ Follow-up traps
Is RETURN_GENERATED_KEYS ignored by Oracle? Often yes, returning ROWID, so pass the column names.
What if the table uses a sequence called in the insert? Still request the column by name.
#generated-keys#oracle
Q81
How do you apply savepoints so a failing optional step does not abort the whole transaction?
intermediate
Set a savepoint before the optional step, roll back to it on failure, and continue; commit at the end persists the main work and skips the optional work.
Release savepoints in loops (releaseSavepoint) to avoid resource buildup; PostgreSQL subtransactions beyond 64 per transaction degrade performance.
Spring NESTED propagation does the same with DataSourceTransactionManager.
⚠ Follow-up traps
Does rolling back to a savepoint release locks acquired after it? Typically yes for row locks taken in the rolled-back part (engine specific).
Does a savepoint help with a connection failure? No.
#savepoint#partial-rollback
Q82
How do you avoid an N+1 pattern when loading orders with their items via plain JDBC?
intermediate
Load orders in one query, then load all items for those order ids in a second query and group in memory; or use a single LEFT JOIN and fold rows into aggregates while iterating.
Map<Long, Order> byId = new LinkedHashMap<>();String sql = "select o.id, o.status, i.sku from orders o left join order_items i on i.order_id = o.id where o.id = any (?)";// for each row: byId.computeIfAbsent(rs.getLong("id"), ...).addItem(rs.getString("sku"))
2 queries total instead of N+1; with the join approach LIMIT applies to joined rows, so page parents in a subquery first.
Verify with a statement counter in tests (datasource-proxy assertSelectCount).
⚠ Follow-up traps
Why does LIMIT 10 over a join return fewer than 10 orders? It limits joined rows, not parents.
When is the join approach worse? Wide parents with many children duplicate parent data.
#n+1#join#mapping
Q83
A reader loops over a `ResultSet` and runs a query per row; it takes 20 minutes for 100k rows. How do you speed it up?
intermediate
100k round trips at ~0.2 ms is 20 s of network alone; the rest is per-query overhead and server work. Replace per-row lookups with set-based queries: join, IN/ANY in chunks of 500-1000, or load a lookup map once.
Reuse a single PreparedStatement at least, so parse cost is paid once.
For per-row updates use addBatch/executeBatch rather than executeUpdate per row.
Consider whether the logic can run entirely in SQL (UPDATE ... FROM, INSERT ... SELECT).
⚠ Follow-up traps
Does a faster network fix it? It reduces the constant, not the O(N) round trips.
Is a thread pool the answer? It adds parallelism but increases DB load and connection use.
#n+1#batch#performance
Q84
A `ResultSet` is returned from a DAO method inside try-with-resources and the caller gets `ResultSet is closed`. Why?
basic
Exiting try-with-resources closes the Statement and Connection, which closes the ResultSet before the caller iterates. The DAO must map rows to objects (or take a callback/RowMapper) inside the scope.
public List<User> findAll() throws SQLException { List<User> out = new ArrayList<>(); try (Connection c = ds.getConnection(); PreparedStatement ps = c.prepareStatement("select id, name from users"); ResultSet rs = ps.executeQuery()) { while (rs.next()) { out.add(new User(rs.getLong(1), rs.getString(2))); } } return out;}
This is what JdbcTemplate.query(sql, rowMapper) does for you.
⚠ Follow-up traps
Could you keep the connection open for the caller? Possible but then the caller owns resource cleanup, causing leaks.
Is it valid to return a CachedRowSet instead? Yes, it is disconnected, but heavy.
#resultset#lifecycle
Q85
A `Statement` reused in a nested loop closes the outer `ResultSet` mid-iteration. Why?
basic
Executing a second query on the same Statement closes its existing ResultSet (JDBC spec). The outer loop's next call to rs.next() fails with ResultSet closed.
Use a separate statement per concurrently open result set.
Better, avoid nested queries: use a join or a second set-based query.
MySQL streaming and some drivers also restrict multiple open cursors per connection.
⚠ Follow-up traps
Does re-executing a PreparedStatement with different parameters also close it? Yes, same rule.
Do different statements on one connection share a transaction? Yes, they share the connection's transaction.
#resultset#statement
Q86
`ps.setObject(1, null)` fails on PostgreSQL with `Can't infer the SQL type`. Why?
intermediate
Without a value the driver cannot determine the parameter type, and PostgreSQL requires it for planning. Provide a type: setNull(1, Types.VARCHAR) or setObject(1, null, Types.VARCHAR).
In dynamic filters (where (? is null or col = ?)) the type of the first ? is unknown; add casts: (?::text is null or col = ?).
That pattern also kills index use, since the plan is generic; build the SQL conditionally instead.
⚠ Follow-up traps
Is setString(1, null) enough? Yes, it carries the type VARCHAR.
Is (? is null or col = ?) performant? Often not; generate predicates only for provided filters.
#null#preparedstatement#postgresql
Q87
A long-running query must be abandoned when the HTTP client disconnects. How?
advanced
Call Statement.cancel() from another thread; the driver sends a cancel request (PostgreSQL via a new connection and the backend PID, MySQL via KILL QUERY), and the executing thread receives SQLException (e.g. SQLState 57014). Combine with setQueryTimeout as a hard cap.
cancel() is the only statement method safe to call across threads.
Interrupting the Java thread does not stop the server query, because JDBC I/O is not interruptible.
Servers also support statement_timeout/max_execution_time to bound runaway queries regardless of the client.
⚠ Follow-up traps
Does Thread.interrupt() cancel a running query? No, the blocking socket read ignores interrupts.
Does cancel guarantee instant stop? No, the server stops at the next check.
#cancel#timeouts#statement
Q88
PgBouncer in transaction mode causes `prepared statement "S_1" does not exist`. What now?
advanced
Server-side prepared statements live on a backend connection; in transaction pooling, consecutive transactions may use different backends, so a handle prepared earlier is missing. Disable server prepares in the driver with prepareThreshold=0, or use PgBouncer 1.21+ with max_prepared_statements set.
Other session features also break: SET (without LOCAL), advisory locks, LISTEN/NOTIFY, temp tables.
Alternatively use session pooling mode with a smaller app pool.
⚠ Follow-up traps
Will preparedStatementCacheQueries=0 alone fix it? Not reliably; prepareThreshold=0 is the key setting.
Is client-side PreparedStatement still safe from SQL injection with prepareThreshold=0? Yes, parameters are still sent separately via protocol.
#pgbouncer#preparedstatement#postgresql
Q89
A queue of jobs is polled by 5 workers with `select ... limit 1`. Jobs are processed twice. How do you fix it?
advanced
Workers read the same row before any marks it. Claim jobs atomically with FOR UPDATE SKIP LOCKED (or an UPDATE ... RETURNING claim) in a short transaction, mark them RUNNING, commit, then process.
update jobs set status = 'RUNNING', worker = ?where id = (select id from jobs where status = 'NEW' order by id limit 1 for update skip locked)returning id, payload;
Add a lease timestamp and a reaper for crashed workers.
Processing should be idempotent (at-least-once semantics).
Do not hold the lock during processing; it would keep the connection busy.
⚠ Follow-up traps
Does SKIP LOCKED provide exactly-once? No; with crashes you still get at-least-once.
MySQL 5.7? No SKIP LOCKED (8.0+); use an atomic UPDATE ... LIMIT 1 claim.
#queue#skip-locked#concurrency
Q90
`leakDetectionThreshold` logs a stack trace for a batch job every night. Is it a leak?
intermediate
Not necessarily. The warning means the connection was held longer than the threshold, which fits a legitimately long job. Verify whether the connection is later returned ("Previously reported leaked connection ... was returned to the pool").
Run long jobs on a separate DataSource with leak detection disabled or a higher threshold.
If it is never returned, the stack trace shows the borrowing site to fix.
Check whether the long hold is justified; chunking the job into shorter transactions frees connections.
⚠ Follow-up traps
Should batch and online traffic share one pool? Better to isolate, so the job cannot starve requests.
Does the warning stack show where the code is stuck now? No, where it borrowed the connection.
#leak#hikaricp#batch
Q91
How do you read the output of `pg_stat_activity` to find a stuck application?
advanced
Filter by application_name/client address and look at state, state_change, xact_start, wait_event and query. Many idle in transaction rows with old xact_start mean the app opened transactions and stopped sending statements: leaks or remote calls inside transactions.
select pid, state, now() - xact_start as xact_age, wait_event_type, wait_event, left(query, 80)from pg_stat_activitywhere datname = 'app' order by xact_start nulls last;
wait_event_type = Lock shows blocked sessions; pg_blocking_pids(pid) shows the blocker.
Mitigate with idle_in_transaction_session_timeout so DB kills leaked transactions.
Set ApplicationName in the JDBC URL to identify the service.
⚠ Follow-up traps
Does idle (not in transaction) indicate a problem? No, that is a healthy pooled connection.
Why are idle-in-transaction sessions dangerous? They hold locks and block vacuum.
#postgresql#debugging#idle-in-transaction
Q92
Why can a connection hold a lock after the Java code finished with a request?
intermediate
Locks last until the transaction ends. If code did setAutoCommit(false), wrote rows, and returned the connection without commit() or rollback() (exception path), the transaction may stay open, so row locks persist and other sessions block.
HikariCP rolls back uncommitted work on return when auto-commit is false (isolateInternalQueries aside), but other pools or raw usage may not.
Always commit or roll back explicitly, and prefer frameworks managing transaction boundaries.
Detect with pg_locks/pg_stat_activity or information_schema.innodb_trx.
⚠ Follow-up traps
Does Oracle roll back on close? No, Oracle commits on normal close() of a connection with pending work.
Can an idle connection block others? Yes, when holding row locks.
#transactions#locks#leak
Q93
An endpoint becomes slow only when the Hikari pool is under contention. How do you prove connection acquisition is the bottleneck?
advanced
Compare hikaricp_connections_acquire_seconds (wait to get a connection) with hikaricp_connections_usage_seconds (hold time) and hikaricp_connections_pending. If acquire time is large and pending threads are non-zero, requests queue for connections.
Then decide: reduce hold time (shorter transactions, indexes), raise pool size within DB limit, or add instances.
If acquire time is near zero and queries are slow, the bottleneck is in the DB or network.
Add percentile timers rather than averages; tail latency exposes queueing.
⚠ Follow-up traps
Why not just watch active count? It shows saturation but not whether requests waited.
What does connections_timeout_total measure? The count of failed acquisitions after connectionTimeout.
#hikaricp#metrics#debugging
Q94
Wrapping a CLOB read in a try-with-resources and returning the `Clob` fails later. Why?
intermediate
Clob/Blob are locators tied to the connection/transaction. After the connection is returned or the ResultSet closed, reading raises SQLException (invalid LOB locator, or "Large Objects may not be used in auto-commit mode" in PostgreSQL). Read the content inside the scope.
String text = rs.getString("body"); // small text// or stream large contenttry (Reader r = rs.getCharacterStream("body")) { r.transferTo(writer); }
Use Clob.free()/Blob.free() to release resources early (JDBC 4).
For big objects, copy to a stream or file inside the transaction.
⚠ Follow-up traps
Why does PostgreSQL complain about auto-commit? Large Objects (oid) require a transaction.
Does getString work on a CLOB? In most drivers yes, loading all into memory.
#lob#clob#lifecycle
Q95
A report uses `select count(*)` before every paginated query and is slow on a 50M row table. Options?
intermediate
Exact count(*) scans an index or the table (PostgreSQL has no cached count; MVCC), so it costs O(n). Options: avoid the total (show "next page" only), use an approximate count (pg_class.reltuples, EXPLAIN estimate), cap it (select count(*) from (select 1 from t where ... limit 10001)), or maintain a counter table.
Use keyset pagination for deep pages.
Cache totals for popular filters for a short TTL.
Make sure filters are covered by an index so count can use an index-only scan.
⚠ Follow-up traps
Is count(1) faster than count(*)? No, they are equivalent.
Does MyISAM differ? It stores an exact row count, but InnoDB does not.
#pagination#count#performance
Q96
Hikari logs `Failed to validate connection ... (This connection has been closed.)` followed by a new connection. Is it a problem?
intermediate
Not by itself. It means Hikari's borrow-time check found a dead connection (idle too long, killed by firewall or DB), discarded it and opened another; callers did not see an error. Frequent warnings point to a maxLifetime/idle timeout mismatch.
Lower maxLifetime under the network/server idle limit and add keepaliveTime.
If errors still reach callers, those connections died in-flight (DB restart, network drop): add retries.
Warnings that coincide with a DB restart are expected.
⚠ Follow-up traps
Does Hikari validate every borrow? No, skips if the connection was used within 500 ms.
Does this prove a driver bug? No; usually infrastructure timeouts.
#hikaricp#validation#stale-connection
Q97
A virtual-thread (Java 21) service sees `pool exhausted` under high concurrency. Why?
advanced
Virtual threads make it cheap to have tens of thousands of concurrent requests, but connections remain scarce; all of them still queue for the same bounded pool. JDBC calls block (cheaply, unpinning in newer drivers) but hold the connection throughout.
Pool size should be driven by DB capacity, not thread count; keep connectionTimeout small and shed load.
Older drivers using synchronized around I/O pin carrier threads (JDK 21); update drivers or use -Djdk.tracePinnedThreads.
Limit concurrency to DB-bound work with a Semaphore so queues form before the pool.
⚠ Follow-up traps
Do virtual threads remove the need for pooling? No; they remove the thread bottleneck, not the connection one.
Does a bigger pool help with 10k virtual threads? Only up to DB capacity.
#virtual-threads#pool-exhausted#java21
Q98
A migration adds a column while the app runs and queries fail with `cached plan must not change result type`. What's going on?
advanced
The PostgreSQL driver cached a server-side prepared statement (e.g. select * from t) whose result shape changed after ALTER TABLE. The server rejects the stale plan. The driver auto-retries only outside a transaction; within one the error surfaces.
Avoid select *; list columns explicitly so the shape is stable.
Roll connections after DDL (restart or let maxLifetime recycle), or use backward-compatible deploy sequencing.
Setting prepareThreshold=0 avoids server-side caching at some performance cost.
⚠ Follow-up traps
Does adding a nullable column break explicit-column queries? No, their result type stays the same.
Would the error happen with MySQL? Not in this form.
#postgresql#statement-cache#schema-change
Q99
A report query returns incorrect currency totals like 0.30000000000000004. What is wrong?
basic
Values were read as double (rs.getDouble) or the column is FLOAT/DOUBLE, so binary floating-point error appears when summing. Store money as NUMERIC(p,s) and read with getBigDecimal.
BigDecimal total = BigDecimal.ZERO;while (rs.next()) { total = total.add(rs.getBigDecimal("amount")); }
new BigDecimal(0.1) vs BigDecimal.valueOf(0.1)? The first captures binary error (0.1000000000000000055...), the second uses the string form.
Does NUMERIC without precision limit digits? In PostgreSQL it is arbitrary precision.
#bigdecimal#types#money
Q100
A read-only reporting endpoint hits the primary and slows OLTP. How can JDBC route it to a replica?
advanced
Use a separate DataSource for the replica (explicit routing, or Spring AbstractRoutingDataSource keyed by @Transactional(readOnly = true)), or a driver feature such as MySQL jdbc:mysql:replication:// with setReadOnly(true), or PostgreSQL targetServerType=preferSecondary.
Replication lag means reads may be stale: do not read-your-writes from a replica without safeguards.
Pools must be sized separately per target.
Route by transaction (not statement) so a transaction never mixes both.
⚠ Follow-up traps
Does setReadOnly(true) alone move queries to the replica? Only with a driver/proxy that implements routing.
Can a replica serve a just-committed write? Not reliably; lag exists.
#replica#read-only#routing
Q101
A service retries a transaction on every SQLException three times. What's wrong with that policy?
intermediate
Non-transient errors (syntax, constraint violation, permission, data truncation) will never succeed and retries only add load and delay. Retries must be selective and bounded, using exception class or SQLState: deadlock/serialization (40001, 40P01), connection (08xxx), and lock timeouts.
static boolean retryable(SQLException e) { String s = e.getSQLState(); return e instanceof SQLTransientException || e instanceof SQLRecoverableException || (s != null && (s.startsWith("08") || s.equals("40001") || s.equals("40P01")));}
Add exponential backoff with jitter and a max elapsed time.
Ensure idempotency; never retry inside a nested transaction.
⚠ Follow-up traps
Should unique violations be retried? No, except to switch to an update/read path.
Do retries amplify outages? Yes (retry storms); cap attempts and use circuit breakers.
#retry#sqlexception#design
Q102
After switching from MySQL to PostgreSQL, a query with `select * from t where flag = 1` fails. Why?
basic
PostgreSQL has a real boolean type and does not implicitly cast integers to boolean, giving operator does not exist: boolean = integer. Use where flag = true or bind with setBoolean. MySQL treats BOOLEAN as TINYINT(1).
Other portability traps: identifier case (PostgreSQL folds unquoted names to lower case), LIMIT vs FETCH FIRST, AUTO_INCREMENT vs identity/sequences, string comparison case sensitivity, empty string vs NULL in Oracle.
Use JDBC escape syntax or an abstraction only when needed; test against the real engine (Testcontainers).
⚠ Follow-up traps
Is where flag = 1 OK in Oracle? Oracle has no SQL boolean column type, so numeric flags are common.
Does H2 in MySQL mode equal MySQL? No, only approximates it.
#portability#types#postgresql
Q103
How does a DataSource-level proxy help detect JDBC problems such as N+1 or slow queries in tests and production?
intermediate
Libraries like datasource-proxy, p6spy or Hibernate statistics wrap the DataSource and intercept every Statement, so you can log SQL with bound values and timings, count statements per test, and warn on slow queries.
In tests assert query counts (assertSelectCount(1)) to stop N+1 regressions.
In production sample slow queries; avoid logging parameters with PII.
Combine with pg_stat_statements/performance schema for server-side timing.
Cost is small but non-zero; keep verbose logging off by default.
⚠ Follow-up traps
Does enabling hibernate.show_sql show bound values? No, only ? placeholders.
Is the proxy aware of time spent waiting for a connection? No; use pool metrics for that.
#observability#datasource-proxy#p6spy
Q104
A CallableStatement call to a PostgreSQL function returning a refcursor returns no rows. Why?
advanced
Refcursors are only valid within a transaction, so auto-commit must be off; the cursor must be registered as Types.REF_CURSOR (or Types.OTHER) and read via getObject, cast to ResultSet.
With auto-commit the cursor closes at statement end, so reading fails or gives nothing.
PostgreSQL 11+ procedures use CALL and OUT parameters; set escapeSyntaxCallMode=callIfNoReturn as needed.
⚠ Follow-up traps
Do functions and procedures behave the same in PostgreSQL? No; functions are invoked with SELECT, procedures with CALL.
Why must the cursor be read before commit? Commit closes the cursor.
#callablestatement#postgresql#refcursor
Q105
A job reads a huge table and updates each row; after a few minutes the DB shows bloated tables and the job slows down. What is happening?
advanced
A single long transaction (or a streaming read with auto-commit off) pins an old snapshot; PostgreSQL vacuum cannot remove dead row versions newer than it, and InnoDB's undo history grows, so bloat and slowdowns appear. Updating every row in one transaction also creates massive WAL/undo.
Process in chunks with keyset pagination and commit per chunk (e.g. 1000-5000 rows).
Use a read-only cursor on one connection and a separate connection to write in batches.
Run heavy jobs off-peak and monitor xact_start/history list length.
⚠ Follow-up traps
Does autoVacuum fix it during the transaction? No; dead tuples visible to the old snapshot cannot be removed.
Is a read-only long transaction harmless? Not for MVCC cleanup; it still holds the snapshot.