Using a Primary Key with a WithoutRowID

风流意气都作罢 提交于 2020-05-13 18:54:04

问题


I am trying to squeeze every little bit of performance out of SQLite and I have a problem which seems to be odd, in that the functionality seems pointless in SQLite.

Take for example:

CREATE TABLE "A_TEST" ( "ID" INTEGER PRIMARY KEY ,  "X" TEXT NULL) WITHOUT ROWID

then try to insert a record:

Insert into A_TEST (X) VALUES('Test String')

You will get an error of "NOT NULL constraint failed"

Does this mean, with a WithoutRowID, I have to specify my own Primary Key Value when inserting?

The reason why I think the WithoutRowID is pointless is that:

  1. You have to specify your own Primary Key Value which makes any mass insert select statement redundant as I would have to specify my own value in the primary key when inserting....

  2. I will in effect, have 2 primary keys if I don't use WithoutRowID, because SQLite manages its own RowID as well as my own Primary Key value. On the 1.7GB database, having WithoutRowID reduces the size of the indexes in the file to just 1.3GB so 400MB difference is pretty huge savings.

Please tell me that I don't have to provide my own Primary Key ID and that it will in fact provide a Unique ID against a Primary Key if it is an INTEGER.


回答1:


and I just found that SQLite 4 is available - and it looks (from initial reading of the web page) that Primary Keys will indeed be REAL primary keys without the ROWID! Whoop Whoop. http://sqlite.org/src4/doc/trunk/www/design.wiki




回答2:


There are three kinds of tables in SQLite:

  1. WITHOUT ROWID tables, which are stored as a B-tree sorted by the declared primary key;
  2. rowid tables with an INTEGER PRIMARY KEY (where the PK column is an alias for the internal rowid), which are stored as a B-tree sorted by the declared primary key;
  3. rowid tables with any other (or no) primary key, which are stored as a B-tree sorted by the internal rowid.

1. and 2. are pretty much identical, except that you do not get autoincrementing with 1.

So if you want to have autoincrementing, just drop the WITHOUT ROWID (to move from case 1 to case 2). The WITHOUT ROWID is an improvement only to case 3, where the primary key constraint would require a separate index.




回答3:


Does this mean, with a WithoutRowID, I have to specify my own Primary Key Value when inserting?

Yes (although perhaps no with some effort or restrictions as per below), as if you try :-

    CREATE TABLE IF NOT EXISTS table001 (col1 INTEGER, col2 INTEGER) WITHOUT ROWID;

Then you get something along the lines of :-

SQLiteManager: Likely SQL syntax error: CREATE TABLE IF NOT EXISTS table001 (col1 INTEGER, col2 INTEGER) WITHOUT ROWID; [ PRIMARY KEY missing on table table001 ]
Exception Name: NS_ERROR_FAILURE
Exception Message: Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [mozIStorageConnection.createStatement]

So you need to have a PRIMARY KEY and SQLite is not going to provide you with a value(s) as by specifying WITHOUT ROWID you've told SQLite that you don't want that feature.

Changing the above to :-

CREATE TABLE IF NOT EXISTS table001 (col1 INTEGER, col2 INTEGER, PRIMARY KEY (col1,col2)) WITHOUT ROWID;

works. However, you need to specify values.

The above is useful say for associative tables where it may be more efficient if the main index is according to the columns that you will reference, rather than by the rowid, which would basically be in insert order. Of course for such tables you would likely not be inserting a row without knowing the values of the columns.

So there is a use for WITHOUT ROWID tables.

If you really wanted you could very easily introduce an automatically incrementing number using a 'TRIGGER' accompanied by a table with a single row/column that stores the lastused or next number to use but then why would you forgo rowid's simply to replicate what rowid's do for you anyway.

Another alternative if all you wanted as a unique key would be to use CURRENT_TIMESTAMP as the default value. However, you couldn't do mass inserts as the interval between inserts would result in a UNIQUE constraint conflict.

I will in effect, have 2 primary keys if I don't use WithoutRowID, because SQLite manages its own RowID as well as my own Primary Key value. On the 1.7GB database, having WithoutRowID reduces the size of the indexes in the file to just 1.3GB so 400MB difference is pretty huge savings.

Please tell me that I don't have to provide my own Primary Key ID and that it will in fact provide a Unique ID against a Primary Key if it is an INTEGER.

Surely your "my own Primary Key value" isn't magically generated i.e you know the value (assuming it's not just a copy of the rowid) so that would used for the Primary Key.

What is a little interesting is that using :-

CREATE TABLE IF NOT EXISTS table002 (pk INTEGER PRIMARY KEY, col1 TEXT) 
WITHOUT ROWID;
INSERT INTO table002 VALUES
    (2,'fred'),
    (3,'bert'),
    (-100,'alfred'),
    ('june','mary');

Shows that using WITHOUT ROWID provides greater flexibility of the PRIMARY KEY as it is not restricted to an INTEGER as is a rowid, there again neither would TEXT PRIMARY KEY.....

i.e the above results in :-

Another consideration for Android

WITHTOUT ROWID was introduced in Version 3.8.2 some devices may not have that or a higher release installed so the WITHOUT ROWID may not even be an option for some Android Apps.

Example if you really wanted :-

This is an example of managing your own psuedo_autoincrement. This instead of incrementing by 1 increments according to a specified amount (10 in this example). It's not complete as it has no upper limit checking/handling.

Here's the result after inserting 3 rows :-

and here's the accompanying sequence table (ready for the next insert i.e _seq is 31) :-

  • _incby is the amount to increment by (10 in this case)
  • _offset column would jump to 1, 2 ...9 (after 9 max allowed) for each time _limit is reached. So when offset is 1 (after 5000 inserts) so 2, 12 ,22 ..... would be used. However, this hasn't been implemented.
  • I guess you could use the above to have an "autodecrement" with a little fiddling.

The key (pun unintentional) is the TRIGGER :-

CREATE TRIGGER seqtrg_mytable 
AFTER INSERT ON mytable 
BEGIN 
    UPDATE my_sequence SET _seq = _seq + _inc_by 
    WHERE _name = 'mytable';
END

And also the INSERT :-

INSERT INTO mytable VALUES ((SELECT _seq FROM my_sequence) ,'Test001')

This as checked/used on Android using the following :-

DBHelper.java

public class DBHelper extends SQLiteOpenHelper {

    public static final String DBNAME = "weird";
    public static final int DBVERSION = 1;
    public static final String TBMYSEQ = "my_sequence";
    public static final String COL_MYSEQ_NAME = "_name";
    public static final String COL_MYSEQ_SEQ = "_seq";
    public static final String COL_MYSEQ_INCBY = "_inc_by";
    public static final String COL_MYSEQ_OFFSET = "_offset";
    public static final String COl_MYSEQ_LIMIT = "_limit";

    public static final String TBMYTABLE = "mytable";
    public static final String COL_SPECIALIX = "_special_primary_autoinc_index";
    public static final String COL_MYVALUE = "myvalue";
    public static final int MYTABLE_INCYBY = 10;
    public static final int MYTABLE_LIMIT = 50000;

    SQLiteDatabase mDB;

    public DBHelper(Context context) {
        super(context, DBNAME, null, DBVERSION);
        mDB = this.getWritableDatabase();
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        String crt_myseq_tbl = "CREATE TABLE IF NOT EXISTS " +
                TBMYSEQ + "(" +
                COL_MYSEQ_NAME + " TEXT PRIMARY KEY," +
                COL_MYSEQ_SEQ + " INTEGER DEFAULT 1, " +
                COL_MYSEQ_INCBY + " INTEGER DEFAULT 1, " +
                COL_MYSEQ_OFFSET + " INTEGER DEFAULT 0," +
                COl_MYSEQ_LIMIT + " INTEGER NOT NULL" +
                ")";
        db.execSQL(crt_myseq_tbl);

        String crt_mytable_tbl = "CREATE TABLE IF NOT EXISTS " +
                TBMYTABLE + "(" +
                COL_SPECIALIX + " TEXT PRIMARY KEY DEFAULT -1," +
                COL_MYVALUE + " TEXT" +
                ")";
        db.execSQL(crt_mytable_tbl);
        insertMySeqRow(TBMYTABLE,MYTABLE_INCYBY,MYTABLE_LIMIT,db);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }

    private void insertMySeqRow(String for_table,int incby, int limit, SQLiteDatabase optionaldb) {
        ContentValues cv = new ContentValues();
        cv.put(COL_MYSEQ_NAME,for_table);
        cv.put(COL_MYSEQ_INCBY,incby);
        cv.put(COl_MYSEQ_LIMIT,limit);
        if (optionaldb == null) {
            optionaldb = mDB;
        }
        optionaldb.insert(TBMYSEQ,null,cv);

        String crt_trigger_sql =
                "CREATE TRIGGER IF NOT EXISTS seqtrg_" + for_table +
                        " AFTER INSERT ON " + for_table +
                        " BEGIN " +
                        " UPDATE " + TBMYSEQ +
                        " SET " + COL_MYSEQ_SEQ + " = " +
                        COL_MYSEQ_SEQ + " + " + COL_MYSEQ_INCBY +
                        " WHERE " + COL_MYSEQ_NAME + " = '" + for_table +
                        "';" +
                        "END";
        optionaldb.execSQL(crt_trigger_sql);
    }

    public void insertMyTableRow(String value) {
        String insertsql = "INSERT INTO " +TBMYTABLE +
                " VALUES (" +
                "(SELECT " + COL_MYSEQ_SEQ + " " +
                "FROM " + TBMYSEQ +
                ") " +
                ",'" + value + "')";
        mDB.execSQL(insertsql);
    }
}

MainActivty.java :-

public class MainActivity extends AppCompatActivity {

    DBHelper mDBHlpr;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDBHlpr = new DBHelper(this);
        mDBHlpr.insertMyTableRow("Test001");
        mDBHlpr.insertMyTableRow("Test002");
        mDBHlpr.insertMyTableRow("TEST003");
        Cursor csr1 = CommonSQLiteUtilities.getAllRowsFromTable(
                mDBHlpr.getWritableDatabase(),
                DBHelper.TBMYTABLE,
                true,
                null
        );
        CommonSQLiteUtilities.logCursorData(csr1);
        csr1.close();
        Cursor csr2 = CommonSQLiteUtilities.getAllRowsFromTable(
                mDBHlpr.getWritableDatabase(),
                DBHelper.TBMYSEQ,
                true,
                null
        );
        CommonSQLiteUtilities.logCursorData(csr2);
        csr2.close();
    }
}
  • Note Uses:- CommonSQLiteUtilities

Output was :-

04-22 11:57:53.120 2910-2910/? D/SQLITE_CSU: DatabaseList Row 1 Name=main File=/data/data/weird.weirdprimarykey/databases/weird
    PRAGMA -  sqlite_version = 3.7.11
    PRAGMA -  user_version = 1
04-22 11:57:53.128 2910-2910/? D/SQLITE_CSU: PRAGMA -  encoding = UTF-8
    PRAGMA -  auto_vacuum = 1
    PRAGMA -  cache_size = 2000
    PRAGMA -  foreign_keys = 0
    PRAGMA -  freelist_count = 0
    PRAGMA -  ignore_check_constraints = 0
04-22 11:57:53.132 2910-2910/? D/SQLITE_CSU: PRAGMA -  journal_mode = persist
    PRAGMA -  journal_size_limit = 524288
    PRAGMA -  locking_mode = normal
    PRAGMA -  max_page_count = 1073741823
    PRAGMA -  page_count = 7
    PRAGMA -  page_size = 4096
    PRAGMA -  recursive_triggers = 0
    PRAGMA -  reverse_unordered_selects = 0
    PRAGMA -  secure_delete = 0
04-22 11:57:53.136 2910-2910/? D/SQLITE_CSU: PRAGMA -  synchronous = 2
    PRAGMA -  temp_store = 0
    PRAGMA -  wal_autocheckpoint = 100


    Table Name = android_metadata Created Using = CREATE TABLE android_metadata (locale TEXT)
    Table = android_metadata ColumnName = locale ColumnType = TEXT Default Value = null PRIMARY KEY SEQUENCE = 0
    Number of Indexes = 0
    Number of Foreign Keys = 0
    Number of Triggers = 0
    Table Name = my_sequence Created Using = CREATE TABLE my_sequence(_name TEXT PRIMARY KEY,_seq INTEGER DEFAULT 1, _inc_by INTEGER DEFAULT 1, _offset INTEGER DEFAULT 0,_limit INTEGER NOT NULL)
    Table = my_sequence ColumnName = _name ColumnType = TEXT Default Value = null PRIMARY KEY SEQUENCE = 1
    Table = my_sequence ColumnName = _seq ColumnType = INTEGER Default Value = 1 PRIMARY KEY SEQUENCE = 0
    Table = my_sequence ColumnName = _inc_by ColumnType = INTEGER Default Value = 1 PRIMARY KEY SEQUENCE = 0
    Table = my_sequence ColumnName = _offset ColumnType = INTEGER Default Value = 0 PRIMARY KEY SEQUENCE = 0
    Table = my_sequence ColumnName = _limit ColumnType = INTEGER Default Value = null PRIMARY KEY SEQUENCE = 0
    Number of Indexes = 1
    INDEX NAME = sqlite_autoindex_my_sequence_1
        Sequence = 0
        Unique   = true
        Index Origin indicator unsupported
        Index Partial indicator unsupported
        INDEX COLUMN = _name COLUMN ID = 0 SEQUENCE = 0
    Number of Foreign Keys = 0
    Number of Triggers = 0
    Table Name = mytable Created Using = CREATE TABLE mytable(_special_primary_autoinc_index TEXT PRIMARY KEY DEFAULT -1,myvalue TEXT)
04-22 11:57:53.140 2910-2910/? D/SQLITE_CSU: Table = mytable ColumnName = _special_primary_autoinc_index ColumnType = TEXT Default Value = -1 PRIMARY KEY SEQUENCE = 1
    Table = mytable ColumnName = myvalue ColumnType = TEXT Default Value = null PRIMARY KEY SEQUENCE = 0
    Number of Indexes = 1
    INDEX NAME = sqlite_autoindex_mytable_1
        Sequence = 0
        Unique   = true
        Index Origin indicator unsupported
        Index Partial indicator unsupported
        INDEX COLUMN = _special_primary_autoinc_index COLUMN ID = 0 SEQUENCE = 0
    Number of Foreign Keys = 0
    Number of Triggers = 1
        TRIGGER NAME =seqtrg_mytable
        SQL = CREATE TRIGGER seqtrg_mytable AFTER INSERT ON mytable BEGIN  UPDATE my_sequence SET _seq = _seq + _inc_by WHERE _name = 'mytable';END


    Cursor has 3 rows and 2 Columns.
    Information for Row 1 offset = 0
        For Column _special_primary_autoinc_indexType is STRING value as String is 1 value as long is 1 value as double is 1.0
        For Column myvalueType is STRING value as String is Test001 value as long is 0 value as double is 0.0
    Information for Row 2 offset = 1
        For Column _special_primary_autoinc_indexType is STRING value as String is 11 value as long is 11 value as double is 11.0
        For Column myvalueType is STRING value as String is Test002 value as long is 0 value as double is 0.0
    Information for Row 3 offset = 2
        For Column _special_primary_autoinc_indexType is STRING value as String is 21 value as long is 21 value as double is 21.0
        For Column myvalueType is STRING value as String is TEST003 value as long is 0 value as double is 0.0
    Cursor has 1 rows and 5 Columns.
    Information for Row 1 offset = 0
        For Column _nameType is STRING value as String is mytable value as long is 0 value as double is 0.0
        For Column _seqType is INTEGER value as String is 31 value as long is 31 value as double is 31.0
        For Column _inc_byType is INTEGER value as String is 10 value as long is 10 value as double is 10.0
        For Column _offsetType is INTEGER value as String is 0 value as long is 0 value as double is 0.0
        For Column _limitType is INTEGER value as String is 50000 value as long is 50000 value as double is 50000.0



回答4:


Unfortunately, it looks like its a bit of a dogs-dinner: https://www.sqlite.org/rowidtable.html

and I quote (in case you dont want to read the whole page): "the need to preserve backwards compatibility for the hundreds of billions of SQLite database files in circulation. In a perfect world, there would be no such thing as a "rowid" and all tables would following the standard semantics implemented as WITHOUT ROWID tables, only without the extra "WITHOUT ROWID" keywords. Unfortunately, life is messy. The designer of SQLite offers his sincere apology for the current mess."




回答5:


Does this mean, with a WithoutRowID, I have to specify my own Primary Key Value when inserting?

Yes, but you can generate it with a subselect automatically at the time of inserting:

CREATE TABLE "A_TEST" ( 
    "ID" INTEGER PRIMARY KEY,
    "X" TEXT NULL
) WITHOUT ROWID;

INSERT INTO A_TEST (ID,X) VALUES (
    (SELECT IFNULL(MAX(id),0)) + 1 FROM "A_TEST"),
    'Test String'
);

The IFNULL(MAX(id),0)) + 1 will return 1 for an empty table, and otherwise one more than the row with the current highest value.

This does not work unchanged for mass inserts, since all IFNULL(MAX(id),0)) + 1 are evaluated at the same time and would have the same value, leading to a unique constraint violation. However, you can generate the mass insert statements so that the offset is + 1 for the first one, + 2 for the second one and so on:

INSERT INTO A_TEST (ID,X) VALUES 
(
    (SELECT IFNULL(MAX(id),0)) + 1 FROM "A_TEST"),
    'Test String 1'
),
(
    (SELECT IFNULL(MAX(id),0)) + 2 FROM "A_TEST"),
    'Test String 2'
);


来源:https://stackoverflow.com/questions/49963559/using-a-primary-key-with-a-withoutrowid

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!