|
| 1 | +--- |
| 2 | +title: Performance tuning - Hyperscale (Citus) - Azure Database for PostgreSQL |
| 3 | +description: Improving query performance in the distributed database |
| 4 | +ms.author: jonels |
| 5 | +author: jonels-msft |
| 6 | +ms.service: postgresql |
| 7 | +ms.subservice: hyperscale-citus |
| 8 | +ms.topic: conceptual |
| 9 | +ms.date: 08/30/2022 |
| 10 | +--- |
| 11 | + |
| 12 | +# Hyperscale (Citus) performance tuning |
| 13 | + |
| 14 | +[!INCLUDE[applies-to-postgresql-hyperscale](../includes/applies-to-postgresql-hyperscale.md)] |
| 15 | + |
| 16 | +Running a distributed database at its full potential offers high performance. |
| 17 | +However, reaching that performance can take some adjustments in application |
| 18 | +code and data modeling. This article covers some of the most common--and |
| 19 | +effective--techniques to improve performance. |
| 20 | + |
| 21 | +## Client-side connection pooling |
| 22 | + |
| 23 | +A connection pool holds open database connections for reuse. An application |
| 24 | +requests a connection from the pool when needed, and the pool returns one that |
| 25 | +is already established if possible, or establishes a new one. When done, the |
| 26 | +application releases the connection back to the pool rather than closing it. |
| 27 | + |
| 28 | +Adding a client-side connection pool is an easy way to boost application |
| 29 | +performance with minimal code changes. In our measurements, running single-row |
| 30 | +insert statements goes about **24x faster** on a Hyperscale (Citus) server |
| 31 | +group with pooling enabled. |
| 32 | + |
| 33 | +For language-specific examples of adding pooling in application code, see the |
| 34 | +[app stacks guide](quickstart-app-stacks-overview.md). |
| 35 | + |
| 36 | +> [!NOTE] |
| 37 | +> |
| 38 | +> Hyperscale (Citus) also provides [server-side connection |
| 39 | +> pooling](concepts-connection-pool.md) using pgbouncer, but it mainly serves |
| 40 | +> to increase the client connection limit. An individual application's |
| 41 | +> performance benefits more from client- rather than server-side pooling. |
| 42 | +> (Although both forms of pooling can be used at once without harm.) |
| 43 | +
|
| 44 | +## Scoping distributed queries |
| 45 | + |
| 46 | +### Updates |
| 47 | + |
| 48 | +When updating a distributed table, try to filter queries on the distribution |
| 49 | +column--at least when it makes sense, when the new filters don't change the |
| 50 | +meaning of the query. |
| 51 | + |
| 52 | +In some workloads, it's easy. Transactional/operational workloads like |
| 53 | +multi-tenant SaaS apps or the Internet of Things distribute tables by tenant or |
| 54 | +device. Queries are scoped to a tenant- or device-ID. |
| 55 | + |
| 56 | +For instance, in our [multi-tenant |
| 57 | +tutorial](tutorial-design-database-multi-tenant.md#use-psql-utility-to-create-a-schema) |
| 58 | +we have an `ads` table distributed by `company_id`. The naive way to update an |
| 59 | +ad is to single it out like this: |
| 60 | + |
| 61 | +```sql |
| 62 | +-- slow |
| 63 | + |
| 64 | +UPDATE ads |
| 65 | + SET impressions_count = impressions_count+1 |
| 66 | + WHERE id = 42; -- missing filter on distribution column |
| 67 | +``` |
| 68 | + |
| 69 | +Although the query uniquely identifies a row and updates it, Hyperscale (Citus) |
| 70 | +doesn't know, at planning time, which shard the query will update. Citus takes a |
| 71 | +ShareUpdateExclusiveLock on all shards to be safe, which blocks other queries |
| 72 | +trying to update the table. |
| 73 | + |
| 74 | +Even though the `id` was sufficient to identify a row, we can include an |
| 75 | +extra filter to make the query faster: |
| 76 | + |
| 77 | +```sql |
| 78 | +-- fast |
| 79 | + |
| 80 | +UPDATE ads |
| 81 | + SET impressions_count = impressions_count+1 |
| 82 | + WHERE id = 42 |
| 83 | + AND company_id = 1; -- the distribution column |
| 84 | +``` |
| 85 | + |
| 86 | +The Hyperscale (Citus) query planner sees a direct filter on the distribution |
| 87 | +column and knows exactly which single shard to lock. In our tests, adding |
| 88 | +filters for the distribution column increased parallel update performance by |
| 89 | +**100x**. |
| 90 | + |
| 91 | +### Joins and CTEs |
| 92 | + |
| 93 | +We've seen how UPDATE statements should scope by the distribution column to |
| 94 | +avoid unnecessary shard locks. Other queries benefit from scoping too, usually |
| 95 | +to avoid the network overhead of unnecessarily shuffling data between worker |
| 96 | +nodes. |
| 97 | + |
| 98 | + |
| 99 | + |
| 100 | +```sql |
| 101 | +-- logically correct, but slow |
| 102 | + |
| 103 | +WITH single_ad AS ( |
| 104 | + SELECT * |
| 105 | + FROM ads |
| 106 | + WHERE id=1 |
| 107 | +) |
| 108 | +SELECT * |
| 109 | + FROM single_ad s |
| 110 | + JOIN campaigns c ON (s.campaign_id=c.id); |
| 111 | +``` |
| 112 | + |
| 113 | +We can speed up the query up by filtering on the distribution column, |
| 114 | +`company_id`, in the CTE and main SELECT statement. |
| 115 | + |
| 116 | +```sql |
| 117 | +-- faster, joining on distribution column |
| 118 | + |
| 119 | +WITH single_ad AS ( |
| 120 | + SELECT * |
| 121 | + FROM ads |
| 122 | + WHERE id=1 and company_id=1 |
| 123 | +) |
| 124 | +SELECT * |
| 125 | + FROM single_ad s |
| 126 | + JOIN campaigns c ON (s.campaign_id=c.id) |
| 127 | + WHERE s.company_id=1 AND c.company_id = 1; |
| 128 | +``` |
| 129 | + |
| 130 | +In general, when joining distributed tables, try to include the distribution |
| 131 | +column in the join conditions. However, when joining between a distributed and |
| 132 | +reference table it's not required, because reference table contents are |
| 133 | +replicated across all worker nodes. |
| 134 | + |
| 135 | +If it seems inconvenient to add the extra filters to all your queries, keep in |
| 136 | +mind there are helper libraries for several popular application frameworks that |
| 137 | +make it easier. Here are instructions: |
| 138 | + |
| 139 | +* [Ruby on Rails](https://docs.citusdata.com/en/stable/develop/migration_mt_ror.html), |
| 140 | +* [Django](https://docs.citusdata.com/en/stable/develop/migration_mt_django.html), |
| 141 | +* [ASP.NET](https://docs.citusdata.com/en/stable/develop/migration_mt_asp.html), |
| 142 | +* [Java Hibernate](https://www.citusdata.com/blog/2018/02/13/using-hibernate-and-spring-to-build-multitenant-java-apps/). |
| 143 | + |
| 144 | +## Efficient database logging |
| 145 | + |
| 146 | +Logging all SQL statements all the time adds overhead. In our measurements, |
| 147 | +using more a judicious log level improved the transactions per second by |
| 148 | +**10x** vs full logging. |
| 149 | + |
| 150 | +For efficient everyday operation, you can disable logging except for errors and |
| 151 | +abnormally long-running queries: |
| 152 | + |
| 153 | +| setting | value | reason | |
| 154 | +|---------|-------|--------| |
| 155 | +| log_statement_stats | OFF | Avoid profiling overhead | |
| 156 | +| log_duration | OFF | Don't need to know the duration of normal queries | |
| 157 | +| log_statement | NONE | Don't log queries without a more specific reason | |
| 158 | +| log_min_duration_statement | A value longer than what you think normal queries should take | Shows the abnormally long queries | |
| 159 | + |
| 160 | +> [!NOTE] |
| 161 | +> |
| 162 | +> The log-related settings in our managed service take the above |
| 163 | +> recommendations into account. You can leave them as they are. However, we've |
| 164 | +> sometimes seen customers change the settings to make logging aggressive, |
| 165 | +> which has led to performance issues. |
| 166 | +
|
| 167 | +## Lock contention |
| 168 | + |
| 169 | +The database uses locks to keep data consistent under concurrent access. |
| 170 | +However, some query patterns require an excessive amount of locking, and faster |
| 171 | +alternatives exist. |
| 172 | + |
| 173 | +### System health and locks |
| 174 | + |
| 175 | +Before diving into common locking inefficiencies, let's see how to view locks |
| 176 | +and activity throughout the database cluster. The |
| 177 | +[citus_stat_activity](reference-metadata.md#distributed-query-activity) view |
| 178 | +gives a detailed view. |
| 179 | + |
| 180 | +The view shows, among other things, how queries are blocked by "wait events," |
| 181 | +including locks. Grouping by |
| 182 | +[wait_event_type](https://www.postgresql.org/docs/14/monitoring-stats.html#WAIT-EVENT-TABLE) |
| 183 | +paints a picture of system health: |
| 184 | + |
| 185 | +```sql |
| 186 | +-- general system health |
| 187 | + |
| 188 | +SELECT wait_event_type, count(*) |
| 189 | + FROM citus_stat_activity |
| 190 | + WHERE state != 'idle' |
| 191 | + GROUP BY 1 |
| 192 | + ORDER BY 2 DESC; |
| 193 | +``` |
| 194 | + |
| 195 | +A NULL `wait_event_type` means the query isn't waiting on anything. |
| 196 | + |
| 197 | +If you do see locks in the stat activity output, you can view the specific |
| 198 | +blocked queries using `citus_lock_waits`: |
| 199 | + |
| 200 | +```sql |
| 201 | +SELECT * FROM citus_lock_waits; |
| 202 | +``` |
| 203 | + |
| 204 | +For example, if one query is blocked on another trying to update the same row, |
| 205 | +you'll see the blocked and blocking statements appear: |
| 206 | + |
| 207 | +``` |
| 208 | +-[ RECORD 1 ]-------------------------+-------------------------------------- |
| 209 | +waiting_gpid | 10000011981 |
| 210 | +blocking_gpid | 10000011979 |
| 211 | +blocked_statement | UPDATE numbers SET j = 3 WHERE i = 1; |
| 212 | +current_statement_in_blocking_process | UPDATE numbers SET j = 2 WHERE i = 1; |
| 213 | +waiting_nodeid | 1 |
| 214 | +blocking_nodeid | 1 |
| 215 | +``` |
| 216 | + |
| 217 | +To see not only the locks happening at the moment, but historical patterns, you |
| 218 | +can capture locks in the PostgreSQL logs. To learn more, see the |
| 219 | +[log_lock_waits](https://www.postgresql.org/docs/current/runtime-config-logging.html#GUC-LOG-LOCK-WAITS) |
| 220 | +server setting in the PostgreSQL documentation. Another great resource is |
| 221 | +[seven tips for dealing with |
| 222 | +locks](https://www.citusdata.com/blog/2018/02/22/seven-tips-for-dealing-with-postgres-locks/) |
| 223 | +on the Citus Data Blog. |
| 224 | + |
| 225 | +### Common problems and solutions |
| 226 | + |
| 227 | +#### DDL commands |
| 228 | + |
| 229 | +DDL Commands like `truncate`, `drop`, and `create index` all take write locks, |
| 230 | +and block writes on the entire table. Minimizing such operations reduces |
| 231 | +locking issues. |
| 232 | + |
| 233 | +Tips: |
| 234 | + |
| 235 | +* Try to consolidate DDL into maintenance windows, or use them less often. |
| 236 | + |
| 237 | +* PostgreSQL supports [building indices |
| 238 | + concurrently](https://www.postgresql.org/docs/current/sql-createindex.html#SQL-CREATEINDEX-CONCURRENTLY), |
| 239 | + to avoid taking a write lock on the table. |
| 240 | + |
| 241 | +* Consider setting |
| 242 | + [lock_timeout](https://www.postgresql.org/docs/14/runtime-config-client.html#GUC-LOCK-TIMEOUT) |
| 243 | + in a SQL session prior to running a heavy DDL command. With `lock_timeout`, |
| 244 | + PostgreSQL will abort the DDL command if the command waits too long for a write |
| 245 | + lock. A DDL command waiting for a lock can cause later queries to queue behind |
| 246 | + itself. |
| 247 | + |
| 248 | +#### Idle in transaction connections |
| 249 | + |
| 250 | +Idle (uncommitted) transactions sometimes block other queries unnecessarily. |
| 251 | +For example: |
| 252 | + |
| 253 | +```sql |
| 254 | +BEGIN; |
| 255 | + |
| 256 | +UPDATE ... ; |
| 257 | + |
| 258 | +-- Suppose the client waits now and doesn't COMMIT right away. |
| 259 | +-- |
| 260 | +-- Other queries that want to update the same rows will be blocked. |
| 261 | + |
| 262 | +COMMIT; -- finally! |
| 263 | +``` |
| 264 | + |
| 265 | +To manually clean up any long-idle queries on the coordinator node, you can run |
| 266 | +a command like this: |
| 267 | + |
| 268 | +```sql |
| 269 | +SELECT pg_terminate_backend(pid) |
| 270 | +FROM pg_stat_activity |
| 271 | +WHERE datname = 'citus' |
| 272 | + AND pid <> pg_backend_pid() |
| 273 | + AND state in ('idle in transaction') |
| 274 | + AND state_change < current_timestamp - INTERVAL '15' MINUTE; |
| 275 | +``` |
| 276 | + |
| 277 | +PostgreSQL also offers an |
| 278 | +[idle_in_transaction_session_timeout](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-IDLE-IN-TRANSACTION-SESSION-TIMEOUT) |
| 279 | +setting to automate idle session termination. |
| 280 | + |
| 281 | +#### Deadlocks |
| 282 | + |
| 283 | +Citus detects distributed deadlocks and cancels their queries, but the |
| 284 | +situation is less performant than avoiding deadlocks in the first place. A |
| 285 | +common source of deadlocks comes from updating the same set of rows in a |
| 286 | +different order from multiple transactions at once. |
| 287 | + |
| 288 | +For instance, running these transactions in parallel: |
| 289 | + |
| 290 | +Session A: |
| 291 | + |
| 292 | +```sql |
| 293 | +BEGIN; |
| 294 | +UPDATE ads SET updated_at = now() WHERE id = 1 AND company_id = 1; |
| 295 | +UPDATE ads SET updated_at = now() WHERE id = 2 AND company_id = 1; |
| 296 | +``` |
| 297 | + |
| 298 | +Session B: |
| 299 | + |
| 300 | +```sql |
| 301 | +BEGIN; |
| 302 | +UPDATE ads SET updated_at = now() WHERE id = 2 AND company_id = 1; |
| 303 | +UPDATE ads SET updated_at = now() WHERE id = 1 AND company_id = 1; |
| 304 | + |
| 305 | +-- ERROR: canceling the transaction since it was involved in a distributed deadlock |
| 306 | +``` |
| 307 | + |
| 308 | +Session A updated ID 1 then 2, whereas the session B updated 2 then 1. Write |
| 309 | +SQL code for transactions carefully to update rows in the same order. (The |
| 310 | +update order is sometimes called a "locking hierarchy.") |
| 311 | + |
| 312 | +In our measurement, bulk updating a set of rows with many transactions went |
| 313 | +**3x faster** when avoiding deadlock. |
| 314 | + |
| 315 | +## I/O during ingestion |
| 316 | + |
| 317 | +I/O bottlenecking is typically less of a problem for Hyperscale (Citus) than |
| 318 | +for single-node PostgreSQL because of sharding. The shards are individually |
| 319 | +smaller tables, with better index and cache hit rates, yielding better |
| 320 | +performance. |
| 321 | + |
| 322 | +However, even with Hyperscale (Citus), as tables and indices grow larger, disk |
| 323 | +I/O can become a problem for data ingestion. Things to look out for are an |
| 324 | +increasing number of 'IO' `wait_event_type` entries appearing in |
| 325 | +`citus_stat_activity`: |
| 326 | + |
| 327 | +```sql |
| 328 | +SELECT wait_event_type, wait_event count(*) |
| 329 | + FROM citus_stat_activity |
| 330 | + WHERE state='active' |
| 331 | + GROUP BY 1,2; |
| 332 | +``` |
| 333 | + |
| 334 | +Run the above query repeatedly to capture wait event related information. Note |
| 335 | +how the counts of different wait event types change. |
| 336 | + |
| 337 | +Also look at [metrics in the Azure portal](concepts-monitoring.md), |
| 338 | +particularly the IOPS metric maxing out. |
| 339 | + |
| 340 | +Tips: |
| 341 | + |
| 342 | +- If your data is naturally ordered, such as in a time series, use PostgreSQL |
| 343 | + table partitioning. See [this |
| 344 | + guide](https://docs.citusdata.com/en/stable/use_cases/timeseries.html) to learn |
| 345 | + how to partition distributed tables in Hyperscale (Citus). |
| 346 | + |
| 347 | +- Remove unused indices. Index maintenance causes I/O amplification during |
| 348 | + ingestion. To find which indices are unused, use [this |
| 349 | + query](howto-useful-diagnostic-queries.md#identifying-unused-indices). |
| 350 | + |
| 351 | +- If possible, avoid indexing randomized data. For instance, some UUID |
| 352 | + generation algorithms follow no order. Indexing such a value causes a lot |
| 353 | + overhead. Try a bigint sequence instead, or monotonically increasing UUIDs. |
| 354 | + |
| 355 | +## Summary of results |
| 356 | + |
| 357 | +In benchmarks of simple ingestion with INSERTs, UPDATEs, transaction blocks, we |
| 358 | +observed the following query speedups for the techniques in this article. |
| 359 | + |
| 360 | +| Technique | Query speedup | |
| 361 | +|-----------|---------------| |
| 362 | +| Scoping queries | 100x | |
| 363 | +| Connection pooling | 24x | |
| 364 | +| Efficient logging | 10x | |
| 365 | +| Avoiding deadlock | 3x | |
| 366 | + |
| 367 | +## Next steps |
| 368 | + |
| 369 | +* [Advanced query performance tuning](https://docs.citusdata.com/en/stable/performance/performance_tuning.html) |
| 370 | +* [Useful diagnostic queries](howto-useful-diagnostic-queries.md) |
| 371 | +* Build fast [app stacks](quickstart-app-stacks-overview.md) |
0 commit comments