I'm currently practising on SQLite and making a simple text based RPG, but I need some advice with table structure.
So far I've come up with a "Player" table which stores the Player information.
An "Inventory" table, connected to its "Player" ID.
An "Item" table that holds all the Items.
Here is my issue. I have a "Weapon" model, "Shield", "Chest", "Legs" etc. etc. for each Item-type equipment, which holds maybe 50-100 items each. Should I store ALL items in a long list of "Item" table or should I make Sub-Tables? Like a "Weapon" table, a "Shield" table etc. and remove the "Item" table?
Thank you for your time!
The correct answer should be to follow recognised realtional database design guidelines which would very much depend upon the full functionality requirements of the game.
However, I'd suggest that the resolution would be neither just a list of items nor just separate tables for item types. Rather a table for items which has a column for the "type" which references tables (Weapon, Armour, collectables(etc)) and perhaps a type table.
Say for example a Weapon had a force value (how hard it hits) and a speed value (how frequently it hits), but armour only has a defence value and collectables had a weight value. The item list could be quite a complicated affair i.e. in this simple scenario that's 4 additional columns, with quite a bit of redundancy i.e. collectables and armour only utilise 25%, whilst 50% for a weapon, so perhaps you could introduce more complicated processing to utilise just 2 columns.
SQLite wise perhaps the tables could be :-
CREATE TABLE IF NOT EXISTS rpg_player (_id INTEGER PRIMARY KEY, player_name TEXT);
CREATE TABLE IF NOT EXISTS rpg_item(_id INTEGER PRIMARY KEY, type_reference INTEGER, item_subtype_reference INTEGER, UNIQUE(type_reference,item_subtype_reference));
CREATE TABLE IF NOT EXISTS rpg_inventory(player_id INTEGER, item_id INTEGER, number_held INTEGER, PRIMARY KEY(player_id, item_id)) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS rpg_item_types(_id INTEGER PRIMARY KEY, type_name TEXT, type_flags INTEGER);
CREATE TABLE IF NOT EXISTS rpg_weapon(weapon_id INTEGER PRIMARY KEY, weapon_name TEXT, weapon_flags INTEGER, weapon_force INTEGER, weapon_speed INTEGER);
CREATE TABLE IF NOT EXISTS rpg_armour(armour_id INTEGER PRIMARY KEY, armour_name TEXT, armour_flags INTEGER, armour_defence INTEGER);
CREATE TABLE IF NOT EXISTS rpg_collectable(collectable_id INTEGER PRIMARY KEY, collectable_name TEXT, collectable_flags INTEGER, collectable_weight INTEGER);
These say are populated as :-
Player Table
Item Table
The master Item table catering for all items to be easily referenced. (e.g. in the inventory). An entry has it's own unqiue id that references the type (weapon, armour.....) and then the item within the sub type table:-
The first row has a unique id of 1, (1st column) the item is of type 2(2nd column) (id 2 in the rpg_item_types table) (Armour) and is the item of armour that has an id of 5 (3rd column) in the rpg_armour table (Arm Thinggies).
Likewise item 3 is a Weapon (column 1 is 1, so type 1) that being the weapon that has an id in the weapon table of 2 (Great Sword).
Only references other tables but all items have a unique id
type_reference is the type whilst item_subtype_reference is the id of the item in that respective type table (weapon, armour, collecatble).
A table Constraint is set so that a combination of type_reference and item_subtype_reference must be unique as per UNIQUE(type_reference,item_subtype_reference)
Inventory Table
Item Types
This table has an entry for each subclass of items.
Sub Item Tables
Tables that model the specifics of the item e.g. rpg_weapon has a weapon_force and weapon_speed column, whilst rpg_armour only has an armour_defence column
You could create a simple list (output wise) of the Items using the following :-
--LIST ALL ITEMS
SELECT
CASE
WHEN type_name = 'Weapon' THEN weapon_name || ' Type (' || type_name || ')'
WHEN type_name = 'Armour' THEN armour_name || ' Type (' || type_name || ')'
WHEN type_name = 'Collectable' THEN collectable_name || ' Type (' || type_name || ')'
END AS description
FROM rpg_item
JOIN rpg_item_types ON type_reference = rpg_item_types._id
LEFT JOIN rpg_weapon ON item_subtype_reference = weapon_id
LEFT JOIN rpg_armour ON item_subtype_reference = armour_id
LEFT JOIN rpg_collectable ON item_subtype_reference = collectable_id
Resulting in :-
The following would list the all inventory items :-
SELECT
CASE
WHEN rpg_item.type_reference = 1 THEN 'Player - ' || player_name || ' has ' || weapon_name || ' it is a ' || type_name || ' it has a force of ' || weapon_force
WHEN rpg_item_types.type_name = 'Armour' THEN 'Player - ' || player_name || ' has ' || armour_name || ' it is a ' || type_name || ' with a defence rating of ' || armour_defence
WHEN rpg_item.type_reference = 3 THEN 'Player - ' || player_name || ' has ' || collectable_name || ' it is a ' || type_name || ' it has a weight of ' || collectable_weight
END AS description
FROM rpg_inventory
JOIN rpg_player ON player_id = rpg_player._id
JOIN rpg_item ON rpg_inventory.item_id = rpg_item._id
JOIN rpg_item_types ON rpg_item.type_reference = rpg_item_types._id
LEFT JOIN rpg_weapon ON rpg_item.item_subtype_reference = rpg_weapon.weapon_id
LEFT JOIN rpg_armour ON rpg_item.item_subtype_reference = rpg_armour.armour_id
LEFT JOIN rpg_collectable ON rpg_item.item_subtype_reference =rpg_collectable.collectable_id
The result based upon the above (ooops no weapons held by anyone) :-
A simple WHERE clause (WHERE player_id = ?) would restrict the list to a single player.
e.g. WHERE player_id = 2 would only list Fredrica's inventory.
You may well want to copy and paste the above and use it in an SQLite tool, there's quite a few around (I'm personally quite happy with SQLite Manager, other's would recommend other tools, all of the above was done using such a tool). There's probably quite a good chance that you could create the core Database access functionality just using such a tool.
Related
We have a requirement where some fields in a table need to have the same value as their ID. Unfortunately, we currently have to insert a new record and then, if needed, run another update to set the duplicate field (ID_2) value to equal the ID.
Here is the Android Sqlite code:
mDb.beginTransaction();
// ... setting various fields here ...
ContentValues contentValues = new ContentValues();
contentValues.put(NAME, obj.getName());
// now insert the record
long objId = mDb.insert(TABLE_NAME, null, contentValues);
obj.setId(objId);
// id2 needs to be the same as id:
obj.setId2(objId);
// but we need to persist it so we update it in a SECOND call
StringBuilder query = new StringBuilder();
query.append("update " + TABLE_NAME);
query.append(" set " + ID_2 + "=" + objId);
query.append(" where " + ID + "=" + objId);
mDb.execSQL(query.toString());
mDb.setTransactionSuccessful();
As you can see, we are making a second call to set ID_2 to the same value of ID. Is there any way to set it at INSERT time and avoid the second call to the DB?
Update:
The ID is defined as follows:
ID + " INTEGER PRIMARY KEY NOT NULL ," +
The algorithm used for autoincrementing columns is documented, so you could implement it manually in your code, and then use the new value for the INSERT.
This is quite a ugly hack, but it may be possible :
with id_table as (
select coalesce(max(seq), 0) + 1 as id_column
from sqlite_sequence
where name = 'MY_TABLE'
)
insert into MY_TABLE(ID_1, ID_2, SOME, OTHER, COLUMNS)
select id_column, id_column, 'SOME', 'OTHER', 'VALUES'
from id_table
It only works if the table ID is an AUTOINCREMENT, and is therefore managed via the documented sqlite_sequence table.
I also have no idea what happen in case of concurrent executions.
You could use an AFTER INSERT TRIGGER e.g.
Create your table (at least for this example) so that ID_2 is defined as INTEGER DEFAULT -1 (0 or any negative value would be ok)
e.g. CREATE TABLE IF NOT EXISTS triggertest (_id INTEGER PRIMARY KEY ,name TEXT ,id_2 INTEGER DEFAULT -1);
Then you could use something like (perhaps when straight after the table is created, perhaps create it just before it's to be used and drop it after done with it ) :-
CREATE TRIGGER triggertesting001
AFTER INSERT ON triggertest
BEGIN
UPDATE triggertest SET id_2 = `_id`
WHERE id_2 = -1;
END;
Drop using DROP TRIGGER IF EXISTS triggertesting001;
Example usage (testing):-
INSERT INTO triggertest (name) VALUES('Fred');
INSERT INTO triggertest (name) VALUES('Bert');
INSERT INTO triggertest (name) VALUES('Harry');
Result 1 :-
Result 2 (trigger dropped inserts run again ):-
Result 3 (created trigger) same as above.
Result 4 (ran inserts for 3rd time) catch up i.e. 6 rows updated id_2 with _id.
I'd strongly suggest reading SQL As Understood By SQLite - CREATE TRIGGER
Alternative solution
An alternative approach could be to simply use :-
Before starting transaction, retrieve mynextid from table described below
ContentValues contentValues = new ContentValues();
contentValues.put(ID,mynextid);
contentvalues.put(ID_2,mynextid++);
contentValues.put(NAME, obj.getName());
Then at end of the transactions update/store the value of mynextid in a simple single column, single row table.
i.e. you are managing the id's (not too dissimilar to how SQLite manages id's when 'AUTOINCREMENT' is specified)
Performance wise, which one is better (for a single table) ?:
Multiple select in single query or
Single select in multiple queries?
Example:
/*table schema*/
create table ACTIVITY_TABLE (
ID integer primary key autoincrement,
INTERVAL_ID integer not null,
ACTIVITY_TYPE text ,
WIFI_LIST text,
COUNT integer not null );
/*Multipe selects in single query*/
select * from
(select sum(COUNT)/60 as Physical from ACTIVITY_TABLE where INTERVAL_ID >=1 and INTERVAL_ID <=3 and (ACTIVITY_TYPE="Still" or ACTIVITY_TYPE = "Walking" or ACTIVITY_TYPE = "Running" ) ),
(select sum(COUNT)/60 as vehicular from ACTIVITY_TABLE where INTERVAL_ID >=1 and INTERVAL_ID <=3 and (ACTIVITY_TYPE="InVehicle" ) );
For your example, you can modify the query in the following way which will be much more efficient, I didn't tested the query.
select
sum(case when activitytype='walking' or activitytype='still' or activitytype='running' then COUNT else 0 end )/60 as physical,
sum(case when activitytype='InVehicle' then COUNT else 0 end)/60 as vehicular
from
activity_table
where interval_id>=1 and interval_id<=3
My database has 6384 records and I am using the below query:
SELECT T.t_name, S.s_code, S.s_name, R.s_code, R.s_name, M.arrival_time, L.arrival_time, M.dest_time, M.train_id, S.id, R.id
FROM TRAIN_SCHEDULE M,
TRAIN_SCHEDULE L,
TRAIN T,
STATION S,
STATION R
WHERE S.s_name = 'Versova'
AND R.s_name = 'Ghatkopar'
AND M.arrival_time > '00:00:00'
AND M.arrival_time < L.arrival_time
AND M.train_id = L.train_id
AND M.dest_time = L.dest_time
AND T.id = M.train_id
AND S.id = M.station_id
AND R.id = L.station_id
This query takes 8 second to fetch the data.
I have also indexed my tables, but fetching time is reduced to only 2 seconds.
Schema:
CREATE TABLE [STATION] (
[id] INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
[s_code] VARCHAR(10) NOT NULL,
[s_name] VARCHAR(50) NOT NULL);
CREATE TABLE TRAIN_SCHEDULE(
id INT,
station_id INT,
train_id INT,
arrival_time NUM,
departure_time NUM,
dest_time NUM
);
CREATE TABLE TRAIN(id INT,t_name TEXT);
CREATE INDEX idx_arrival_time ON train_schedule (arrival_time);
CREATE INDEX idx_dest_time ON train_schedule (dest_time);
CREATE INDEX idx_id ON train (id);
How can I improve this?
You can check with EXPLAIN QUERY PLAN which indexes are being used.
In this query, the database needs to scan through the STATION table; an index on the name column would improve this (although not by much with such a small table):
CREATE INDEX Station_Name ON STATION(s_name);
Also, lookups on the TRAIN_SCHEDULE table are done over multiple columns.
The query optimizer cannot use more than one index per table instance, so you should create a multi-column index.
And a column with a non-equality comparison must come last (see the documentation):
CREATE INDEX Schedule_Station_Train_DestTime_ArrivalTime
ON TRAIN_SCHEDULE(station_id, train_id, dest_time, arrival_time);
Also execute ANALYZE once to help the optimizer pick the right index.
I am working with GTFS data on Android (SQlite). And I would like to improve performance when I do select queries in my database filled with GTFS data.
The query below select the stop times associated to a route at a stop:
The first sub query gets the daily stop times on thursday.
The second gets all the exception stop times which are not valid for TODAY (2013-07-25).
The third one gets all the exception stop time which are only valid for TODAY (2013-07-25).
Then I remove the non-valid one and add the valid one to the first sub query.
select distinct stop_times_arrival_time
from stop_times, trips, calendar
where stop_times_trip_id=trip_id
and calendar_service_id=trip_service_id
and trip_route_id='11821949021891616'
and stop_times_stop_id='3377699721872252'
and calendar_start_date<='20130725'
and calendar_end_date>='20130725'
and calendar_thursday=1
and stop_times_arrival_time>='07:40'
except
select stop_times_arrival_time
from stop_times, trips, calendar, calendar_dates
where stop_times_trip_id=trip_id
and calendar_service_id=trip_service_id
and calendar_dates_service_id = trip_service_id
and trip_route_id='11821949021891694'
and stop_times_stop_id='3377699720880977'
and calendar_thursday=1
and calendar_dates_exception_type=2
and stop_times_arrival_time > '07:40'
and calendar_dates_date = 20130725
union
select stop_times_arrival_time
from stop_times, trips, calendar, calendar_dates
where stop_times_trip_id=trip_id
and calendar_service_id=trip_service_id
and calendar_dates_service_id = trip_service_id
and trip_route_id='11821949021891694'
and stop_times_stop_id='3377699720880977'
and calendar_thursday=1
and calendar_dates_exception_type=1
and stop_times_arrival_time > '07:40'
and calendar_dates_date = 20130725;
It took about 15 seconds to compute (which is very long).
I am sure there is a better to do this query since I do 3 different queries (almost the same by the way) which take time.
Any idea how to improve it?
EDIT:
Here is the schema:
table|calendar|calendar|2|CREATE TABLE calendar (
calendar_service_id TEXT PRIMARY KEY,
calendar_monday INTEGER,
calendar_tuesday INTEGER,
calendar_wednesday INTEGER,
calendar_thursday INTEGER,
calendar_friday INTEGER,
calendar_saturday INTEGER,
calendar_sunday INTEGER,
calendar_start_date TEXT,
calendar_end_date TEXT
)
index|sqlite_autoindex_calendar_1|calendar|3|
table|calendar_dates|calendar_dates|4|CREATE TABLE calendar_dates (
calendar_dates_service_id TEXT,
calendar_dates_date TEXT,
calendar_dates_exception_type INTEGER
)
table|routes|routes|8|CREATE TABLE routes (
route_id TEXT PRIMARY KEY,
route_short_name TEXT,
route_long_name TEXT,
route_type INTEGER,
route_color TEXT
)
index|sqlite_autoindex_routes_1|routes|9|
table|stop_times|stop_times|12|CREATE TABLE stop_times (
stop_times_trip_id TEXT,
stop_times_stop_id TEXT,
stop_times_stop_sequence INTEGER,
stop_times_arrival_time TEXT,
stop_times_pickup_type INTEGER
)
table|stops|stops|13|CREATE TABLE stops (
stop_id TEXT PRIMARY KEY,
stop_name TEXT,
stop_lat REAL,
stop_lon REAL
)
index|sqlite_autoindex_stops_1|stops|14|
table|trips|trips|15|CREATE TABLE trips (
trip_id TEXT PRIMARY KEY,
trip_service_id TEXT,
trip_route_id TEXT,
trip_headsign TEXT,
trip_direction_id INTEGER,
trip_shape_id TEXT
)
index|sqlite_autoindex_trips_1|trips|16|
And here is the query plan:
2|0|0|SCAN TABLE stop_times (~33333 rows)
2|1|1|SEARCH TABLE trips USING INDEX sqlite_autoindex_trips_1 (trip_id=?) (~1 rows)
2|2|2|SEARCH TABLE calendar USING INDEX sqlite_autoindex_calendar_1 (calendar_service_id=?) (~1 rows)
3|0|3|SCAN TABLE calendar_dates (~10000 rows)
3|1|2|SEARCH TABLE calendar USING INDEX sqlite_autoindex_calendar_1 (calendar_service_id=?) (~1 rows)
3|2|0|SEARCH TABLE stop_times USING AUTOMATIC COVERING INDEX (stop_times_stop_id=?) (~7 rows)
3|3|1|SEARCH TABLE trips USING INDEX sqlite_autoindex_trips_1 (trip_id=?) (~1 rows)
1|0|0|COMPOUND SUBQUERIES 2 AND 3 USING TEMP B-TREE (EXCEPT)
4|0|3|SCAN TABLE calendar_dates (~10000 rows)
4|1|2|SEARCH TABLE calendar USING INDEX sqlite_autoindex_calendar_1 (calendar_service_id=?) (~1 rows)
4|2|0|SEARCH TABLE stop_times USING AUTOMATIC COVERING INDEX (stop_times_stop_id=?) (~7 rows)
4|3|1|SEARCH TABLE trips USING INDEX sqlite_autoindex_trips_1 (trip_id=?) (~1 rows)
0|0|0|COMPOUND SUBQUERIES 1 AND 4 USING TEMP B-TREE (UNION)
Columns that are used for lookups should be indexed, but for a single (sub)query, it is not possible to use more than one index per table.
For this particular query, the following additional indexes would help:
CREATE INDEX some_index ON stop_times(
stop_times_stop_id,
stop_times_arrival_time);
CREATE INDEX some_other_index ON calendar_dates(
calendar_dates_service_id,
calendar_dates_exception_type,
calendar_dates_date);
I have a query with a subquery that returns multiple rows.
I have a table with lists and a table with users. I created a many-to-many table between these two tables, called list_user.
LIST
id INTEGER
list_name TEXT
list_description TEXT
USER
id INTEGER
user_name TEXT
LIST_USER
id INTEGER
list_id INTEGER
user_id INTEGER
My query with subquery
SELECT * FROM user WHERE id = (SELECT user_id FROM list_user WHERE list_id = 0);
The subquery works (and I use it in code so the 0 is actually a variable) and it returns multiple rows. But the upper query only returns one row, which is pretty logical; I check if the id equals something and it only checks against the first row of the subquery.
How do I change my statement so I get multiple rows in the upper query?
I'm surprised the = works in SQLite. It would return an error in most databases. In any case, you want the in statement:
SELECT *
FROM list
WHERE id in (SELECT user_id FROM list_user WHERE list_id = 0);
For a better performance, use this query:
SELECT LIST.ID,
LIST.LIST_NAME,
LIST.LIST_DESCRIPTION
FROM LIST,
USER,
LIST_USER
WHERE LIST.ID = LIST_USER.USER_ID = USER.ID AND
LIST.LIST_ID = 0