Комментарии 77
Что-то у меня упорно не хочет работать explain.tensor.ru — везде отдаёт пустой файл браузеру (последние стабильные версии Firefox и Chromium под линуксом).
Попробовал несколько разных планов из личной коллекции «Как не надо писать SQL».
Например такой план:
Finalize Aggregate (cost=2684564053637.09..2684564053637.10 rows=1 width=8)
-> Gather (cost=2684564053636.88..2684564053637.08 rows=2 width=8)
Workers Planned: 2
-> Partial Aggregate (cost=2684564052636.88..2684564052636.88 rows=1 width=8)
-> Parallel Index Only Scan using index_listings_on_source_id on listings
(cost=0.56..2684564041656.88 rows=4391999 width=0)
Filter: (NOT (SubPlan 1))
SubPlan 1
-> Materialize (cost=0.00..607553.21 rows=1474540 width=4)
-> Seq Scan on products (cost=0.00..594420.51 rows=1474540 width=4)
Filter: ((site_id IS NOT NULL) AND (item_id IS NOT NULL))
К такому запросу:
SELECT COUNT(*) FROM "listings"
WHERE "listings"."source_id" NOT IN (
SELECT "products"."id" FROM "products"
WHERE "products"."site_id" IS NOT NULL AND "products"."item_id" IS NOT NULL
)

Вставку наладим немного позже.
Запрос с cost в несколько триллионов попугаев с указанием analyze будет выполняться сутки, возможно, что и не одни :-D
Так и не смог понять, что надо скормить постгресу, чтобы он sequental scan превратил в index scan. Там поиск по таблице с ~1 млн фейковых записей, и фильтр по колонкам, которые в 99.5% истинны. То есть, возможно, там действительно укорять нечего.
И да, я так понимаю, что cost-попугаи – условная стоимость чтения с диска и выполнения операций, а не предсказанное время. По крайней мере, так документация говорит.
Но если нечего делать и задачка интересна, то вот запрос. :)
Типы данных, кмк, и так ясны, но у фильтров и юзеров есть
TEXT[]
, если что.Там быстродействие не столь важно, результаты кешируются бекендом на пару минут для повторных запросов, но нет предела совершенству. И преждевременной оптимизации.
Чисто из спортивного интереса — а что за задача такая? То есть мы сортируем всех 1.1M юзеров по близости к заданному — а зачем такое может быть нужно?
На самом деле я хочу сделать более защищённый алгоритм поиска расстояния между пользователями, но не хватает времени, всё на более приоритетные задачи уходит.
Более защищённый в том плане, чтобы нельзя было вычислить настоящие координаты другого пользователя, сдвигая свои.
Сейчас расстояние до другого пользователя показывается логарифмически "<1км", «5км», «10км», «25км», ..., но простым округлением. И если сдвигать свои координаты как раз на границе округления 1км-5км, то можно очень даже неплохо триангуляцией узнать приватные данные. Просто округлять в БД координаты WGS84 до четырёх знаков после запятой нехорошо, будет много пользователей с расстоянием в 0 км. Надо почитать на эту тему научных статей.
А, и да, сортируются не все миллион пользователей, а тысяча-другая, которая осталась после фильтрации по всем условиям в WHERE. Хотя если фильтр будет крайне щадящий и пропустит всех пользователей, то, на удивление, всего 2.5 секунды выполняется. Я ожидал худшего. Хотя база на 60-гиговом ssd, external merge быстрый.
Если фильтр малоселективный, то не эффективнее ли будет идти от сортировки по расстоянию, отфильтровывая неподходящих юзеров вложенным запросом?
идти от сортировки по расстоянию, отфильтровывая неподходящих юзеров вложенным запросомА вот тут моих знаний sql и опыта уже явно не хватает, я просто не представляю, как и куда это сделать.
... WHERE id (SELECT /*здесь фильтры*/) ...
Как-то так? Или через CTE?
SELECT
...
FROM
users u
WHERE
apply_filter(u)
ORDER BY
st_distance(coord(u), coord($1))
LIMIT 100
Ну и что-то типа KNN-gist в качестве индекса.
Хотел задействовать типы данных и индексы из постгиса, но не дошли руки. ':)
...
WHERE
CASE WHEN simple_cond(u) THEN complex_cond(u) END
то есть что-то типа
...
WHERE
CASE WHEN u.sex = 'male' THEN
EXISTS(
SELECT NULL FROM children WHERE parent = u.id LIMIT 1
)
END
Поправили вставку запроса в паре с "простым" explain (без analyze), смотрите:
https://explain.tensor.ru/archive/explain/74aca500-908b-1308-16ec-2d63ece409ba:0:2019-11-27
Мне кажеться та будет быстрее:
SELECT COUNT(*) FROM "listings"
INNER JOIN "products" ON products.id = listings.source_id
WHERE "products"."site_id" IS NOT NULL AND "products"."item_id" IS NOT NULL
то есть без sub query
в where
которая исполняеться для каждой строчки listing
.
Ну я же говорил, что это из моей личной коллекции «Как не надо писать SQL» :-D
В этом примере я рассказываю людям о том, что использовать WHERE NOT IN
для логического отрицания, хоть и логически правильно, очень часто убийственно для производительности и запросы надо переписывать явно, используя IN
и перенося отрицание внутрь подзапроса:
SELECT COUNT(*) FROM "listings"
WHERE "listings"."source_id" IN (
SELECT "products"."id" FROM "products"
WHERE ("products"."site_id" IS NULL OR "products"."item_id" IS NULL)
);
И, хоть это немного и контринтуитивно, но такой вариант с подзапросом работает ощутимо быстрее вашего примера с JOIN (1 секунда против 8), хотя я сам думал, что ваш будет быстрее.
EXPLAIN (ANALYZE,BUFFERS) SELECT COUNT(*) FROM "listings" WHERE "listings"."source_id" IN (SELECT "products"."id" FROM "products" WHERE ("products"."site_id" IS NULL OR "products"."item_id" IS NULL));
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=390913.04..390913.05 rows=1 width=8) (actual time=912.009..912.010 rows=1 loops=1)
Buffers: shared hit=1069212
-> Gather (cost=390912.82..390913.03 rows=2 width=8) (actual time=910.658..1022.559 rows=3 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=1069212
-> Partial Aggregate (cost=389912.82..389912.83 rows=1 width=8) (actual time=901.824..901.825 rows=1 loops=3)
Buffers: shared hit=1069212
-> Nested Loop (cost=8447.83..387299.92 rows=1045160 width=0) (actual time=160.246..871.369 rows=293153 loops=3)
Buffers: shared hit=1069212
-> Parallel Bitmap Heap Scan on products (cost=8447.40..262462.63 rows=127617 width=4) (actual time=160.078..421.076 rows=52771 loops=3)
Recheck Cond: ((site_id IS NULL) OR (item_id IS NULL))
Heap Blocks: exact=44070
Buffers: shared hit=129371
-> BitmapOr (cost=8447.40..8447.40 rows=319359 width=0) (actual time=120.626..120.626 rows=0 loops=1)
Buffers: shared hit=17749
-> Bitmap Index Scan on index_products_on_site_id (cost=0.00..4836.83 rows=159680 width=0) (actual time=87.821..87.821 rows=158311 loops=1)
Index Cond: (site_id IS NULL)
Buffers: shared hit=8895
-> Bitmap Index Scan on index_products_on_item_id (cost=0.00..3457.43 rows=159680 width=0) (actual time=32.799..32.799 rows=158312 loops=1)
Index Cond: (item_id IS NULL)
Buffers: shared hit=8854
-> Index Only Scan using index_listings_on_source_id_and_site_id on listings (cost=0.43..0.88 rows=10 width=4) (actual time=0.005..0.007 rows=6 loops=158312)
Index Cond: (source_id = products.id)
Heap Fetches: 521
Buffers: shared hit=939841
Planning Time: 1.057 ms
Execution Time: 1022.784 ms
vs
EXPLAIN (ANALYZE, BUFFERS) SELECT COUNT(*) FROM "listings" INNER JOIN "products" ON products.id = listings.source_id WHERE "products"."site_id" IS NOT NULL OR "products"."item_id" IS NOT NULL;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
----
Finalize Aggregate (cost=810010.97..810010.98 rows=1 width=8) (actual time=8011.359..8011.361 rows=1 loops=1)
Buffers: shared hit=6341781 read=332216 dirtied=1, temp read=51798 written=51944
-> Gather (cost=810010.76..810010.97 rows=2 width=8) (actual time=8006.995..8194.180 rows=3 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=6341781 read=332216 dirtied=1, temp read=51798 written=51944
-> Partial Aggregate (cost=809010.76..809010.77 rows=1 width=8) (actual time=7999.486..7999.486 rows=1 loops=3)
Buffers: shared hit=6341781 read=332216 dirtied=1, temp read=51798 written=51944
-> Parallel Hash Join (cost=509013.67..792489.42 rows=6608535 width=0) (actual time=5442.368..7512.710 rows=5027767 loops=3)
Hash Cond: (listings.source_id = products.id)
Buffers: shared hit=6341781 read=332216 dirtied=1, temp read=51798 written=51944
-> Parallel Index Only Scan using index_listings_on_showcase_id_and_source_id on listings (cost=0.43..210880.60 rows=6653162 width=4) (actual time=0.100..2465.326 rows=5320919 loops
=3)
Heap Fetches: 17438
Buffers: shared hit=6186249 dirtied=1
-> Parallel Hash (cost=495773.71..495773.71 rows=806922 width=4) (actual time=1639.397..1639.397 rows=598917 loops=3)
Buckets: 262144 Batches: 16 Memory Usage: 6496kB
Buffers: shared hit=155434 read=332216, temp written=5044
-> Parallel Seq Scan on products (cost=0.00..495773.71 rows=806922 width=4) (actual time=0.060..1435.249 rows=598917 loops=3)
Filter: ((site_id IS NOT NULL) OR (item_id IS NOT NULL))
Rows Removed by Filter: 52770
Buffers: shared hit=155434 read=332216
Planning Time: 0.930 ms
Execution Time: 8196.618 ms
А оригинальный вариант с NOT IN
работал сутками, за что и попал в кунсткамеру.
Ну я же говорил, что это из моей личной коллекции «Как не надо писать SQL» :-D
Не увидел, виноват (главное читал же комент))
И, хоть это немного и контринтуитивно, но такой вариант с подзапросом работает ощутимо быстрее вашего примера с JOIN (1 секунда против 8), хотя я сам думал, что ваш будет быстрее.
Не знал о таком. Спасибо)
Хм. В варианте с inner join ошибка: должно быть не "IS NOT NULL", а "IS NULL".
По експлейнах вижу Seq Scan on products
в случаи с JOIN.
Интересно что будет если все условия перенести в JOIN:
SELECT COUNT(*) FROM "listings"
INNER JOIN "products" ON products.id = listings.source_id and (
"products"."site_id" IS NULL OR "products"."item_id" IS NULL
);
С условием да — mea culpa, mea maxima culpa — условие поправил и план похорошел и стал точно таким же, как и у подзапроса.
EXPLAIN (ANALYZE, BUFFERS) SELECT COUNT(*) FROM "listings" INNER JOIN "products" ON products.id = listings.source_id WHERE "products"."site_id" IS NULL OR "products"."item_id" IS NULL;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=390928.53..390928.54 rows=1 width=8) (actual time=921.498..921.499 rows=1 loops=1)
Buffers: shared hit=1111266
-> Gather (cost=390928.31..390928.52 rows=2 width=8) (actual time=920.300..1049.853 rows=3 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=1111266
-> Partial Aggregate (cost=389928.31..389928.32 rows=1 width=8) (actual time=914.771..914.772 rows=1 loops=3)
Buffers: shared hit=1111266
-> Nested Loop (cost=8450.03..387315.41 rows=1045160 width=0) (actual time=119.589..883.410 rows=293190 loops=3)
Buffers: shared hit=1111266
-> Parallel Bitmap Heap Scan on products (cost=8449.60..262464.83 rows=127617 width=4) (actual time=119.403..369.221 rows=52779 loops=3)
Recheck Cond: ((site_id IS NULL) OR (item_id IS NULL))
Heap Blocks: exact=53046
Buffers: shared hit=129389
-> BitmapOr (cost=8449.60..8449.60 rows=319359 width=0) (actual time=85.178..85.178 rows=0 loops=1)
Buffers: shared hit=17760
-> Bitmap Index Scan on index_products_on_site_id (cost=0.00..4837.93 rows=159680 width=0) (actual time=57.117..57.117 rows=158355 loops=1)
Index Cond: (site_id IS NULL)
Buffers: shared hit=8900
-> Bitmap Index Scan on index_products_on_item_id (cost=0.00..3458.53 rows=159680 width=0) (actual time=28.055..28.055 rows=158356 loops=1)
Index Cond: (item_id IS NULL)
Buffers: shared hit=8860
-> Index Only Scan using index_listings_on_source_id_and_site_id on listings (cost=0.43..0.88 rows=10 width=4) (actual time=0.006..0.008 rows=6 loops=158337)
Index Cond: (source_id = products.id)
Heap Fetches: 58308
Buffers: shared hit=981877
Planning Time: 0.908 ms
Execution Time: 1050.009 ms
<!--</spoiler>-->
Пока не делали поддержку сторонних форков типа Greenplum и спецузлов в них вроде Gather Motion/Partition Selector/Dynamic Table Scan.
А вам чего именно не хватает? Пишите — посмотрим, добавим.
Но зато теперь сразу и Greenplum, и Citus, и Timescale, и Redshift:
habr.com/ru/company/tensor/blog/531620
explain.depesz.com
theartofpostgresql.com/explain-plan-visualizer
Про часть из них я рассказывал на PGConf.
www.depesz.com/2020/01/07/finally-fixed-time-calculations-for-parallel-plans-in-explain-depesz-com
Вот чего вам не хватает — так это флеймграфов, вот как эта тулза строит: https://github.com/mgartner/pg_flame (пример: https://mgartner.github.io/pg_flame/flamegraph.html )
Мне кажется, что это самый понятный способ визуализации происходящего. Не хотите добавить себе? Там в issues уже предлагали сделать подобное и даже js-реализацию находили: https://github.com/mgartner/pg_flame/issues/2
Поэтому пока остановились на piechart + граф.
По сути, piechart (справа при наведении на шеврон выезжает) — и есть flamegraph, только свернутый в кольцо.
Кабы вы не рассказали про шеврон — ни в жизнь бы его не нашёл и не навёл бы на него. Круто, красота!
Бажный он, немного, правда, на этом плане, но, надеюсь, почините как-нибудь.
В параллельных запросах Finalize-узлы отдают данные только с ведущего процесса, а остальные узлы — сумму по всем воркерам. Потому в результате отрицательные величины. :(
В v12 есть хотя бы более детальная стата по каждому воркеру, а что делать с более ранними версиями пока не придумалось.
1) Под Firefox мелкие косячки, потестируйте, пожалуйста. Например, размножающиеся диаграммы и т.п.
2) Рекомендации попадаются из разряда «пальцем в небо», вот прям совсем мимо.
3) Планируется ли английская версия интерфейса? Это ведь бета для хабратестеров?
2. ссылку на пример покажите, плз, и почему она кажется неподходящей
3. не в близком будущем
«Таблица сильно разрежена, рекомендуется произвести очистку с помощью VACUUM [FULL]». На самом деле этот план получен на staging машине, где только что развернули свежий дамп в единой транзакции, и не выполнили ни единого DML запроса. То есть таблица ну никак не может быть разреженной.
3) Жалко, хотелось бы поделиться с коллегами. С другой стороны, правильно, пускай учат великий могучий, будет стимул :)
На самом деле, он означает, что записи лежат сильно далеко друг от друга — (rows = 1245, RRbF = 232), но при этом 1453 buffers. То есть вычитывается практически 1 страница данных/запись, а это достаточно много.
И такое обычно бывает или если мы много-много раз апдейтили таблицу (тогда как раз VACUUM поможет), или если записи по ключу лежат «немного вначале, немного в конце» (тогда стоит посмотреть в сторону CLUSTER ON).
Планируется ли английская версия интерфейса?Наконец можно пробовать. И еще немного «удобностей»:
habr.com/ru/company/tensor/blog/562796
когда вы решили сделать JOIN нескольких CTE по паре десятков тысяч записей
миллионов?
и уже не хватает выделенных вычислительных мощностей CPU, постоянно прокачиваются гигабайты памяти или диск не успевает за всеми «хотелками» БД
то вы скорее всего уже гугл, ну или очень жадный
Есть еще вот такая шикарнейшая штука, просто невероятно полезна при анализе больших запросов
PEV полезен, но принимает планы только в JSON, мы пока только в Text. Но подтянемся. :)
А то я все чаще встречаю людей, которые говорят что join это очень тяжелая операция.
Чем больше переписываешь проблемных запросов, тем чаще убеждаешься, что JOIN'ы порой приносят больше проблем, чем решают. Я в последнее время предпочитаю выносить условия по другим таблицам в подзапросы со ссылкой на внешний запрос — как правило это и работает быстрее и читается лучше. Например:
SELECT posts.*
FROM posts
WHERE posts.user_id = 42
AND id IN (
SELECT post_id FROM comments
WHERE posts.id = comments.post_id -- JOIN-условие и подсказка планировщику
AND comments.something = …
)
Тем более, что выбирать все комменты по всем статьям пользователя не нужно никогда.
Посты и комменты здесь только, чтобы показать абстрактную связь один-ко-многим. На практике часто нужно выбирать записи, у которых есть определённные связанные записи. Просто вместе с JOIN'ом придётся лепить DISTINCT, чтобы избежать дублей (некоторые ORM так любят делать), а там прощай и производительность и читабельность.
Нет не лучше читается.
Дело вкуса, наверное
Посты и комменты здесь только, чтобы показать абстрактную связь один-ко-многим. На практике часто нужно выбирать записи, у которых есть определённные связанные записи. Просто вместе с JOIN'ом придётся лепить DISTINCT, чтобы избежать дублей (некоторые ORM так любят делать), а там прощай и производительность и читабельность.
Абстрактные примеры очень плохо отображают реальный мир. В реальном мире вы просто выведите список постов, плюсиков и левым джином докинете тему.
ЗЫ только у меня на хабре скролл плющит нереально? На двух разных машинах в двух разных броузерах.
Причем все время будет потрачено именно на CTE Scan-узле, а сам Join — дешевый, да. :)
Примерно так — через hstore/json:
WITH hs AS (
SELECT hstore(
array_agg(id::text)
, array_agg(T::text)
)
FROM
pktable T
)
SELECT
fk.*
, (((TABLE hs) -> fk::text)::fktable).*
FROM
fktable
Достаточно представить, что в pktable 10M записей, и извлечение каждой отдельной из таблицы стоит существенно дороже, чем ее сериализация-десериализация.
А после этого допустим, что в fktable 1K записей, но всего 10 _разных_ fk-ключей.
Итого имеем:
— 10 поисков по pktable, 10 сериализаций
— 1K десериализаций
vs (если будет Nested Loop)
— 1K поисков по pktable
Так что не бывает «говнокода в чистом виде», бывают вопросы эффективности применения метода. Хочу — применяю, не хочу — не применяю.
И пока ФОТ разработчиков стоит кратно меньше, дешевле их научить таким подходам, чем вкладывать деньги в красиво написанный, но медленно работающий код.
О чем молчит EXPLAIN, и как его разговорить