I'm quite new in web app so apologize if my question is abit basic. I'm developing a Web app with R shiny where the inputs are very large tables from Azure SQL server. They are 20 tables each in the order of hundred-thousand rows and hundreds of columns containing numbers, Characters and etc. I have no problem calling them, my main issue is that it takes so much time to fetch everything from Azure SQL server. It takes approximately 20 minutes. So the user of the web app needs to wait quite a long. I'm using DBI package as follows:
db_connect <- function(database_config_name){
dbConfig <- config::get(database_config_name)
connection <- DBI::dbConnect(odbc::odbc(),
Driver = dbConfig$driver,
Server = dbConfig$server,
UID = dbConfig$uid,
PWD = dbConfig$pwd,
Database = dbConfig$database,
encoding = "latin1"
)
return(connection)
}
and then fetching tables by :
connection <- db_connect(db_config_name)
table <- dplyr::tbl(con, dbplyr::in_schema(fetch_schema_name(db_config_name,table_name,data_source_type), fetch_table_name(db_config_name,table_name,data_source_type)))
I searched a lot but didn't come across a good solution, I appreciate any solutions can tackle this problem.
CodePudding user response:
I work with R accessing SQL Server (not Azure) daily. For larger data (as in your example), I always revert to using the command-line tool sqlcmd, it is significantly faster. The only pain point for me was learning the arguments and working around the fact that it does not return proper CSV, there is post-query munging required. You may have an additional pain-point of having to adjust my example to connect to your Azure instance (I do not have an account).
In order to use this in a shiny environment and preserve its interactivity, I use the processx package to start the process in the background and then poll its exit status periodically to determine when it has completed.
Up front: this is mostly a "loose guide", I do not pretend that this is a fully-functional solution for you. There might be some rough-edges that you need to work through yourself. For instance, while I say you can do it asynchronously, it is up to you to work the polling process and delayed-data-availability into your shiny application. My answer here provides starting the process and reading the file once complete. And finally, if encoding= is an issue for you, I don't know if sqlcmd does non-latin correctly, and I don't know if or how to fix this with its very limited, even antiquated arguments.
Steps:
Save the query into a text file. Short queries can be provided on the command-line, but past some point (128 chars? I don't know that it's clearly defined, and have not looked enough recently) it just fails. Using a query-file is simple enough and always works, so I always use it.
I always use temporary files for each query instead of hard-coding the filename; this just makes sense. For convenience (for me), I use the same tempfile base name and append
.sqlfor the query and.csvfor the returned data, that way it's much easier to match query-to-data in the temp files. It's a convention I use, nothing more.tf <- tempfile() # using the same tempfile base name for both the query and csv-output temp files querytf <- paste0(tf, ".sql") writeLines(query, querytf) csvtf <- paste0(tf, ".csv") # these may be useful in troubleshoot, but not always [^2] stdouttf <- paste0(tf, ".stdout") stderrtf <- paste0(tf, ".stderr")Make the call. I suggest you see how fast this is in a synchronous way first to see if you need to add an async query and polling in your shiny interface.
exe <- "/path/to/sqlcmd" # or "sqlcmd.exe" args <- c("-W", "b", "-s", "\037", "-i", querytf, "-o", csvtf, "-S", dbConfig$server, "-d", dbConfig$database, "-U", dbConfig$uid, "-P", dbConfig$pwd) ## as to why I use "\037", see [^1] ## note that the user id and password will be visible on the shiny server ## via a `ps -fax` command-line call proc <- processx::process$new(command = exe, args = args, stdout = stdouttf, stderr = stderrtf) # other args exist # this should return immediately, and should be TRUE until # data retrieval is done (or error) proc$is_alive() # this will hang (pause R) until retrieval is complete; if/when you # shift to asynchronous queries, do not do this proc$wait()One can use
processx::runinstead ofprocess$newandproc$wait(), but I thought I'd start you down this path in case you want/need to go asynchronous.If you go with an asynchronous operation, then periodically check (perhaps every 3 or 10 seconds)
proc$is_alive(). Once that returnsFALSE, you can start processing the file. During this time, shiny will continue to operate normally. (If you do not go async and therefore choose toproc$wait(), then shiny will hang until the query is complete.)If you make a mistake and do not
proc$wait()and try to continue with reading the file, that's a mistake. The file may not exist, in which case it will err withNo such file or directory. The file may exist, perhaps empty. It may exist and have incomplete data. So really, make a firm decision to stay synchronous and therefore callproc$wait(), or go asynchronous and poll periodically untilproc$is_alive()returnsFALSE.Reading in the file. There are three "joys" of using
sqlcmdthat require special handling of the file.- It does not do embedded quotes consistently, which is why I chose to use
"\037"as a separator. (See [^1].) - It adds a line of dashes under the column names, which will corrupt the auto-classing of data when R reads in the data. For this, we do a two-step read of the file.
- Nulls in the database are the literal
NULLstring in the data. For this, we update thena.strings=argument when reading the file.
exitstat <- proc$get_exit_status() if (exitstat == 0) { ## read #1: get the column headers tmp1 <- read.csv(csvtf, nrows = 2, sep = "\037", header = FALSE) colnms <- unlist(tmp1[1,], use.names = FALSE) ## read #2: read the rest of the data out <- read.csv(csvtf, skip = 2, header = FALSE, sep = "\037", na.strings = c("NA", "NULL"), quote = "") colnames(out) <- colnms } else { # you should check both stdout and stderr files, see [^2] stop("'sqlcmd' exit status: ", exitstat) }- It does not do embedded quotes consistently, which is why I chose to use
Note:
After a lot of pain with several issues (some in
sqlcmd.exe, some indata.table::freadand other readers, all dealing with CSV-format non-compliance), at one point I chose to stop working with comma-delimited returns, instead opting for the"\037"field Delimiter. It works fine with all CSV-reading tools and has fixed so many problems (some not mentioned here). If you're not concerned, feel free to change the args to"-s", ","(adjusting the read as well).sqlcmdseems to use stdout or stderr in different ways when there are problems. I'm sure there's rationale somewhere, but the point is that if there is a problem, check both files.I added the use of both
stdout=andstderr=because of a lot of troubleshooting I did, and continue to do if I munge a query. Using them is not strictly required, but you might be throwing caution to the wind if you omit those options.By the way, if you choose to only use
sqlcmdfor all of your queries, there is no need to create a connection object in R. That is,db_connectmay not be necessary. In my use, I tend to use "real" RDBIconnections for known-small queries and the bulksqlcmdfor anything above around 10K rows. There is a tradeoff; I have not measured it sufficiently in my environment to know where the tipping point is, and it is likely different in your case.
