Как стать автором
Обновить

Комментарии 278

Тот же эллипсоид, вид сбоку: "Реализация сервера объектного представления средствами реляционной СУБД"
20 лет назад мы обертывали РСУБД слоем реализации ООП (не путать с ORM), сегодня вы снова оборачиваете движок Кодда в функциональную абстракцию.
Вывод: выбор в пользу реляционой модели в 1970-х был обоснованным :)
Функциональное программирование тоже появилось давно, но только сейчас оно стало более менее популярно.
К сожалению, в вашей ссылке не очень понял, в чем схожесть. Тут важнее не объекты, а функции.
Схожесть в подходе: берем реляционный движок и прикручивем к нему сверху новый слой абстракций, позволяющий разрабатывать в удобной авторам парадигме (ООП в нашем случае, «фукциональщина» в вашем)
В данном случае, реляционный движок был взят только с целью упрощения реализации. В идеале лучше реализовывать специальный движок именно под эту логику.
Весьма спорное упрощение реализации. Открыт вопрос, например, интеграции этого в волшебную страну .NET
Писать через DbScope? Путаница будет
Используем EF — не пишем sql и так(если ошибаюсь, не кидайте тапками, лично им не пользовался нормально)
Так в чем в итоге выигрыш? Возможно, для некоторых случаев это упростит задачу интеграции модели данных и БД, но смысл в этом небольшой
Если вы не используете SQL, а используете базу данных только для сериализации/десериализации объектов, то Вам не нужна даже реляционная СУБД. Используйте лучше NoSQL базы данных.

Не "не используем", а "не пишем" (в смысле, прикладной программист руками не пишет). А внизу там все равно РСУБД со всеми ее достоинствами.

А можете показать как задачу с 2* решить так чтобы «внизу РСУБД со всеми достоинствами»? На том же C#.
class Person
{
  IEnumerable<Person> Likes {get;}
  IEnumerable<Person> Friends {get;}
}

//вариант 1
from a in persons
from b in a.Friends
from c in b.Friends.Except(a.Friends)
where a.Likes.Contains(c)
select (a, b, c)

//вариант 2
persons
.SelectMany(a => a.Friends)
.SelectMany((a, b) => b.Friends.Except(a.Friends)))
.Where((a, b, c) => a.Likes.Contains(c))
Так неправильно же работать будет. Это задача 1 со звездочкой. А я про задачу 2 со звездочкой.

Нет, это будет работать строго в соответствии с условиями задачи:


A дружит с B

from b in a.Friends


B дружит с C

from c in b.Friends


A нравится C

a.Likes.Contains(c)


A не дружит с C

from c in [...].Except(a.Friends)


дружба не симметрична

Коллекции Friends однонаправлены (я больше того скажу, в конкретно взятом EF одно время надо было повозиться, чтобы это работало в две стороны, как задаче с одной звездочкой)

A дружит с B

Так а где B дружит с A?
Коллекции Friends однонаправлены

Это как? Что вообще такое «однонаправленные коллекции»?
И как вообще EF из вашего кода определяет он задачу с одной звездочкой или с двумя решает. Точнее приведите пожалуйста тогда решение задачи с одной звездочкой? Ну и SQL, который при этом .NET генерит, очень интересно посмотреть.
Так а где B дружит с A?

А его в условиях нет. Но это легко сделать, очевидно:


IQueryable<(Person, Person)> FriendsOrFriendOf(this Person p)
  => p.SelectMany(_ => AllOf<Person>).Where((a, b) => a.Friends.Contains(b) || b.Friends.Contains(a))

Это как?

Очень просто: a.Friends.Contains(b) не означает b.Friends.Contains(a).


И как вообще EF из вашего кода определяет он задачу с одной звездочкой или с двумя решает.

Из моего кода — никак. Это определяется на этапе создания модели данных, где и указывается, как связаны два объекта.


Ну и SQL, который при этом .NET генерит, очень интересно посмотреть.

Я уже писал один раз, повторю еще раз: .NET не генерит SQL. SQL генерят провайдеры, которых больше одного.

А его в условиях нет. Но это легко сделать, очевидно:

В задаче 2* есть. Это в задаче 1* нет.
И вы уверены, что там запрос как внизу в примере снизу с CTE и UNION'ами не получится, когда она будет две таблицы friends union'ить. И вы же вроде как говорили что || не поддерживаются. И поэтому очень интересно посмотреть на реальный запрос. Это же по идее должно быть очень просто. А то все же возмущаются, как это я не могу контролировать формирование SQL запросов.
Из моего кода — никак. Это определяется на этапе создания модели данных, где и указывается, как связаны два объекта.

Ну то есть, это не весь код. А можно тогда «всех посмотреть»?
Я уже писал один раз, повторю еще раз: .NET не генерит SQL. SQL генерят провайдеры, которых больше одного.

Ок, что сгенерит провайдер, который используется в большинстве случаев. Или что сгенерит самый лучший провайдер из существующих?
В задаче 2* есть.

Нет, в задаче 2* написано дословно следующее: "есть также задача с двумя звездочками. В ней дружба не симметрична.". Ничего больше.


И вы же вроде как говорили что || не поддерживаются.

Я говорил, что не поддерживается комбинаторика двух предикатов, объявленных как деревья, через обычные операторы языка, не более.


Ок, что сгенерит провайдер, который используется в большинстве случаев.

А какая, собственно, разница? Обсуждается форма записи, а не эффективность генерации SQL-кода.

А можете профайлером посмотреть, какой запрос к базе данных сформирует LINQ? Чисто для интереса, если не трудно. Не будет ли там случаем много маленьких запросов?

Нет, не могу: на этой машине нет ни среды разработки, ни SQL-сервера.


Ну и на всякий случай: запрос к БД формирует не LINQ, запрос к БД формирует провайдер, которых совсем не один.

Жаль.
А можете, раз пошла такая пьянка, еще и задачу 2 из сложных написать на LINQ (там где PARTITION)?
Для каждого покупателя (имя, фамилия) найти два товара (название), на которые покупатель потратил больше всего денег в 1997-м году.

customers
.SelectMany(c => c
  .Orders
  .Where(o => o.OrderDate.Year == 1997))
  .SelectMany(o => o.Lines)
  .GroupBy(
    ol => ol.Item,
    (item, lines) => (item, total: lines.Sum(l => l.Total))
  .OrderByDesc((_, total) => total)
  .Select((item, _) => item)
  .Take(2)
)
.Select((customer, item) => (
  customer.FirstName,
  customer.LastName,
  item.Name
))
Спасибо. А еще подскажите — можно ли в LINQ как-то объявлять «промежуточные» функции? Опять же как в том примере, чтобы разбить на несколько объявлений. Это важно, что потом составные части могут использоваться в других выражениях и запросах.
И конечно было бы интересно увидеть запросы к базе данных (для PostgreSQL желательно).
А еще подскажите — можно ли в LINQ как-то объявлять «промежуточные» функции?

Да, конечно. LINQ — это набор операций над IQueryable<T>, соответственно, вы просто объявляете методы (обычно это методы-расширения, самый близкий аналог функций из ФП), которые принимают IQueryable<A> и возвращают что-нибудь (например, IQueryable<B>). Эти методы дальше компонуются произвольным образом.


Я недавно спрашивал про "как объявить предикат "скидочный товар"", так вот, в LINQ это делается так (я покажу два этапа, просто чтобы было видно два разных уровня компонуемости):


Expression<Func<Item,bool>> IsDiscounted = item => item.Discount != null || item.SaleCampaign != null.

//использование предиката
someItems.Where(IsDiscounted)

//функция для композиции
IQueryable<Item> DiscountedItems(this IQueryable<Item> items) => items.Where(IsDiscounted);

//и ее использование
someItems.Where(...).OrderBy(...).DiscountedItems()

… на самом деле, одна важная оговорка: C#, как он есть сейчас, не умеет сам объединять деревья выражений (ну то есть нельзя сделать p => IsDiscounted(p) || IsRecommended(p) (ну или по крайней мере не умел тогда, когда мне это последний раз было нужно). Для AND это делается цепочкой Where, а вот для OR пришлось сильно извращаться. Но это не фундаментальное ограничение, нужные операторы-то в деревьях есть, просто у language development team другие задачи.

А можете все таки целиком пример разбить на те же функции, что и у меня? Я тоже хочу к задаче прикрепить.
А можете все таки целиком пример разбить на те же функции, что и у меня?

В этом нет никакого смысла, потому что совершенно другая семантика операций. Но можно разбить на другие, просто развлечения ради.


IQueryable<decimal> TotalByProduct(this IQueryable<Order> orders) => orders
.SelectMany(o => o.Lines)
.GroupBy(
  l => l.Item,
  (item, lines) => (item, lines.Sum(l => l.Total))
);

IQueryable<Order> OrdersForYear(this IQueryable<Order> orders, int year)
=> orders.Where(o => o.OrderDate.Year == year)

customers
.SelectMany(c => c
  .OrdersForYear(1997)
  .TotalByProduct()
  .OrderByDesc((_, total) => total)
  .Select((item, _) => item)
  .Take(2)
)
.Select((customer, item) => (
  customer.FirstName,
  customer.LastName,
  item.Name
))
И Вы считаете, что это проще и понятнее, чем в моем решении? А для обычного человека (не матерого программиста)?

И почему это бессмысленно? Ведь TotalByProduct потом можно использовать много раз в других подсчетах. И потом изменить его реализацию при необходимости.
И Вы считаете, что это проще и понятнее, чем в моем решении?

Я считаю, что это не сложнее, чем в вашем решении. Ну и лично мне — понятнее, потому что нет совершенно неочевидных для меня умолчаний, сделанных в вашей платформе.


Но я вроде и не утверждал, что это проще.


И почему это бессмысленно?

Бесмысленно разбивать на те же функции, что у вас. Я разбил на другие.

Речь сейчас шла не о Вас, а об обычным человеке с IQ чуть выше среднего, который не знаком с программированием. По-моему, мое решение проще. Но тут не будем спорить.

В вашем решении есть нюанс. Вы в параметры везде передаете Order. А предположим, что продажи считаются не только на основе Orders, а на основе 10 других таблиц (но в конечном итоге все выводится в сумму по покупателю/продукту). Как тогда? Вы везде будете передавать все 10 классов? А как же инкапсуляция?
Речь сейчас шла не о Вас, а об обычным человеке с IQ чуть выше среднего, который не знаком с программированием.

Я понятия не имею, где взять достаточное количество таких людей для проведения теста.


Вы везде будете передавать все 10 классов?

Нет, введу промежуточную операцию, которая соберет Order из ваших "десяти таблиц".

Странные конечно задачи у language development team. То есть AND поддержали, а OR нет? То есть два передних колеса сделаем, а без задних перебьются?
То есть AND поддержали, а OR нет?

Нет, не поддержали ни то, ни другое.

Ну filter.filter.filter то можно писать. Это по сути AND. А с OR'ами как?

Я, вроде, выше уже все написал по этому поводу.

В любом случае, спасибо за откровенность. В таких случаях «своих» обычно защищают до последнего, пока не припрут к стенке.

Хотя я понимаю почему они не реализовали. Эта проблема с OR'ами первая с которой мы столкнулись 8 лет назад. С AND'ами все очень просто, а добавление OR'ов переводит задачу в плоскость булевой логики, оптимизация которой, как известно, NP-полная задача. И существующие модели работы с булевой логики для компиляции запросов не подходят, по разным причинам. Тот же ДНФ из-за компактности: (f1 or f2 or f3 or… fN) and (g1 or g2… or gM) при преобразовании в ДНФ даст N*M предикатов. И поддержка логики OR'ов впоследствии создает сложности везде. Но без нее никуда. Собственно задача с 2* яркий тому пример.

А в .NET походу спасовали при первой сложности (хотя все же думаю какие-то частные случаи они поддержали, не верю что все так плохо)
Хотя я понимаю почему они не реализовали.

Вы даже не поняли, что они не реализовали (это хорошо видно по вашему дальнейшему тексту), так что почему вы тоже вряд ли поняли.

Поиск подстроки во множестве, созданной из трех других множеств посредством from-from. Получили оценочную стоимость как NP-задача.
В этом и заключается отличие конфигуратора от разработчика и программиста

  1. Можете показать, как у вас получилась NP-задача? У меня подсчет дает O(N^4), если Contains делается перебором, и вплоть до O(N^3), если там константная операция (например, хэштаблица).


  2. Можете привести решение с меньшей О-оценкой, которое при этом было бы не хуже по читаемости?


  1. в оценочной стоимости есть степень более 2-х-полиномиальная сложность, грубо но можно отнести к NP. Хотя она имеет и более лучшее решение, потому в этом плане к NP относить ее неправильно. Но Ваш приведенный пример приближает к этому
  2. с ходу не скажу, но в инете можно найти хорошие решения замены поиска подстроки на более оптимальные варианты, про остальное надо посидеть, подумать и поэкспериментировать
грубо но можно отнести к NP

Нет, нельзя. Прямо скажем, эта задача в ее текущей формулировке вообще не может быть отнесена к классу NP безотносительно оценочной стоимости, потому что она не является задачей разрешимости.


Но на самом деле, я даже в первом своем вопросе был не прав: даже если вы сведете эту задачу к задаче разрешимости, то все равно, класс P входит в класс NP, поэтому правильный ответ на ваше утверждение — "ну и что"?


с ходу не скажу, но в инете можно найти

Ну то есть вы еще не знаете, есть ли у этой задачи лучшее с точки зрения поставленных критериев решение, но уже критикуете чужое. В таком случае...


В этом и заключается отличие конфигуратора от разработчика и программиста

В чем конкретно оно заключается в этом примере?

знаю что можно решить без поиска подстроки и возможно без from-from, а Вы не удосужились привести пример или хотя бы подписать, что использование подстроки-не самое лучшее решение, но быстро хорошее решение не готовы предоставить. Этого достаточно, чтобы критиковать. Повторюсь наизусть методы поиска без подстроки я не помню, но в интернете примеры есть

знаю что можно решить без поиска подстроки

Так, давайте начнем с простого вопроса: а при чем тут вообще поиск подстроки?


использование подстроки-не самое лучшее решение

А я и не использую подстроку.


но быстро хорошее решение не готовы предоставить

Я предоставил хорошее решение — в рамках тех критериев качества, которые обсуждаются в этом посте. Хотите критиковать? Объясните, почему это решение плохо с точки зрения этих критериев качества.

уже писал выше-использование Contains и from-from

При чем тут подстрока? В задаче вообще строки (strings) не используются.


Каким образом это говорит, что решение не удовлетворяет критерим качества?

НЛО прилетело и опубликовало эту надпись здесь
Кстати у SQL, на самом деле, есть как минимум один недостаток даже просто на уровне запросов. Они зачем-то разделили логику отбора на типы JOIN'ов и собственно WHERE. То есть для меня загадка, почему они не сделали просто условие IN JOIN, чтобы можно было писать:
SELECT FROM A JOIN B ON A.x=B.y WHERE IN JOIN A AND IN JOIN B — что эквивалентно INNER JOIN
SELECT FROM A JOIN B ON A.x=B.y WHERE IN JOIN A — эквивалентно LEFT JOIN
SELECT FROM A JOIN B ON A.x=B.y WHERE IN JOIN A OR IN JOIN B — эквивалентно UNION / FULL JOIN
Тогда такого огорода как в задаче с 2* (смотри решение внизу на SQL) не пришлось бы городить.

Самый серьезный недостаток SQL — его как бы не существует как единого языка. В каждой БД свои нюансы и моменты реализации. Так что для каждый БД по сути нужно учить свой sql диалект.

Вот в lsFusion эта проблема решается — она разными БД работает, а язык один

В обычных языках программирования эта проблема решается условными query builder или orm. Проблема в том, что даже sql-based база данных подвести под одну гребенку получается довольно плохо.


А уж про другие базы данных и говорить не стоит, некоторые возможности приходится только эмулировать на уровне кода. Как пример, в условной rethinkdb нет автоматического выбора индекса, бд нужно задавать какой индекс брать и что с ним делать. А в условной couchdb обычные человеческие запросы нужно в случаях агрегации переписывать на view, создавать дополнительную сущность в бд (или не создать, если она уже есть) и делать еще кучу оберток.


Боюсь в общем случае эта проблема так сложна, что решать ее не имеет смысла.

Это да. Но здесь-то речь идет о самом базовом функционале, который в SQL-92 есть.

1) WHERE взаимозаменяемо только с INNER JOIN.
Для остальных типов JOIN есть серьезные нюансы.


2) С каких пор UNION / FULL JOIN эквивалентны?


3) В SQL масса других странных вещей. Например, есть HAVING, который полностью эквивалентен подзапросу и WHERE на подзапрос. Причем с WHERE код более читаемый, т.к. уже можно использовать алиасы для вычисляемых колонок.

Для остальных типов JOIN есть серьезные нюансы.

Я знаю, серьезные, но не очень серьезные. Так как это преобразование из условия в типы JOIN и собственно WHERE (например как в задаче 2* и ее решении на SQL) и пришлось реализовывать. И каждый раз про себя матерился, что почему вот это сама СУБД не делает, у нее есть вся информация для этого, в частности статистика. Ладно там constraint'ы, trigger'ы и materialized без ограничений на view, там инкрементальность нужна нормальная и это существенно более сложная задача. Но преобразование как в задаче 2*, могли бы сделать, хотя бы Oracle, все таки компания с капитализацией под 150 млрд.
2) С каких пор UNION / FULL JOIN эквивалентны?

А я и не говорил что они эквивалентны, они просто решают одну задачу — OR оператор, то есть:
SELECT FROM A JOIN B ON A.x=B.y WHERE IN JOIN A OR IN JOIN B, если x и y ключи, можно сделать как
SELECT A.x FROM A UNION SELECT B.y FROM B, а можно:
SELECT COALESCE(A.x, B.y) FROM A FULL JOIN B ON A.x=B.y
Кстати, в том же постгрес приходится UNION использовать, так как в FULL JOIN шаг влево, шаг вправо — FULL JOIN is only supported with merge-joinable join conditions.
3) В SQL масса других странных вещей. Например, есть HAVING, который полностью эквивалентен подзапросу и WHERE на подзапрос. Причем с WHERE код более читаемый, т.к. уже можно использовать алиасы для вычисляемых колонок.

Да, HAVING меня тоже всегда забавлял. И мы его при компиляции не используем, так как смысла в нем достаточно мало.
Я знаю, серьезные, но не очень серьезные. Так как это преобразование из условия в типы JOIN и собственно WHERE (например как в задаче 2* и ее решении на SQL) и пришлось реализовывать.

Не совсем понял о чем вы… Сейчас условия в JOIN применяются к исходным данным. WHERE к результатам соединения.
У вас предикат в JOIN остался, но добавилось еще многословное объяснение в WHERE какие строчки оставлять, то, что сейчас подразумевается типом JOIN. Выглядит более заумно, а дает ли дополнительные возможности?


А я и не говорил что они эквивалентны, они просто решают одну задачу — OR оператор

Я думал что совершенно разные. UNION работает с множествами однотипных данных, грубо говоря ставит таблички с данными одну за другой по вертикали.
JOIN работает с разнотипными данными. По аналогии — ставит разные таблички рядом друг с другом по горизонтали.
То что вы продемонстрировали в примере — это выхолощенный случай когда выбирается одно ключевое поле., соответственно разницы нет.
Конечно, можно извращаться — сводя разнотипные данные к однотипным добивая их NULL колонками. Но только не от хорошей жизни.

Не совсем понял о чем вы… Сейчас условия в JOIN применяются к исходным данным. WHERE к результатам соединения.

Речь не про условия в JOIN. В то же Oracle их можно и в JOIN и в WHERE писать. Речь про типы JOIN.
У вас предикат в JOIN остался, но добавилось еще многословное объяснение в WHERE какие строчки оставлять, то, что сейчас подразумевается типом JOIN. Выглядит более заумно, а дает ли дополнительные возможности?

Дает в случаях когда есть OR.
То есть SELECT FROM A JOIN B JOIN C JOIN D WHERE (IN JOIN A OR IN JOIN B) AND (IN JOIN C OR IN JOIN D), потребует от вас написания четырех подзапросов через UNION (собственно это и есть задача с думая звездочками)
То что вы продемонстрировали в примере — это выхолощенный случай когда выбирается одно ключевое поле., соответственно разницы нет.

Тоже самое будет и с двумя и с тремя ключевыми полями. И в том числе когда есть не ключевые поля. Я вам больше скажу, в Postgres FULL JOIN вообще в частных случаях поддерживается, и там именно UNION'ами выкручиваются.
Например, есть HAVING, который полностью эквивалентен подзапросу и WHERE на подзапрос

Да, HAVING меня тоже всегда забавлял. И мы его при компиляции не используем, так как смысла в нем достаточно мало.

в смысле эквивалентен и смысла мало?
Напишите, пожалуйста, вот такой запрос на lsfusion и покажите, в какой SQL запрос он скомпилируется

select
    department_id,
    array_agg(id) as employees,
    sum(tax) as tax
from employee
group by
    department_id
having
    sum(salary) > 100000 and
    count(*) < 10 and
    count(distinct gender) = 2

select
    department_id,
    employees,
    tax
from
(
   select
       department_id,
       array_agg(id) as employees,
       sum(tax) as tax,
       sum(salary) as salary,
       count(*)  as cnt,
       count(distinct gender) as genders
   from employee
   group by
       department_id
)
where
    salary > 100000 and
    cnt < 10 and
    genders = 2
да, согласен, просто неверно прочитал «Например, есть HAVING, который полностью эквивалентен подзапросу и WHERE на подзапрос» — почему-то у менять осталось впечатление, что будет что-то вроде: where (select sum(salary) ...) > 100000

Не совсем согласен, что HAVING не нужен, но согласен, что функционально его можно заменить. Можете привести код на lsfusion или Вы не по этой части?
Я же ниже уже писал запрос. Или речь о каком-то другом?

Если планировщик запроса HAVING никак не оптимизирует, то это просто синтаксический сахар.
Недавно столкнулся с тем, что Oracle почему-то не понимает, что в запросе SELECT x FROM (SELECT x,… UNION SELECT x,...) WHERE x = 444 не догадывается «протолкнуть» выражение внутрь (он честно считает вложенный запрос, а потом фильтрует. PostgreSQL и подавно этого не умеет. Поэтому нам приходится в компиляторе запроса это делать самостоятельно. В итоге SQL запросы часто получаются очень странными с точки зрения человека (там дублируются подзапросы), но зато они гораздо эффективнее выполняются.

"Нет повести печальнее на свете, чем повесть о SQL-оптимизаторе"!


Работаю с MS SQL 2008R2 — сложилось впечатление, что ядро оптимизатора там серьезно не меняли еще со времен Sybase.
Некоторые запросы оптимизируются очень круто. Но иногда тупит на самых примитивных запросах, чем вводит меня в полный ступор.


Из последнего, фрагмент запроса:


SELECT 
   ...
FROM 
    sometable t1
         INNER JOIN sometable t2
              ON t1.field = t2.field AND
                 t1.id <> t2.id
WHERE
    ...

Тупит страшно, хотя в таблице жалкий лям записей.


Добавляю в WHERE условие:


WHERE
     t1.field IS NOT NULL AND 
     t2.field IS NOT NULL AND 
     ...

О чудо! Выполняется за доли секунды. Записей с заданным field порядка 1% от общего количества.


Что мешает вложить это магическое заклинание в оптимизатор — хз. Ведь очевидно, что предикат в JOIN будет истинным только если field не NULL...

У постгрес кстати та же беда, помню при компиляции запросов приходилось искать такие случаи (когда IS NOT NULL существенно уменьшает статистику) и докидывать такие условия в WHERE.
справедливости ради надо сказать — сейчас 2019 год, с 2008R2 многое поменялось. Но вообще странно, да.

Могу предположить, что т.к. обычно таблицы связываются по первичному ключу, где NULL нет, то оптимизацией таких случаев как у меня просто не занимались.

2017 сервер. что так, что иначе — план запроса одинаковый.

Чтобы протестировать данный нюанс оптимизатора, надо бы знать определение таблицы, список индексов и статистик, а также точный запрос со всеми предикатами. Ну и важно иметь набор данных статистически похожий на реальные.


Как вы умудрились все это восстановить по моему фрагменту запроса — загадка.


У меня для запроса с доп. проверкой на NULL выбирается план с MERGE JOIN и статистика такая:


Table 'Worktable'. Scan count 1130, logical reads 20345, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'sometable'. Scan count 2, logical reads 73908, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
 SQL Server Execution Times:
   CPU time = 390 ms,  elapsed time = 1195 ms.

Для запроса без доп. проверки на NULL выбирается план с HASH JOIN и статистика такая:


Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'sometable'. Scan count 2, logical reads 73908, physical reads 4822, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 27315782 ms,  elapsed time = 27328614 ms.
Использовал предоставленные данные. ~1кк записей и 1% данных. =)

Создал две таблицы, заполнил и запустил. (а потом ещё индексами покрыл разными и подёргал по-разному)

Что то какой то подозрительный план запроса во втором случае. Вообще не трогающий таблицу Worktable. =)

Очевидно, что дело вполне может быть не в данному куске кода и не вина оптимизатора что он такой простой вещи не увидел.

Можете сами составить тестовый вариант при котором оптимизматор лажает, тогда всё станет понятно. (ну или большая часть)

ЗЫ: нет, я не говорю, что он НИКОГДА не лажает (иначе у меня бы работы небыло), но в данном конкретном случае проблема, мне кажется, была не на его стороне.
Я только что проверил на SQL 2019, да, косячит. И в принципе понятно почему: он берет таблицу sometable t1 дальше смотрит его предикаты видит что в предикате t1.field=t2.field есть t2, которую он еще не считал, и благополучно этот предикат игнорирует, а должен был построить виртуальный предикат t1.field IS NOT NULL и использовать его. А он так не умеет.
Сможете написать тестовый скрипт это показывающий? )

Ну, кстати, предикат она умеет вставлять.

image

У меня целая статья на эту тему будет скоро.

Но вообще возьмите любую большую таблицу которую не жалко, добавьте туда поле, постройте индекс, заполните 100 полей и сделайте запрос.

Вот план 1
SELECT * FROM ShipmentDetail s1, ShipmentDetail s2 WHERE s1.sd=s2.sd AND s1.id <> s2.id AND s1.sd IS NOT NULL AND s2.sd IS NOT NULL

image

Вот план 2
SELECT * FROM ShipmentDetail s1, ShipmentDetail s2 WHERE s1.sd=s2.sd AND s1.id <> s2.id

image
=)) Скрины такого размера доставили много радости. (как и русская локализация)

К сожалению, мои тестовые примеры всё-равно строят другие планы и я не могу ничего сказать о ваших данных. Но то, что оптимизатор строит предикат по вашим условиям — я показал.
Вопрос только — когда и почему он считает или нет нужным его ставить… если понять этот момент — будет понятно почему в вашем случае он это не применяет.

Но, спасибо за интересный момент. =)
Мне будет интересно почитать статью.

ЗЫ: Ирония в том, что запросы
SELECT * 
FROM #tmp1 s1, #tmp1 s2 
WHERE s1.ID=s2.ID 
  AND s1.GUID1 <> s2.GUID1          
OPTION (MAXDOP 1)   

SELECT * 
FROM #tmp1 AS t1
JOIN #tmp1 AS t2 ON t2.[ID] = t1.[ID]
                AND t2.GUID1 <> t2.GUID1      
OPTION (MAXDOP 1)  


имеют разные планы. Причём последний — быстрее.
вполне вероятно что проблема в этом.

и запрос
SELECT * 
FROM #tmp1 AS t1
JOIN #tmp1 AS t2 ON t2.[ID] = t1.[ID]
                AND t2.GUID1 <> t2.GUID1      
OPTION (MAXDOP 1)  


быстрее

SELECT * 
FROM #tmp1 s1, #tmp1 s2 
WHERE s1.ID=s2.ID 
  AND s1.GUID1 <> s2.GUID1 
  AND s1.ID IS NOT NULL 
  AND s2.ID IS NOT NULL            
OPTION (MAXDOP 1)   
И вы индекс по ID вообще построили? В этом же вся суть.

Ну и во времянках нет clustered index'а. Это все таки не Postgres. Там в принципе другие планы, можете попробовать не на времянках.

Возможно предикат она может добавлять, но видимо это делает грубо говоря в конце когда план уже построен и scan выбран, а должен сначала добавить предикат, а потом уже выбирать scan/seek или что там будет.
эээ. Да, построил. (ну, кластерный у меня по GUID столбцу) Потом ещё покрывающих индексов отсыпал до кучи.
И во времянках есть кластерные индексы. (и это видно в плане запроса)
И планы (и статистика) по ним строятся как и по обычным таблицам.

Не думаю, что логика там такая топорная, если честно.

Ну да не суть )

1. Да, данная проблема может быть в таких запросах, хоть оптимизатор и умеет сам ставить этот предикат.
2. Старый синтаксис хреново обрабатывается сервером.
3. Пути оптимизатора неисповедимы, поэтому стоит брать в свои руки критичные места.

Такие итоги устроят? )
Так а сбросьте какие у вас планы получаются? А то по тому скрину непонятно.
Блин.
Я уже удалил тестовый пример (Спасибо голангу, блин), а рабочее время кончилось.
Попробуйте написать тестовый пример сами — всё же это намного удобнее.
SELECT FROM A JOIN B ON A.x=B.y WHERE IN JOIN A

GROUP SUM sum(Detail d) IF

не знаю, что скажут носители английского, но для меня это всё выглядит очень коряво.
GROUP SUM sum(Detail d) IF

Можно префиксно писать: GROUP SUM IF f(a) THEN g(a). Но так дольше, а IF очень частая операция. Но вообще тут речь шла не про lsFusion, а именно про SQL.
речь про то, что sql очень близок к анлийскому языку, а ваши предложения что в lsFusion, что в самом sql звучат (как мне кажется) коряво.
почему не
SELECT FROM A JOIN B ON A.x=B.y WHERE EXISTS ROW IN A

про GROUP — IF точно не к месту (WHERE/FOR?), и в целом конструкция вызывает сомнения.
ИМХО стоит обсудить сначала конструкции языка с носителями английского, чтобы код выглядел максимально естественно.
А смысл? Тот же SQL что и был, только скобочек добавилось, сложность выборки, как была так и осталась. Написание каким было таким и осталось. Оборачивать выборку как раньше было нужно так и до сих пор нужно.
сложность выборки, как была так и осталась

Вот, например, задача со звездочкой. Сможете написать такое же на SQL? Вы считаете, что это одинаково сложно?
На SQL это напишет любой дурак, потому что задача — элементарная (для SQL).
А вот в чем смысл вашей функциональной модели и чем она лучше — я так и не понял. Вы придумали для нее синтаксис, но явно забыли придумать все остальное.

Если она элементарная, то напишите, пожалуйста. Я обновлю статью с решением на sql.

SELECT 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
FROM Persons                 AS p
--Лайки                      
JOIN PersonRelationShip      AS pl ON pl.PersonAID = p.PersonID
                                  AND pl.Relation  = 'Like'
--Друзья                     
JOIN PersonRelationShip      AS pf ON pf.PersonAID = p.PersonID 
                                  AND pf.Relation = 'Friend'
--Друзья Друзей              
JOIN PersonRelationShip      AS pff ON pff.PersonAID = pf.PersonID
                                   AND pff.PersonBID = pl.PersonID
                                   AND pff.Relation = 'Friend'
--Ещё не дружат         
LEFT JOIN PersonRelationShip AS pnf ON pnf.PersonAID = p.PersonID
                                   AND pnf.PersonBID = pff.PersonBID
                                   AND pnf.Relation = 'Friend'
WHERE pnf.PersonAID IS NULL 


Это сложно?
Можно по аналогии с функционалом сделать через экзисты.
А можно ещё через рекурсию указывать количество «рук» друзей.
Итд итп.

ЗЫ: единственное, это MS SQL, но не думаю что он сильно отличается от постгре.

Как бы да. А задача с двумя звездочками как тогда будет выглядеть?

Как бы нет (или вы боретесь просто за то что бы буковок было меньше?).
«логических» связок столько же сколько и в функциональном примере.
Каждый вызов функции — такой же запрос к «структуре» где хранятся взаимоотношения. (таблице)
Это, собственно, и есть задача с двумя звёздочками.
Если человек лайкает кого то, и если дружит с человеком, который дружит с тем кого лайкает — он его выберет.

ЗЫ: вам выше правильно заметили. Вы придумали синтаксис (ок), но смысла в этом нет, потому что он не добавляет ни прозрачности, ни простора для оптимизации ни каких то ещё полезностей. Та же логика — вид сбоку. =)

Я не совсем понял какие колонки у вас в PersonRelationShip потому как вы там и к PersonID и к PersonAID и к PersonBID обращаетесь

Вообще, если предположить что вместо pf.PersionID и pl.PersionID у вас pf.PersionBID и pl.PersionBID, то у вас получилось likes(a,b) AND friends(a,c) AND friends(b,c) AND NOT friends(a,b). Извините, что перевожу на функциональную, у меня от реляционной мозг сводит. Но likes(a,b) AND friends(a,c) AND friends(c,b) AND NOT friends(a,b) вроде не равно likes(a, c) AND NOT friends(a, c) AND (friends(a, b) OR friends(b, a)) AND (friends(b, c) OR friends(c, b)). Хотя надо конечно в булевом анализаторе проверить.
Я конечно могу ошибаться но без UNION/FULL JOIN вы в двух звездочках не обойдетесь, так как в задаче с двумя звездочками OR'ы есть.
Извините, что перевожу на функциональную, у меня от реляционной мозг сводит.

Почему вы это называете функциональной парадигмой? Примеры без группировок похожи на задачки из туториалов по прологу, который есть логическая парадигма.


Пусть имеется некоторый набор фактов


likes(john, alice).
likes(john, bob).
likes(eve, bob).

friends(john, bob).
friends(bob, alice).
friends(eve, alice).

И тогда мы можем написать такой "запрос":


suggestion(A,B,C) :- likes(A,C),
                    (friends(A,B); friends(B,A)),
                    (friends(B,C); friends(C,B)),
                    not(friends(A,C)).

Выполним его и получим все варианты A,B,C, удовлетворяющие условию


?- suggestion(A,B,C).
A = john,
B = bob,
C = alice ;
A = eve,
B = alice,
C = bob ;
Функциональную я имел ввиду, что мне гораздо проще понимать логику в терминах функций, а не таблиц, как написал автор. Но может я один такой.

Это все конечно красиво, но вопрос как выполнить этот запрос на SQL. Пролог конечно хороший язык, но я если честно не знаю как он на больших объемах данных работает. Ну и как остальные возможности SQL поддерживает, там группировки всякие и ACID.
вопрос как выполнить этот запрос на SQL

Зачем на SQL? Вы же сами пишете "можно реализовать систему управления базой данных, которая будет использовать в качестве хранилища некую структуру, адаптированную именно под функциональную логику." Есть например Datomic которая разработана для такой парадигмы.

Хм. Интересно. А как у нее с ACID? И на сколько у нее мощные оптимизаторы, как она на больших объемах работает? И что с In-memory cache всякими? Просто если этого нет, то уж лучше все таки в SQL компилировать.

ACID есть. Насчет объёмов не знаю, надо гуглить или проверять.

Datomic is a single-writer system. A single thread in a single process is responsible for writing transactions.

Вот это уже очень стремная штука. То есть по факту под нагрузкой такая база ляжет почти сразу.
Datomic Cloud is a new kind of database, designed to be:
— Transactional (ACID compliant and always consistent)
— Elastically read scalable (built for AWS auto-scaling)
— Immutable (accumulating facts over time)
— Queryable (via Datomic datalog, a powerful declarative query language)
— Flexible (in schema design and modification, deployment topology, and usage scenarios)

А еще в ней похоже нельзя изменять данные. Применимость соответственно от этого сильно страдает.

Не, иммутабельность тут про то, что никакие данные не исчезают и всегда можно сделать запрос на слепок базы в какой-то момент прошлого (что-то вроде логирования в вашей системе). В туториале под заголовком Retract описаны удаления и обновления данных

У нас немножко разная аудитория с Datomic. Они направлены на программистов, а мы (как и SQL, когда создавался) на бизнес-пользователей.

Но меня у них на сайте поразило вот что:
— distributed (conceived with the cloud in mind from day 1)
— transactional (it is fully ACID compliant and always consistent)

А как они решили CAP-теорему?
У нас немножко разная аудитория с Datomic. Они направлены на программистов, а мы (как и SQL, когда создавался) на бизнес-пользователей.

Вот только в итоге вышло, что бизнес-пользователям не пишут сами SQL и его пишут разработчики/DBA/аналитики.


А как они решили CAP-теорему?

А что не так?


Datomic is a single-writer system. A single thread in a single process is responsible for writing transactions.
А dba и аналитики — это разве программисты? Здесь практически та же целевая аудитория.

CAP теорема значит по сути, что нельзя построить настоящий acid и распределенность. Вот и удивляюсь, что они это заявляют.
CAP теорема значит по сути, что нельзя построить настоящий acid и распределенность.

Нет, не значит. CAP значит, что распределенная система не может быть одновременно consistent, available и partition-tolerant, но не более того. ACID — это требование C. Если вы отказываетесь либо от availability, либо от partitioning tolerance, у вас получается распределенная система с ACID.

PersonAID — id персоны, исходной для отношения. (если я кого то лайкаю — то А — это мой id)
PersonBID — id второго человека, к которому применяется отношение (тот, кого я лайкнул)

Можно, конечно, обойтись без исходной таблицы Person, но так немного нагляднее получается.

ЗЫ: Таблица, соответственно, состоит из PersonAID, PersonBID, RelationShip. =)
Я не про то, у вас там pf.PersionID. А pf это PersonRelationShip. У вас там три колонки получается PersionID, PersionAID и PersionBID.
Я ниже уже привёл исправленный вариант, это была опечатка (реально, я бы использовал немного другие имена, что б не так сливались...)
Спасибо что указали )
К сожалению, вчерашний коментарий я не могу отредактировать =(
Каждый вызов функции — такой же запрос к «структуре»

Очевидно, что целью не ставилось сделать проще для компьютера, целью становилось сделать проще для человека

Спасибо за решение. Вставил в статью. Жду решения задачи с 2мя звездочками.
Но Вы реально считаете, что Ваше решение проще? Если допустим взять человека, который не знает SQL.
Так оно не правильное, там как минимум колонки не правильные.

И он считает что это решение с двумя звездочками.
SELECT 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
FROM Persons                 AS p
--Лайки                      
JOIN PersonRelationShip      AS pl ON pl.PersonAID = p.PersonID
                                  AND pl.Relation  = 'Like'
--Друзья                     
JOIN PersonRelationShip      AS pf ON pf.PersonAID = p.PersonID 
                                  AND pf.Relation = 'Friend'
--Друзья Друзей              
JOIN PersonRelationShip      AS pff ON pff.PersonAID = pf.PersonBID
                                   AND pff.PersonBID = pl.PersonBID
                                   AND pff.Relation = 'Friend'
--Ещё не дружат         
LEFT JOIN PersonRelationShip AS pnf ON pnf.PersonAID = p.PersonID
                                   AND pnf.PersonBID = pff.PersonBID
                                   AND pnf.Relation = 'Friend'
WHERE pnf.PersonAID IS NULL 


Спасибо за уточнение. В одной связи и впрямь ошибся (вечер, однака)
Спасибо, обновил пост. Так а задачу с 2* решите? У меня это заняло 10 секунд, а сколько займет у вас?
Разница в том, что нужно смотреть связь Friend в обе стороны. То есть должна быть хоть одна из них.
Тут вопрос в хранении данных таких связей.

К примеру
Связи хранятся в одностороннем виде.
т.е. если (условно) отношения представлены в виде двух строчек в таблице.
ID 12352 PersonAID 100 PersonBID 101 RelationShip 'Like'
ID 12353 PersonAID 101 PersonBID 100 RelationShip 'Like'
Если вы понимаете о чём я.

Тогда как то так:
;WITH PersonRelationShipCollapsed AS (
  SELECT pl.PersonAID
        ,pl.PersonBI
        ,pl.Relation 
  FROM PersonRelationShip      AS pl 

  UNION 

  SELECT pl.PersonBID AS PersonAID
        ,pl.PersonAID AS PersonBID
        ,pl.Relation
  FROM PersonRelationShip      AS pl 
)
SELECT 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
FROM Persons                      AS p
--Лайки                      
JOIN PersonRelationShipCollapsed  AS pl ON pl.PersonAID = p.PersonID
                                 AND pl.Relation  = 'Like'                                  
--Друзья                          
JOIN PersonRelationShipCollapsed  AS pf ON pf.PersonAID = p.PersonID 
                                 AND pf.Relation = 'Friend'
--Друзья Друзей                   
JOIN PersonRelationShipCollapsed  AS pff ON pff.PersonAID = pf.PersonBID
                                 AND pff.PersonBID = pl.PersonBID
                                 AND pff.Relation = 'Friend'
--Ещё не дружат                   
LEFT JOIN PersonRelationShipCollapsed AS pnf ON pnf.PersonAID = p.PersonID
                                   AND pnf.PersonBID = pff.PersonBID
                                   AND pnf.Relation = 'Friend'
WHERE pnf.PersonAID IS NULL 


Хотел описать второй вариант, с экономией строк (когда в каждой строке хранится PersonAID, PersonBID, Relation и RelationTypeID: 1 от А к Б, 2 от Б к А, 3 — взаимно), но такой вариант сводится к первому на самом деле. =)
А вы уверены насчет плана выполнения такой штуки? Что она реально не будет сначала UNION по всей базе делать? Такой запрос положит всю базу. Правильный запрос я ниже приводил.

Ну и в любом случае это уже точно не проще, чем SELECT WHERE и булево условие.
А что у Вас вызывает сомнения в плане? )
Скорее всего он будет практически идентичен плану вашего запроса:
6 индекс сиков (в случае поиска по одной персоне) и два скана.
Или все 8 сканов, если поиск производится «вообще».

Возможно, с изменением статистики по таблицы план изменится в ту или иную сторону.

В любом случае — а кто вам сказал, что гипотетический функциональный язык сможет сделать это оптимальнее? )
А что у Вас вызывает сомнения в плане?

Что она реально UNION выполнит (а если likes мало, а friends много будет очень критично). Насколько она его INLINE'ть сможет.
В любом случае — а кто вам сказал, что гипотетический функциональный язык сможет сделать это оптимальнее? )

Он не гипотетический, а уже давно в продакшне, причем жестком. И он сформирует запрос именно, который я внизу кидал, и который с куда большей вероятностью выполнится лучше чем ваш.
Прогнал ваш запрос.
1. В нём есть много опечаток )
2. В нём проверка только на одностороннее неналичие в друзьях (насколько я понял)
3. План, в принципе, строится такой же. =D

4. Сравнивал Ваш вариант и свой и нашёл ещё ошибку. Да что за жизнь то )

;WITH PersonRelationShipCollapsed AS (
  SELECT pl.PersonAID
        ,pl.PersonBID
        ,pl.Relation 
  FROM #PersonRelationShip      AS pl 
  
  UNION 

  SELECT pl.PersonBID AS PersonAID
        ,pl.PersonAID AS PersonBID
        ,pl.Relation
  FROM #PersonRelationShip      AS pl 
)
SELECT 
   pl.PersonAID
  ,pf.PersonBID
  ,pff.PersonBID
FROM #Persons                      AS p
--Лайки                      
JOIN PersonRelationShipCollapsed  AS pl ON pl.PersonAID = p.PersonID
                                 AND pl.Relation  = 'Like'                                  
--Друзья                          
JOIN PersonRelationShipCollapsed  AS pf ON pf.PersonAID = p.PersonID 
                                 AND pf.Relation = 'Friend'
--Друзья Друзей                   
JOIN PersonRelationShipCollapsed  AS pff ON pff.PersonAID = pf.PersonBID
                                 AND pff.PersonBID = pl.PersonBID
                                 AND pff.Relation = 'Friend'
--Ещё не дружат                   
LEFT JOIN PersonRelationShipCollapsed AS pnf ON pnf.PersonAID = p.PersonID
                                   AND pnf.PersonBID = pff.PersonBID
                                   AND pnf.Relation = 'Friend'
WHERE pnf.[PersonAID] IS NULL 
Тогда это likes(a,b) AND friends(a,c) AND friends(b,c) AND NOT friends(a,b).

А нужно likes(a, c) AND NOT friends(a, c) AND
(friends(a, b) OR friends(b, a)) AND
(friends(b, c) OR friends(c, b)).

А дальше я вам покажу как силой булевой логики найти ошибку. Все эти значения первичны, то есть никак связаны друг с другом. То есть по сути мы получаем два условия.

YOUR = LAB & FAC & FBC & !FAB
NEEDED = LAC & !FAC & (FAB | FBA) & (FBC | FCB)

А теперь берем YOUR & !NEEDED скармливаем его булевому оптимизатору

Получаем таблицу истинности берем первое TRUE.

FAB FAC FBA FBC FCB LAB LAC output
0 1 1 1 0 1 0 1

Соответственно возьмем A = 1, B =2, C=3, а значит в случаях когда таблица PersonRelationShip
A B Relation
1 2 L
1 3 F
2 1 F
2 3 F
У вас будет ошибка.
Очевидно, что условия были не очень ясно указаны.
К примеру, несимметричные отношения, по моему пониманию, подразумевали что нужно брать только тех, кого Я лайкнул и кого выбрал другом человек, которого выбрал другом Я.
А те, кого я не лайкнул, а кто меня лайкал в таком случае не должны попасть в выборку.

Но если не важно направление связи — то я описал варианты чуть выше.
Спасибо за живое участие, посмотрите, вдруг я и там опечатался )
Руки никак не дойдут создать таки эти таблицы и сделать тесты. Работа-с.

Очевидно, что ваше решение сложнее, оно слишком объёмное. Вы сами запутались в нём (как минимум с именами полей)

Для задачи «со звездочкой», КМК, подходит RDF и SPARQL. Там графы вообще естественная штука.
Но это именно для конкретного случая, когда нужен поиск по базе графов — в большинстве случаев этого не надо от слова «совсем»

З.Ы. И да, это было придумано уже давно и есть имплементации — просто не популярно.
Так тут не нужны никакие графы (а точнее рекурсия). Это чисто для SQL задача. Вон человек сверху утверждал, что она вообще элементарная, что ее любой дурак напишет.
Я немножко не о том.
Конечно, можно и на SQL можно спроектировать и написать все что угодно.
Но основная мысль ТС, как я понял, что его новомодный подход может решать задачи проще/лучше, чем известные решения. Иначе бы не было такого срача в комментариях.

Я просто отметил, что для задачи «со звездочкой», которая очевидно не является очевидно типичной для традиционных SQL баз данных, существуют другие типы баз данных и другие решения (ну, или можно придумать велосипед над SQL — за это в тюрьму не сажают, и подчас это оправдано).

Если мы говорим о лучшем синтаксисе/хранилище для задачи — RDF + SPARQL для конкретной задачи выглядит лучше, чем вся эта функциональщина, КМК.

Это все, что я хотел сказать.
Ок, а можете тогда показать как эта задача на RDF+SPARQL выглядит? Потому как здесь решение в 2 строки и с виду достаточно простое. И раз там выглядит лучше значит еще проще/короче должна быть, то есть можно за полминуты сделать.
Проблема в том, что в жизни, часто надо решать разные задачи в рамках одного большого приложения (например, ERP-системы). Вы предлагаете под каждый тип запроса использовать свою СУБД? Допусти RDF + SPARQL решает задачу со звездочкой. А остальные 8 задач эта связка тоже решает?

Есть мультипарадигменные БД, типа OrientDB или Cosmos DB.

Эх, жаль специалистов по ним нету в ветке. Интересно было бы посмотреть как эти задачи решаются на них. Устроить такую «олимпиаду среди баз данных».
Когда писал решения для этих восьми задач (так как рука набита), у меня ушло непосредственно на решение минут 10. Так что это времени не должно было много занять.
Так давайте задачу усложним немного. Пусть они дружат по трое (большой шведской семьей). У меня в решении просто добавится один параметр везде. А как вы такую «дружбу» на графы переведете?
Ну, во-первых я бы для этого использовал графовую БД.
И в принципе в РСУБД вы этот код так же можете завернуть в функции и будет читаться так же.
Я имел ввиду алгоритмическую сложность достать данные с диска.
И в принципе в РСУБД вы этот код так же можете завернуть в функции и будет читаться так же.

пример можно?
Пока отвечал на комментарий выше, самому стало интересно, как решается задача с 2-мя звездочками.

Собственно скормил платформе эту задачу:
TABLE likes (Person, Person);
likes = DATA BOOLEAN (Person, Person) TABLE likes;
TABLE friends (Person, Person);
friends = DATA BOOLEAN (Person, Person) TABLE friends;

run() {
    EXPORT FROM Person a, Person b, Person c WHERE 
        likes(a, c) AND NOT friends(a, c) AND 
        (friends(a, b) OR friends(b, a)) AND 
        (friends(b, c) OR friends(c, b));
}

Получил вот такой запрос:
SELECT a.jkey0,
       a.jkey1,
       a.jkey2
FROM
  (SELECT t0.key0 AS key0,
          t1.key1 AS key1,
          t0.key1 AS key2,
   FROM likes t0
   JOIN friends t1 ON t1.key0=t0.key1
   JOIN friends t2 ON t2.key1=t1.key1
   AND t2.key0=t0.key0
UNION 
   SELECT t0.key0 AS key0,
                t1.key1 AS key1,
                t0.key1 AS key2,
   FROM likes t0
   JOIN friends t1 ON t1.key0=t0.key1
   JOIN friends t2 ON t2.key1=t0.key0
   AND t2.key0=t1.key1
UNION 
   SELECT t0.key0 AS key0,
                t1.key0 AS key1,
                t0.key1 AS key2
   FROM likes t0
   JOIN friends t1 ON t1.key1=t0.key1
   JOIN friends t2 ON t2.key1=t1.key0
   AND t2.key0=t0.key0
UNION 
   SELECT t0.key0 AS key0,
                t1.key0 AS key1,
                t0.key1 AS key2
   FROM likes t0
   JOIN friends t1 ON t1.key1=t0.key1
   JOIN friends t2 ON t2.key1=t0.key0
   AND t2.key0=t1.key0) s
LEFT JOIN friends t3 ON t3.key1=s.key2
AND t3.key0=s.key0
WHERE t3.key0 IS NULL

Но что-то я засомневался, кажется можно проще все же сделать. Кто нибудь знает правильное решение? Это вроде известная задача.
Это с двумя звёздочками?

Если отношения
1 3 L
1 2 F
2 3 F
Какие данные вернёт этот скрипт?

*там в selec части немного не те данные и лишние запятые

Прежде чем начинать хоть какое-то обсуждение, хочется определиться с понятиями. Вы там пишете "будут использоваться классы и функции" — а что конкретно вы понимаете под "функцией"? Что это такое, как оно себя ведет, и, что важнее, какие у него свойства?

А могут быть разные трактования?
Функция — в математике соответствие между элементами двух множеств, установленное по такому правилу, что каждому элементу одного множества ставится в соответствие некоторый элемент из другого множества

Например, из одного примера: bought: X -> Y, где X — это множество кортежей (Customer, Product, INTEGER), а Y — NUMERIC[14,3].
А могут быть разные трактования?

Конечно, могут. Особенно в части свойств.


Функция — в математике...

Вы вот пишете про математику, а в тексте пишете про функции из программирования ("Такая база данных оперирует функциями [...] точно также, как и в классическом программировании"), хотя это два разных понятия.


Так что давайте все-таки разберемся, какими свойствами обладают эти ваши "функции", что они делают, и какие операции над ними можно совершать.

В декларативной логике, когда отсутствует состояние, функция из программирования = функции математической. Это в императивной логике функция может выполняться и изменять какое-то состояние.

Не очень понимаю, какими свойствами может обладать отображение одного множества на другое?
функция из программирования = функции математической

… и это ваш случай?


какими свойствами может обладать отображение одного множества на другое?

Например, биективности или ее отсутствия.

Очевидно, что они в общем случае ни сюръективны, ни инъективны.

Давайте более предметно. Это тоже самое, что спросить какими свойствами обладает стол. Очень многими — вопрос лишь в том, какие именно нужны в конкретном контексте. Если Вам интересно какое-то конкретно свойство, то спрашивайте.

Мне интересно, в первую очередь, очень простое свойство: являются ли ваши функции детерминированными (в том значении, которые в языках программирования), то есть всегда ли для одного и того же значения аргумента будет одно и то же значение результата.


(насколько я понимаю, это является требованием для математического определения функции, но я не могу найти точную цитату)

Не является требованием для математического определения функции — и такие и такие бывают.
Вы с чистой функцией перепутали.
А что имеете ввиду?

Не является требованием для математического определения функции — и такие и такие бывают

Приведите, пожалуйста, пример математической функции, которая бы давала разные значения при применении к одному и тому же аргументу.


Вы с чистой функцией перепутали.

… что именно перепутал? Вот тут википедия пишет нам:


Thus a pure function is a computational analogue of a mathematical function.

Вы уж определитесь — вам математические функции нужны или в том значении, которые в языках программирования.
Два вопроса следуют друг за другом, смысл их не ясен, что имеете ввиду непонятно.

Дело ровно в том, что я хочу понять, в каком значении употребляется слово функция в статье. Пока что автор статьи утверждает что в том значении, в котором это слово используется в математике, и это мне немного… удивительно.

Вы к вопросу о непрерывности и дискретности толкуете?
Как уже было сказано, в СУБД область определения функции дискретна и определена содержимым самой СУБД.

Нет, я "толкую" к списку свойств этих самых функций (и заодно операций над ними).

Какие ещё свойства, кроме детерминированности интересуют? Их много свойств разных — должна же быть цель какая-то, кроме перекидывания мяча.
Про детерминированность ответили уже.

Какие ещё свойства, кроме детерминированности интересуют?

Отсутствие побочных эффектов, конечно же.


Про детерминированность ответили уже.

В том смысле, что согласились, что ее нет, а потому это точно не математическая функция? Или что-то другое?

Это спор виниловых пластинок с компакт дисками.
Я люблю послушать дома чистый ламповый звук, однако на работе использую частотно дискретизированное воспроизведение.

Это пока не спор, а всего лишь попытка выяснить, что же автор статьи понимает под центральным термином статьи.

Вы неверно определяете центральный термин статьи и уводите дискуссию в сторону. Статья черным по белому про функциональные субд, а не про функции.

Хорошо, что такое «функциональная СУБД»? В определении, понятное дело, не должно быть ни одного упоминания функций.

Будут детерминированы для текущего состояния базы данных. Так как функции рассчитываются от других функций, то на узлах дерева будут конечные точки — DATA-функции. Их значения и есть состояние базы данных.
Будут детерминированы для текущего состояния базы данных.

То есть только что сказанное вами выше "когда отсутствует состояние, функция из программирования = функции математической" к вашему случаю неприменимо, потому что у вас состояние есть. Следовательно, ваши функции — это не функции из математики.

Это не функциональная СУБД, а объектно-ориентированная СУБД. Лучше читать в англоязычной вики. Тема очень старая.
Первые публикации об объектно-ориентированных базах данных появились в середине 80-х годов.

Их кстати, изначально было два типа — чистые объектные и гибридные — объектная модель поверх классического движка
Чтобы не разводить демагогию давайте сравнивать с какой-то конкретной реализацией ООБД. В википедии нету ни одной ссылки на такие. Можете привести пример production-ready СУБД?
Как я раньше уже писал функциональные языки появились тоже очень давно, но популярность у них появилась только в последнее время (с появлением Scala, Clojure, поддержкой в JavaScript и прочим).
Как выше уже заметил lair, демагогия в определениях как раз идет от Вас. Стандартный SQL — уже декларативный [почти] функциональный язык. У Вас же просто объектное (причем простейшее, без вложенных объектов) расширение над RDBMS с попыткой изобрести велосипед новый язык запросов.

По поводу конкретных реализаций — я же написал — смотреть в английской версии Вики (слева в меню кнопочка English).

Еще можно посмотреть по популярности тут.
Давайте по существу. Я конкретно внизу статьи описал в чем преимущество по сравнению с SQL. Вы согласны, что такой язык проще? Решите задачу с двумя звездочками на SQL?

Кроме того, это далеко не объектно ориентированная БД. В ООБД (как и в ООП) все крутится вокруг объекта. У него есть методы, поля и т.д. А что делать, когда функция от нескольких объектов? Она к какому из них должна относится с точки зрения ООБД?
По существу ООСУБД прошли долгий практический путь и сначала стоит изучить, что и как там сделано.
А на Ваш хелловорлд в две таблички с очевидными выводами смотреть неинтересно — здесь не с чем спорить.

Я бы развернул Ваш вопрос ровно наоборот — почему ООСУБД не используются повсеместно, если такие удобные?
По существу. Повторюсь, я описывал не ООСУБД, а функциональную СУБД. Если будет язык программирования (а они есть), в котором не будет методов, а только объекты и функции. Он будет объектно-ориентированным или нет?

Я бы развернул Ваш вопрос ровно наоборот — почему ООСУБД не используются повсеместно, если такие удобные?

Потому что они неудобны. А функциональная СУБД может быть таковой.

Вы уходите от ответа. Мой пример был сделан за 10 секунд. Вы определитесь — на SQL его сделать просто или нет? Если да, то, пожалуйста, напишите ответ, и мы закончим этот бессмысленный спор.
Если будет язык программирования (а они есть), в котором не будет методов, а только объекты и функции. Он будет объектно-ориентированным или нет?

Вопрос, будет ли этот язык функциональным.

Смотря как ввести определение. Если даже оно не подходит под общепризнанное, то это не самоцель. Можно назвать Функцио-ориентированная СУБД, если так будет лучше.
Смотря как ввести определение.

А в этот момент заявленное вами преимущество "отсутствие семантического разрыва" не просто пропадает, а становится хуже, чем при наличии явного семантического разрыва — вы называете функцией не то же самое, что ей называет человек с опытом в другом языке, и ему приходится каждый раз себе напоминать про эти различия.


Ну и да, это начинает выглядеть как типичный marketing bullshit: давайте скажем, что наш продукт — X, потому что X сейчас модно и на волне, а внизу мелким шрифтом напишем "X означает Y".

вы называете функцией не то же самое

Функция как раз точно такая же, как и в языках программирования. Речь шла о термине «функциональный». Если он вам не нравится, то я предложил использовать другой.

Ну и да, это начинает выглядеть как типичный marketing bullshit: давайте скажем, что наш продукт — X, потому что X сейчас модно и на волне, а внизу мелким шрифтом напишем «X означает Y».

Я описывал концепцию и хотел обсудить именно ее. А внизу просто указал, что реализация теоретически возможна (и пример).
Функция как раз точно такая же, как и в языках программирования.

Вот только в языках программирования функции имеют разные свойства в зависимости от языка. Что возвращает нас к началу обсуждения.


Я описывал концепцию и хотел обсудить именно ее.

А для того, чтобы описывать концепцию, особенно новую, надо, опять-таки ж, определять используемые термины.

Я сломался уже на первом примере


CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);

Почему это одна таблица, а не две? Движок объединяет таблицы с одинаковыми наборами ключевых полей?

name и price — это не таблицы, а функции. Именно классы соответствуют таблицам, а функции — полям.
Движок объединяет таблицы с одинаковыми наборами ключевых полей?

Да. Под каждый класс создается своя таблица, а функции раскидываются по таблицам с соответствующими ключами. Но при необходимости этим можно управлять вручную и создать под классы несколько таблиц, в явную указав в какую именно «положить» функцию.
CLASS a;
CLASS b;
CLASS c;

f1 = DATA STRING[10] (a);
f2 = DATA STRING[10] (a, c);
f3 = DATA STRING[10] (b);
f4 = DATA STRING[10] (a, c);

такая декларация корректна? создадутся 3 таблицы? (для f1, f3, f2+f4)?
Да, совершенно верно.
у вас определённо не хватает ссылок между статьями по isFusion )
Так там справа же есть ссылки :) К блогу же компании привязано…
решение задачи * на SQL

select
    AB.first_person_id as a_person_id,
    AB.second_person_id as b_person_id,
    BC.second_person_id as c_person_id
from friends as AB
    inner join friends as BC on
        BC.first_person_id = AB.second_person_id
where
    not exists (select * from friends as tt where tt.first_person_id = AB.first_person_id and tt.second_person_id = BC.second_person_id) and
    exists (select * from likes as tt where tt.first_person_id = AB.first_person_id and tt.second_person_id = BC.second_person_id)


Задача с ** на MS SQL может выглядеть примерно так:

with cte_friends as (
	select
		t.first_person_id,
		t.second_person_id,
		c.dummy_first_person_id,
		c.dummy_second_person_id
	from friends as t
		cross apply (values
			(t.first_person_id, t.second_person_id),
			(t.second_person_id, t.first_person_id)
		) as c(dummy_first_person_id, dummy_second_person_id)
)
select distinct
	AB.first_person_id as a_person_id,
	AB.second_person_id as b_person_id,
	BC.second_person_id as c_person_id
from cte_friends as AB
	inner join cte_friends as BC on
		BC.dummy_second_person_id = AB.dummy_first_person_id
where
	not exists (select * from friends as tt where tt.first_person_id = AB.first_person_id and tt.second_person_id = BC.second_person_id) and
	exists (select * from likes as tt where tt.first_person_id = AB.first_person_id and tt.second_person_id = BC.second_person_id)


Я не тестировал запросы, но что-то подобное.
Такое уже кидали, и есть небольшой вопрос как это все будет выполняться. Но в любом случае вопрос, это точно проще, чем:
EXPORT FROM Person a, Person b, Person c WHERE 
        likes(a, c) AND NOT friends(a, c) AND 
        (friends(a, b) OR friends(b, a)) AND 
        (friends(b, c) OR friends(c, b));
нет, явно не проще. В целом мне нравится Ваш подход, не уверен, нравится ли мне синтаксис обычных запросов, сходу он не читается так легко как SQL, но это может быть вопросом привычки, надо более вдумчиво прочитать статью.
В принципе, думаю, что-то подобное Вашему выражению можно написать, например, в postgresql используя простые функции, каторые будут заинлайнены (я, к сожалению, не использую postrgresql в продакшн поэтому не 100% уверен).
Нет времения сейчас пробовать, потому что рабочий день же, но, если не забуду, попробую позже
ну вот на постегрс можно написать что-то подобное прям сходу. Не уверен насчет производительности.

dbfiddle.uk/?rdbms=postgres_11&fiddle=367e2b43f668490e4190fbecee647025

create table friends (first_person_id int, second_person_id int);
create table likes (first_person_id int, second_person_id int);
create table person (id int, name varchar);

create function person_friend_with_person(_first_person_id int, _second_person_id int)
returns boolean
as $$
	select exists (select from friends where first_person_id = _first_person_id and second_person_id = _second_person_id);
$$
language sql;

create function person_likes_person(_first_person_id int, _second_person_id int)
returns boolean
as $$
	select exists (select from likes where first_person_id = _first_person_id and second_person_id = _second_person_id);
$$
language sql;

insert into person select 1, 'A';
insert into person select 2, 'B';
insert into person select 3, 'C';

insert into friends select 1, 2;
insert into friends select 2, 3;

insert into likes select 1, 3;

select *
from person as A, person as B, person as C
where
	(person_friend_with_person(A.id, B.id) or person_friend_with_person(B.id, A.id)) and
    (person_friend_with_person(B.id, C.id) or person_friend_with_person(C.id, B.id)) and
    person_likes_person(A.id, C.id) and not person_friend_with_person(A.id, C.id)
Что-то мне подсказывает, что по производительности это будет очень плохо. Особенно имея большой опыт работы с планировщиком postgres.

Мой здравый смысл подсказывает, что если это и так, то только из-за or в условии. В этом смысле запрос можно конечно поменять, чтоб стало получше. Было бы интересно сравнить производительность sql запроса и sql запроса сгенерённого Вашей платформой на хорошем объеме данных.

Мой здравый смысл подсказывает, что если это и так, то только из-за or в условии.

Это факт, or в жизни конечно встречается реже чем and, но не сильно. И поэтому игнорировать его это даже не знаю как назвать.
Было бы интересно сравнить производительность sql запроса и sql запроса сгенерённого Вашей платформой на хорошем объеме данных.

Ну запрос сгенерированный нашей платформой я уже привел. И он выполнится точно нормально, так как руками я бы также написал. Ваш возможно тоже, но учитывая сколько проблем пришлось решить с этим безудержно глупым оптимистом — планировщиком postgres (глупый еще ладно, но глупый оптимист — это всегда проблема), надеяться на это я бы не стал.
И поэтому игнорировать его это даже не знаю как назвать.

Тут как-то Вы читаете между строк, я не предлагал игнорировать. В конкретном случае графов, если запрос который вы привели выполняется больше 1 раза в день я бы начал с изменения схемы данных. В запросе, генерируемом вашей платформой, насколько я понимаю, каждый or будет увеличивать количество обращений к таблице в два раза + потом все эти данные надо будет сджойнить во время юниона. Я бы точно в данном случае предпочел видеть и иметь возможность править sql запрос.
with cte_friends as (
select
t.first_person_id,
t.second_person_id,
c.dummy_first_person_id,
c.dummy_second_person_id
from friends as t
cross apply (values
(t.first_person_id, t.second_person_id),
(t.second_person_id, t.first_person_id)
) as c(dummy_first_person_id, dummy_second_person_id)
)

лучше Union, спасибо )
не понял, сарказм это или нет. В плане экспрессивности мне cross apply нравится больше, ну и если фильтры применять, то применяются один раз, а не несколько, как в юнионе.
В плане производительности бывает всякое.
Нет, это не сарказм.
В плане производительности — это один констант скан, вместо двух или целых двух запросов к таблице, так что всяко лучше.

=)
ну иногда Sql Server себя ведет странно в плане применения индексов на поля которые использованы в apply, это надо, по-крайней мере, иметь в виду. Но в целом да, я предпочитаю начинать с apply.

Вкину ещё решение 2* на Datomic


Создание БД, схемы и накидывание сэмплов
(require '[datomic.api :as d])
(def db-uri "datomic:sql://hehe?jdbc:postgresql://localhost:5432/?user=datomic&password=datomic")
(d/create-database db-uri)
(def conn (d/connect db-uri))

(def schema
  [{:db/ident :person
      :db/valueType :db.type/string
      :db/unique :db.unique/identity
      :db/cardinality :db.cardinality/one}

    {:db/ident :likes
      :db/valueType :db.type/ref
      :db/cardinality :db.cardinality/many}

    {:db/ident :friends
      :db/valueType :db.type/ref
      :db/cardinality :db.cardinality/many}])

@(d/transact conn schema)

@(d/transact conn [
  {:person "John"}
  {:person "Alice"}
  {:person "Bob"}
  {:person "Eve"}
])

@(d/transact conn [
  {:person "John"
  :likes [[:person "Alice"] [:person "Bob"]]
  :friends [[:person "Bob"]]}

  {:person "Alice"}

  {:person "Bob"
  :friends [[:person "Alice"]]}

  {:person "Eve"
  :likes [[:person "Bob"]]
  :friends [[:person "Alice"]]}
])

(def db (d/db conn))

Сам запрос, один в один калька с пролога:


(d/q '[:find (pull ?a [:person]) (pull ?b [:person]) (pull ?c [:person])
        :where[?a :likes ?c]
              (or [?a :friends ?b] [?b :friends ?a])
              (or [?b :friends ?c] [?c :friends ?b])
              (not [?a :friends ?c])]
  db)

Результат
[[{:person "Eve"} {:person "Alice"} {:person "Bob"}]
[{:person "John"} {:person "Bob"} {:person "Alice"}]]
Большое спасибо. Очень познавательно. Только есть несколько вопросов:
  • Где в ident likes задается, что именно person likes person?
  • Почему person — это String? Можно пример, если у person есть имя и фамилия? И можно транзакцию, как изменить у person имя?

В likes/friends любой другой ident фигачить. Нравиться может не только человек :)


Person — string потому что мне было лень делать другой первичный ключ и я сделал его первичным ключем. А так оно добавляется аналогично:


Заголовок спойлера
(def schema
  [{:db/ident :person
      :db/valueType :db.type/uuid
      :db/unique :db.unique/identity
      :db/cardinality :db.cardinality/one}

    {:db/ident :name
      :db/valueType :db.type/string
      :db/cardinality :db.cardinality/one}

    {:db/ident :likes
      :db/valueType :db.type/ref
      :db/cardinality :db.cardinality/many}

    {:db/ident :friends
      :db/valueType :db.type/ref
      :db/cardinality :db.cardinality/many}])

@(d/transact conn schema)

(def john_id (str (java.util.UUID/randomUUID)))
(def alice_id (str (java.util.UUID/randomUUID)))
(def bob_id (str (java.util.UUID/randomUUID)))
(def eve_id (str (java.util.UUID/randomUUID)))

@(d/transact conn [
  {:person john_id :name "John"}
  {:person alice_id :name "Alice"}
  {:person bob_id :name "Bob"}
  {:person eve_id :name "Eve"}
])

@(d/transact conn [
  {:person john_id 
  :likes [[:person alice_id] [:person bob_id]]
  :friends [[:person bob_id]]}

  {:person bob_id 
  :friends [[:person alice_id]]}

  {:person eve_id 
  :likes [[:person bob_id]]
  :friends [[:person alice_id]]}
])

(def db (d/db conn))

(d/q '[:find (pull ?a [:name]) (pull ?b [:name]) (pull ?c [:name])
        :where[?a :likes ?c]
              (or [?a :friends ?b] [?b :friends ?a])
              (or [?b :friends ?c] [?c :friends ?b])
              (not [?a :friends ?c])]
  db)

Изменение имени — также, как в первом примере я докинул лайки и друзей в существующих людей


@(d/transact conn [
  {:person john_id :name "JOJO"}])
Я правильно понял, что эта база данных не типизирована? То есть в ней вообще нету классов? Если допустим я хочу добавить phone для person, то я спокойно могу его добавить и для order?
Боюсь даже предположить, как она тогда хранит данные в PostgreSQL.
Можете рассказать как в такой схеме у нее будут храниться данные и какой будет компилироваться SQL-запрос? Выше NitroJunkie кидал текст запроса, который компилирует платформа lsFusion. А какой будет здесь?

Да, она динамически типизированна. Тут в начале можно почитать про её принципы.
Нижележащую хранилку я так понял она использует как key-value storage.


Таблица в Postgres
CREATE TABLE datomic_kvs
(
 id text NOT NULL,
 rev integer,
 map text,
 val bytea,
 CONSTRAINT pk_id PRIMARY KEY (id )
)
WITH (
 OIDS=FALSE
);
Спасибо. Тогда использовать ее в production с такой моделью данных, в частности, для бизнес-приложений по факту невозможно. План выполнения запроса для вышеописанной логики будет ужасным. А если попробуете описать в ней 2й пример из сложной порции, то там будет еще хуже.

Кроме того, без типизации, это просто аналог классической NoSQL key-value базы данных. Если честно, то сходства с описанной моделью у нее гораздо меньше, чем у классической реляционной базы данных.
План выполнения запроса для вышеописанной логики будет ужасным

Она имеет какие-то свои индексы, благодаря которым на основе запроса понимает что откуда доставать. При выполнении запроса из начала треда в постгрес у меня прилетело только несколько селектов по id. Но тут уже надо полноценно читать документацию и тестировать чтобы понять насколько оно всё внутри эффективно.


просто аналог классической NoSQL key-value базы данных

Что что, но это точно не просто key-value база. Нагуглил термин Дедуктивная база данных. Отлично подходит для Datomic.
Частью своей сути, ваша система тоже является дедуктивной БД поверх реляционной логики. Правда вы, как уже отметили несколько раз, переназвали термины своими словами.

Но тут уже надо полноценно читать документацию и тестировать чтобы понять насколько оно всё внутри эффективно.

Не будет в любом случае выполнятся эффективно, так как в качестве «движка» она будет использовать PostgreSQL. И при такой структуре данных его оптимизатор точно не сможет эффективно выполнять данные запросы. Но если хотите это опровергнуть, то просто запустите пример, который вы написали и профайлером запишите запросы (а еще желательно с EXPLAIN ANALYZE). Увидите сами.
Также там будут большие вопросы с транзакционностью (в частности, с CONFLICT UPDATE), так как разные транзакции будут создавать на уровне PostgreSQL разные записи и не попадут в row conflict. И им придется тогда блокировать всю таблицу, что приведет к огромному падению производительности.

Нагуглил термин Дедуктивная база данных.

Я специально писал эту статью на примерах, чтобы избежать бесполезного терминологического спора. Приведите, пожалуйста, все таки аналог СУБД с типизацией и нормальным отображением на РСУБД или собственным движком.
Нет смысла лишний раз устраивать холивар между РСУБД и key-value СУБД. На эту тему написано сотни статей (в том числе и на хабре). Общий вывод, что у них разные сферы применения. Так вот у приведенной в статье базы данных сфера применения такая же как и у РСУБД. Datomic (судя по структуре хранения и описанию) — это классическая key-value база данных, так как там нету статической типизации. Сравнивать их бессмысленно.

Логика выполнения запросов, поддержка ACID лежит на уровне Datomic. Вместо postgres туда можно сунуть DynamoDB или Кассандру какую-нибудь. Насколько качественно запросы реализованы в самом Datomic сходу сказать не могу. Выше мы уже выяснили, что из-за single writer для OLTP она скорее всего не очень. Для OLAP — запросы на Datalog (функциональная парадигма вида likes(a, c) AND NOT friends(a, c) в ваших терминах) могут быть более удобными и короткими чем sql, что и показывает пример задачек со звездочками.


Я нигде не утверждал, что Datomic это аналог вашей СУБД. Всё началось с того, что я отметил, что называемая вами функциональная парадигма в подмножестве запросов семантически смахивает на логическую. И привёл пример предиката в логической парадигме равной сложности с вашим запросом (в отличие от более сложного sql запроса). Выше ещё упоминали, что на такие запросы к данным можно смотреть как запросы к графам. А причём здесь слово функциональная так никто кажется и не понял. Впрочем хотите называть это функциональной парадигмой — ваше право, спорить об этом у меня больше желания нет.

А причём здесь слово функциональная так никто кажется и не понял.


А почему реляционная СУБД называется именно так? Ведь там есть также таблицы, поля и связи между ними. Я думаю это потому, что там все «вертится» вокруг связей. Точно также и здесь я назвал ее функциональной (хотя можно еще функциоориентированной), так как все «вертится» вокруг функций. Но название тут не суть важно. Главное — парадигма. Но парадигм действительно много. Ценность статьи именно в конкретике (то есть как можно строить на основе ее СУБД + конкретная реализация).
Правда вы, как уже отметили несколько раз, переназвали термины своими словами.

Многие оппоненты свели аргументацию к спору по определениям. Вместе с тем примеры и кейсы были приведены исключительно практические. Лично я не встретил ни одного валидированного аргумента против выводов о преимуществах, приведенных в заключении статьи.
Лично я не встретил ни одного валидированного аргумента против выводов о преимуществах, приведенных в заключении статьи.

А как бы для вас мог выглядеть принятый вами валидированный аргумент против преимущества "на мой взгляд, писать такие запросы значительно проще"?

Выше уже показали сходство с Прологом, поэтому остаётся лишь подчеркнуть — авторы изобрели способ декларации декартова произведения, аналогичный давно известному способу, используемому в прологе. Ну и понятно, для этого нужны новые инструменты, например точно такие же, как в Прологе. Но названы эти инструменты «функциями». В принципе термы Пролога можно интерпретировать как функции, но общепринятые функции не умеют возвращать все значения для всех аргументов, а термы (с соответствующим движком) умеют. То есть умеет движок, конечно, но это применимо вообще ко всему и уводит от полезных абстракций.
Я не изобретал ни функции, ни классы, ни их композицию. Я лишь показал способ, как на основе этой парадигмы построить СУБД. Чтобы не разводить пустую болтовню, можете показать СУБД, которая построена по подобному принципу?
У любого востребованного продукта должны выполняться два условия: применимость и инновационность. Применимость по примерам, я вроде как показал (она гораздо проще SQL). Инновационность можно опровергнуть только одним способом: приведите аналог.
У любого востребованного продукта должны выполняться два условия: применимость и инновационность

Востребованны ли сейчас РСУБД как хранилища данных? Да. Что в них инновационного сейчас?

В момент их появления они были инновационными. Иначе бы ими сейчас никто не пользовался.

Иначе бы ими сейчас никто не пользовался.

Я не понимаю, как одно вытекает из другого. Сейчас РСУБД (как концепция) не инновационны, но при этом прекрасно востребованы.

Инновационность обеспечивает взрывной рост и долгосрочное конкурентное преимущество, поэтому и востребовано до сих пор, пока не выйдет замещающая инновация.

Инновационность обеспечивает взрывной рост и долгосрочное конкурентное преимущество,

Любая? Всегда? Мне очень хотелось бы понимать, на чем основано такое всеобъемлющее утверждение.


А с другой стороны, вот у меня на кухне стоит десяток разных чашек. Ни одна из них не инновационна. Все они востребованы. Как так получается?

Конечно не любая и далеко не всегда. Руны этого процесса вы найдете на Crunchbase

Конечно не любая и далеко не всегда. Руны этого процесса вы найдете на Crunchbase

Проставленный минус для этого моего комментария хороший показатель отрыва от реальности. Деньги зарабатывать очень тяжело. Не лишнее напоминание об этом в субботу к обеду может у кого-то вызвать негатив.
Применимость по примерам, я вроде как показал (она гораздо проще SQL)


На примере графа — возможно. Но в реальной системе, если запрос с односторонней дружбой делается чаще чем раз в день, я бы, по-крайней мере, поменял хранение данных, чтобы упростить запрос. На остальных примерах — даже в текущей интерпретации не проще (и SQL можно упростить, например, добавив представления).
Так что нет, не показал.
Вы не поняли. Сложность здесь не в графах (выше я уже писал, пусть дружат по трое и граф тогда тут ни причем). Сложность в AND и OR'ах. В этом случае в SQL приходится совмещать LEFT/RIGHT JOIN'ы и UNION/FULL JOIN'ы. И получаются такие монструозные конструкции, как описано выше. А задачи на AND и OR встречаются в жизни очень часто (например, если вы пишите сложные бизнес-приложения). Если же Вам такие не встречаются, то вам однозначно лучше использовать NoSQL.
Кроме того, вы сравниваете проще/сложнее исходя из того, что вы уже знаете SQL и реляционную логику, но не знаете новый синтаксис и новую парадигму. Если взять человека, который не знает ни того, ни другого, то моя схема будет значительно проще для понимания и разработки.
Я могу Вам накидать много примеров, если хотите, которые я напишу минут за 5 на своей логике. А Вы потратите минимум в 5 раз больше. Хотите?
И получаются такие монструозные конструкции, как описано выше

монструозные конструкции работают плохо в любом случае. Если вам постоянно нужно запускать такие запросы на большом количестве данных то надо думать об изменении структуры хранения данных.

Кроме того, вы сравниваете проще/сложнее исходя из того, что вы уже знаете SQL и реляционную логику, но не знаете новый синтаксис и новую парадигму. Если взять человека, который не знает ни того, ни другого, то моя схема будет значительно проще для понимания и разработки.

нет, я помню как я начинал писать на sql совершенно его не зная и он читался как натуральный язык, в отличии от вашего синтаксиса.

Вы не поняли

Я могу Вам накидать много примеров, если хотите, которые я напишу минут за 5 на своей логике. А Вы потратите минимум в 5 раз больше. Хотите?

Ну как не понял. Вы же наверху уже дали примеры, и sql варианты не выглядят сложнее, в некоторых случаях даже наоборот, только в случае запросов с графами ваш вариант выглядит предпочтительнее (хотя результирующий запрос оптимизма не внушает всё равно).

А задачи на AND и OR встречаются в жизни очень часто

Так у оптимизаторов проблем с AND обычно не бывает, проблемы бывают с OR, тут конечно надо думать, и не всегда разбиение запроса на union это то, что я хотел бы делать (честно говоря, практически никогда на union не разбиваю, но, может просто предметная область другая)
монструозные конструкции работают плохо в любом случае. Если вам постоянно нужно запускать такие запросы на большом количестве данных то надо думать об изменении структуры хранения данных.

Это не правда. У нас при сложной логике могут формироваться запросы в миллионы символов, а выполняет их PostgreSQL за несколько миллисекунд. Так что длина запроса — не показатель сложности его выполнения.
Ну как не понял. Вы же наверху уже дали примеры, и sql варианты не выглядят сложнее, в некоторых случаях даже наоборот, только в случае запросов с графами ваш вариант выглядит предпочтительнее (хотя результирующий запрос оптимизма не внушает всё равно).

Остальные примеры тоже гораздо проще и прозрачнее, так как сложность в них разбивается на блоки, каждый из которых относительно просто для понимания. Да, это можно делать и через CREATE VIEW. Но если вы всю программу будете писать через CREATE VIEW читабельность у нее будет ужасная.
Вообще, я думаю напишу продолжение статьи еще с несколькими примерами из жизни, в которых приложу варианты на SQL и на этом языке. Там будет еще более явное преимущество.
Так у оптимизаторов проблем с AND обычно не бывает, проблемы бывают с OR, тут конечно надо думать, и не всегда разбиение запроса на union это то, что я хотел бы делать (честно говоря, практически никогда на union не разбиваю, но, может просто предметная область другая)

Еще раз советую вам посмотреть в сторону NoSQL, если у вас такой потребности не возникает. В сложных приложениях — это сплошь и рядом.

Если вам постоянно нужно запускать такие запросы на большом количестве данных то надо думать об изменении структуры хранения данных.

И предложите, как в случае с friends, вы измените структуру хранения данных.
слушайте, очень тяжело выдерживать нейтральный тон в разговоре с вами. Я работаю db разработчиком и архитектором больше 15 лет, не надо пожалуйста мне рассказывать про сложные приложения и советовать переключаться на NoSql, это вас характеризует не с лучшей стороны.
Вы так говорите, как будто предложение попробовать NoSQL — это как будто послать человека. К сожалению, я не знал Вашего опыта и часто бывает, что новички используют не ту технологию не для тех целей. И в отличии от большинства комментаторов, я действительно открыт к новому и при любой возможности ищу варианты это использовать.
я на самом деле полностью поддерживаю идею языка-надстройки над sql. Как уже говорил в комментариях, я считаю sql очень выразительным языком для работы с данными, но его можно сделать лучше — например, конкретно в Ms Sql довольно сложно разбивать логику на модули. Можно, но не сказать, что это красиво выглядит.
Ну а в комментариях я просто пробую на прочность вашу модель.
Спасибо за конструктивную критику. И прошу прощения, если обидел чем-то. Я честно не хотел показать неуважение.
И предложите, как в случае с friends, вы измените структуру хранения данных.

ну например хранить отдельно двустороннюю дружбу.

У нас при сложной логике могут формироваться запросы в миллионы символов, а выполняет их PostgreSQL за несколько миллисекунд

на каком количестве данных? если данных немного, то и O(n*n) работает быстро. Снова повторюсь — тот запрос который я видел выше, с юнионами, будет работать хуже чем запрос на структуру данных предназначенную для таких запросов.

Остальные примеры тоже гораздо проще и прозрачнее, так как сложность в них разбивается на блоки, каждый из которых относительно просто для понимания.

можете мне привести номер задачи, которая, на ваш взгляд, выглядит проще для понимания?
ну и, раз уж вы хотите приводить примеры и у вас есть на это время

select
    department_id,
    array_agg(id) as employees,
    sum(tax) as tax
from employee
group by
    department_id
having
    sum(salary) > 100000 and
    count(*) < 10 and
    count(distinct gender) = 2


приведите, пожалуйста, пример такого выражения на lsfusion вместе с запросом sql, в который он компилируется
salarySum 'Сумма зарплат' (Department d) = GROUP SUM salary(Employee e) IF department(e) = d;
taxSum 'Сумма налогов' (Department d) = GROUP SUM tax(Employee e) IF department(e) = d;
countEmployees 'Кол-во сотрудников' (Department d) = GROUP SUM 1 IF department(Employee e) = d;

countEmployees 'Кол-во сотрудников' (Department d, Gender g) = GROUP SUM 1 IF department(Employee e) = d AND gender(e) = g;

countGenders 'Кол-во различных полов' (Department d) = GROUP SUM 1 IF countEmployees(d, Gender e);

// тут я возвращаю строку, так как многие драйверы не примут массив - правильнее будет возвращать их отдельными строками
employeesId 'Список сотрудников (ИД)' (Department d) = GROUP CONCAT id(Employee e) IF department(e) = d, ',';

SELECT Department d, employeesId(d), taxSum(d) WHERE salarySum(d) > 100000 AND countEmployees(d) < 10 AND countGenders(d) = 2;


Я специально не «инлайнил» все в один запрос как у вас, чтобы показать как декомпозируется сложность. Если сформулировать задачу на русском языке, то она будет звучать вот как:
Вывести (SELECT) все отделы (Department d), список сотрудников (employeesId(d)), сумму налогов(taxSum(d)), у которых (WHERE) сумма зарплат больше 100000 (salarySum(d) > 100000), кол-во сотрудников меньше 10 (countEmployees(d) < 10) и кол-во различных полов равно 2 (countGenders(d) = 2).
Вы согласны, что простота определяется не длинной запросов, а близости к русскому языку? То есть когда человек получает задачу на русском языке, то ему гораздо проще транслировать ее в lsFusion, чем на SQL. Кстати, спасибо за пример, использую его в следующей статье.
Вы согласны, что простота определяется не длинной запросов, а близости к русскому языку?

Я вот точно не согласен. Не говоря уже о том, что ваше решение к русскому языку не близко.

А зачем я написал в условии задачи для каждого слова часть результирующего запроса? Оно один в один транслируется из русского языка. Можно даже попробовать написать автоматический транслятор.
Вы слишком много комментариев пишите, а я могу отвечать только раз в 5 минут, так что, не обижайтесь, но часть комментариев я буду игнорировать :)
А зачем я написал в условии задачи для каждого слова часть результирующего запроса?

Вот только решение вашей задачи не ограничивается одной строчкой результирующего запроса, оно еще включает в себя все вспомогательные функции, которые вы создали.


(и это еще не придираясь к тому, что вообще-то следуя вашей логике решение .coge. algo a, nos(a), imp(a) .t. caja(a) > 100000 y px(a) < 10 y sx(a) = 2 тоже "один в один транслируется из русского языка")

Вот только решение вашей задачи не ограничивается одной строчкой результирующего запроса, оно еще включает в себя все вспомогательные функции, которые вы создали.

Вот именно, что уже создали.
Зато теперь этим люди пользуются.
Зато теперь этим люди пользуются.

проблема в том, что эти функции имеют очень узкую область применения — что делать если вы хотите использовать одну и ту же агрегационную функцию но хотите иметь возможность фильтровать данные (сумма зарплат сотрудником мужчин, женщин, с фамилией начинающейся на П, из Албании, получающих зарплату в долларах и т.п.)?
у вас есть какой-то эталонный «человек со стороны», к которому у меня доступа нет

Я в свое время был таким человеком со стороны — не программист, бэкофисный менеджер. Очень пригодился мне этот язык в итоге — думаю тоже свою статью для Recovery Mode напишу позднее.

сумма зарплат сотрудником мужчин, женщин, с фамилией начинающейся на П, из Албании, получающих зарплату в долларах и т.п.

sumByCategory 'Сумма зарплат по критериям' 
(Sex s, Currency c, Symbol p, Country w) = 
     GROUP SUM salary(Employee e) 
          IF currency(e) == c 
             AND sex(e) == s 
             AND substr(surname(e),1,1) == p 
             AND country(e) == w;
Ну вот и выходит, что как только вы хотите поменять фильтр вам необходимо задавать новую функцию. И вам приходится заносить фильтр внутрь функции, в случае нескольких агрегатов это будет выглядеть просто ужасно. Я все надеюсь, может быть у вас как-то можно задать отдельную функцию с фильтром и потом применять агрегации уже на неё?
Ну вот и выходит, что как только вы хотите поменять фильтр вам необходимо задавать новую функцию.

Я смогу переиспользовать старую функцию, и сократить вычисления, если она была материализована, вот удобство! То есть, если вдруг потребовалось по всем мужчинам и женщинам такую вборку сделать:
sumSexByCategory 'Сумма зарплат по критериям и мужчин, и женщин' 
(Currency c, Symbol p, Country w) = 
     GROUP SUM sumByCategory(Sex all, c, p, w);
Мне кажется, мы с вами друг друга не понимаем, вам кажется что вы экономите время, добавляя такие функции. Исходя из моего опыта, очень сложно предусмотреть какие функции и где вам будут нужны, особенно если это делает ваш гипотетический «человек с улицы», а не архитектор. В одном отчете мне надо вывести суммы по отделам, а в другом по странам. Там где я буду писать простой запрос, вам придется писать отдельные функции + запросы.
Я не архитектор, а бизнес-пользователь, который раньше привлекал архитектора, чтобы что-то там автоматизировать у себя в процессе. Мне прекрасно известно какие функции мне нужны и я добавляю их в систему всего в одну строчку кода и обновляю решение по автоматизации сразу.
Я понимаю, что вы хотите, чтоб это звучало просто, но я не понимаю, как это будет работать в реальной жизни.
Вот пример наверху, с having. Сколько строк вам надо поменять, чтобы сделать так чтобы этот пример брал данные только по сотрудникам с зарплатой > 10000?
Вот пример наверху, с having.

Честно, не знаю что такое having, не довелось изучить sql, встречался только в университете.

Сколько строк вам надо поменять, чтобы сделать так чтобы этот пример брал данные только по сотрудникам с зарплатой > 10000?

В зависимости от условий задачи — могу представить три жизненных варианта:
1 Требования упростились и старая выборка больше нужна — я добавлю ещё один IF в старую конструкцию;
2. Требования усложнились и надо по разным порогам смотреть — я добавлю порог как INTEGER параметр в функцию
3. Как в пункте 2, но порог не меняется и всегда равен 10000, а старая функция тоже нужна, то в новой функции буду группировать старую с дополнительным IF.
Могут быть иные варианты в зависимости от требований ситуации. В данном примере в любом случае одной строчкой обходится.
что если требования такие — только в одном месте должен появиться фильтр по зарплате, в остальных местах все остаётся как прежде?

Пусть так — можете написать вариант кода который позволит сымитировать поведение такой функции:

select
    department_id,
    array_agg(id) as employees,
    sum(tax) as tax
from employee
where
     salary between [param1] and [param2]
group by
    department_id
having
    sum(salary) > [param3] and
    count(*) < 10 and
    count(distinct gender) = 2
А как сделать, если нужно условия having «включать/выключать» в зависимости от надобности?
если это мне вопрос, то не понял вопроса
Допустим мне нужно сделать одинаковые отчеты (или просто встроенные функции), но уметь включать/выключать фильтры по сумма сотрудников, количеству сотрудников и по полу. Мне нужно копипастить запрос 6 раз или как делать предполагается?
ну вообще-то в тех RDBMS в которых я работал (MS SQL в основном, но вроде в постгресе тоже), можно определять табличные функции, так что ничего копипастить не надо
Проблема табличных функций, что они очень плохо воспринимаются планировщиком запросов. Как правило там в плане «торчит» полное вычисление функции во временную таблицу и затем ее использование. А если «сверху» есть контекст (то есть выражение WHERE) оптимизатор не «протолкнет» ее в вычисление, и запрос будет неэффективно выполняться.
Это утверждение ложно как минимум в MS SQL, в PostgreSQL насколько знаю тоже — если функция это чистый sql то она инлайнится.
Мне прекрасно известно какие функции мне нужны

Это типичное заблуждение. Вы знаете, какие функции вам удобны. Это не значит, что они вам нужны (вполне может быть, что есть другое решение, которое будет эффективнее), и тем более это не значит, что они нужны кому-то еще.


Это, если что, опыт анализа требований, приносимых такими вот бизнес-пользователями.

Это, если что, опыт анализа требований, приносимых такими вот бизнес-пользователями.

Каждый из участников этого треда попросил уважительного отношения к себе за 10-to-15 летний опыт архитектуры и разработки.
Вот у меня не меньший опыт бизнес-пользователя, поэтому ваш сарказм мне неприятен.
Тем более, что заблуждения или находки для пользователей моей категории являются предпринимательским риском. Это достаточный уровень ответственности, при котором реальную стоимость решений понимаешь очень быстро.
Вот у меня не меньший опыт бизнес-пользователя, поэтому ваш сарказм мне неприятен.

А почему, собственно? Я не утверждаю, что я знаю, как делать ваш бизнес, и ожидаю, что вы не будете утверждать, что знаете, как программировать.


Я, на всякий случай, напомню, что речь шла не о функциях ПО (в смысле, что ПО умеет делать), а о функциях как объектах программного кода (которые есть декомпозиция задачи на элементы).

ожидаю, что вы не будете утверждать, что знаете, как программировать.

Я не являюсь программистом, уже писал где-то пару раз.
Но пользуюсь вполне сносно разными высокоуровневыми инструментами и рад, что они многие вещи позволяют решить. Хабр вроде такая площадка, чтобы опытом делиться, а не лекции читать.
Мой опыт, что специфическую ERPшку можно быстро на lsFusion сделать. Или, кстати, запрототипировать — она потом вместо ТЗ и будет.
По аналогии как математические модели из фреймворков типа Scilab от исследователей используются в качестве ТЗ программистами аппаратных комплексов.
спасибо, я абсолютно согласен с этим заявлением. Здесь это усугубляется тем, что предполагается, что на этой платформе разрабатывают бизнес пользователи, а не разработчики. Судя по опыту, бизнес пользователем уж точно всё равно что будет завтра с этими функциями, им надо сейчас сделать чтоб отчёт работал.
Я все надеюсь, может быть у вас как-то можно задать отдельную функцию с фильтром и потом применять агрегации уже на неё?

Я несколько раз просмотрел все примеры в посте, и так и не нашел ни одной функции, которая могла бы принимать множество объектов или возвращать множество объектов.

sumByCategory 'Сумма зарплат по критериям' 
(Sex s, Currency c, Symbol p, Country w) = 
GROUP SUM salary(Employee e) 
IF currency(e) == c 
AND sex(e) == s 
AND substr(surname(e),1,1) == p 
AND country(e) == w;

… и вот это было бы прекрасное место, где можно было бы показать функциональность "функциональной" СУБД. Потому что в фунциональной СУБД первое, лобовое решение этой проблемы выглядит так:


def conditionalSalarySum predicate employees =
  employees |> filter predicate |> sum salary

А второе решение той же проблемы выглядит вот так:


def salarySum employees = employees |> sum salary
//...
employees |> filter predicate |> salarySum

Хотите по департаменту?


def conditionalSalarySumByDeparment predicate deparments =
  departments |> map (d => 
    (d, d |> employees |> filter predicate |> salarySum)
  )

Любой участок этого конвеера выносится в функцию.

именно этого мне не хватает в стандарте sql — query as first class citizen.

Просто занятно: в каком-то смысле, любая query — это функция. Казалось бы, это логичный шаг.

да, абсолютно. Все селект запросы в sql это типичные функции без сайд эффектов.
Собственно, мы с моим коллегой вынашиваем идею языка-надстройки над sql который позволит так работать — передавать запросы как параметры в функции, возвращать запросы и т.д. Сейчас, в принципе, можно такое сделать через курсоры, но это сразу убивает всю set-based-thinking парадигму.
Как обычно, времени на это нет.

Пытаюсь внедрить эту идею в мозги разработчиков Postgres на митапах :) Потому что, насколько знаю, Postgresовская система типов лучшая среди всех RDBMS, было бы логично её расширить.
Зато теперь этим люди пользуются.

Это еще не известно: у нас один конкретный запрос в вакууме, и никто не может сказать, какие его части понадобятся потом.


Но дело даже не в этом, а просто в том, что читаемость этих функций тоже надо учитывать в общей читаемости.

Вот только решение вашей задачи не ограничивается одной строчкой результирующего запроса, оно еще включает в себя все вспомогательные функции, которые вы создали.


Потому что ТЗ не полное. На такую формулировку задачи нужен дополнительный вопрос. А что такое «сумма зарплат по отделу»?
Ответ: сумма зарплат (salarySum) по отделу (Deparment d) — это (=) сумма (GROUP SUM) зарплаты (salary) каждого сотрудника (Employee e), у которого (IF) отдел (department(e)) совпадает (=) с данным отделом (d). Видите, все красиво ложится (в отличии от SQL). Как тебе такое Илон Маск?
ну в вашем понимании если надо вывести в одном запросе сумму зарплат по отделу, среднюю зарплату по тому же отделу, средний возраст сотрудников, в ТЗ будет написано вот так:

Вывести сумму зарплат по отделу:
сумма зарплат (salarySum) по отделу (Deparment d) — это (=) сумма (GROUP SUM) зарплаты (salary) каждого сотрудника (Employee e), у которого (IF) отдел (department(e)) совпадает (=) с данным отделом (d).
Вывести среднюю зарплату по отделу:
средняя зарплата (avergeSalary) по отделу (Deparment d) — это (=) средняя (GROUP AVG) зарплаты (salary) каждого сотрудника (Employee e), у которого (IF) отдел (department(e)) совпадает (=) с данным отделом (d).
Вывести средний возраст по отделу:
средний возраст (avergeAge) по отделу (Deparment d) — это (=) средняя (GROUP AVG) возраста (age) каждого сотрудника (Employee e), у которого (IF) отдел (department(e)) совпадает (=) с данным отделом (d).

Это же просто эталон копи-пейста, разве нет?
В реляционной базе — фильтрация/группировка и агрегация задаются отдельно, что делает код гораздо более изменяемым. Не знаю, как у Илона Маска, а мне такая постановка задачи кажется просто на порядки более простой.
Потому что ТЗ не полное.

ТЗ вообще не было.


Как тебе такое Илон Маск?

Отвратительно.


Во-первых, сравните: "каждого сотрудника (Employee e), у которого (IF)" c "все отделы (Department d) [...] у которых (WHERE)". Почему?..


Во-вторых: "сумма (GROUP SUM)". Я просил сумму, я не просил никакую группу.


Ну и в-третьих, и в самых важных:


countGenders 'Кол-во различных полов' (Department d) = GROUP SUM 1 IF countEmployees(d, Gender e);

Давайте, значит, разберем по-вашему. Количество различных полов (countGenders) по отделу (Deparment d) — это (=) сумма (GROUP SUM) единиц (1), у которых (IF) число сотрудников (countEmployees) данного отдела (d) и данного пола Gender e. Что? Что я сейчас прочитал?


Число полов в отделе — это число различных значений атрибута пол каждого сотрудника в отделе. Ну то есть вот так:


CountGenders(Deparment d) =>
  d.Employees.Select(e => e.Gender).Distinct()
Ладно, давайте не будем спорить, чьи фломастеры вкуснее.
Лучше приведите решение второго примера от OrmEugensson на LINQ и, главное, текст запроса на SQL (как сделал я). Зачищайте честь мундира LINQ, сэр :)

И чтобы дважды не вставить пример с friends.
Ладно, давайте не будем спорить, чьи фломастеры вкуснее.

Круто, то есть сначала вы утверждаете, что ваш язык проще, и называете это его важным преимуществом, а потом — "давайте не будем спорить".


Лучше приведите решение второго примера от OrmEugensson

Вы бы хоть ссылки давали, а то фиг поймешь, где здесь "второй пример".


Вот этот?


select
    department_id,
    array_agg(id) as employees,
    sum(tax) as tax
from employee
where
     salary between [param1] and [param2]
group by
    department_id
having
    sum(salary) > [param3] and
    count(*) < 10 and
    count(distinct gender) = 2

departments
.Select(d => (d, 
  d.Employees
  .Where(e => e.Salary >= param1 && e.Salary <= param2)
))
.Where((_, ee) =>
  ee.Sum(e => Salary) > param3
  && ee.Count() < 10
  && ee.Select(e => e.Gender).Distinct.Count() == 2
)
.Select((d, ee) => (d,
  ee.ToArray(),
  ee.Sum(e => e.Tax)
))

Кстати, вы там рядом спрашивали, как включать или выключать условия в HAVING? Берете нижнее (внешнее) Where и модифицируете его предикат. Можно даже снаружи его передавать, вот прямо целиком как предикат.


и, главное, текст запроса на SQL

Для меня это не главное. Вы предлагаете "концепцию функциональной СУБД", мне интересно, как та же самая концепция может быть реализована лучше в части языка. А меряться, кто лучше напишет транслятор из одного языка в другой, мне не интересно.


Я, собственно, вообще считаю, что если сделана заявка на функциональную СУБД, то хотелось бы видеть новую СУБД, со своими собственными примитивами и их реализацией, а не просто надстройку над существующими РСУБД с трансляцией из внешнего языка во внутренний.

Для меня это не главное

Концепция без реализации — это просто фундаментальные рассуждения. Спорить о них достаточно бесполезно. Считайте, что я теперь рассматриваю концепцию+реализацию и сравниваю только с ними. Так что SQL в студию.
Считайте, что я теперь рассматриваю концепцию+реализацию и сравниваю только с ними.

Для меня это выглядит как "я не готов отстаивать свою концепцию, поэтому переведу разговор в более удобное мне русло". Это мне не интересно.

ваш последний пример, кстати, не эквивалентен моему запросу. И linq, как раз, именно в этом примере обыгрывает sql за счет комбинируемости. В sql я могу сделать так

select
    department_id,
    array_agg(id) as employees,
    sum(tax) as tax
from employee
group by
    department_id
having
    sum(salary) > 100000 and
    count(*) < 10 and
    count(distinct gender) = 2


а потом мне надо отфильтровать employee по каким-то критериям, мне придется править исходный запрос

select
    department_id,
    array_agg(id) as employees,
    sum(tax) as tax
from employee
where
    salary > 10000
group by
    department_id
having
    sum(salary) > 100000 and
    count(*) < 10 and
    count(distinct gender) = 2


и в sql я не могу сделать так, чтобы employee была переменной (я могу конечно сделать темп таблицу, и использовать её, но это перекладывание данных). А вот что я хотел бы уметь делать в sql


set @employee = (select * from employee);

--optional
set @employee = (select * from @employee where salary > 10000);

select
    department_id,
    array_agg(id) as employees,
    sum(tax) as tax
from @employee
group by
    department_id
having
    sum(salary) > 100000 and
    count(*) < 10 and
    count(distinct gender) = 2;


и linq как раз тут просто обыгрывает стандартный сиквел на порядки.
а всего-то надо сделать query first class citizen
Мне кажется в вашем примере все поставлено с ног на голову. sql пример читается очень логично — я пишу откуда я хочу брать данные, как хочу их группировать и что хочу получить в результате + фильтры до и после группировки (в моём примере я еще не добавлял where). В вашем коде группировка была задана как минимум четыре раза. Как такой код будет поддерживаться?

И в какой sql запрос всё это скомпилируется?
Вы мыслите категориями реляционной базы. Естественно, вам кажется проще. Но когда человек со стороны задает вопрос, то ему удобно отталкиваться именно от русского языка. То есть он берет задачу, «декомпозирует» каждое условие в отдельную функции и реализует эти функции. У вас достаточно простой пример. На более сложной логике это еще заметнее выглядеть.

Скомпилируется в такой же запрос (с одним GROUP BY), как и у вас, только без HAVING (то есть с подзапросом). С точки зрения выполнения это идентично, так как и с HAVING СУБД придется сначала высчитать значения для всех отделов, а потом фильтровать.

Такой подход еще дает определенные преимущества. Например, для любой функции (допустим countEmployees) вы можете повесить флаг MATERIALIZED. Тогда система, при изменении количества сотрудников, сама будет обновлять поле с количеством сотрудников. А при запросе будет обращаться уже к сохраненному полю (без изменения кода запроса). Тем самым можно отделить логическую от физической модели. В обычной базе DBA может только менять структуру индексов. В этой базе он по сути может добавлять промежуточные агрегации без изменения логики, тем самым балансируя запись и чтение.
Вы мыслите категориями реляционной базы. Естественно, вам кажется проще. Но когда человек со стороны задает вопрос, то ему удобно отталкиваться именно от русского языка.

с такими утверждениями сложно спорить, у вас есть какой-то эталонный «человек со стороны», к которому у меня доступа нет.

Скомпилируется в такой же запрос (с одним GROUP BY), как и у вас, только без HAVING (то есть с подзапросом). С точки зрения выполнения это идентично, так как и с HAVING СУБД придется сначала высчитать значения для всех отделов, а потом фильтровать.

не уверен что это идентично, особенно если я, например, начну добавлять фильтрацию с where. И опять же, представьте, что надо будет поменять требования, сделать запрос не по всем сотрудникам, а только по сотрудникам определенной профессии. В вашем случае придется добавлять фильтр в каждую функцию или есть подход проще?
Вы можете, как будет время, всё-таки проверить, как будет выглядеть запрос скомпилированный вашей системой, интересно было бы всё таки посмотреть.

Такой подход еще дает определенные преимущества. Например, для любой функции (допустим countEmployees) вы можете повесить флаг MATERIALIZED. Тогда система, при изменении количества сотрудников, сама будет обновлять поле с количеством сотрудников

Вот это, конечно, очень полезно, в данный момент у меня есть подсистема, написанная на t-sql, которая позволяет делать то же самое, но настройка, конечно, немного более сложная чем указание MATERIALIZED как свойства функции.
Можно поподробнее рассказать, как технически реализована материализация?
Отвечаю сразу и на комментарий по предыдущей ветке. Вы почему-то всегда забываете про декомпозицию и инкапсуляцию. Чаще всего понятие «зарплата по отделу» будет использоваться не только в одном запросе, а во многих. И в вашем случае, эту логику надо дублировать постоянно (и там тоже копи-паст). В моей же схеме вы один раз объявляете это понятие (сумма по отделу) и затем используете многократно (более того, с возможностью материализации). Так что с какой-то точки зрения там копи-паст, но с другой точки зрения у вас будет гораздо больший копипаст при повторном использовании.

Запрос напишу, как только будет возможность.

По поводу MATERIALIZED, не берите на свой счет, но для одного человека расписывать все подробно слишком трудозатратно. Мы это все опишем в следующих статьях. Подписывайтесь и следите, если есть желание.
Я ничего не забываю, я говорю с точки зрения своего опыта. Вы так говорите, как будто у вас ТЗ сделано сразу хорошо и никогда не меняется. В моей жизни люди хотят поменять конкретный отчет, чтобы «зарплата по отделу» показывалась только по сотрудникам с зп больше 10000, но не хотят, чтобы все отчеты, где использовалась понятие «зарплата по отделу» поменялись. Вы предлагаете мне поменять короткий sql запрос, который я могу мгновенно прочитать и понять, что он делает на имя функции, по которой я должен догадаться, что у неё внутри происходит. Я не понимаю, где тут мой выигрыш, но понимаю, что этим создам себе огромную проблему. Проблема поиска наименований для функций/переменных/etc это одна из самых сложных проблем в разработке ПО.
У нас тоже никогда ТЗ нету — это я образно выражаюсь. На практике это выглядит следующим образом. Приходит junior и спрашивает — как это делать. Я отвечаю — сформулируй по русски. Дальше построй функцию A, функцию B, потом на основе их C и т.д. То есть декомпозирую задачу. И если человек не может решить целиком, то по частям — без проблем.
В моей жизни люди хотят поменять конкретный отчет, чтобы «зарплата по отделу» показывалась только по сотрудникам с зп больше 10000, но не хотят, чтобы все отчеты, где использовалась понятие «зарплата по отделу» поменялись

Так в этом случае, у вас функция salarySum для Department не меняется. Вы просто меняете сами запросы на выбор:
SELECT Department d WHERE salarySum(d) > 10000
SELECT Department d WHERE salarySum(d) > 10
и т.д.

В вашем случае, в каждом из таких запросов придется повторять логику группировки. А тут — нет.

Проблема поиска наименований для функций/переменных/etc это одна из самых сложных проблем в разработке ПО

Вот тут очень помогает IDE и как раз в этом там тоже есть большое преимущество. У нас сейчас нету «чьего-то» кода. Все прекрасно читают и правят чужой код.
Например, тут вы просто жмете Find symbol и вводите salarySum(Deparment, Gender) и IDE находит не только, где оно введено, но и где используется (Find usages). Если нужного нету, то вы просто объявляете его и используете где хотите.
Так в этом случае, у вас функция salarySum для Department не меняется.

меняется — я имел в виду «сумма зарплаты по сотрудникам, у которых зарплата больше N». Посмотрите мой запрос c having выше. А теперь представьте что мне надо мне поменять этот запрос так, чтобы он брал данные только по сотрудникам с определенными параметрами. В описанном примере вам придется написать новые функции
Там же запрос sum(salary) > 100000. Это же как я понимаю, отделы, у которых суммарная зарплата > 100000. Разве нет.

Но в вашем случае, да. Строится функция salarySum (Department d, INTEGER s) = GROUP SUM salary(Employee e) IF department(e) = d AND salary(e) > s;
Ее уже можно использовать в миллионе других запросов, как WHERE salarySum(d, 5000) > 10000.
Опять же, построение этих функций дает не только MATERIALIZED, но хорошо потом идет в логику представлений (то есть быстро можно нарисовать форму). Вот Вам, кстати пример, который можете вставить в Try online (раздел Платформа) и посмотреть как все само пересчитывается:
CLASS Department 'Отдел';
name 'Имя' = DATA STRING[100] (Department) IN id;

CLASS Gender 'Пол';
name 'Имя' = DATA STRING[20] (Gender) IN id;


CLASS Employee 'Сотрудник';
department = DATA Department (Employee);
name 'Имя' = DATA STRING[100] (Employee) IN id;
salary 'Зарплата'  = DATA INTEGER (Employee);
tax 'Налог' = DATA INTEGER (Employee);

gender = DATA Gender (Employee);
nameGender 'Пол' (Employee e) = name(gender(e));

salarySum 'Сумма зарплат' (Department d) = GROUP SUM salary(Employee e) IF department(e) = d;
taxSum 'Сумма налогов' (Department d) = GROUP SUM tax(Employee e) IF department(e) = d;
countEmployees 'Кол-во сотрудников' (Department d) = GROUP SUM 1 IF department(Employee e) = d;

countEmployees 'Кол-во сотрудников' (Department d, Gender g) = GROUP SUM 1 IF department(Employee e) = d AND gender(e) = g;

countGenders 'Кол-во различных полов' (Department d) = GROUP SUM 1 IF countEmployees(d, Gender e);

// тут я возвращаю строку, так как многие драйверы не примут массив - правильнее будет возвращать их отдельными строками
employeesName 'Список сотрудников' (Department d) = GROUP CONCAT name(Employee e) IF department(e) = d, ',';

FORM employees 'Сотрудники'
	OBJECTS d = Department
    PROPERTIES(d) name, salarySum, countEmployees, countGenders, employeesName, NEW, DELETE
    
    FILTERGROUP filters
    	FILTER 'Мой фильтр' salarySum(d) > 100000 AND countEmployees(d) < 10 AND countGenders(d) = 2
    
    OBJECTS e = Employee
    PROPERTIES(e) name, salary, tax, nameGender, NEW, DELETE
    FILTERS department(e) = d
;

NAVIGATOR {
	NEW employees;
}
я имел в виду — как ваш пример придется поменять если я хочу добавить условие where e.salary > 10000.
за tryonline спасибо, выглядит интересно
То вам нужно будет объявить одну функцию, как в предыдущем комментарии (salarySum (Department, INTEGER)), а затем в запросе заменить salarySum(d) > 100000 на salarySum(d, N) > 100000.
я наверное плохо объясняю. Вот такое как сделать

select
    department_id,
    array_agg(id) as employees,
    sum(tax) as tax
from employee
where
     salary > 10000
group by
    department_id
having
    sum(salary) > 100000 and
    count(*) < 10 and
    count(distinct gender) = 2
Нет. Все правильно. Сначала строится функция, которая суммирует (GROUP SUM) зарплату (salary) сотрудников (Employee e), у которых (IF) отдел (department(e)) совпадает (=) с входным отделом (d) и (AND) зарплата (salary(e)) больше (>) входного порога (s).
А затем эта функция подставляется в WHERE исходного запроса:
SELECT Department d, employeesId(d), taxSum(d) WHERE salarySum(d, N) > 100000 AND countEmployees(d) < 10 AND countGenders(d) = 2;
где вместо N подставляйте, что хотите.
если я правильно понимаю как оно у вас работает это не эквивалентно запросу выше. Либо N надо передать во все функции либо у вас там какой-то искуственный интеллект
На самом деле эквивалентно. Проблема запроса выше, что он никак не оптимизируется. В любом случае, будет план, в котором будет сначала прогон по всей таблице Employee, затем группировка, затем фильтрация по полученным значениям. Скомпилируется, скорее всего, в такую же логику (сначала одним подзапросом подсчет всех сумм по Department, а затем их фильтрация). Только это все будет без HAVING (так как вообще HAVING — это, по сути, синтаксический сахар). Компиляция запросов — это вообще очень сложная тема, и там действительно очень сложный алгоритм, построенный на булевой логике. Искусственный интеллект там не нужен, тут конкретная задача, для который можно построить достаточно эффективный алгоритм.
я почти уверен что неэквивалентно (и, похоже, вы не понимаете, как работает having). Можете, пожалуйста, написать полный запрос на lsfusion и показать в какой sql он трансформируется в вашей платформе.
Не надо мне рассказывать про проблему оптимизации sql запроса, просто напишите этот запрос и покажите sql который получается в результате. Тогда я смогу оценить, эквивалентно оно или нет.
Я понимаю, что ваша платформа позволяет писать стандартные формы очень быстро, и это хорошее и, похоже, нужное вам свойство, но вы также утверждаете, что язык более выразителен чем sql, и с этим я совершенно не согласен.
Не надо мне рассказывать про проблему оптимизации sql запроса, просто напишите этот запрос и покажите sql который получается в результате. Тогда я смогу оценить, эквивалентно оно или нет.

Вот это очень странное утверждение. Я как бы тоже неплохо разбираюсь в запросах, но сказать на сто процентов какой запрос будет эффективнее без реального плана выполнения я не смогу. Более того, на разной статистике у разных запросов будут разные планы и, соответственно, разная эффективность.
Кроме того, часто не важно будет запрос выполняться 10мс или 50мс. Важно, чтобы там не пошел совсем дикий план, когда все тормозит в 1000 раз.

В целом, оптимизатор запросов там не на 100 процентов эффективный (то есть можно построить эффективнее запросы). Но если взять 1000 запросов, то он будет «писать» их эффективнее среднестатистического разработчика SQL запросов.

Вот запрос по получению списка отделов (затем отдельным запросом читаются выражения — это нужно, чтобы РСУБД «случайно» не начала вычислять их для всего множества Department, если окажется, что их в выборке очень мало).

Запрос для изначальной задачи:
SELECT t0.k0 AS jkey0,
       t0.k0 AS jprop0
FROM
  (SELECT t0.k0 AS k0,
          notZero(SUM(1)) AS e0
   FROM
     (SELECT t0.Main_department_Employee AS k0,
             t0.Main_gender_Employee AS k1,
             notZero(SUM(1)) AS e0
      FROM _auto_Main_Employee t0
      WHERE (t0.Main_department_Employee IS NOT NULL
             AND t0.Main_gender_Employee IS NOT NULL)
      GROUP BY 1,
               2) t0
   WHERE notZero(COALESCE(t0.e0, 0)) IS NOT NULL
   GROUP BY 1) t0
JOIN
  (SELECT t1.Main_department_Employee AS k0,
          notZero(SUM(t1.Main_salary_Employee)) AS e0,
          notZero(SUM(1)) AS e1
   FROM _auto_Main_Employee t1
   JOIN
     (SELECT t0.k0 AS k0,
             notZero(SUM(1)) AS e0
      FROM
        (SELECT t0.Main_department_Employee AS k0,
                t0.Main_gender_Employee AS k1,
                notZero(SUM(1)) AS e0
         FROM _auto_Main_Employee t0
         WHERE (t0.Main_department_Employee IS NOT NULL
                AND t0.Main_gender_Employee IS NOT NULL)
         GROUP BY 1,
                  2) t0
      WHERE notZero(COALESCE(t0.e0, 0)) IS NOT NULL
      GROUP BY 1) t0 ON t0.k0=t1.Main_department_Employee
   WHERE t0.e0=2
   GROUP BY 1) t1 ON t1.k0=t0.k0
WHERE ((notZero(COALESCE(t1.e0, 0))>100000)
       AND (10>notZero(COALESCE(t1.e1, 0)))
       AND t0.e0=2)

Запрос состоит из 2х частей: первым подзапросом считаются кол-во полов, вторым два остальных выражения, и идет их JOIN, чтобы получить все отделы, которые удовлетворяют обоим условиям.

Запросы не делались читабельными, так как не предполагается, что их будут читать (например, как Javascript сгенерированный GWT)

Кроме того, этот запрос сгенерирован на текущей статистике (я там ввел 3 отдела и 15 сотрудников). На другой статистике может получиться другой запрос, если система посчитает, что он эффективнее.
немного переформатировал запрос, чтобы стало понятнее. Но где там фильтр по employee.salary > 10000?

with cte_employee as (
	select
		t0.Main_department_Employee as k0,
		t0.Main_gender_Employee as k1,
		notZero(count(*)) as e0
	from _auto_Main_Employee as t0
	where
		t0.Main_department_Employee is not null and
		t0.Main_gender_Employee is not null
    group by
		t0.Main_department_Employee,
		t0.Main_gender_Employee
), cte_employee_count as (
	select
		t0.k0 as k0,
		notZero(count(*)) as e0
	from cte_employee as t0
	where
		notZero(coalesce(t0.e0, 0)) is not null
	group by
		t0.k0
)
select
	t0.k0 as jkey0,
	t0.k0 as jprop0
from cte_employee_count as t0
	inner join (
		select
			t1.Main_department_Employee as k0,
			notZero(sum(t1.Main_salary_Employee)) as e0,
			notZero(count(*)) as e1
		from _auto_Main_Employee as t1
			inner join cte_employee_count as t0 on
				t0.k0 = t1.Main_department_Employee
		where
			t0.e0 = 2
		group by
			t1.Main_department_Employee
	) as t1 on
		t1.k0 = t0.k0
where
	(notZero(coalesce(t1.e0, 0)) > 100000) and
	(10 > notZero(coalesce(t1.e1, 0))) and
	t0.e0 = 2
Да, я же писал, что это к исходному. Вот к с ограничением зарплаты (только я поставил 5000, а не 10000):
SELECT t0.k0 AS jkey0,
       t0.k0 AS jprop0
FROM
  (SELECT t0.k0 AS k0,
          notZero(SUM(1)) AS e0
   FROM
     (SELECT t0.Main_department_Employee AS k0,
             t0.Main_gender_Employee AS k1,
             notZero(SUM(1)) AS e0
      FROM _auto_Main_Employee t0
      WHERE (t0.Main_department_Employee IS NOT NULL
             AND t0.Main_gender_Employee IS NOT NULL)
      GROUP BY 1,
               2) t0
   WHERE notZero(COALESCE(t0.e0, 0)) IS NOT NULL
   GROUP BY 1) t0
JOIN
  (SELECT t1.Main_department_Employee AS k0,
          notZero(SUM(CASE
                          WHEN (t1.Main_salary_Employee>5000) THEN t1.Main_salary_Employee
                          ELSE NULL
                      END)) AS e0,
          notZero(SUM(1)) AS e1
   FROM _auto_Main_Employee t1
   JOIN
     (SELECT t0.k0 AS k0,
             notZero(SUM(1)) AS e0
      FROM
        (SELECT t0.Main_department_Employee AS k0,
                t0.Main_gender_Employee AS k1,
                notZero(SUM(1)) AS e0
         FROM _auto_Main_Employee t0
         WHERE (t0.Main_department_Employee IS NOT NULL
                AND t0.Main_gender_Employee IS NOT NULL)
         GROUP BY 1,
                  2) t0
      WHERE notZero(COALESCE(t0.e0, 0)) IS NOT NULL
      GROUP BY 1) t0 ON t0.k0=t1.Main_department_Employee
   WHERE t0.e0=2
   GROUP BY 1) t1 ON t1.k0=t0.k0
WHERE ((notZero(COALESCE(t1.e0, 0))>100000)
       AND (10>notZero(COALESCE(t1.e1, 0)))
       AND t0.e0=2)


Кстати, смотрю план выполнения — один прогон по Employee всего. То есть сложность O(кол-во сотрудников). Хотя там специфика данных, реально могло быть 3 прохода. Но реально сложность линейная от количества сотрудников.
ну вот, а вы говорили оптимизатор Postgres глупый. Но все равно этот запрос не эквивалентен моему. Постараюсь объяснить
1. Мой первый запрос — группировка сотрудника по департаментам -> фильтр по результатам агрегации.
2. Мой второй запрос — фильтр сотрудников -> группировка по департаментам -> фильтр по результатам агрегации.
Давайте так, тут по хорошему надо не запросами, а планами меряться. И в нашем и в вашем все будет хорошо, чудеса начнутся когда вы их материализовать захотите. Но в любом случае спор уже куда то не туда ушел.

Можно вопрос не совсем по теме, но тоже касательно sql (просто одна из следующих статей будет про сравнение с sql):

Смотрите, допустим у вас есть таблица A.
Вы к ней создаете триггер (извините не знаю какую конкретно СУБД вы используете поэтому будут писать в каком то абстрактном синтаксисе)
ON UPDATE ROW TABLE A DO
f := SELECT FROM B JOIN C ON… JOIN D… WHERE D.key = rowA.someField
IF f > 5 THEN ABORT;
То есть в триггере делаем запрос, и проверяем что какое-то значение больше 5 и отменяем транзакцию (ну или обновляем другое поле, но не суть).
А потом я делаю:
UPDATE A SET someField = someField + 1 WHERE someOtherField < 5;
И допустим таких записей обновляемых 100.
Что произойдет на физическом уровне. Реально СУБД выполнит 100 запросов, или она как-то сможет это оптимизировать? Заранее спасибо за ответ.
Давайте так, тут по хорошему надо не запросами, а планами меряться. И в нашем и в вашем все будет хорошо, чудеса начнутся когда вы их материализовать захотите. Но в любом случае спор уже куда то не туда ушел.

да я не хотел меряться. Вы просто утверждаете, что lsfusion как язык программирования более выразителен для работы с данными, чем sql, а я с этим не согласен. Поэтому хотел увидеть, как в вашем случае выглядит пример такого запроса (элементарного в sql) и как будет выглядеть его модификация при необходимости.
По поводу триггеров.

Я в основном работаю на Ms Sql и там никогда не было триггеров per row, триггеры только per statement. То есть в вашем конкретном случае в на один стейтмент один запуск триггера и в контексте триггера будут две таблицы inserted и deleted, которые будут содержать данные до и после апдейта (100 записей в каждой)
То есть для Ms Sql ответ будет — на один стейтмент один запуск триггера.

В Postgres как минимум в версии 11 уже были два варианта — можно объявить триггер per row а можно объявить триггер per statement. Соответственно, per statement работает так же, как и Ms Sql, а per row реально запустит триггер 100 раз.

Я не большой спец в Postgres поэтому не могу дать 100% гарантии что per row триггер не оптимизируется, но когда я около полугода назад это тестировал на 10000 записях, пустой per row триггер работал явно медленнее пустого per statement триггера.