Performance: reuse query plans instead of re-planning every statement - #3067
Performance: reuse query plans instead of re-planning every statement#3067TheBeast85 wants to merge 1 commit into
Conversation
rdttmr
left a comment
There was a problem hiding this comment.
Thank you for putting in the effort to improve our code!
I have added a whole bunch of comments and would kindly ask you to split the caching of prepared statements and the GET_TARGETS performance improvements into two separate PRs.
In addition, please let me know if you prefer to fix our concerns yourself, or if you would prefer for us to take over your PR(s), adjust the changes to our codebase and team decisions and take it from there.
| /* Name and UUID come from the iterator instead of one | ||
| * statement each. Over 1000 targets that saved two | ||
| * statements per target; get_targets issued 12196 in | ||
| * total while spending only 156 ms of 1498 ms actually | ||
| * executing SQL, so the statement count is what costs. | ||
| * | ||
| * g_strdup keeps ownership as credential_name() and | ||
| * credential_uuid() had it - both returned g_strdup'd | ||
| * memory - so the free() calls at the end of the loop | ||
| * stay correct for this branch and the trash branch | ||
| * alike. The iterator owns its own row buffer. */ |
There was a problem hiding this comment.
This comment explains the change, not the code.
Without knowing the previous code, so outside of the context of this change set, most of this explanation is useless.
| ssh_name | ||
| = g_strdup (target_iterator_ssh_credential_name (&targets)); | ||
| ssh_uuid | ||
| = g_strdup (target_iterator_ssh_credential_uuid (&targets)); |
There was a problem hiding this comment.
I think improving the GET_TARGETS behavior should be a separate PR.
Both changes are good, however reusing prepared statements and improving the performance of GET_TARGETS specifically are two different things.
| smb_name = credential_name (smb_credential); | ||
| smb_uuid = credential_uuid (smb_credential); | ||
| smb_name | ||
| = g_strdup (target_iterator_smb_credential_name (&targets)); | ||
| smb_uuid | ||
| = g_strdup (target_iterator_smb_credential_uuid (&targets)); |
There was a problem hiding this comment.
GET_TARGETS improvement, not prepared statements improvement
| esxi_name = credential_name (esxi_credential); | ||
| esxi_uuid = credential_uuid (esxi_credential); | ||
| esxi_name | ||
| = g_strdup (target_iterator_esxi_credential_name (&targets)); | ||
| esxi_uuid | ||
| = g_strdup (target_iterator_esxi_credential_uuid (&targets)); |
There was a problem hiding this comment.
GET_TARGETS improvement, not prepared statements improvement
| snmp_name = credential_name (snmp_credential); | ||
| snmp_uuid = credential_uuid (snmp_credential); | ||
| snmp_name | ||
| = g_strdup (target_iterator_snmp_credential_name (&targets)); | ||
| snmp_uuid | ||
| = g_strdup (target_iterator_snmp_credential_uuid (&targets)); |
There was a problem hiding this comment.
GET_TARGETS improvement, not prepared statements improvement
| /** | ||
| * @brief Get the SSH credential name from a target iterator. | ||
| * | ||
| * @param[in] iterator Iterator. | ||
| * | ||
| * @return Credential name, or NULL if iteration is complete. | ||
| */ | ||
| DEF_ACCESS (target_iterator_ssh_credential_name, | ||
| GET_ITERATOR_COLUMN_COUNT + 23); | ||
|
|
||
| /** | ||
| * @brief Get the SMB credential name from a target iterator. | ||
| * | ||
| * @param[in] iterator Iterator. | ||
| * | ||
| * @return Credential name, or NULL if iteration is complete. | ||
| */ | ||
| DEF_ACCESS (target_iterator_smb_credential_name, | ||
| GET_ITERATOR_COLUMN_COUNT + 24); | ||
|
|
There was a problem hiding this comment.
GET_TARGETS improvement, not prepared statements improvement
(whole changes below as well)
| /* The UUID is bound rather than formatted in, so that the statement text | ||
| * only varies with type and trash - a handful of combinations, which the | ||
| * server side statement cache in sql_pg.c can keep plans for. With the | ||
| * UUID in the text every call produced a different statement and | ||
| * PostgreSQL planned it afresh; get_targets over 1000 targets spent 403 ms | ||
| * planning against 115 ms executing. |
There was a problem hiding this comment.
This again explains a change
| assert (0); | ||
| case -1: | ||
| g_free (statement); | ||
| g_free (quoted_uuid); |
There was a problem hiding this comment.
quoted_uuid may be freed sooner, since this code path uses a prepared statement now
| * Worth doing because it is called once per row of every listing: over | ||
| * 1000 targets it ran 1000 times and, with the values formatted into the | ||
| * text, PostgreSQL planned it 1000 times. Measured with | ||
| * pg_stat_statements.track_planning on, that was 57 ms of planning per | ||
| * get_targets call against 2 ms of execution. */ |
There was a problem hiding this comment.
It is worth doing anyway, because our plan is to move every parameterized query to a prepared statement :)
| /* Bound, not formatted in: this runs once per row of every target listing, | ||
| * and with the id in the text each call was a statement PostgreSQL had | ||
| * never seen before, so it parsed and planned it again every time. */ |
There was a problem hiding this comment.
There is no need to justify moving any query to a prepared statement, as long as there are parameters in the query
|
Thanks for the review, and for the offer — I would like to do the changes myself. On splitting: the diff falls into three parts, not two.
The remaining points I will fix as requested:
One heads-up: #3068 is based on this branch, so I will rebase it once the split is done. |
gvmd re-planned nearly every statement it sent. With pg_stat_statements.track_planning turned on, get_targets with rows=-1 over 1000 targets showed where the time went: 10058 statements planning 403 ms execution 115 ms Planning cost three and a half times what executing did. track_planning is off by default, so pg_stat_statements reported only the 115 ms and the database looked harmless; the cgroup CPU told the real story, with postgres at 105 % of a core against gvmd's 13 %. * sql_pg.c gains a server side prepared statement cache. The existing _ps API only used PQexecParams, which sends the statement text every time and plans it every time. Now each statement is prepared once with PQprepare and run with PQexecPrepared, keyed by its text and cached per connection. Only statements that actually bind parameters are cached: the printf style calls put their values in the text, so every call would be a new key and a plan for "WHERE id = 4711" is of no use to "WHERE id = 4712". The cache is dropped wherever the connection goes away, including sql_close_fork, where the child abandons the parent's connection without closing it. A failed PQprepare is not an error, the caller falls back to PQexecParams. * manage_acl.c binds the resource value and the user UUID in the Super clause and in acl_user_owns_uuid. The Super clause was the single most expensive plan in gvmd, 241 ms of 387 ms. acl_user_owns_uuid runs once or twice for every row of every listing. * manage_sql_resources.c, manage_sql_tags.c and manage_sql_targets.c bind the UUID in find_resource_with_permission, the type, resource and parent type in resource_tag_count, and the target id in target_in_use. Verified: ACL isolation in both directions, a test user sees 0 of 1000 foreign targets but its own, and the owner does not see the test user's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Head branch was pushed to by a user without write access
b7c133e to
388ee8a
Compare
What
sql_pg.c, keyed by the statement text. The existing_psAPI only usedPQexecParams, which sends the text every time and plans it every time.acl_user_owns_uuidinmanage_acl.c,find_resource_with_permission,resource_tag_countandtarget_in_use.Why
gvmd re-planned nearly every statement it sent. With
pg_stat_statements.track_planningon,get_targetswithrows=-1over 1000 targets showed 10058 statements, 403 ms of planning against 115 ms of execution.track_planningis off by default, which is whypg_stat_statementsreported only the 115 ms and the database looked harmless.The Super clause alone was 241 ms of 387 ms.
Only statements that bind parameters are cached; the printf style calls put their values in the text, so every call would be a new key and a plan for
WHERE id = 4711is of no use toWHERE id = 4712. The cache is dropped wherever the connection goes away, includingsql_close_fork. A failedPQprepareis not an error, the caller falls back toPQexecParams.References
Split out of the original PR as requested. The
GET_TARGETSpart is now #3080, the severity part #3081. #3068 is based on this branch.Checklist