【问题标题】:Using a Primary Key with a WithoutRowID将主键与 WithoutRowID 一起使用
【发布时间】:2018-10-02 11:09:19
【问题描述】:

我试图从 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. 您必须指定自己的主键值,这会使任何批量插入选择语句变得多余,因为我必须在插入时在主键中指定自己的值....

  2. 实际上,如果我不使用WithoutRowID,我将拥有2 个主键,因为SQLite 管理它自己的RowID 以及我自己的主键值。在 1.7GB 的数据库上,使用 WithoutRowID 可将文件中的索引大小减少到仅 1.3GB,因此 400MB 的差异是相当大的节省。

请告诉我,我不必提供我自己的主键 ID,如果主键是 INTEGER,它实际上会提供一个唯一 ID。

【问题讨论】:

  • 赞成不使用 row_id 的有趣类比。

标签: android sqlite android-sqlite


【解决方案1】:

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

【讨论】:

  • SQLite 4 正在开发中,也就是说,并不是真正“可用”。所描述的行为正是您使用 WITHOUT ROWID 得到的。
  • 内脏。设计师已经完成了开发,看起来我们必须等待每个人都赶上来(.net驱动程序等)。看起来我现在必须注释掉不使用 rowid 的代码。虽然它不是一个交易破坏者,但我喜欢 SQLite 3,它的可移植性非常好。
【解决方案2】:

SQLite 中的表分为三种:

  1. 没有 ROWID 表,它们存储为按声明的主键排序的 B 树;
  2. 具有 INTEGER PRIMARY KEY(其中 PK 列是内部 rowid 的别名)的 rowid 表,它们存储为按声明的主键排序的 B 树;
  3. 具有任何其他(或没有)主键的 rowid 表,它们存储为按内部 rowid 排序的 B 树。

1。和 2. 几乎相同,只是您不会使用 1 自动递增。

因此,如果您想要自动递增,只需删除 WITHOUT ROWID(从案例 1 移动到案例 2)。 WITHOUT ROWID 只是对案例 3 的改进,其中主键约束需要单独的索引。

【讨论】:

    【解决方案3】:

    这是否意味着,对于无行 ID,我必须指定我自己的 Primary 插入时的键值?

    是的(尽管按照下面的一些努力或限制可能不会),就好像你尝试了:-

        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 不会为你提供一个值,因为你已经告诉 SQLite 你不想要那个功能。

    将以上内容改为:-

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

    有效。但是,您需要指定值。

    以上对于关联表很有用,如果主索引是根据您将引用的列而不是 由rowid,这基本上是插入顺序。当然,对于此类表,您可能不会在不知道列值的情况下插入行。

    所以WITHOUT ROWID 表是有用的。

    如果你真的想要,你可以很容易地使用“TRIGGER”引入一个自动递增的数字,伴随着一个带有单行/列的表,该表存储最后使用或下一个要使用的数字,但是为什么你会放弃 rowid 只是为了复制无论如何,rowid 为你做了什么。

    如果你想要一个唯一的键,另一种选择是使用 CURRENT_TIMESTAMP 作为默认值。但是,您不能进行批量插入,因为插入之间的间隔会导致 UNIQUE 约束冲突。

    如果我不使用WithoutRowID,我将有两个主键, 因为 SQLite 管理自己的 RowID 以及我自己的主键 价值。在 1.7GB 的数据库上,使用 WithoutRowID 可以减少 文件中的索引只有 1.3GB,所以 400MB 的差异是相当的 节省大量资金。

    请告诉我,我不必提供自己的主键 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 ROWID 提供了PRIMARY KEY 更大的灵活性,因为它不限于INTEGER,就像rowid 一样,TEXT PRIMARY KEY.....

    即上述结果:-

    Android 的另一个考虑因素

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

    如果你真的想要的例子:-

    这是管理您自己的 psuedo_autoincrement 的示例。这不是增加 1,而是根据指定的数量(在本例中为 10)增加。它不完整,因为它没有上限检查/处理。

    这是插入 3 行后的结果:-

    这是随附的序列表(准备好进行下一次插入,即 _seq 为 31):-

    • _incby 是增加的数量(在这种情况下为 10)
    • 每次达到 _limit 时,_offset 列将跳转到 1, 2 ...9(允许最大 9 之后)。因此,当偏移量为 1(插入 5000 次后)时,将使用 2, 12 ,22 .....。但是,这尚未实施。
    • 我想您可以使用上面的方法来进行“自动减量”,但需要稍作调整。

    关键(双关语无意)是TRIGGER:-

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

    还有 INSERT :-

    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
    

    【讨论】:

      【解决方案4】:

      这是否意味着,在使用无行 ID 时,我必须在插入时指定自己的主键值?

      是的,但是您可以在插入时使用子选择自动生成它:

      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 都会同时进行评估并且具有相同的值,从而导致唯一的约束违规。但是,您可以生成批量插入语句,使第一个语句的偏移量为+ 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'
      );
      

      【讨论】:

        【解决方案5】:

        不幸的是,它看起来有点像狗晚餐: https://www.sqlite.org/rowidtable.html

        我引用(以防您不想阅读整个页面): “需要保持对流通中的数千亿 SQLite 数据库文件的向后兼容性。在一个完美的世界中,不会有“rowid”这样的东西,所有表都将遵循实现为 WITHOUT ROWID 表的标准语义,只有没有额外的“WITHOUT ROWID”关键字。不幸的是,生活一团糟。SQLite 的设计者为目前的混乱表示诚挚的歉意。”

        【讨论】:

          猜你喜欢
          • 2017-08-20
          • 2012-02-26
          • 2012-07-07
          • 2018-03-12
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-06-16
          相关资源
          最近更新 更多