2 回答
TA貢獻(xiàn)1744條經(jīng)驗(yàn) 獲得超4個(gè)贊
作為 JDBC 驅(qū)動(dòng)程序維護(hù)者(我承認(rèn),我進(jìn)行了一些不一定適用于所有 JDBC 驅(qū)動(dòng)程序的全面概括),行值通常存儲(chǔ)在數(shù)組或列表中,因?yàn)檫@最自然地與從數(shù)據(jù)庫(kù)服務(wù)器接收數(shù)據(jù)的方式相匹配。
因此,按索引檢索值將是最簡(jiǎn)單的。它可能像這樣的東西一樣簡(jiǎn)單(忽略實(shí)現(xiàn)JDBC驅(qū)動(dòng)程序的一些更令人討厭的細(xì)節(jié)):
public Object getObject(int index) throws SQLException {
checkValidRow();
checkValidIndex(index);
return currentRow[index - 1];
}
這幾乎是最快的。
另一方面,按列名查找需要更多的工作。列名需要處理為不區(qū)分大小寫(xiě)的,無(wú)論使用小寫(xiě)還是大寫(xiě)進(jìn)行規(guī)范化,或者使用不區(qū)分大小寫(xiě)的查找,這都會(huì)產(chǎn)生額外的成本。TreeMap
一個(gè)簡(jiǎn)單的實(shí)現(xiàn)可能是這樣的:
public Object getObject(String columnLabel) throws SQLException {
return getObject(getIndexByLabel(columnLabel));
}
private int getIndexByLabel(String columnLabel) {
Map<String, Integer> indexMap = createOrGetIndexMap();
Integer columnIndex = indexMap.get(columnLabel.toLowerCase());
if (columnIndex == null) {
throw new SQLException("Column label " + columnLabel + " does not exist in the result set");
}
return columnIndex;
}
private Map<String, Integer> createOrGetIndexMap() throws SQLException {
if (this.indexMap != null) {
return this.indexMap;
}
ResultSetMetaData rsmd = getMetaData();
Map<String, Integer> map = new HashMap<>(rsmd.getColumnCount());
// reverse loop to ensure first occurrence of a column label is retained
for (int idx = rsmd.getColumnCount(); idx > 0; idx--) {
String label = rsmd.getColumnLabel(idx).toLowerCase();
map.put(label, idx);
}
return this.indexMap = map;
}
根據(jù)數(shù)據(jù)庫(kù)的 API 和可用的語(yǔ)句元數(shù)據(jù),可能需要額外的處理來(lái)確定查詢的實(shí)際列標(biāo)簽。根據(jù)開(kāi)銷,這可能僅在實(shí)際需要時(shí)才確定(按名稱訪問(wèn)列標(biāo)簽時(shí),或檢索結(jié)果集元數(shù)據(jù)時(shí))。換句話說(shuō),成本可能相當(dāng)高。createOrGetIndexMap()
但是,即使該成本可以忽略不計(jì)(例如,從數(shù)據(jù)庫(kù)服務(wù)器準(zhǔn)備元數(shù)據(jù)的語(yǔ)句包括列標(biāo)簽),將列標(biāo)簽映射到索引然后按索引檢索的開(kāi)銷顯然高于直接按索引檢索的開(kāi)銷。
驅(qū)動(dòng)程序甚至可以每次都循環(huán)訪問(wèn)結(jié)果集元數(shù)據(jù),并使用標(biāo)簽匹配的第一個(gè);這可能比為具有少量列的結(jié)果集構(gòu)建和訪問(wèn)哈希映射更便宜,但成本仍然高于通過(guò)索引直接訪問(wèn)。
正如我所說(shuō),這是一個(gè)全面的概括,但是如果這(按名稱查找索引,然后按索引檢索)不是它在大多數(shù)JDBC驅(qū)動(dòng)程序中的工作方式,我會(huì)感到驚訝,這意味著我希望按索引查找通常會(huì)更快。
快速瀏覽一下許多驅(qū)動(dòng)程序,情況如下:
火鳥(niǎo)(杰伯德,披露:我維護(hù)這個(gè)司機(jī))
MySQL (MySQL Connector/J)
PostgreSQL
神諭
斷續(xù)器
SQL Server (Microsoft JDBC Driver for SQL Server)
我不知道JDBC驅(qū)動(dòng)程序按列名檢索的成本會(huì)相等,甚至更便宜。
TA貢獻(xiàn)1797條經(jīng)驗(yàn) 獲得超6個(gè)贊
在制作 jOOQ 的早期,我考慮了這兩個(gè)選項(xiàng),即按索引或名稱訪問(wèn) JDBC 值。出于以下原因,我選擇按索引訪問(wèn)內(nèi)容:ResultSet
數(shù)據(jù)庫(kù)管理系統(tǒng)支持
并非所有 JDBC 驅(qū)動(dòng)程序?qū)嶋H上都支持按名稱訪問(wèn)列。我忘記了哪些沒(méi)有,如果它們?nèi)匀粵](méi)有,因?yàn)槲以?3年內(nèi)再也沒(méi)有接觸過(guò)JDBC API的那一部分。但有些人沒(méi)有,這對(duì)我來(lái)說(shuō)已經(jīng)是一個(gè)節(jié)目的阻礙。
名稱的語(yǔ)義
此外,在那些支持列名的列名中,列名有不同的語(yǔ)義,主要是兩個(gè),JDBC調(diào)用:
關(guān)于上述兩個(gè)的實(shí)現(xiàn)有很多歧義,盡管我認(rèn)為意圖非常明確:
列名應(yīng)該產(chǎn)生列的名稱,而不管別名如何,例如 如果投影表達(dá)式是
TITLEBOOK.TITLE AS X列標(biāo)簽應(yīng)該生成列的標(biāo)簽(或別名),如果沒(méi)有可用的別名,則生成名稱,例如 如果投影表達(dá)式是
XBOOK.TITLE AS X
因此,名稱/標(biāo)簽的這種模糊性已經(jīng)非常令人困惑和擔(dān)憂。一般來(lái)說(shuō),ORM似乎不應(yīng)該依賴它,盡管在Hibernate的情況下,人們可以爭(zhēng)辯說(shuō)休眠控制著大多數(shù)SQL的生成,至少是為獲取實(shí)體而生成的SQL。但是,如果用戶編寫(xiě) HQL 或本機(jī) SQL 查詢,我將不愿意依賴名稱/標(biāo)簽 - 至少不要先在 中查找內(nèi)容。ResultSetMetaData
歧義
在SQL中,在頂層有不明確的列名是完全可以的,例如:
SELECT id, id, not_the_id AS id FROM book
這是完全有效的 SQL。不能將此查詢嵌套為派生表,因?yàn)椴辉试S出現(xiàn)多義詞,但在頂級(jí)中可以?,F(xiàn)在,您將如何處理頂層的重復(fù)標(biāo)簽?您無(wú)法確定在按名稱訪問(wèn)事物時(shí)會(huì)得到哪一個(gè)。前兩個(gè)可能相同,但第三個(gè)非常不同。SELECTID
清楚地區(qū)分列的唯一方法是按索引,索引是唯一的:, , 。123
性能
我當(dāng)時(shí)也嘗試過(guò)表演。我不再有基準(zhǔn)測(cè)試結(jié)果,但很容易快速編寫(xiě)另一個(gè)基準(zhǔn)測(cè)試。在下面的基準(zhǔn)測(cè)試中,我正在對(duì) H2 內(nèi)存中實(shí)例運(yùn)行一個(gè)簡(jiǎn)單的查詢,并使用訪問(wèn)內(nèi)容:ResultSet
按索引
按名稱
結(jié)果令人震驚:
Benchmark Mode Cnt Score Error Units
JDBCResultSetBenchmark.indexAccess thrpt 7 1130734.076 ± 9035.404 ops/s
JDBCResultSetBenchmark.nameAccess thrpt 7 600540.553 ± 13217.954 ops/s
盡管基準(zhǔn)測(cè)試在每次調(diào)用時(shí)運(yùn)行整個(gè)查詢,但按索引訪問(wèn)的速度幾乎是其兩倍!你可以看看H2的代碼,它是開(kāi)源的。它執(zhí)行以下操作(版本 2.1.212):
private int getColumnIndex(String columnLabel) {
checkClosed();
if (columnLabel == null) {
throw DbException.getInvalidValueException("columnLabel", null);
}
if (columnCount >= 3) {
// use a hash table if more than 2 columns
if (columnLabelMap == null) {
HashMap<String, Integer> map = new HashMap<>();
// [ ... ]
columnLabelMap = map;
if (preparedStatement != null) {
preparedStatement.setCachedColumnLabelMap(columnLabelMap);
}
}
Integer index = columnLabelMap.get(StringUtils.toUpperEnglish(columnLabel));
if (index == null) {
throw DbException.get(ErrorCode.COLUMN_NOT_FOUND_1, columnLabel);
}
return index + 1;
}
// [ ... ]
所以,有一個(gè)帶有上寫(xiě)字母的哈希圖,每次查找也執(zhí)行上寫(xiě)。至少,它將映射緩存在預(yù)準(zhǔn)備語(yǔ)句中,因此:
您可以在每行上重復(fù)使用它
您可以在語(yǔ)句的多次執(zhí)行中重用它(至少這是我解釋代碼的方式)
因此,對(duì)于非常大的結(jié)果集,它可能不再那么重要,但對(duì)于較小的結(jié)果集,它肯定很重要。
關(guān)于管理權(quán)的結(jié)論
像休眠或jOOQ這樣的ORM可以控制大量的SQL和結(jié)果集。它確切地知道哪個(gè)列在什么位置,這項(xiàng)工作在生成SQL查詢時(shí)已經(jīng)完成。因此,當(dāng)結(jié)果集從數(shù)據(jù)庫(kù)服務(wù)器返回時(shí),絕對(duì)沒(méi)有理由進(jìn)一步依賴列名。每個(gè)值都將位于預(yù)期位置。
在休眠中,使用列名一定是一些歷史性的事情。這可能也是為什么他們?cè)?jīng)生成這些不那么可讀的列別名,以確保每個(gè)別名都是不明確的。
這似乎是一個(gè)明顯的改進(jìn),無(wú)論在現(xiàn)實(shí)世界(非基準(zhǔn))查詢中的實(shí)際收益如何。即使改進(jìn)只有 2%,也是值得的,因?yàn)樗鼤?huì)影響每個(gè)基于 Hibernate 的應(yīng)用程序執(zhí)行的每個(gè)查詢。
下面的基準(zhǔn)代碼,用于復(fù)制
package org.jooq.test.benchmarks.local;
import java.io.*;
import java.sql.*;
import java.util.Properties;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.*;
@Fork(value = 1)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 7, time = 3)
public class JDBCResultSetBenchmark {
@State(Scope.Benchmark)
public static class BenchmarkState {
Connection connection;
@Setup(Level.Trial)
public void setup() throws Exception {
try (InputStream is = BenchmarkState.class.getResourceAsStream("/config.properties")) {
Properties p = new Properties();
p.load(is);
connection = DriverManager.getConnection(
p.getProperty("db.url"),
p.getProperty("db.username"),
p.getProperty("db.password")
);
}
}
@TearDown(Level.Trial)
public void teardown() throws Exception {
connection.close();
}
}
@FunctionalInterface
interface ThrowingConsumer<T> {
void accept(T t) throws SQLException;
}
private void run(BenchmarkState state, ThrowingConsumer<ResultSet> c) throws SQLException {
try (Statement s = state.connection.createStatement();
ResultSet rs = s.executeQuery("select c as c1, c as c2, c as c3, c as c4 from system_range(1, 10) as t(c);")) {
c.accept(rs);
}
}
@Benchmark
public void indexAccess(Blackhole blackhole, BenchmarkState state) throws SQLException {
run(state, rs -> {
while (rs.next()) {
blackhole.consume(rs.getInt(1));
blackhole.consume(rs.getInt(2));
blackhole.consume(rs.getInt(3));
blackhole.consume(rs.getInt(4));
}
});
}
@Benchmark
public void nameAccess(Blackhole blackhole, BenchmarkState state) throws SQLException {
run(state, rs -> {
while (rs.next()) {
blackhole.consume(rs.getInt("C1"));
blackhole.consume(rs.getInt("C2"));
blackhole.consume(rs.getInt("C3"));
blackhole.consume(rs.getInt("C4"));
}
});
}
}
添加回答
舉報(bào)
