Update moz_origins index column order to match desktop - #7521
Conversation
This change ports the fix from https://bugzilla.mozilla.org/show_bug.cgi?id=2025999 which replaces the (prefix, host) unique index on moz_origins with a (host, prefix) index instead. This improves the performance of the index by putting the higher-cardinality column first, and also makes queries which don't filter on prefix eligible for the index. Similar to the fix on desktop, we create a new table and copy the data over due to sqlite limitations on modifying constraints. However, we can't use defer_foreign_keys like we do on desktop, since the foreign key here is ON DELETE CASCADE -- therefore we have to also temporarily null out the foreign key references and copy them back afterward.
Instead of rebuilding the entire moz_origins table, which requires nulling out and then restoring the entire origin_id column in the large moz_places table, we can just rewrite the schema in sqlite_schema and then trigger a REINDEX. This is not as safe, and has the risk of causing silent data corruption if the schema is modified incorrectly (like for example if it was unknowingly modified by application code). But, it is significantly faster and generates significantly less WAL.
ec21654 to
47dd6a4
Compare
|
Thanks for this!
Thanks for this too, and the performance does scare me a little - 2s for a migration on a phone that's probably fairly recent and probably not an outlier in terms of size sounds bad. The fact Lina already did for the 17 migration does make me think we should consider it. When you say risky, do you just mean that if we do something dumb we might destroy the DB, or is there something more subtle here? |
Actually smaller than our maintenance target of 75MiB |
Here are some of the risks I see with using writable_schema here
Overall I think it's probably worth it to use writable_schema here, especially since we already used it in the past, but since this is such a widely used product I figured I should think of some alternatives first |
|
In general we try to avoid writable_schema, exacly because you're removing any guard rail from corrupting your own database. But we still support it (we did not add SQLITE_DBCONFIG_DEFENSIVE) because sometimes it's the only way to do certain things without paying excessive costs. It can be 20x faster. If the choice is multiple seconds of I/O VS ms, considered this is an index change, using writable_schema is probably ok, with the due diligence |
I would have thought you could use |
DROP INDEX can't remove indexes associated with constraints, and there's no ALTER TABLE DROP CONSTRAINT support in sqlite. e.g. regarding triggers, they aren't a problem here. the ON DELETE CASCADE is implemented without using triggers |
|
Oh, that makes total sense sorry to sidetrack this. writable_schema seems okay to me and the downsides you list seem acceptable. In particular, there's many ways that users can mess up their database if they manually change it. Maybe this would be the first way that they could cause us to corrupt the database, but there are other ways to make it more-or-less unusable. Does SQLite reindex the columns after the |
|
you have to manually call REINDEX, but thankfully it can be done transactionally together with the schema update. As far as I can tell, there's basically three things you need to do for correctness purposes after changing the schema in this case:
This can all be done within a transaction together with the UPDATE on sqlite_schema and a rollback correctly brings you back to the previous state |
|
ok, since it sounds like everyone is interested in going with the writable_schema approach, i pushed that change into this branch |
bendk
left a comment
There was a problem hiding this comment.
The new code looks great to me. My only concern is that it continues to handle CREATE_SHARED_SCHEMA_SQL like previous upgrades did. Maybe now is a good time to break the pattern.
| AND sql LIKE '%UNIQUE (host, prefix)%'", | ||
| [], | ||
| )?; | ||
| if !already_inverted { |
There was a problem hiding this comment.
IIUC, this check is required because previous schema upgrades execute CREATE_SHARED_SCHEMA_SQL which means they might have already ran this upgrade. This seems dangerous to me, since it means wherever that changes we'll need to think through the effects on every upgrade function.
How do others feel about copying the v19 version of that SQL into some other module and call it something like legacy_schemas. The upgrades can use legacy_schemas::CREATE_SHARED_SCHEMA_SQL and then we can change the real CREATE_SHARED_SCHEMA_SQL without needing to worry about that. There's probably a better naming system, but I like freezing the code that the upgrade functions run as much as possible.
There was a problem hiding this comment.
i actually put that check here just to better copy what desktop does, which has that same check because of (i think) the possibility of downgrades and re-applications of migrations that have already been applied?
but now that I think about it, you are right. it also defends against the scenario where the unversioned CREATE_SHARED_SCHEMA_SQL already ran with the new definition in an older migration. and I agree, having older migrations apply newer and potentially changing SQL is risky. i will prepare a change with your proposed design based on the contents of CREATE_SHARED_SCHEMA_SQL at the time of each past migration's migration 19's commit
This PR serves as a follow up to #7484 as well as a port of the fix in desktop for bug 2025999.
This doesn't add any new indexes, like #7484 did. Instead, it just reverses the column order of the existing (prefix, host) unique index on moz_origins with a (host, prefix) index instead. In addition to mitigating the bad query plan from bug 2056115, this change also has the advantage of improved performance due to putting the higher-cardinality column first, and also makes queries which don't filter on prefix eligible for the index.
Similar to the fix on desktop, we create a new table and copy the data over due to sqlite limitations on modifying constraints. However, we can't use defer_foreign_keys like we do on desktop, since the foreign key here is ON DELETE CASCADE -- therefore we have to also temporarily null out the foreign key references and copy them back afterward. This makes the migration much slower and generate much more WAL than the respective migration on desktop.
As an alternative, we could modify
sqlite_schemadirectly: this is more risky, but far more efficient: only ~76ms and ~0MB WAL as opposed to ~2060ms and ~22MB WAL on my actual 58MB places DB. It also has some precedent in this codebase (migration 17 does it), unlike desktop. An example of that approach can be seen here: shawnz@e157a2bPull Request checklist
[ci full]to the PR title.