iOS sqlite3_step hold / freeze after last row data - ios

My sqlite3_step holds for a 1s after read of last row data. Why?
-(NSDictionary*)specificationItemsForConfigurationsIds:(NSString*)configurationsIdsStr
{
[self databaseOpen];
NSString *query = [NSString stringWithFormat:#"SELECT SpecItem.id,SpecItem.name,ConfigurationSpec.configuration_id\
FROM (SpecItem INNER JOIN ConfigurationSpec ON ConfigurationSpec.spec_item_id=SpecItem.id)\
WHERE (SpecItem.parent_id=12 OR SpecItem.parent_id=34 OR SpecItem.id=23 OR SpecItem.id=27) AND ConfigurationSpec.configuration_id IN (%#)",configurationsIdsStr];
sqlite3_stmt *statement;
NSMutableDictionary* configurationsWithSpecItems = [NSMutableDictionary new];
if (sqlite3_prepare_v2(database, [query UTF8String], -1, &statement, nil) == SQLITE_OK)
{
while (sqlite3_step(statement) == SQLITE_ROW)
{
int specItemId = sqlite3_column_int(statement, 0);
NSString* specItemName = [self sqlite3_column_text_asString_ofStatement:statement
atColumn:1];
int configId = sqlite3_column_int(statement, 2);
NSString* configIdNumber = [NSString stringWithFormat:#"%d",configId];
NSMutableArray* specItems = [configurationsWithSpecItems objectForKey:configIdNumber];
if(specItems == nil)
{
specItems = [NSMutableArray new];
[configurationsWithSpecItems setObject:specItems
forKey:configIdNumber];
}
SpecificationItem* specItem = [SpecificationItem specificationItemWithId:specItemId
name:specItemName];
[specItems addObject:specItem];
// When we read last row data, getting from here to POINT 2 takes 1s
}
// POINT 2
sqlite3_finalize(statement);
}
[self databaseClose];
return configurationsWithSpecItems;
}
Single read of one row takes 2-3ms, but after last one getting out of while loop takes 1s, which is too much for me.
EXPLAIN QUERY PLAN output for this query:
0 0 1 SCAN TABLE Configuration (~100000 rows)
0 0 0 EXECUTE LIST SUBQUERY 1
1 0 0 SEARCH TABLE Configuration USING AUTOMATIC COVERING INDEX (model_id=?) (~7 rows)
0 1 0 SEARCH TABLE SpecItem USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)

There are two explanations for the delay; one or both might apply:
All the records that match are at the beginning of the Configuration table. After the last matching record, SQLite still has to search through all the remaining records, but none matches.
SQLite creates a temporary index on the model_id column because it estimates that the query would be even slower without it. After the query has finished, that index must be deleted again; what you see is the time needed to synchronize at the end of the (automatic) transaction.
Create an index on the model_id column will help avoiding both of these points.
If possible, you should try to merge the subquery (in configurationsIdStr) into the outer query; instead of:
... ConfigurationSpec.configuration_id IN (
SELECT configuration_id FROM Configuration WHERE model_id = 42)
use something like this:
... ConfigurationSpec.model_id = 42
Avoiding that indirection makes it much easier for SQLite to optimize the query execution.

Related

How to execute multiple select statement in one query in sqlite in iOS?

Is it possible to execute two or more select statement in one query in SQLite? For example,
We can execute create or insert query,
NSString *create_query = #"create table if not exists Employee (id integer primary key, firstName text, lastName text);create table if not exists Department (id integer primary key, department text, devision text)";
By using,
sqlite3_exec(self.contactDB,[create_query UTF8String], NULL, NULL, &errorMessage) == SQLITE_OK)
we can execute it.
But if query is something like,
NSString *select_query = #"select * from Employee;select * from Department";
Then is it possible to execute? If yes then how to differentiate data from sqlite3_step?
Generally we are fetching data like,
if (sqlite3_prepare_v2(self.contactDB, [select_query UTF8String], -1, &statement, NULL) == SQLITE_OK) {
NSLog(#"prepared from data get");
while (sqlite3_step(statement) == SQLITE_ROW) {
NSString *department = [[NSString alloc]initWithUTF8String:(const char*)sqlite3_column_text(statement, 1)];
NSString *devision = [[NSString alloc]initWithUTF8String:(const char *)sqlite3_column_text(statement, 2)];
NSLog(#"Department : %#, Devision : %#",department,devision);
}
NSLog(#"errror1 is %s",sqlite3_errmsg(self.contactDB));
}
But if there is a two select statement then how to identify column and row in sqlite3_step?
We can execute two select statements together (i.e. select * from Employee;select * from Department ) in terminal, so it should some way in iOS I think.
Yes, you can use sqlite3_exec() to execute two SELECT statements in one call. You just have to provide a callback function where you handle the result rows. I've never used that feature, and how I understand the doc you're on your own to distinguish the rows of each statement; perhaps one can use the column count for that.
I advise against using sqlite3_exec() that way; it seems rather tedious and error-prone. sqlite3_prepare_*() should be the way to go, and it can only handle one result set (SELECT query), but you can have open multiple statements at a time, represented by the ppStmt handle. If you have problems with that you should describe them instead of posting a XY Problem question.
We can perform this by using C style callback function with sqlite3_exec.
There is no proper code available on internet (I haven't found!) so i would like to answer with code sample.
We can implement C - style callback method like
int myCallback(void *parameter, int numberOfColumn, char **resultArr, char **column)
{
NSLog(#"number of column %d",numberOfColumn); // numberOfColumn is return total number of column for correspond table
NSString *columnName = [[NSString alloc]initWithUTF8String:column[0]]; // This will return column name column[0] is for first, column[1] for second column etc
NSLog(#"column name is %#",columnName);
NSString *result = [[NSString alloc]initWithUTF8String:resultArr[2]]; // resultArr returns value for row with respactive column for correspond table. resultArr[2] considered as third column.
NSLog(#"result is %#",result);
return 0;
}
And we can call this callback function in our sqlite3_exec function like,
NSString *getData = #"select * from Department;select * from Employee";
if (sqlite3_exec(self.contactDB, [getData UTF8String], myCallback, (__bridge void *)(self), &err) == SQLITE_OK ) {
if (err) {
NSLog(#"error : %s",err);
}
else {
NSLog(#"executed sucessfully");
}
}
We have make bride : (__bridge void *)(self) as parameter of sqlite3_exec. We can pass NULL in this case because we have implemented c style function. But if we implement Objective - c style function or method then we must pass (__bridge void *)(self) as parameter.
So, By callback function we can execute multiple queries in one statement whether it is select type queries or else.
Reference : One-Step Query Execution Interface

sqlite3 IOS query bugging table

i have this odd bug where i'm querying my table 'Children' with quite a complex query. It works fine, but for some reason it bugs a this other view from updating the database. You see this database holds stickers and one easy way to give them is to access this admin page, which is where its bugging. I can query the information fine, BUT ! when i update the table it hates and doesn't work. But its strange inside the core view controller it doesn't bug when i update the table there. I narrowed down the code to the cause of this problem:
-(void)leaderboardsystem
{
NSString *nexttargetsql = [NSString stringWithFormat:#"SELECT * FROM Children WHERE Completed > %d OR (Completed = %d AND Current_Stickers > %d) ORDER BY Completed ASC, Current_Stickers ASC LIMIT 1",completecount,completecount,stickercount]; //Queries table for the childs name and returns more data.
NSString *behindyousql = [NSString stringWithFormat:#"SELECT * FROM Children WHERE Completed < %d OR (Completed = %d AND Current_Stickers < %d) ORDER BY Completed DESC, Current_Stickers DESC LIMIT 1",completecount,completecount,stickercount];
nexttarget.text = [self leaderboardQuery:nexttargetsql];
behindyou.text = [self leaderboardQuery:behindyousql];
}
-(NSString*)leaderboardQuery:(NSString*)sql//does the querying
{
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(Childdb, [sql UTF8String], -1, &statement, nil)==SQLITE_OK) {
while (sqlite3_step(statement)==SQLITE_ROW) {
char *ffield1 = (char *) sqlite3_column_text(statement, 0);
NSString *ffield1Str = [[NSString alloc]initWithUTF8String:ffield1];
char *ffield2 = (char *) sqlite3_column_text(statement, 8);
NSString *ffield2Str = [[NSString alloc]initWithUTF8String:ffield2];
char *ffield3 = (char *) sqlite3_column_text(statement, 10);
NSString *ffield3Str = [[NSString alloc]initWithUTF8String:ffield3];
NSLog(#"Name:%#",ffield1Str);
NSLog(#"this is completecount: %#", ffield2Str);
NSLog(#"this is stickcount: %#",ffield3Str);
return ffield1Str;
}
}
return NULL;
}
whenever i call the method leaderboardsystem it causes this bug, but if i don't then it works fine ! funny enough, i was a little surprised to be honest. It surprises me because it affects a completely different view controller that has no connection to the main view. The table layout is:
[self createTable:#"Children" withField1:#"Name" withField2:#"Password" withField3:#"House" withField4:#"Sticker Collection" withField5:#"Tickets Gathered" withField6:#"Tickets Removed" withField7:#"Last Ticket Scanned" withField8:#"Current Tickets" withField9:#"Completed" withField10:#"Complete" withField11:#"Current_Stickers"];
This is the updating code that seems to fail when i include the leaderboard system in the main view(this is on a completely different view controller)
-(void)UpdateDatabase//update table, if value has been incremented
{
NSString *sql = [NSString stringWithFormat:#"UPDATE Children SET 'Current Tickets' = %d, 'Tickets Removed' = %d, 'Tickets Gathered' = %d WHERE Name = '%#'",[self.currenttickets.text integerValue], [self.removedtickets.text integerValue], [self.totaltickets.text integerValue], name];
[self updatetable:sql];
}
-(void)updatetable:(NSString*)sql
{
char *err;
if (sqlite3_exec(Childdb, [sql UTF8String], NULL, NULL, &err)!=SQLITE_OK) {
sqlite3_close(Childdb);
NSAssert(0, #"Could not update Table");
} else {
NSLog(#"Table updated");
}
}
I'm trying not to overload the description here and keeping it brief my program is quite large, if you require any more information let me know. BUT i guarantee that the leaderboard system is causing the problem. Thanks a million if you can solve this problem, been working on it all day ! :(
ALSO it also disturbs the place where i add records to the table, so the updating code is not causing it. Its that leaderboard query, no idea why :(
Okay, well i didn't want to do this. But instead of having a method to manage the query, i just repeated it twice, inside the method, but it worked ! Not sure what was wrong with it. Seems no one else could help me so, i'll just leave it to that.
You need to call sqlite3_finalize(Childdb). It would look something like the following:
-(NSString*)leaderboardQuery:(NSString*)sql//does the querying
{
sqlite3_stmt *statement;
if (sqlite3_prepare_v2(Childdb, [sql UTF8String], -1, &statement, nil)==SQLITE_OK) {
while (sqlite3_step(statement)==SQLITE_ROW) {
...
}
}
sqlite3_finalize(Childdb);
return NULL;
}
This is a really simple example because there is no error handling. The documentation that describes this is the following (located here):
*ppStmt is left pointing to a compiled prepared statement that can be executed using sqlite3_step(). If there is an error, *ppStmt is set to
NULL. If the input text contains no SQL (if the input is an empty
string or a comment) then *ppStmt is set to NULL. The calling
procedure is responsible for deleting the compiled SQL statement using
sqlite3_finalize() after it has finished with it. ppStmt may not be
NULL.
This example code may help you: https://github.com/ccgus/fmdb/blob/master/src/FMDatabase.m#L519

Is this Sqlite3 statement OK?

I'm a noob when it comes to sqlite and not quite sure how to do this.
I want a database with a bunch of row, containing one word each. When the user types a word, I will validate it by checking if its in the database.
Things I dont have, I guess, and don't know how to create, is an index? How do I insert that? How do I write the query to take advantage of index?
I also have two columns in there, "id and word". Is it good to have the id or does it just take up space?
This is what I got so far:
CREATE TABLE words (id INTEGER PRIMARY KEY, word VARCHAR(15));
I don't want words longer then 15 characters, so I set the VARCHAR(15);
INSERT INTO words(word) VALUES('hello');
INSERT INTO words(word) VALUES('bye');
etc. for all words
And to check a word:
NSString *sql = [NSString stringWithFormat:#"SELECT EXISTS(SELECT 1 FROM words WHERE word=\"%#\" LIMIT 1)", word];
const char *sqlStatement = [sql UTF8String];
if (sqlite3_prepare_v2(db, sqlStatement, -1, &selectStmt, NULL) == SQLITE_OK)
{
int count = 0;
while(sqlite3_step(selectStmt) == SQLITE_ROW)
{
count = sqlite3_column_int(selectStmt, 0);
}
NSLog(#"COUNT: %i",count);
//If count is 1, we have a match.
}
Yes. Your Statement is ok.
You can also use ' intead of ":
NSString *sql = [NSString stringWithFormat:#"SELECT EXISTS(SELECT 1 FROM words WHERE word='%#' LIMIT 1)", word];
Is it good to have the id or does it just take up space?
It depends on your need, I will suggest you should keep an Id field as primary key.
For creating index you can use:
CREATE INDEX yourIndexName ON yourTableName ( yourColumnName )
For more about indexing check sqlite

Rows order in SQLite Database (iOS)

I have a database with a table called 'connection', for simplicities' sake, let's say I only have one column which is called 'rowName'. Now let's say I add a row with rowName = a; now I add a row with rowName = q, and lastly I add a row with rowName = w (letters are completely random). Now, I irritate thru the results with the statement:
NSString * queryStatements = [NSString stringWithFormat:#"SELECT rowName, FROM tableName"];
and using the code:
NSMutableArray * rows = [[NSMutableArray alloc] init]; //create a new array
sqlite3_stmt * statement;
if(sqlite3_prepare_v2(databaseHandle, [queryStatements UTF8String], -1, &statement, NULL) == SQLITE_OK){
while (sqlite3_step(statement) == SQLITE_ROW){
NSString * rowName = [NSString stringWithUTF8String : (char*) sqlite_column_text(statement, 1)];
[rows addObject : connection];
} sqlite3_finalize(statement_;
}
In the array rows, will the object at index 0 be rowName = a, and at index 1 rowName=q, and at index 2 rowName = w? or will it be random? Is there a way to make it not-random?
Also, if i delete a row, will it have any affect on the other rows order?
Never depend on a sort order from your database. Always specify one if it is required.
SELECT rowName FROM tableName order by rowName
gives you the data sorted by rowName. If you need a different order, you need another column.
You can also sort your NSArray if need be.
What sort order are you looking for?

Multiple Queries not Running in FMDB

I'm using FMDB to create a SQLite database on iPhone. I have a initial.sql that is of the form
CREATE TABLE Abc ... ;
CREATE TABLE Def ... ;
I load this by loading the file into an NSString and running it
NSString * str = // string from file initial.sql
[db executeUpdate: str];
This succeeds but later on I get a failure:
no such table: Def
It's clear that the second statement is not being called. How can I do this so that all of the queries will be called?
According to the SQLite documentation:
"The routines sqlite3_prepare_v2(), sqlite3_prepare(), sqlite3_prepare16(), sqlite3_prepare16_v2(), sqlite3_exec(), and sqlite3_get_table() accept an SQL statement list (sql-stmt-list) which is a semicolon-separated list of statements."
So, this should all work.
I got bitten by this one too; it took me an entire morning of stepping through FMDatabase and reading the sqlite3 API documentation to find it. I am still not entirely sure about the root cause of the issue, but according to this bug in PHP, it is necessary to call sqlite3_exec instead of preparing the statement with sqlite3_prepare_v2 and then calling sqlite3_step.
The documentation does not seem to suggest that this behaviour would happen, hence our confusion, and I would love for someone with more experience with sqlite to come forward with some hypotheses.
I solved this by developing a method to execute a batch of queries. Please find the code below. If you prefer, you could rewrite this into a category instead of just adding it to FMDatabase.h, your call.
Add this to the FMDatabase interface in FMDatabase.h:
- (BOOL)executeBatch:(NSString*)sql error:(NSError**)error;
Add this to the FMDatabase implementation in FMDatabase.m:
- (BOOL)executeBatch:(NSString *)sql error:(NSError**)error
{
char* errorOutput;
int responseCode = sqlite3_exec(db, [sql UTF8String], NULL, NULL, &errorOutput);
if (errorOutput != nil)
{
*error = [NSError errorWithDomain:[NSString stringWithUTF8String:errorOutput]
code:responseCode
userInfo:nil];
return false;
}
return true;
}
Please note that there are many features missing from executeBatch which make it unsuitable for a lot of purposes. Specifically, it doesn't check to see if the database is locked, it doesn't make sure FMDatabase itself isn't locked, it doesn't support statement caching.
If you need that, the above is a good starting point to code it yourself. Happy hacking!
FMDB v2.3 now has a native wrapper for sqlite3_exec called executeStatements:
BOOL success;
NSString *sql = #"create table bulktest1 (id integer primary key autoincrement, x text);"
"create table bulktest2 (id integer primary key autoincrement, y text);"
"create table bulktest3 (id integer primary key autoincrement, z text);"
"insert into bulktest1 (x) values ('XXX');"
"insert into bulktest2 (y) values ('YYY');"
"insert into bulktest3 (z) values ('ZZZ');";
success = [db executeStatements:sql];
It also has a variant that employs the sqlite3_exec callback, implemented as a block:
sql = #"select count(*) as count from bulktest1;"
"select count(*) as count from bulktest2;"
"select count(*) as count from bulktest3;";
success = [db executeStatements:sql withResultBlock:^int(NSDictionary *dictionary) {
NSInteger count = [dictionary[#"count"] integerValue];
NSLog(#"Count = %d", count);
return 0; // if you return 0, it continues execution; return non-zero, it stops execution
}];
Split Batch Statement
Add in .h file:
#import "FMSQLStatementSplitter.h"
#import "FMDatabaseQueue.h"
FMSQLStatementSplitter can split batch sql statement into several separated statements, then [FMDatabase executeUpdate:] or other methods can be used to execute each separated statement:
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:databasePath];
NSString *batchStatement = #"insert into ftest values ('hello;');"
#"insert into ftest values ('hi;');"
#"insert into ftest values ('not h!\\\\');"
#"insert into ftest values ('definitely not h!')";
NSArray *statements = [[FMSQLStatementSplitter sharedInstance] statementsFromBatchSqlStatement:batchStatement];
[queue inDatabase:^(FMDatabase *adb) {
for (FMSplittedStatement *sqlittedStatement in statements)
{
[adb executeUpdate:sqlittedStatement.statementString];
}
}];

Resources