sqlite 使用带有WithoutRowID的主键

wfauudbj  于 2023-10-23  发布在  SQLite
关注(0)|答案(5)|浏览(142)

我试图从SQLite中挤出每一点性能,我有一个问题,这似乎很奇怪,因为SQLite中的功能似乎毫无意义。
例如:

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

然后尝试插入一条记录:

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

你会得到一个错误“NOT NULL constraint failed”
这是否意味着,对于WithoutRowID,我必须在插入时指定自己的主键值?
我认为WithoutRowID毫无意义的原因是:
1.你必须指定你自己的主键值,这使得任何批量插入选择语句都是多余的,因为我必须在插入时在主键中指定我自己的值。
1.如果我不使用WithoutRowID,我实际上将有2个主键,因为SQLite管理自己的RowID以及我自己的主键值。在1.7GB的数据库上,使用WithoutRowID将文件中索引的大小减少到1.3GB,因此400MB的差异是相当大的节省。
请告诉我,我不必提供自己的主键ID,如果它是INTEGER,它实际上会提供一个针对主键的唯一ID。

pobjuy32

pobjuy321#

我刚刚发现SQLite 4是可用的-它看起来(从最初的阅读网页),主键将确实是真实的主键没有ROWID!Whoop Whoop。http://sqlite.org/src4/doc/trunk/www/design.wiki

8fsztsew

8fsztsew2#

SQLite中有三种表:

  1. WITHOUT ROWID表,存储为按声明的主键排序的B树;
    1.具有INTEGER PRIMARY KEY的rowid表(其中PK列是内部rowid的别名),它们存储为按声明的主键排序的B树;
  2. rowid表与任何其他(或没有)主键,这是存储为B树排序的内部rowid。
    1.和2.几乎是相同的,除了你不会自动递增1。
    因此,如果你想有自动递增,只需删除WITHOUT ROWID(从case 1移动到case 2)。WITHOUT ROWID只是对第三种情况的改进,在第三种情况下,主键约束需要一个单独的索引。
jfewjypa

jfewjypa3#

这是否意味着,对于WithoutRowID,我必须在插入时指定自己的主键值?
是的(虽然可能没有一些努力或限制如下),如果你尝试:

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

然后你得到的东西沿着线:-

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]

所以你需要有一个PRIMARY KEY,而SQLite不会提供一个值,因为通过指定WITHOUT ROWID,你已经告诉SQLite你不想要这个特性。
将上述内容更改为:

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

但是,您需要指定值。
上面的方法对于关联表很有用,如果主索引是根据你将要引用的列,而不是根据rowid,这基本上是按照插入顺序的,那么它可能会更有效。当然,对于这样的表,在不知道列的值的情况下,您可能不会插入一行。
因此,WITHOUT ROWID表有其用途。
如果你真的想,你可以很容易地引入一个自动递增的数字使用'触发器'伴随着一个表与一个单一的行/列,存储最后使用或下一个数字使用,但为什么你会放弃rowid的简单复制rowid的为你做什么反正。
另一种替代方法是使用CURRENT_TIMESTAMP作为默认值。但是,不能执行批量插入,因为插入之间的间隔会导致UNIQUE约束冲突。
如果我不使用WithoutRowID,我实际上将有2个主键,因为SQLite管理自己的RowID以及我自己的主键值。在1.7GB的数据库上,使用WithoutRowID将文件中索引的大小减少到1.3GB,因此400 MB的差异是相当大的节省。
请告诉我,我不必提供自己的主键ID,如果它是一个整数,它实际上会提供一个针对主键的唯一ID。
当然,你的 “我自己的主键值” 不是神奇地生成的,也就是说,你知道这个值(假设它不仅仅是rowid的副本),所以它会被用于主键。
有趣的是,使用:

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');

显示使用WITHOUT ROWIDPRIMARY KEY提供了更大的灵活性,因为它不像rowid那样仅限于INTEGERTEXT PRIMARY KEY.....也不限于INTEGER
即上述结果:

Android的另一个注意事项

WITHTOUT ROWID是在3.8.2版本中引入的,某些设备可能没有安装该版本或更高版本,因此WITHOUT ROWID甚至可能不是某些Android应用程序的选项。

如果你真的想要的话,可以举个例子:-

这是一个管理你自己的psuedo_autoincrement的例子。这不是按照1递增,而是根据指定的量(本例中为10)递增。它不完整,因为它没有上限检查/处理。
下面是插入3行后的结果:

下面是随附的序列表(准备好下一个插入,即e _seq是31):

  • _incby是增量(本例中为10)
  • _offset列将跳转到1,2... 9(在9个最大允许值之后),每次达到time _limit。因此,当偏移量为1时(插入5000次后),则为2,12,22.....将被使用。然而,这一点并未得到落实。
  • 我想你可以用上面的方法来做一个“自动递减”。

关键(双关语无意)是触发器

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

还有那个**:

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

这是在Android上使用以下方法检查/使用的:

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();
    }
}

产出是:

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
insrf1ej

insrf1ej4#

这是否意味着,对于WithoutRowID,我必须在插入时指定自己的主键值?
是的,但您可以在插入时自动使用子选择生成它:

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'
);

对于空表,IFNULL(MAX(id),0)) + 1将返回1,否则将比具有当前最高值的行多一个。
对于质量插入,这不会保持不变,因为所有IFNULL(MAX(id),0)) + 1都是同时计算的,并且具有相同的值,从而导致唯一约束冲突。但是,您可以生成mass insert语句,以便第一个偏移量为+ 1,第二个偏移量为+ 2,依此类推:

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'
);
11dmarpk

11dmarpk5#

不幸的是,它看起来有点像狗的晚餐:https://www.sqlite.org/rowidtable.html
我引用(如果你不想读整页):“需要为流通中的数千亿个SQLite数据库文件保持向后兼容性。在一个完美的世界里,不会有“rowid”这样的东西,所有的表都将遵循WITHOUT ROWID表实现的标准语义,只是没有额外的“WITHOUT ROWID”关键字。不幸的是,生活是混乱的。SQLite的设计师为目前的混乱局面表示诚挚的歉意。

相关问题