У меня есть два таких SQL-запроса для postgresql
:
Первый запрос — это выборка подписок, которые никогда не синхронизировались или были недавно обновлены и синхронизированы давным-давно:
SELECT *
FROM subscriptions
WHERE (last_sync_at IS NULL OR last_sync_at < updated_at) AND updated_at < $1
ORDER BY last_sync_at NULLS FIRST
LIMIT $2`,
Второй запрос — получение подписок, но первой синхронизируется та, которая была синхронизирована раньше всего.
SELECT *
FROM subscriptions
ORDER BY last_sync_at NULLS FIRST
LIMIT $1`,
Я хочу объединить эти два запроса следующим образом: Если первый запрос возвращает меньше результатов, чем лимит, я хочу выполнить второй запрос, чтобы заполнить пакет.
Например, если предел равен 100
и первый запрос возвращает 90
, я хочу выполнить второй с пределом 10
.
Я могу легко сделать это с помощью кода, но мне интересно, есть ли способ сделать это с помощью одного SQL-запроса и будет ли он более производительным.
Я пробовал что-то вроде этого, но, очевидно, это не работает:
`(SELECT *, COUNT(*) as nb_rows_selected
FROM subscriptions
WHERE (last_chartmogul_sync_at IS NULL OR last_chartmogul_sync_at < updated_at) AND updated_at < $1
ORDER BY last_chartmogul_sync_at NULLS FIRST
GROUP BY id
LIMIT $2)
UNION
(SELECT *
FROM subscriptions
ORDER BY last_sy NULLS FIRST
LIMIT nb_rows_selected - $2)`,
Любая помощь будет оценена по достоинству. Спасибо!
Один из трюков, который вы можете использовать, — это добавить вычисляемый столбец для отслеживания каждой половины объединения. Затем снова выполните подзапрос и ограничите порядок, используя этот вычисляемый столбец для обеспечения порядка:
SELECT *
FROM
(
(
SELECT *, 1 AS src
FROM subscriptions
WHERE (last_sync_at IS NULL OR last_sync_at < updated_at) AND
updated_at < $1
ORDER BY last_sync_at NULLS FIRST
LIMIT $2
)
UNION
(
SELECT *, 2
FROM subscriptions
ORDER BY last_sync_at NULLS FIRST
LIMIT $1
)
) t
ORDER BY src
LIMIT 100;
В приведенном выше примере будут использоваться записи из первого запроса по приоритету для заполнения предельной квоты в 100. Только если этот первый запрос возвращает менее 100 записей, будут использоваться записи из второго запроса.
Спасибо! У меня есть два замечания: 1. С точки зрения производительности, я думаю, это довольно плохо, не так ли? Потому что второй запрос всегда будет вычисляться, даже если первый вернул 100 записей? 2. Также, если записей меньше лимита. Например, 80 записей. Этот запрос заполнит результат, добавив дубликаты, чтобы достичь предела в 100. Что проблематично с моей точки зрения
1. Да, возможно, производительность не очень хорошая, но вы всегда можете проверить план объяснения, индекс и т. д. 2. Ограничение в 100 означает, что 100 записей означает возврат наиболее доступных, но не более 100. Если доступно только 80, это будет вернуть 80.
Я нашел правильный ответ, немного изменив свой первый подход. Я по-прежнему использую UNION
и COUNT
, но на этот раз я упростил сложность, используя предложение С:
WITH unsynced_or_recently_updated_subscriptions AS (
SELECT *
FROM subscriptions
WHERE (last_sync_at IS NULL OR last_sync_at < updated_at)
AND updated_at < $1
ORDER BY last_sync_at NULLS FIRST
LIMIT $2
),
oldest_synced_subscriptions AS (
SELECT *
FROM subscriptions
WHERE id NOT IN (SELECT id FROM unsynced_or_recently_updated_subscriptions)
ORDER BY last_sync_at NULLS FIRST
LIMIT $2 - (SELECT COUNT(*) FROM unsynced_or_recently_updated_subscriptions)
)
SELECT *
FROM unsynced_or_recently_updated_subscriptions
UNION ALL
SELECT *
FROM oldest_synced_subscriptions;
При таком подходе второй запрос ограничивается [the number of rows of the first query] - [the limit]
. Что очень эффективно. Потому что, если первый запрос превысил лимит, второй запрос будет иметь лимит 0
и немедленно вернет результат.
Насколько я понимаю, этот комбинированный подход обычно более эффективен, чем выполнение двух отдельных запросов, поскольку он снижает накладные расходы, связанные с несколькими подключениями к базе данных.
Единственный сценарий, в котором два отдельных запроса могут быть более производительными, — это когда первый запрос последовательно удовлетворяет пределу, и в этом случае второй запрос не нужен. Однако в моем случае такая ситуация вряд ли будет часто возникать.
Кроме того, предложение NOT IN
должно оказывать минимальное влияние на производительность, поскольку столбец id
индексируется.
Еще лучшим решением может быть использование NOT EXISTS
вместо NOT IN
для второго подзапроса:
oldest_synced_subscriptions AS (
SELECT *
FROM subscriptions
WHERE NOT EXISTS (
SELECT id FROM unsynced_or_recently_updated_subscriptions
WHERE unsynced_or_recently_updated_subscriptions.id = subscriptions.id
)
ORDER BY last_chartmogul_sync_at NULLS FIRST
LIMIT $2 - (SELECT COUNT(*) FROM unsynced_or_recently_updated_subscriptions)
)
Предложение NOT EXISTS
обычно более эффективно, чем NOT IN
, особенно при работе со значениями NULL
. С точки зрения производительности NOT EXISTS
часто работает быстрее, чем NOT IN
, потому что он может прекратить оценку, как только найдет совпадение, тогда как NOT IN
, возможно, придется сканировать весь набор результатов подзапроса.
Пусть запрос включает результаты обоих типов, но используйте
ORDER BY CASE WHEN <criteria 1> THEN 1 WHEN <criteria 2> THEN 2 END
, чтобы расставить приоритеты строк, которые должны находиться в пределах лимита.