NestJS: Raw SQL query execution with SQL Server too slow. Configuration problems? - sql-server
APP in Nestjs that is connected to a SQL Server database. All the queries are written on the database side, so the connection between them is with simple raw SQL and the use of mssql package.
Here is the thing: when I run in SSMS, a very small query (let's say returning < 20 records) is executed in milliseconds (even larger and complex queries or stored procedures have good performance).
When I run in the app, with a local database connection, queries start having some delay (let's say 1 second for the same query).
But when I start using the database on Azure, the same small query takes 3 to 5 seconds (for 20 records).
I read some of causes could be related to parameter sniffing, but I don't think it is the case.
What I guess, is that my backend is restarting the database connection every time a new query arrives.
Here is the logic of the app: one centralized CRUD service to be used by the controllers.
In main.ts is the connection:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const logger = new Logger('Bootstrap', { timestamp: true });
const configService = app.get(ConfigService);
// Database configuration
const sqlConfig = {
user:
configService.get('DB_USELOCAL') === 'false'
? configService.get('DB_USERNAME')
: configService.get('DB_USERNAME_LOCAL'),
password:
configService.get('DB_USELOCAL') === 'false'
? configService.get('DB_PASSWORD')
: configService.get('DB_PASSWORD_LOCAL'),
server:
configService.get('DB_USELOCAL') === 'false'
? configService.get('DB_SERVER')
: configService.get('DB_SERVER_LOCAL'),
database:
configService.get('DB_USELOCAL') === 'false'
? configService.get('DB_DATABASE')
: configService.get('DB_DATABASE_LOCAL'),
pool: {
max: 10,
min: 0,
idleTimeoutMillis: 30000,
},
requestTimeout: 180000, //3 minutes to wait for a request to the database.
options: {
// encrypt: false, // for azure
encrypt: configService.get('DB_USELOCAL') === 'false' ? true : false,
trustServerCertificate: false, // change to true for local dev / self-signed certs
},
};
sql.connect(sqlConfig);
logger.log('App connected to SQL Server database');
// CORS: Cross-origin resource sharing (CORS) is a mechanism that allows resources to be requested from another domain.
app.enableCors();
// App running
await app.listen(configService.get('PORT') || 3000);
logger.log(`App running on port ${configService.get('PORT') || 3000}`);
}
bootstrap();
At the CRUD Service the queries requested
import { HttpException, HttpStatus, Injectable, Logger } from '#nestjs/common';
import { fxSQLerrorMsg } from './function/SLQerrorMsg.fx';
import * as sql from 'mssql';
import { FxArrayObjectStr } from './function/arrayObjectStr.fx';
import { FxObjectStr } from './function/objectStr.fx';
import { FindBodyDTO } from './findBody.dto';
#Injectable()
export class CrudService {
private logger = new Logger('Crud Service', { timestamp: true });
async find(
sp: string,
DB: string,
body?: FindBodyDTO | null,
query?: Record<string, any> | null,
email?: string,
filter?: string,
): Promise<Record<string, any>[]> {
const method = "'" + 'find' + "'";
const storedProcedure =
process.env.SPECIFYDB == 'true'
? 'EXECUTE [' + DB + '].[ml_sp].[' + sp + ']'
: 'EXECUTE [ml_sp].[' + sp + ']';
const bodyParam = body
? "'" + JSON.stringify(body).replace('%20', ' ') + "'"
: null;
const queryParam = FxObjectStr(query);
const emailScript = email ? "'" + email + "'" : null;
const filterScript = filter ? "'" + filter + "'" : null;
const spScript =
storedProcedure +
' ' +
method +
', ' +
bodyParam +
', ' +
queryParam +
',' +
emailScript +
',' +
filterScript;
this.logger.verbose(spScript);
try {
return (await sql.query<Record<string, any>[]>(spScript))
.recordset as unknown as Record<string, any>[];
} catch (error) {
this.logger.error(error);
throw new HttpException(
fxSQLerrorMsg(error.message, 'Find'),
HttpStatus.BAD_REQUEST,
);
}
}
async post(
sp: string,
DB: string,
body: Record<string, any>[],
email?: string,
filter?: string,
): Promise<Record<string, string>> {
const method = "'" + 'post' + "'";
const storedProcedure =
process.env.SPECIFYDB == 'true'
? 'EXECUTE [' + DB + '].[ml_sp].[' + sp + ']'
: 'EXECUTE [ml_sp].[' + sp + ']';
const bodyParam = FxArrayObjectStr(body);
const queryParam = null;
const emailScript = email ? "'" + email + "'" : null;
const filterScript = filter ? "'" + filter + "'" : null;
const spScript =
storedProcedure +
' ' +
method +
', ' +
bodyParam +
', ' +
queryParam +
', ' +
emailScript +
',' +
filterScript;
this.logger.verbose(spScript);
try {
return (
(await sql.query<string>(spScript)).recordset as any[]
)[0] as Record<string, string>;
} catch (error) {
this.logger.error(error);
throw new HttpException(
fxSQLerrorMsg(error.message, 'Post'),
HttpStatus.BAD_REQUEST,
);
}
}
async updateOne(
sp: string,
DB: string,
body: Record<string, any>[],
query?: Record<string, any>,
email?: string,
filter?: string,
): Promise<Record<string, string>> {
const method = "'" + 'updateOne' + "'";
const storedProcedure =
process.env.SPECIFYDB == 'true'
? 'EXECUTE [' + DB + '].[ml_sp].[' + sp + ']'
: 'EXECUTE [ml_sp].[' + sp + ']';
const bodyParam = FxArrayObjectStr(body);
const queryParam = FxObjectStr(query);
const emailScript = email ? "'" + email + "'" : null;
const filterScript = filter ? "'" + filter + "'" : null;
const spScript =
storedProcedure +
' ' +
method +
', ' +
bodyParam +
', ' +
queryParam +
', ' +
emailScript +
',' +
filterScript;
this.logger.verbose(spScript);
try {
return (
(await sql.query<string>(spScript)).recordset as any[]
)[0] as Record<string, string>;
} catch (error) {
this.logger.error(error);
throw new HttpException(
fxSQLerrorMsg(error.message, 'Update'),
HttpStatus.BAD_REQUEST,
);
}
}
async updateMany(
sp: string,
DB: string,
body: Record<string, any>[],
query?: Record<string, any>,
email?: string,
filter?: string,
): Promise<Record<string, string>> {
const method = "'" + 'updateMany' + "'";
const storedProcedure =
process.env.SPECIFYDB == 'true'
? 'EXECUTE [' + DB + '].[ml_sp].[' + sp + ']'
: 'EXECUTE [ml_sp].[' + sp + ']';
const bodyParam = FxArrayObjectStr(body);
const queryParam = FxObjectStr(query);
const emailScript = email ? "'" + email + "'" : null;
const filterScript = filter ? "'" + filter + "'" : null;
const spScript =
storedProcedure +
' ' +
method +
', ' +
bodyParam +
', ' +
queryParam +
', ' +
emailScript +
',' +
filterScript;
this.logger.verbose(spScript);
try {
return (
(await sql.query<string>(spScript)).recordset as any[]
)[0] as Record<string, string>;
} catch (error) {
this.logger.error(error);
throw new HttpException(
fxSQLerrorMsg(error.message, 'Update'),
HttpStatus.BAD_REQUEST,
);
}
}
async deleteOne(
sp: string,
DB: string,
query?: Record<string, any>,
email?: string,
filter?: string,
): Promise<Record<string, string>> {
const method = "'" + 'deleteOne' + "'";
const storedProcedure =
process.env.SPECIFYDB == 'true'
? 'EXECUTE [' + DB + '].[ml_sp].[' + sp + ']'
: 'EXECUTE [ml_sp].[' + sp + ']';
const bodyParam = null;
const queryParam = FxObjectStr(query);
const emailScript = email ? "'" + email + "'" : null;
const filterScript = filter ? "'" + filter + "'" : null;
const spScript =
storedProcedure +
' ' +
method +
', ' +
bodyParam +
', ' +
queryParam +
', ' +
emailScript +
',' +
filterScript;
this.logger.verbose(spScript);
try {
return (
(await sql.query<string>(spScript)).recordset as any[]
)[0] as Record<string, string>;
} catch (error) {
this.logger.error(error);
throw new HttpException(
fxSQLerrorMsg(error.message, 'Delete'),
HttpStatus.BAD_REQUEST,
);
}
}
async deleteMany(
sp: string,
DB: string,
body: Record<string, any>[],
query: Record<string, any>,
email?: string,
filter?: string,
): Promise<Record<string, string>> {
const method = "'" + 'deleteMany' + "'";
const storedProcedure =
process.env.SPECIFYDB == 'true'
? 'EXECUTE [' + DB + '].[ml_sp].[' + sp + ']'
: 'EXECUTE [ml_sp].[' + sp + ']';
const bodyParam = FxArrayObjectStr(body);
const queryParam = FxObjectStr(query);
const emailScript = email ? "'" + email + "'" : null;
const filterScript = filter ? "'" + filter + "'" : null;
const spScript =
storedProcedure +
' ' +
method +
', ' +
bodyParam +
', ' +
queryParam +
', ' +
emailScript +
',' +
filterScript;
this.logger.verbose(spScript);
try {
return (
(await sql.query<string>(spScript)).recordset as any[]
)[0] as Record<string, string>;
} catch (error) {
this.logger.error(error);
throw new HttpException(
fxSQLerrorMsg(error.message, 'Delete'),
HttpStatus.BAD_REQUEST,
);
}
}
}
Additional information: the query that I am testing (what I called a very small query) is:
ALTER VIEW [ml_view].[User2Role] AS
(SELECT [ml_users].[User2Role].[id] as [id],
[User_user_Aux].[email] as [user],
[PortfolioRole_portfoliorole_Aux].[name] as [portfoliorole],
[ml_users].[User2Role].[editiondate] as [editiondate],
[User_editedbyuser_Aux].[email] as [editedbyuser]
FROM [ml_users].[User2Role]
LEFT JOIN [ml_users].[User] as [User_user_Aux] ON [User_user_Aux].[id] = [ml_users].[User2Role].[userid]
LEFT JOIN [ml_setup].[PortfolioRole] as [PortfolioRole_portfoliorole_Aux] ON [PortfolioRole_portfoliorole_Aux].[id] = [ml_users].[User2Role].[portfolioroleid]
LEFT JOIN [ml_users].[User] as [User_editedbyuser_Aux] ON [User_editedbyuser_Aux].[id] = [ml_users].[User2Role].[editedbyuser])
Actually, it is stored as view, and run through a stored procedure. But we tested executing the view directly (Select * from [viewName]), and the result is the same.
Solution: the problem was not in the NestJS configuration to the SQL Server, was precisely in the resources assigned in Azure to the database (DTU or vCores)
I summarize next some of the actions that we undertook, but in the end, it was a silly mistake. However, guess is useful to keep the post.
Data sniffing controlling.
Rebuild index.
Reset statistics.
Trying different SQL configurations in Nestjs (typeorm, tedious, etc.)
Assigned resources to the (DTU or Vcore model). See in Azure "Compute + Storage" section.
Related
SOQL Group by IN Salesforce
String query = 'SELECT id,CreatedById,Product.Id,POCGrades__c ,fm_pocname__c,product.Name ,Visit.placeId, fm_poccode__c, createdby.LastName,Visit.LastModifiedDate, createddate, ActualBooleanValue' + ' FROM retailvisitkpi' + ' WHERE createddate = last_month and ' + ' ( Product.Id = \'01t5j000003tszWAAQ\'' + ' OR Product.Id = \'01t5j000003tszWAAQ\'' + ' OR Product.Id = \'01t5j000003tt5nAAA\'' + ' OR Product.Id = \'01t5j000003tsznAAA\'' + ' OR Product.Id = \'01t5j000003tt1zAAA\'' + ' OR Product.Id = \'01t5j000003tt7AAAQ\'' + ' )'; I am having trouble to removes duplicate records based on Visit.placeID, can anybody help me out with soql
Just use hashmaps with the resulting query? Map<String, retailvisitkpi__c> mapOfItems = new Map<String, retailvisitkpi__c>(); That will remove all the duplicates based on the Visit Place ID
DiscordJS V12 client.guilds.cache.get(...).emojis.forEach is not a function
let static = [], animated = []; client.guilds.cache.get('911491194654175242').emojis.forEach(emoji => emoji.animated ? animated.push([emoji.id, emoji.name]) : static.push([emoji.id, emoji.name])); console.log('Static Emojis\n'); static.forEach(emoji => console.log('<:' + emoji[1] + ':' + emoji[0] + '>')); console.log('\nAnimated Emojis\n'); animated.forEach(emoji => console.log('<a:' + emoji[1] + ':' + emoji[0] + '>')); This is my code for my ready event, I am trying to log all the emojis in my guild to the console to make my life easier but returns an error client.guilds.cache.get(...).emojis.forEach is not a function
Invalid Identifier when passing a string into sql query in snowflake
I have a stored procedure which takes a string(SCHEMA_NAME) as a parameter It then puts this string name into a query The problem I am facing is when I pass the parameter into the sql query I get a Invalid identifier error Below is my code and what I have tried CREATE OR REPLACE PROCEDURE "CREATE_SCHEMA"("SCHNAME" VARCHAR(16777216)) RETURNS VARCHAR(16777216) LANGUAGE JAVASCRIPT COMMENT='Creates schemas' EXECUTE AS CALLER AS $$ var v_sqlCode = "select * from dbschemas where name = " + "''" + SCHNAME + "''"; try{ var sqlStmt = snowflake.createStatement({sqlText:v_sqlCode}); var sqlRS = sqlStmt.execute(); }catch(err){ errMessage = "Failed: Code: " + err.code + "\n State: " + err.state; errMessage += "\n Message: " + err.message + v_sqlCode; errMessage += "\nStack Trace:\n" + err.stackTraceTxt + v_sqlCode; throw 'Encountered error in executing v_sqlCode. \n' + errMessage; } $$; I have tried writing my sql query in three ways 1. var v_sqlCode = `select * from dbschemas where name = `+ SCHNAME; 2. var v_sqlCode = "select * from dbschemas where name = " + "''" + SCHNAME + "''" 3.var v_sqlCode = `SELECT * FROM DBSCHEMAS WHERE NAME = {$SCHENAME}` The way in which I call the stored procedure is as follows : CALL CREATE_SCHEMA('SCHEMA_NAME'); Any help would be greatly appreciated.
It is recommended to parametrize query to be executed instead of concatenating the SQL query string. More info: Binding variables CREATE OR REPLACE PROCEDURE CREATE_SCHEMA(SCHNAME VARCHAR(16777216)) RETURNS VARCHAR(16777216) LANGUAGE JAVASCRIPT COMMENT='Creates schemas' EXECUTE AS CALLER AS $$ var v_sqlCode = "select * from dbschemas where name = ?"; try{ var sqlStmt = snowflake.createStatement({sqlText:v_sqlCode, binds:[SCHNAME]}); var sqlRS = sqlStmt.execute(); }catch(err){ errMessage = "Failed: Code: " + err.code + "\n State: " + err.state; errMessage += "\n Message: " + err.message + v_sqlCode; errMessage += "\nStack Trace:\n" + err.stackTraceTxt + v_sqlCode; throw 'Encountered error in executing v_sqlCode. \n' + errMessage; } $$; Call: CALL CREATE_SCHEMA('SCHEMA_NAME');
Inserting into a table snowflake with a passed variable
I have a table which has 3 attributes ATTRIBUTE DATATYPE FLAG( 0 OR 1) My stored procedure takes a string of records in the form "Att1 Datatype1 1, Att2 Datatype2 0, Att3 Datatype3 0...) so for example a passed string can be "DATABASE VARCHAR 1, SCHEMA VARCHAR 1, TIMESTAMP TIMESTAMP 0" My code gets the column names of the table and stores it in a string My code takes the passed string and puts them into array elements seperated by the , However the issue arrises when I try to do an insertion into my table I keep getting the error "invalid Identifier" CREATE OR REPLACE PROCEDURE "ADMINDB"."TOOLKIT".ADD_ATTRIBUTES_SESSION_META ("P_ATTRIBUTE_DATATYPE_FLAG" VARCHAR(16777216)) RETURNS VARCHAR LANGUAGE JAVASCRIPT COMMENT='Add Attributes to the table SESSION_ATTRIBUTES_META' EXECUTE AS CALLER AS $$ var v_list = P_ATTRIBUTE_DATATYPE_FLAG; var arr_list = []; arr_list = P_ATTRIBUTE_DATATYPE_FLAG.split(','); var v_string; var arr_col_att = []; var v_sqlCode = `SELECT * FROM ` + "ADMINDB" + "." + "TOOLKIT" + "." + "SESSION_ATTRIBUTES_META"; try{ var sqlStmt = snowflake.createStatement({sqlText : v_sqlCode}); var sqlRS = sqlStmt.execute(); }catch(err){ errMessage = "Failed: Code: " + err.code + "\n State: " + err.state; errMessage += "\n Message: " + err.message + v_sqlCode; errMessage += "\nStack Trace:\n" + err.stackTraceTxt + v_sqlCode; throw 'Encountered error in executing v_sqlCode. \n' + errMessage; } for (i = 1; i <= sqlStmt.getColumnCount(); i++) { arr_col_att.push(sqlStmt.getColumnName(i)); } arr_col_att[0] = arr_col_att[0].replace(/\s/g, ','); var v_col_att = arr_col_att.toString(); v_string = arr_list[0].toString(); v_string = v_string.replace(/\s/g, ','); v_sqlCode = `INSERT INTO ` + "ADMINDB" + "." + "TOOLKIT" + "." + "SESSION_ATTRIBUTES_META" + `(` + v_col_att + `) VALUES( ` + v_string + `)`; try{ var sqlStmt = snowflake.createStatement({sqlText : v_sqlCode}); var sqlRS = sqlStmt.execute(); }catch(err){ errMessage = "Failed: Code: " + err.code + "\n State: " + err.state; errMessage += "\n Message: " + err.message + v_sqlCode; errMessage += "\nStack Trace:\n" + err.stackTraceTxt + v_sqlCode; throw 'Encountered error in executing v_sqlCode. \n' + errMessage; } return "SUCCESS!"; $$; CALL "ADMINDB"."TOOLKIT".ADD_ATTRIBUTES_SESSION_META('DATABASE VARCHAR 1,SCHEMA VARCHAR 1,TIMESTAMP TIMESTAMP 0');
Modifying line 33 and 36, fixes the issue: CREATE OR REPLACE PROCEDURE "ADMINDB"."TOOLKIT".ADD_ATTRIBUTES_SESSION_META ("P_ATTRIBUTE_DATATYPE_FLAG" VARCHAR(16777216)) RETURNS VARCHAR LANGUAGE JAVASCRIPT COMMENT='Add Attributes to the table SESSION_ATTRIBUTES_META' EXECUTE AS CALLER AS $$ var v_list = P_ATTRIBUTE_DATATYPE_FLAG; var arr_list = []; arr_list = P_ATTRIBUTE_DATATYPE_FLAG.split(','); var v_string; var arr_col_att = []; var v_sqlCode = `SELECT * FROM ` + "ADMINDB" + "." + "TOOLKIT" + "." + "SESSION_ATTRIBUTES_META"; try{ var sqlStmt = snowflake.createStatement({sqlText : v_sqlCode}); var sqlRS = sqlStmt.execute(); }catch(err){ errMessage = "Failed: Code: " + err.code + "\n State: " + err.state; errMessage += "\n Message: " + err.message + v_sqlCode; errMessage += "\nStack Trace:\n" + err.stackTraceTxt + v_sqlCode; throw 'Encountered error in executing v_sqlCode. \n' + errMessage; } for (i = 1; i <= sqlStmt.getColumnCount(); i++) { arr_col_att.push(sqlStmt.getColumnName(i)); } arr_col_att[0] = arr_col_att[0].replace(/\s/g, ','); var v_col_att = arr_col_att.toString(); v_string = arr_list[0].toString(); v_string = v_string.replace(/\s/g, "','" ); v_sqlCode = `INSERT INTO ` + "ADMINDB" + "." + "TOOLKIT" + "." + "SESSION_ATTRIBUTES_META" + `(` + v_col_att + `) VALUES( '` + v_string + `' )`; try{ var sqlStmt = snowflake.createStatement({sqlText : v_sqlCode}); var sqlRS = sqlStmt.execute(); }catch(err){ errMessage = "Failed: Code: " + err.code + "\n State: " + err.state; errMessage += "\n Message: " + err.message + v_sqlCode; errMessage += "\nStack Trace:\n" + err.stackTraceTxt + v_sqlCode; throw 'Encountered error in executing v_sqlCode. \n' + errMessage; } return "SUCCESS!"; $$; CALL "ADMINDB"."TOOLKIT".ADD_ATTRIBUTES_SESSION_META('DATABASE VARCHAR 1,SCHEMA VARCHAR 1,TIMESTAMP TIMESTAMP 0'); As far I see, you haven't implemented the process multiple records in the argument for now, but I think you can handle it.
PostgresSql + Nodejs (ClaudiaJS) : How to cast array of string to array of timestamp
I am writing API which insert into a table with multiple rows, I am using UNNEST to make it work. What I have done: in js file: api.post(PREFIX + '/class/insert', function (request) { var db = pgp(dbconnect); //Params var data = request.body; //should be an array var classes = []; var starts = []; var ends = []; for (var i = 0; i < data.length; i++) { classes.push(data[i].class_id); starts.push(data[i].timestamp_start); ends.push(data[i].timestamp_end); } const PQ = require('pg-promise').ParameterizedQuery; var sql = "INSERT INTO sa1.class(class_id, timestamp_start, timestamp_end) " + "VALUES( "+ "UNNEST(ARRAY" + JSON.stringify(classes).replace(/"/g, "'") + "), " + "UNNEST(ARRAY" + JSON.stringify(starts).replace(/"/g, "'") + "), " + "UNNEST(ARRAY" + JSON.stringify(ends).replace(/"/g, "'") + ")" const final_sql = new PQ(sql); return db.any(final_sql) .then(function (data) { pgp.end(); return 'successful'; }) .catch(function (error) { console.log("Error: " + error); pgp.end(); }); } Request body [{ "class_id":"1", "timestamp_start":"2017-11-14 14:01:23.634437+00", "timestamp_end":"2017-11-14 15:20:23.634437+00" }, { "class_id":"2", "timestamp_start":"2017-11-14 15:01:23.634437+00", "timestamp_end": "2017-11-14 16:20:23.634437+00" }] When I run api in postman, I get the error is: column "timestamp_start" is of type timestamp with time zone but expression is of type text Issue is obviously from ARRAY of string that I used in sql, my question is how to create ARRAY of timestamp for UNNEST, or any suggestion are appreciated. Thanks
Never initialize the database inside the handler, see: Where should I initialize pg-promise Never call pgp-end() inside HTTP handlers, it destroys all connection pools. Use static ColumnSet type to generate multi-insert queries. Do not return from db.any, there is no point in that context You must provide an HTTP response within an HTTP handler You are providing a confusing semantics for column class_id. Why is it called like that and yet being converted into a timestamp? Never concatenate objects with strings directly. Never concatenate SQL strings manually, it will break formatting and open your code to SQL injection. Use Database methods according to the expected result, i.e. none in your case, and not any. See: https://github.com/vitaly-t/pg-promise#methods Initialize everything needed only once: const db = pgp(/*connection*/); const cs = new pgp.helpers.ColumnSet([ 'class_id', { name: 'timestamp_start', cast: 'timestamp' }, { name: 'timestamp_end', cast: 'timestamp' } ], {table: {table: 'class', schema: 'sa1'}}); Implement the handler: api.post(PREFIX + '/class/insert', request => { const sql = pgp.helpers.insert(request.body, cs); db.none(sql) .then(data => { // provide an HTTP response here }) .catch(error => { console.log('Error:', error); // provide an HTTP response here }); }
Many thanks to #JustMe, It worked after casting array var sql = "INSERT INTO sa1.class(class_id, timestamp_start, timestamp_end) " + "VALUES( "+ "UNNEST(ARRAY" + JSON.stringify(classes).replace(/"/g, "'") + "), " + "UNNEST(ARRAY" + JSON.stringify(starts).replace(/"/g, "'") + "::timestamp[]), " + "UNNEST(ARRAY" + JSON.stringify(ends).replace(/"/g, "'") + "::timestamp[])"