On large databases (~2.5million entries in webform_submitted_data) the unique validation may take dozens of seconds thereby stalling webform submissions. This is because it executes a query with a WHERE condition on the unindexed column webform_submitted_data.data. I propose to add an index to this column.
| Comment | File | Size | Author |
|---|---|---|---|
| #31 | 2213945-index-for-webform_submitted_data.data-30.patch | 1000 bytes | danchadwick |
| #25 | 2213945-index-for-webform_submitted_data.data-25.patch | 606 bytes | torotil |
Comments
Comment #1
torotil commentedComment #2
danchadwick commentedThe only downside to this is that the index will be huge, since its indexing every component of every submission of every webform.
Comment #3
torotil commentedI can check how large this index will be. But having to chose between not using the unique validation or being unable to submit webforms vs. ~100MB index size in a 20GB database is rather easy.
NOTE: there are already two indexes on the same table.
Comment #4
danchadwick commentedI'm not suggesting that you not have an index. There is nothing to prevent you from adding the index on your own (either manually or in a custom module). What I'm wondering is if all 400,000-ish webform installations need the index.
EDIT: BTW, indices on ids will be small. An index on all the data of every component of every webform won't.
Comment #5
torotil commentedThe patch only indexes the first 256bytes of the data column. That's enough for components like email-addresses (the most common use-case for unique) but still pretty fast on inserts. If size is an issue it could also be limited to 128bytes I guess or even 64bytes would help a lot.
I think that all 400.000-ish installations would have this issue. Without an index mysql has to loop through all rows to check if they have equal data (WHERE data='user-submitted-value') and that for each unique component and each webform submission.
Comment #6
torotil commentedSo here are the index and data sizes of our "largest" live-system (2.7mio entries in webform_submitted_data).
Note that this is by far not the largest table for this drupal installation.
Comment #7
danchadwick commentedIncreasing the size of the site's indices by 20+% solely for the case in which someone might add unique validation doesn't sound like a good solution to me.
I think what we need is either an indication to the site builder that an index needs to be added if unique validation is slow and/or a configuration option to do it. I would imagine that adding the index to a large table might take quite a bit of time.
Comment #8
torotil commentedEhm, this is not the entire database - this is just the webform_submitted_data table. The entire database is about 20GB. Overall the 20MB are not noticeable.
For us this is …
* 0.1% increase in DB size.
* A literable non measurable performance decrease during submission insert / update.
… on the one side and …
* performance of unique validation taking ~1s / 10.000 rows (without the index) vs. <0.01 for 2.500.000 rows.
* make webform scaleable by default.
on the other side.
Comment #9
danchadwick commentedI'm not saying that your installation shouldn't have an index. Clearly it should.
Are you saying that every webform installation should have an index even though the vast majority (probably > 99%) don't need or want it?
Comment #10
torotil commentedI'm saying that a module that uses slow-queries in a built-in feature is broken, yes. So as long as webform has a unique validation it's broken unless it has that index.
The performance penalty of not having this index is noticeable even if there are only 10.000 rows in the database (which usually boils down to 1.000 submissions). With 2.500.000 it can bring down your server.
… and for what? Saving <=80KB of disk space in smaller installations?
Comment #11
torotil commentedBTW: with that argument you could eliminate about 90% of all indexes in drupal core and contrib. Installations with <100 nodes won't notice a missing index on nid, type, language, or whatever …
Comment #12
danchadwick commentedBut the cost of indices in small sites is correspondingly low.
So what are the available options?
1) Add an index to every site (a la the patch above)
2) Add a webform admin config option to add the index, explaining that it is useful to speed up unique validation.
3) Add the index automatically when the first webform component with unique validation is added to the site.
4) Remove unique validation from webform and add it to a trivial helper module, which would also add/remove the index upon installation/uninstallation.
5) Add a message when unique validation is turned on (and there isn't yet an index) saying that it may be slow unless an index is added.
5a) ... and provide a link which adds the index.
Any other ideas?
@quicksketch -- whatchathink?
@torotil -- how long did the index take to create on your server with your dataset? Could it reasonably be done via the UI (i.e. not in an update routine off-line) after there is a large amount of data?
BTW, I'm only commenting on this because I'm willing to commit a tested patch that I think is reasonable and that I'm 99% sure quicksketch would support.
Comment #13
torotil commentedFor us creating the index takes about 40s. That's 0.015s / 1000 rows.
Comment #14
danchadwick commentedWell we could try increasing the PHP time-out before adding the index which might (or might not) help. Alternatively, it could be schedule to run on the next cron job.
Comment #15
torotil commentedFor 99% of all installations that's less than 3s. The admins of the other 1% have most likely learned not to expect updates to work via the interface and use drush updb instead.
Comment #16
danchadwick commented@torotil - you're assuming option 1. Options 2, 3, and 5a would not happen via drush.
Speaking of drush....
Option 6: Add a trivial drush command to add the index and reference it in the on-line documentation somewhere and/or in the #description for unique validation.
Comment #17
quicksketchHi guys, thanks for the great discussion in here. I learned a few things just reading through it. I didn't know you could have an index on only part of a database column.
Whenever possible, I prefer to make a decision rather than making a new option. Let's pick the best solution that helps the most sites and hurts the fewest.
From reading over the options, I think @torotil's original suggestion makes a lot of sense. The index only lists the first 256 bytes, and it doesn't combine with any other columns, making for an overall inexpensive index. 40s for adding an index on 2.5 million rows doesn't seem like a problem to me. Most PHP installations of that size need to have a timeout at 60s or more anyway. If we're paranoid about it, we can use ini_set() during the update to increase the timeout time. This will very slightly increase the time it takes to write a new submission, but it makes submission time more consistent. If unique validation is enabled and that suddenly makes submissions take 5-10x longer, that's a problem. I like this solution that very slightly increases the writing time, and unique validation doesn't carry such a burden.
Comment #18
quicksketchI'm fine with this patch's current approach (just adding an index on the first 256 bytes). However, I'd like to know that we're doing this correctly. My optimization experience would tell me that indexing "data" alone would be good but an index of nid, cid, data would be ideal, since that's what the webform_validate_unique() query is against. That way you'd get a fully indexed query instead of only getting an index on data.
On my localhost, running EXPLAIN on the query made no difference before or after adding the index. In both cases the query used the PRIMARY key (which is the nid, sid, cid, no index). The index used probably depends on the amount of data you have in the table however. On my localhost there are only 10K rows, as opposed to several million.
Here's the EXPLAIN with the new "data" index. Note that the primary key is still used:
Here's the EXPLAIN with an index for nid_cid_data added:
I'm not sure about the wisdom of this longer key vs. one just on the data column yet. I'll need to do some more testing.
Comment #19
quicksketchWell, oddly after adding the nid_cid_data index, queries seem to actually take *longer*. Before adding an index, queries took roughly 9ms. After adding the nid_cid_data index, queries took 12ms (this is out of 3 tests, restarting MySQL between tests to clear caches). Adding the data index made no difference for me, since per the EXPLAIN output above, the "data" index doesn't get used on my table anyway.
Of course this is a matter of ~3ms, so I don't think this is important on a database of my size. We'd need a larger test base to determine a significant difference.
@torotil: Could you include the EXPLAIN output on your site for a sample query before/after adding the index? Does a more complete index help or hurt in your case?
Comment #20
torotil commentedThis is on a table with roughly 300.000 entries for this nid and cid:
It uses the data index since it only expects 25 rows to have the same key - but there is 300.000 having the same nid and cid.
You are right that in case you have lots of submissions and lots (we're talking 10.000+ here) of webforms a combined key would be better still.
Comment #21
gagarine commented256 is to long even InnoDB table the limit is 767 bytes (see http://dev.mysql.com/doc/refman/5.1/en/create-index.html)
This is because with MEDIUMTEXT we can use a multibyte character set like utf-8.
256*3 = 768 = FAIL!
255*3 = 765 = OK :)
But UTF8 can have some char than need 4 bytes (as I understand). So I propose we limit the index to 191 to be on the safe side.
Comment #22
gagarine commentedWhat about creating a new colon with a SHA from the data+cid+nid and using a query than has only one WHERE condition on this new field? This shouldn't be faster that the proposed solution from #18 that use combined index?
Comment #23
torotil commentedIndex size
If we take into account multi-byte strings we should calculate with up to 4bytes per character. Strangely I'm using utf-8 too and didn't run into the problem.
So the maximum would be 191 characters, minus 4-bytes for each integer column. I think that's still good enough for most use-cases (especially for unique email addresses).
Combined (nid, cid, data) vs. standalone index (data or cid, data)
As far as I'm concerned the standalone index is still fast enough even if there are lots of submissions with the same data. Why is that? Usually something that has a unique validation is rather unique except if you have lots (~10.000) of forms where it's used.
I'm about to do some benchmarks for the different index types.
Using SHA
Hashing would be a possible way to avoid the character limit altogether. Although we are searching for a hash algorithm with as few as possible collisions but still as fast as possible to calculate - whereas SHA is designed to be slow to calculate.
Comment #24
torotil commentedBenchmark of different index types
Here is some statistical data about the database:
So I'm going to test 3 values: 'asdf' that doesn't occur, 'test@test.de' that does occur on some webform submissions in different cids - and 'f' that does occur very often in various cids. I'll be doing two queries for each of those (unique validation and filter over all submissions).
The query_cache was disabled for this mysql session, athough table caches are active. So I'm always leaving out the first time and give the average of three further timings.
Baseline without index:
With a index on data(191):
Index on cid, data(187):
With index on nid, cid, data(183):
So at least for this database and these queries there is no measurable difference for the unique validation between indexing only the data column and including nid, cid - while it has implications on other queries.
So I'm still voting for the standalone index on data of at least 64chars (should be enough for most cases) and a maximum of 191chars.
Comment #25
torotil commentedHere is an updated patch that uses up to 64chars (up to 256bytes). This still has the same performance for my example database. It was my intention to use 256bytes in the first place.
Comment #26
torotil commentedComment #27
danchadwick commentedYour index choice seems reasonable and well-researched. I'm worried about time-outs though. Imagine:
PHP runs and starts the ALTER TABLE to add the index. It then times out, leaving the update undone. SQL may time-out, or it may complete. It will then re-run the update and I'm not sure what happens on various platforms if you add an index that already exists.
If executed from drush, it could block the site of an unacceptably long time. This is interesting:
http://stackoverflow.com/questions/4244685/create-an-index-on-a-huge-mys...
Accordingly, it might make sense to count the rows (which itself is slow with some engines) and only add the index if there are fewer than some threshold (maybe 100,000? 1,000,000?). Otherwise give a message saying that some tool should be used to add the index.
Any other ideas?
Comment #28
torotil commented@DanChadwick the issue was already addressed in #13 #15 (by me) and #17 (by quicksketch):
Comment #29
torotil commentedComment #31
danchadwick commentedPorted to current 7.x-4.x and committed to 7.x-4.x and 8.x.
Comment #32
david_garcia commentedGreat this breaks SQL Server and Oracle compatiblity, where indexexs cannot be that big.
The "elegant" solution is something like:
- Create a computed column based on the "data" column, maybe a HASH or similar.
- Instead of querying "data" column, query the computed column.
With this solution even index sizes will be kept small, depending on the hashing function used.
I was not able to find a way to create computed columns in the database api of Drupal, that would be a great addition to the database abstraction layer....
Comment #33
danchadwick commented@david_garcia -- Can you quote what the limits are? A quick search seems to indicate 900 bytes for SQL Server and some OS and block/length limit on Oracle, with examples being in the > 700 byte area.
I don't see us hashing or going beyond the abstraction provided in core. Do you have any suggestions?
Comment #34
david_garcia commented@DanChadwick -- Because of the lack of computed columns in database abstraction layer in core I cannot think of a better solution than the one proposed in this thread. Hashing in code and having to maintain a separate column is too much trouble, a computed column would do the trick.
I was actually trying to highlight the need of the abstraction layer to consider the possiblity of having such type of columns.
Comment #35
danchadwick commented@david_garcia -- Are you sure the limits are a problem in SQL Server and Oracle? They don't seem to be from my quick read. References?
Re the abstraction layer / computed fields: core maintainers aren't going to read this issue. You might open an issue in the core queue and link it to this one.
Comment #37
katannshaw commented@david_garcia and @DanChadwick: The latest patch broke compatibility for me as well, as I use SQL Server. I noted this on issue report SQL Server - PDOException: SQLSTATE[42000] when attempting update. In addition to Oracle, I'm sure this issue will happen to Postgres and other non MySQL users as well.
The temporary workaround of "commenting out the line with the index update. (line 1901 of webform.install)" let the rest of the update go through for me, however is there a way that I can do this manually?
Comment #38
danchadwick commentedRe #37 -- If you can determine a query that works with MSSql, please update this issue and set to active. There isn't a problem with Postgres, but may be with Oracle. I'm open to an engine-specific workaround.
If you can manually create an equivalent index, let us know the PHP code to do so. Absent that, comment out the index and proceed with the update. You just won't have indexing for unique validation.
Comment #39
katannshaw commented@DanChadwick: I'm new to creating patches myself but I'll see what I can do. I have been looking into a possible solution since your reply from the other related issue report (thanks by the way). What kind of issues will not having indexing for unique validation cause for me when using webforms?
Comment #40
danchadwick commented@jayhawkfan75 - I you figure out the SQL for MS, I can write the patch.
The consequence will be long delays if you have a webform that a) uses unique validation and b) has lots of data in other submissions for that webform and component. Not normally a problem until you have 10,000+ submissions.
Comment #41
katannshaw commentedVery good. Thanks again. I'll post back if I can figure that out.
Comment #42
torotil commentedI've cherry-picked this into 7.x-3.x and it will be part of the 7.x-3.23 release.
@jayhawkfan75: IMHO this is something the DB-abstraction layer (Drupal core) should handle.
Comment #43
katannshaw commented@torotil: Sorry for the very late response but thanks!