app/vmselect: add /expand-with-exprs page

This commit is contained in:
Aliaksandr Valialkin 2022-12-14 16:01:33 -08:00
parent 1896c47fee
commit 38341802c2
No known key found for this signature in database
GPG Key ID: A72BEC6CD3D0DED1
7 changed files with 609 additions and 6 deletions

View File

@ -102,6 +102,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
{"targets", "status for discovered active targets"},
{"service-discovery", "labels before and after relabeling for discovered targets"},
{"metric-relabel-debug", "debug metric relabeling"},
{"expand-with-exprs", "WITH expressions' tutorial"},
{"api/v1/targets", "advanced information about discovered targets in JSON format"},
{"config", "-promscrape.config contents"},
{"metrics", "available service metrics"},

View File

@ -432,6 +432,10 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
promscrapeTargetRelabelDebugRequests.Inc()
promscrape.WriteTargetRelabelDebug(w, r)
return true
case "/expand-with-exprs":
expandWithExprsRequests.Inc()
prometheus.ExpandWithExprs(w, r)
return true
case "/api/v1/rules", "/rules":
rulesRequests.Inc()
if len(*vmalertProxyURL) > 0 {
@ -601,6 +605,8 @@ var (
graphiteFunctionsRequests = metrics.NewCounter(`vm_http_requests_total{path="/functions"}`)
expandWithExprsRequests = metrics.NewCounter(`vm_http_requests_total{path="/expand-with-exprs"}`)
vmalertRequests = metrics.NewCounter(`vm_http_requests_total{path="/vmalert"}`)
rulesRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/rules"}`)
alertsRequests = metrics.NewCounter(`vm_http_requests_total{path="/api/v1/alerts"}`)

View File

@ -0,0 +1,245 @@
{% import (
"github.com/VictoriaMetrics/metricsql"
) %}
{% stripspace %}
// ExpandWithExprsResponse returns a webpage, which expands with templates in q MetricsQL.
{% func ExpandWithExprsResponse(q string) %}
<html>
<head>
<title>Expand WITH expressions</title>
<style>
p { font-weight: bold }
textarea { margin: 1em }
</style>
</head>
<body>
<div>
<form method="get">
<div>
<p>
<a href="https://docs.victoriametrics.com/MetricsQL.html">MetricsQL</a> query with optional WITH expressions:
</p>
<textarea name="query" style="height: 15em; width: 90%">{%s q %}</textarea><br/>
<input type="submit" value="Expand" />
<p>
<a href="https://docs.victoriametrics.com/MetricsQL.html">MetricsQL</a> query after expanding WITH expressions and applying other optimizations:
</p>
<textarea style="height: 5em; width: 90%" readonly="readonly">{%= expandWithExprs(q) %}</textarea>
</div>
</form>
</div>
<div>{%= withExprsTutorial() %}</div>
</body>
</html>
{% endfunc %}
{% func expandWithExprs(q string) %}
{% if len(q) == 0 %}
{% return %}
{% endif %}
{% code expr, err := metricsql.Parse(q) %}
{% if err != nil %}
Cannot parse query: {%v err %}
{% else %}
{% code expr = metricsql.Optimize(expr) %}
{%z expr.AppendString(nil) %}
{% endif %}
{% endfunc %}
{% endstripspace %}
{% func withExprsTutorial() %}
<h3>Tutorial for WITH expressions in <a href="https://docs.victoriametrics.com/MetricsQL.html">MetricsQL</a></h3>
<p>
Let's look at the following real query from <a href="https://grafana.com/dashboards/1860">Node Exporter Full</a> dashboard:
</p>
<pre>
(
(
node_memory_MemTotal_bytes{instance=~"$node:$port", job=~"$job"}
-
node_memory_MemFree_bytes{instance=~"$node:$port", job=~"$job"}
)
/
node_memory_MemTotal_bytes{instance=~"$node:$port", job=~"$job"}
)
*
100
</pre>
<p>
It is clear the query calculates the percentage of used memory
for the given $node, $port and $job. Isn't it? :)
</p>
<p>
What's wrong with this query? Copy-pasted label filters for distinct timeseries
which makes it easy to mistype these filters during modification.
Let's simplify the query with WITH expressions:
</p>
<pre>
WITH (
commonFilters = {instance=~"$node:$port",job=~"$job"}
)
(
node_memory_MemTotal_bytes{commonFilters}
-
node_memory_MemFree_bytes{commonFilters}
)
/
node_memory_MemTotal_bytes{commonFilters} * 100
</pre>
<p>
Now label filters are located in a single place instead of three distinct places.
The query mentions node_memory_MemTotal_bytes metric twice and {commonFilters}
three times. WITH expressions may improve this:
</p>
<pre>
WITH (
my_resource_utilization(free, limit, filters) = (limit{filters} - free{filters}) / limit{filters} * 100
)
my_resource_utilization(
node_memory_MemFree_bytes,
node_memory_MemTotal_bytes,
{instance=~"$node:$port",job=~"$job"},
)
</pre>
<p>
Now the template function my_resource_utilization() may be used for monitoring arbitrary
resources - memory, CPU, network, storage, you name it.
</p>
<p>
Let's take another nice query from <a href="https://grafana.com/dashboards/1860">Node Exporter Full</a> dashboard:
</p>
<pre>
(
(
(
count(
count(node_cpu_seconds_total{instance=~"$node:$port",job=~"$job"}) by (cpu)
)
)
-
avg(
sum by (mode) (rate(node_cpu_seconds_total{mode='idle',instance=~"$node:$port",job=~"$job"}[5m]))
)
)
*
100
)
/
count(
count(node_cpu_seconds_total{instance=~"$node:$port",job=~"$job"}) by (cpu)
)
</pre>
<p>
Do you understand what does this mess do? Is it manageable? :) WITH expressions are happy to help in a few iterations.
<br/>
<br/>
1. Extract common filters used in multiple places into a commonFilters variable:
</p>
<pre>
WITH (
commonFilters = {instance=~"$node:$port",job=~"$job"}
)
(
(
(
count(
count(node_cpu_seconds_total{commonFilters}) by (cpu)
)
)
-
avg(
sum by (mode) (rate(node_cpu_seconds_total{mode='idle',commonFilters}[5m]))
)
)
*
100
)
/
count(
count(node_cpu_seconds_total{commonFilters}) by (cpu)
)
</pre>
<p>
2. Extract "count(count(...) by (cpu))" into cpuCount variable:
</p>
<pre>
WITH (
commonFilters = {instance=~"$node:$port",job=~"$job"},
cpuCount = count(count(node_cpu_seconds_total{commonFilters}) by (cpu))
)
(
(
cpuCount
-
avg(
sum by (mode) (rate(node_cpu_seconds_total{mode='idle',commonFilters}[5m]))
)
)
*
100
) / cpuCount
</pre>
<p>
3. Extract rate(...) part into cpuIdle variable, since it is clear now that this part calculates the number of idle CPUs:
</p>
<pre>
WITH (
commonFilters = {instance=~"$node:$port",job=~"$job"},
cpuCount = count(count(node_cpu_seconds_total{commonFilters}) by (cpu)),
cpuIdle = sum(rate(node_cpu_seconds_total{mode='idle',commonFilters}[5m]))
)
((cpuCount - cpuIdle) * 100) / cpuCount
</pre>
<p>
4. Put node_cpu_seconds_total{commonFilters} into its own varialbe with the name cpuSeconds:
</p>
<pre>
WITH (
cpuSeconds = node_cpu_seconds_total{instance=~"$node:$port",job=~"$job"},
cpuCount = count(count(cpuSeconds) by (cpu)),
cpuIdle = sum(rate(cpuSeconds{mode='idle'}[5m]))
)
((cpuCount - cpuIdle) * 100) / cpuCount
</pre>
<p>
Now the query became more clear comparing to the initial query.
</p>
<p>
WITH expressions may be nested and may be put anywhere. Try expanding the following query:
</p>
<pre>
WITH (
f(a, b) = WITH (
f1(x) = b-x,
f2(x) = x+x
) f1(a)*f2(b)
) f(foo, with(x=bar) x)
</pre>
{% endfunc %}

View File

@ -0,0 +1,345 @@
// Code generated by qtc from "expand-with-exprs.qtpl". DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
//line app/vmselect/prometheus/expand-with-exprs.qtpl:1
package prometheus
//line app/vmselect/prometheus/expand-with-exprs.qtpl:1
import (
"github.com/VictoriaMetrics/metricsql"
)
// ExpandWithExprsResponse returns a webpage, which expands with templates in q MetricsQL.
//line app/vmselect/prometheus/expand-with-exprs.qtpl:8
import (
qtio422016 "io"
qt422016 "github.com/valyala/quicktemplate"
)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:8
var (
_ = qtio422016.Copy
_ = qt422016.AcquireByteBuffer
)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:8
func StreamExpandWithExprsResponse(qw422016 *qt422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:8
qw422016.N().S(`<html><head><title>Expand WITH expressions</title><style>p { font-weight: bold }textarea { margin: 1em }</style></head><body><div><form method="get"><div><p><a href="https://docs.victoriametrics.com/MetricsQL.html">MetricsQL</a> query with optional WITH expressions:</p><textarea name="query" style="height: 15em; width: 90%">`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:25
qw422016.E().S(q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:25
qw422016.N().S(`</textarea><br/><input type="submit" value="Expand" /><p><a href="https://docs.victoriametrics.com/MetricsQL.html">MetricsQL</a> query after expanding WITH expressions and applying other optimizations:</p><textarea style="height: 5em; width: 90%" readonly="readonly">`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:31
streamexpandWithExprs(qw422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:31
qw422016.N().S(`</textarea></div></form></div><div>`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:36
streamwithExprsTutorial(qw422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:36
qw422016.N().S(`</div></body></html>`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
func WriteExpandWithExprsResponse(qq422016 qtio422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
StreamExpandWithExprsResponse(qw422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
func ExpandWithExprsResponse(q string) string {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
WriteExpandWithExprsResponse(qb422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
return qs422016
//line app/vmselect/prometheus/expand-with-exprs.qtpl:40
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:42
func streamexpandWithExprs(qw422016 *qt422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:43
if len(q) == 0 {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:44
return
//line app/vmselect/prometheus/expand-with-exprs.qtpl:45
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:47
expr, err := metricsql.Parse(q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:48
if err != nil {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:48
qw422016.N().S(`Cannot parse query:`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:49
qw422016.E().V(err)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:50
} else {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:51
expr = metricsql.Optimize(expr)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:52
qw422016.E().Z(expr.AppendString(nil))
//line app/vmselect/prometheus/expand-with-exprs.qtpl:53
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
func writeexpandWithExprs(qq422016 qtio422016.Writer, q string) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
streamexpandWithExprs(qw422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
func expandWithExprs(q string) string {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
writeexpandWithExprs(qb422016, q)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
return qs422016
//line app/vmselect/prometheus/expand-with-exprs.qtpl:54
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:58
func streamwithExprsTutorial(qw422016 *qt422016.Writer) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:58
qw422016.N().S(`
<h3>Tutorial for WITH expressions in <a href="https://docs.victoriametrics.com/MetricsQL.html">MetricsQL</a></h3>
<p>
Let's look at the following real query from <a href="https://grafana.com/dashboards/1860">Node Exporter Full</a> dashboard:
</p>
<pre>
(
(
node_memory_MemTotal_bytes{instance=~"$node:$port", job=~"$job"}
-
node_memory_MemFree_bytes{instance=~"$node:$port", job=~"$job"}
)
/
node_memory_MemTotal_bytes{instance=~"$node:$port", job=~"$job"}
)
*
100
</pre>
<p>
It is clear the query calculates the percentage of used memory
for the given $node, $port and $job. Isn't it? :)
</p>
<p>
What's wrong with this query? Copy-pasted label filters for distinct timeseries
which makes it easy to mistype these filters during modification.
Let's simplify the query with WITH expressions:
</p>
<pre>
WITH (
commonFilters = {instance=~"$node:$port",job=~"$job"}
)
(
node_memory_MemTotal_bytes{commonFilters}
-
node_memory_MemFree_bytes{commonFilters}
)
/
node_memory_MemTotal_bytes{commonFilters} * 100
</pre>
<p>
Now label filters are located in a single place instead of three distinct places.
The query mentions node_memory_MemTotal_bytes metric twice and {commonFilters}
three times. WITH expressions may improve this:
</p>
<pre>
WITH (
my_resource_utilization(free, limit, filters) = (limit{filters} - free{filters}) / limit{filters} * 100
)
my_resource_utilization(
node_memory_MemFree_bytes,
node_memory_MemTotal_bytes,
{instance=~"$node:$port",job=~"$job"},
)
</pre>
<p>
Now the template function my_resource_utilization() may be used for monitoring arbitrary
resources - memory, CPU, network, storage, you name it.
</p>
<p>
Let's take another nice query from <a href="https://grafana.com/dashboards/1860">Node Exporter Full</a> dashboard:
</p>
<pre>
(
(
(
count(
count(node_cpu_seconds_total{instance=~"$node:$port",job=~"$job"}) by (cpu)
)
)
-
avg(
sum by (mode) (rate(node_cpu_seconds_total{mode='idle',instance=~"$node:$port",job=~"$job"}[5m]))
)
)
*
100
)
/
count(
count(node_cpu_seconds_total{instance=~"$node:$port",job=~"$job"}) by (cpu)
)
</pre>
<p>
Do you understand what does this mess do? Is it manageable? :) WITH expressions are happy to help in a few iterations.
<br/>
<br/>
1. Extract common filters used in multiple places into a commonFilters variable:
</p>
<pre>
WITH (
commonFilters = {instance=~"$node:$port",job=~"$job"}
)
(
(
(
count(
count(node_cpu_seconds_total{commonFilters}) by (cpu)
)
)
-
avg(
sum by (mode) (rate(node_cpu_seconds_total{mode='idle',commonFilters}[5m]))
)
)
*
100
)
/
count(
count(node_cpu_seconds_total{commonFilters}) by (cpu)
)
</pre>
<p>
2. Extract "count(count(...) by (cpu))" into cpuCount variable:
</p>
<pre>
WITH (
commonFilters = {instance=~"$node:$port",job=~"$job"},
cpuCount = count(count(node_cpu_seconds_total{commonFilters}) by (cpu))
)
(
(
cpuCount
-
avg(
sum by (mode) (rate(node_cpu_seconds_total{mode='idle',commonFilters}[5m]))
)
)
*
100
) / cpuCount
</pre>
<p>
3. Extract rate(...) part into cpuIdle variable, since it is clear now that this part calculates the number of idle CPUs:
</p>
<pre>
WITH (
commonFilters = {instance=~"$node:$port",job=~"$job"},
cpuCount = count(count(node_cpu_seconds_total{commonFilters}) by (cpu)),
cpuIdle = sum(rate(node_cpu_seconds_total{mode='idle',commonFilters}[5m]))
)
((cpuCount - cpuIdle) * 100) / cpuCount
</pre>
<p>
4. Put node_cpu_seconds_total{commonFilters} into its own varialbe with the name cpuSeconds:
</p>
<pre>
WITH (
cpuSeconds = node_cpu_seconds_total{instance=~"$node:$port",job=~"$job"},
cpuCount = count(count(cpuSeconds) by (cpu)),
cpuIdle = sum(rate(cpuSeconds{mode='idle'}[5m]))
)
((cpuCount - cpuIdle) * 100) / cpuCount
</pre>
<p>
Now the query became more clear comparing to the initial query.
</p>
<p>
WITH expressions may be nested and may be put anywhere. Try expanding the following query:
</p>
<pre>
WITH (
f(a, b) = WITH (
f1(x) = b-x,
f2(x) = x+x
) f1(a)*f2(b)
) f(foo, with(x=bar) x)
</pre>
`)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
func writewithExprsTutorial(qq422016 qtio422016.Writer) {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
qw422016 := qt422016.AcquireWriter(qq422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
streamwithExprsTutorial(qw422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
qt422016.ReleaseWriter(qw422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
}
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
func withExprsTutorial() string {
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
qb422016 := qt422016.AcquireByteBuffer()
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
writewithExprsTutorial(qb422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
qs422016 := string(qb422016.B)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
qt422016.ReleaseByteBuffer(qb422016)
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
return qs422016
//line app/vmselect/prometheus/expand-with-exprs.qtpl:245
}

View File

@ -57,6 +57,15 @@ var (
// Default step used if not set.
const defaultStep = 5 * 60 * 1000
// ExpandWithExprs handles the request to /expand-with-exprs
func ExpandWithExprs(w http.ResponseWriter, r *http.Request) {
query := r.FormValue("query")
bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw)
WriteExpandWithExprsResponse(bw, query)
_ = bw.Flush()
}
// FederateHandler implements /federate . See https://prometheus.io/docs/prometheus/latest/federation/
func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) error {
defer federateDuration.UpdateDuration(startTime)
@ -399,10 +408,7 @@ func exportHandler(qt *querytracer.Tracer, w http.ResponseWriter, cp *commonPara
if format == "promapi" {
WriteExportPromAPIFooter(bw, qt)
}
if err := bw.Flush(); err != nil {
return err
}
return nil
return bw.Flush()
}
type exportBlock struct {

View File

@ -673,7 +673,7 @@ Released at 12-04-2022
* BUGFIX: [VictoriaMetrics cluster](https://docs.victoriametrics.com/Cluster-VictoriaMetrics.html): properly propagate limits at `-search.max*` command-line flags from `vminsert` to `vmstorage`. The limits are `-search.maxUniqueTimeseries`, `-search.maxSeries`, `-search.maxFederateSeries`, `-search.maxExportSeries`, `-search.maxGraphiteSeries` and `-search.maxTSDBStatusSeries`. They weren't propagated to `vmstorage` because of the bug. These limits were introduced in [v1.76.0](https://docs.victoriametrics.com/CHANGELOG.html#v1760). See [this bug](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2450).
* BUGFIX: fix goroutine leak and possible deadlock when importing invalid data via [native binary format](https://docs.victoriametrics.com/#how-to-import-data-in-native-format). See [this pull request](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/2423).
* BUGFIX: [Graphite Render API](https://docs.victoriametrics.com/#graphite-render-api-usage): properly calculate [hitCount](https://graphite.readthedocs.io/en/latest/functions.html#graphite.render.functions.hitcount) function. Previously it could return empty results if there were no original samples in some parts of the selected time range.
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): allow overriding built-in function names inside [WITH templates](https://play.victoriametrics.com/promql/expand-with-exprs). For example, `WITH (sum(a,b) = a + b + 1) sum(x,y)` now expands into `x + y + 1`. Previously such a query would fail with `cannot use reserved name` error. See [this bugreport](https://github.com/VictoriaMetrics/metricsql/issues/5).
* BUGFIX: [MetricsQL](https://docs.victoriametrics.com/MetricsQL.html): allow overriding built-in function names inside [WITH templates](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/expand-with-exprs). For example, `WITH (sum(a,b) = a + b + 1) sum(x,y)` now expands into `x + y + 1`. Previously such a query would fail with `cannot use reserved name` error. See [this bugreport](https://github.com/VictoriaMetrics/metricsql/issues/5).
* BUGFIX: [vmui](https://docs.victoriametrics.com/#vmui): properly display values greater than 1000 on Y axis. See [this issue](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2409).

View File

@ -90,7 +90,7 @@ The list of MetricsQL features:
* `if` binary operator. `q1 if q2` removes values from `q1` for missing values from `q2`.
* `ifnot` binary operator. `q1 ifnot q2` removes values from `q1` for existing values from `q2`.
* `WITH` templates. This feature simplifies writing and managing complex queries.
Go to [WITH templates playground](https://play.victoriametrics.com/promql/expand-with-exprs) and try it.
Go to [WITH templates playground](https://play.victoriametrics.com/select/accounting/1/6a716b0f-38bc-4856-90ce-448fd713e3fe/expand-with-exprs) and try it.
* String literals may be concatenated. This is useful with `WITH` templates:
`WITH (commonPrefix="long_metric_prefix_") {__name__=commonPrefix+"suffix1"} / {__name__=commonPrefix+"suffix2"}`.
* `keep_metric_names` modifier can be applied to all the [rollup functions](#rollup-functions) and [transform functions](#transform-functions).