Rendimiento de proveedores de acceso – Parte 2 Nativo

En la anterior entrada hicimos una breve comparación de rendimiento de los diferentes proveedores de acceso bajo código manejado, esta vez vamos a enfrentarlos en el mundo nativo y ver cual sale vencedor.

OleDB

Como en el articulo anterior empiezo pegando el código usado para la prueba, es el siguiente:

printf("\nNative code and OLEDB\n");
//OLEDB
HRESULT hr;
IDBInitialize* pIDBInitialize = NULL;
IDBProperties* pIDBProperties = NULL;
IDBCreateSession* pIDBCreateSession = NULL;
IDBCreateCommand* pIDBCreateCommand = NULL;
ICommandText* pICommandText = NULL;
ICommandProperties* pICommandProperties = NULL;

DBPROPSET dbPropSet;
DBPROP dbProp[5];

//Fire up COM
hr = CoInitialize(0);

for (int i = 0; i<10; i++)
{
//Create provider instance
hr = CoCreateInstance(CLSID_SQLNCLI10, NULL, CLSCTX_INPROC_SERVER, IID_IDBProperties, (void **) &pIDBProperties);

    //Setup properites
dbProp[0].dwPropertyID = DBPROP_INIT_DATASOURCE;
dbProp[0].dwOptions = DBPROPOPTIONS_REQUIRED;
dbProp[0].colid = DB_NULLID;
V_VT(&(dbProp[0].vValue)) = VT_BSTR;
V_BSTR(&(dbProp[0].vValue)) = SysAllocString(L"PGAVELA-02\\KAWORU");

    dbProp[1].dwPropertyID = DBPROP_AUTH_INTEGRATED;
dbProp[1].dwOptions = DBPROPOPTIONS_REQUIRED;
dbProp[1].colid = DB_NULLID;
V_VT(&(dbProp[1].vValue)) = VT_BSTR;
V_BSTR(&(dbProp[1].vValue)) = SysAllocString( L"SSPI" );

    dbProp[2].dwPropertyID = DBPROP_INIT_CATALOG;
dbProp[2].dwOptions = DBPROPOPTIONS_REQUIRED;
dbProp[2].colid = DB_NULLID;
V_VT(&(dbProp[2].vValue)) = VT_BSTR;
V_BSTR(&(dbProp[2].vValue)) = SysAllocString( L"PerfTest" );

    dbPropSet.rgProperties = dbProp;
dbPropSet.cProperties = 3;
dbPropSet.guidPropertySet = DBPROPSET_DBINIT;

    hr = pIDBProperties->SetProperties(1, &dbPropSet);

    SysFreeString( V_BSTR(&(dbProp[0].vValue)) );
SysFreeString( V_BSTR(&(dbProp[1].vValue)) );
SysFreeString( V_BSTR(&(dbProp[2].vValue)) );

    //Create the Session and Command
hr = pIDBProperties->QueryInterface( IID_IDBInitialize, (void**) &pIDBInitialize);

    hr = pIDBInitialize->Initialize();

    hr = pIDBInitialize->QueryInterface( IID_IDBCreateSession, (void**) &pIDBCreateSession );

    hr = pIDBCreateSession->CreateSession(NULL, IID_IDBCreateCommand, (IUnknown **) &pIDBCreateCommand);

    hr = pIDBCreateCommand->CreateCommand(NULL, IID_ICommandText, (IUnknown **) &pICommandText);

    //Setup Command Properties
pICommandText->QueryInterface( IID_ICommandProperties, (void **) &pICommandProperties );

   dbPropSet.rgProperties = dbProp;
dbPropSet.cProperties = 5;
dbPropSet.guidPropertySet = DBPROPSET_ROWSET;

   hr = pICommandProperties->SetProperties( 1, &dbPropSet);

   pICommandProperties->Release();
pICommandProperties=NULL;

   IRowset* pIRowset = NULL;
IAccessor* pIAccessor = NULL;

   wchar_t* pData = NULL;
DBCOUNTITEM cRowsObtained = 0;
ULONG cCount = 0;

   HROW* pRows = new HROW[1000000];
HACCESSOR hAccessor = 0;
DBBINDING Bind[1];

hr = pICommandText->SetCommandText(DBGUID_SQL, L"SELECT TOP 5000000 * FROM HashTable");
startTicks = GetTickCount();
hr = pICommandText->Execute(NULL, IID_IRowset, NULL, NULL, (IUnknown**)&pIRowset);

//Bind data
Bind[0].dwPart = DBPART_VALUE;
Bind[0].eParamIO = DBPARAMIO_NOTPARAM;
Bind[0].iOrdinal = 1;
Bind[0].pTypeInfo = NULL;
Bind[0].pObject = NULL;
Bind[0].pBindExt = NULL;
Bind[0].dwFlags = 0;
Bind[0].dwMemOwner = DBMEMOWNER_CLIENTOWNED;
Bind[0].obLength = 0;
Bind[0].obStatus = 0;
Bind[0].obValue = 0;
Bind[0].cbMaxLen = 500;
Bind[0].wType = DBTYPE_WSTR;
Bind[0].bPrecision = 0;
Bind[0].bScale = 0;

    hr = pIRowset->QueryInterface(IID_IAccessor, (void**)&pIAccessor);

    hr = pIAccessor->CreateAccessor(DBACCESSOR_ROWDATA, 1, Bind, 0, &hAccessor, NULL);

    pData = new wchar_t[500];

    do
{
//Get Next 10 rows
hr = pIRowset->GetNextRows(NULL,0,10,&cRowsObtained, &pRows);

        for (int cCount =0; cCount < cRowsObtained; cCount++)
{
hr = pIRowset->GetData(pRows[cCount], hAccessor, pData);
}
pIRowset->ReleaseRows(cRowsObtained,pRows,NULL,NULL,NULL);

    }while(cRowsObtained != 0);
endTicks = GetTickCount();
totalTicks = endTicks - startTicks;
printf("Round %d Time: %d ms\n",i,totalTicks);
totalOLEDB += totalTicks;

//Release all objects
pIAccessor->Release();
pIRowset->Release();
pICommandText->Release();
pIDBCreateCommand->Release();
pIDBCreateSession->Release();
pIDBInitialize->Release();
pIDBProperties->Release();
}
printf("Avg Time: %d ms\n",totalOLEDB/10);

Como podemos ver hacemos lo mismo que con el código manejado, diez iteraciones y luego obtenemos la media, los resultados son los siguientes:

Native code and OLEDB
Round 0 Time: 23743 ms
Round 1 Time: 24165 ms
Round 2 Time: 16068 ms
Round 3 Time: 23385 ms
Round 4 Time: 22870 ms
Round 5 Time: 24445 ms
Round 6 Time: 24383 ms
Round 7 Time: 26395 ms
Round 8 Time: 23572 ms
Round 9 Time: 23822 ms
Avg Time: 23284 ms

Como podemos ver el tiempo medio es de 23,3 segundos una mejora importante respecto a la version de .NET pero aún así más lento que SQLClient.

ODBC 

Y por último tenemos ODBC, a continuación pego el código utilizado:

        SQLRETURN ret;
SQLHDBC dbc;
SQLHENV env;
SQLHSTMT stmt;
DWORD startTicks;
DWORD endTicks;
DWORD totalTicks;
SQLWCHAR texto[500];
wchar_t resultado[500];

    DWORD totalODBC = 0;
DWORD totalOLEDB = 0;

    SQLINTEGER cursorOptions = SQL_CO_FFO;
printf("Native code and ODBC\n");
for (int i=0; i<10;i++)
{
SQLAllocHandle(SQL_HANDLE_ENV,SQL_NULL_HANDLE,&env);
SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (void *) SQL_OV_ODBC3, 0);
SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
ret = SQLDriverConnect(dbc, NULL, L"Driver={SQL Server Native Client 10.0};Server=PGAVELA-02\\KAWORU;Database=PerfTest;Trusted_Connection=yes", SQL_NTS,NULL,0,NULL,SQL_DRIVER_COMPLETE);
if (SQL_SUCCEEDED(ret))
{
SQLAllocHandle(SQL_HANDLE_STMT, dbc,&stmt);
SQLPrepare(stmt, L"SELECT TOP 5000000 * FROM HashTable", SQL_NTS);
SQLSetStmtAttr(stmt, SQL_SOPT_SS_CURSOR_OPTIONS,&cursorOptions,NULL);
SQLBindCol(stmt, 1, SQL_C_WCHAR, texto, 500, NULL);
startTicks = GetTickCount();
SQLExecute(stmt);
while (SQL_SUCCEEDED(ret = SQLFetch(stmt)))
{
wsprintf(resultado,texto);
}
endTicks = GetTickCount();
totalTicks = endTicks - startTicks;
printf("Round %d Time: %d ms\n",i,totalTicks);
totalODBC += totalTicks;
SQLDisconnect(dbc);
SQLFreeHandle(SQL_HANDLE_STMT, stmt);
SQLFreeHandle(SQL_HANDLE_DBC, dbc);
SQLFreeHandle(SQL_HANDLE_ENV, env);
}
}

    printf("Avg Time: %d ms\n",totalODBC / 10);

A continuación los tiempos:

Native code and ODBC
Round 0 Time: 10686 ms
Round 1 Time: 10452 ms
Round 2 Time: 10343 ms
Round 3 Time: 10436 ms
Round 4 Time: 8939 ms
Round 5 Time: 11107 ms
Round 6 Time: 10327 ms
Round 7 Time: 9718 ms
Round 8 Time: 10406 ms
Round 9 Time: 10686 ms
Avg Time: 10310 ms

Wow! 10 segundos, bastante más rápido que SQLClient. Por tanto en nativo no hay color, ¡ODBC es el camino a seguir!

Conclusiones

Como podemos ver OleDB es el proveedor más lento de todos accediendo a SQL Server, además teniendo en cuenta que se considera deprecado no se recomienda usarlo, en aquellos desarrollos que ya tengamos os recomendamos cambiar a ODBC o SQLClient(Solo en manejado) lo antes posible.

Con esta recomendación podéis ver que en nativo ODBC es el camino a seguir sin ninguna duda, pero en manejado, usamos SQLClient u ODBC. Pues depende de lo que busquemos, ODBC al tener que traducir de nativo a manejado y viceversa sufre cierta penalización cuando lo usamos bajo código manejado, por tanto si lo que queremos es rendimiento la respuesta es sencilla, SQLClient.

Si nos interesa la portabilidad ya que en un futuro puede que cambiemos de SQL Server a otra base de datos, entonces la respuesta no es tan sencilla. Si tenemos proveedores manejados para otras bases de datos que se ajusten al modelo de acceso de System.Data entonces puede que necesitemos cambiar no muchas lineas de codigo. Pero si usamos ODBC el cambio puede ser muchisimo más sencillo (si no usamos comandos SQL “extraños”) bastaría con cambiar la cadena de conexión y en principio,si hay suerte, puede que nada más.

Pablo Gavela López – Microsoft Customer Support Services