İşlevsel VTYS

Veritabanları dünyası uzun süredir SQL dilini kullanan ilişkisel DBMS'lerin hakimiyetindedir. Öyle ki ortaya çıkan varyantlara NoSQL adı veriliyor. Bu pazarda kendilerine belli bir yer edinmeyi başardılar ancak ilişkisel DBMS'ler ölmeyecek ve amaçları doğrultusunda aktif olarak kullanılmaya devam edecek.

Bu yazımda fonksiyonel veritabanı kavramını anlatmak istiyorum. Daha iyi anlaşılması için bunu klasik ilişkisel modelle karşılaştırarak yapacağım. İnternette bulunan çeşitli SQL testlerinden elde edilen problemler örnek olarak kullanılacaktır.

Giriş

İlişkisel veritabanları tablolar ve alanlar üzerinde çalışır. İşlevsel bir veritabanında bunun yerine sırasıyla sınıflar ve işlevler kullanılacaktır. N tuşlu bir tablodaki alan, N parametrenin bir fonksiyonu olarak temsil edilecektir. Tablolar arasındaki ilişkiler yerine, bağlantı kurulan sınıfa ait nesneleri döndüren işlevler kullanılacaktır. JOIN yerine işlev bileşimi kullanılacaktır.

Direkt olarak görevlere geçmeden önce domain mantığının görevini anlatacağım. DDL için PostgreSQL sözdizimini kullanacağım. İşlevsel için kendi sözdizimine sahiptir.

Tablolar ve alanlar

Ad ve fiyat alanlarına sahip basit bir Sku nesnesi:

ilişkisel

CREATE TABLE Sku
(
    id bigint NOT NULL,
    name character varying(100),
    price numeric(10,5),
    CONSTRAINT id_pkey PRIMARY KEY (id)
)

İşlevsel

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

İki tane duyuruyoruz Özelliklergiriş olarak bir parametre Sku'yu alan ve ilkel bir tür döndüren.

İşlevsel bir DBMS'de her nesnenin, otomatik olarak oluşturulan ve gerektiğinde erişilebilen bazı dahili kodlara sahip olacağı varsayılmaktadır.

Ürün/mağaza/tedarikçinin fiyatını belirleyelim. Zamanla değişebilir, bu nedenle tabloya bir zaman alanı ekleyelim. Kodu kısaltmak için ilişkisel veritabanındaki dizinlere ilişkin tabloları bildirmeyi atlayacağım:

ilişkisel

CREATE TABLE prices
(
    skuId bigint NOT NULL,
    storeId bigint NOT NULL,
    supplierId bigint NOT NULL,
    dateTime timestamp without time zone,
    price numeric(10,5),
    CONSTRAINT prices_pkey PRIMARY KEY (skuId, storeId, supplierId)
)

İşlevsel

CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);

Endeksler

Son örnekte, belirli bir zamanın fiyatını hızlı bir şekilde bulabilmemiz için tüm anahtarlar ve tarih üzerine bir endeks oluşturacağız.

ilişkisel

CREATE INDEX prices_date
    ON prices
    (skuId, storeId, supplierId, dateTime)

İşlevsel

INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);

görevler

İlgili kaynaklardan alınan nispeten basit problemlerle başlayalım. makaleler Habr'da.

Öncelikle domain mantığını deklare edelim (ilişkisel veritabanı için bu direkt olarak yukarıdaki makalede yapılıyor).

CLASS Department;
name = DATA STRING[100] (Department);

CLASS Employee;
department = DATA Department (Employee);
chief = DATA Employee (Employee);
name = DATA STRING[100] (Employee);
salary = DATA NUMERIC[14,2] (Employee);

Görev 1.1

Bir önceki amirininkinden daha fazla maaş alan çalışanların listesini görüntüleyin.

ilişkisel

select a.*
from   employee a, employee b
where  b.id = a.chief_id
and    a.salary > b.salary

İşlevsel

SELECT name(Employee a) WHERE salary(a) > salary(chief(a));

Görev 1.2

Kendi departmanında en fazla maaş alan çalışanları listeleyin

ilişkisel

select a.*
from   employee a
where  a.salary = ( select max(salary) from employee b
                    where  b.department_id = a.department_id )

İşlevsel

maxSalary 'Максимальная зарплата' (Department s) = 
    GROUP MAX salary(Employee e) IF department(e) = s;
SELECT name(Employee a) WHERE salary(a) = maxSalary(department(a));

// или если "заинлайнить"
SELECT name(Employee a) WHERE 
    salary(a) = maxSalary(GROUP MAX salary(Employee e) IF department(e) = department(a));

Her iki uygulama da eşdeğerdir. İlk durumda, ilişkisel bir veritabanında, aynı şekilde içindeki belirli bir departman için maksimum maaşı ilk önce hesaplayacak olan CREATE VIEW'ı kullanabilirsiniz. Aşağıda, netlik sağlamak amacıyla, çözümü daha iyi yansıttığı için ilk durumu kullanacağım.

Görev 1.3

Çalışan sayısı 3 kişiyi aşmayan departman kimliklerinin bir listesini görüntüleyin.

ilişkisel

select department_id
from   employee
group  by department_id
having count(*) <= 3

İşlevsel

countEmployees 'Количество сотрудников' (Department d) = 
    GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;

Görev 1.4

Aynı departmanda görev yapan belirlenmiş bir yöneticisi olmayan çalışanların listesini görüntüleyin.

ilişkisel

select a.*
from   employee a
left   join employee b on (b.id = a.chief_id and b.department_id = a.department_id)
where  b.id is null

İşlevsel

SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));

Görev 1.5

Maksimum toplam çalışan maaşına sahip departman kimliklerinin listesini bulun.

ilişkisel

with sum_salary as
  ( select department_id, sum(salary) salary
    from   employee
    group  by department_id )
select department_id
from   sum_salary a       
where  a.salary = ( select max(salary) from sum_salary )

İşlevsel

salarySum 'Максимальная зарплата' (Department d) = 
    GROUP SUM salary(Employee e) IF department(e) = d;
maxSalarySum 'Максимальная зарплата отделов' () = 
    GROUP MAX salarySum(Department d);
SELECT Department d WHERE salarySum(d) = maxSalarySum();

Başka birinden daha karmaşık görevlere geçelim makaleler. Bu görevin MS SQL'de nasıl uygulanacağına ilişkin ayrıntılı bir analiz içerir.

Görev 2.1

1997 yılında hangi satıcılar 30 No'lu üründen 1 adetten fazla sattı?

Etki alanı mantığı (RDBMS'de daha önce olduğu gibi bildirimi atlıyoruz):

CLASS Employee 'Продавец';
lastName 'Фамилия' = DATA STRING[100] (Employee);

CLASS Product 'Продукт';
id = DATA INTEGER (Product);
name = DATA STRING[100] (Product);

CLASS Order 'Заказ';
date = DATA DATE (Order);
employee = DATA Employee (Order);

CLASS Detail 'Строка заказа';

order = DATA Order (Detail);
product = DATA Product (Detail);
quantity = DATA NUMERIC[10,5] (Detail);

ilişkisel

select LastName
from Employees as e
where (
  select sum(od.Quantity)
  from [Order Details] as od
  where od.ProductID = 1 and od.OrderID in (
    select o.OrderID
    from Orders as o
    where year(o.OrderDate) = 1997 and e.EmployeeID = o.EmployeeID)
) > 30

İşlevsel

sold (Employee e, INTEGER productId, INTEGER year) = 
    GROUP SUM quantity(OrderDetail d) IF 
        employee(order(d)) = e AND 
        id(product(d)) = productId AND 
        extractYear(date(order(d))) = year;
SELECT lastName(Employee e) WHERE sold(e, 1, 1997) > 30;

Görev 2.2

Her bir alıcı için (adı, soyadı), alıcının 1997 yılında en çok para harcadığı iki ürünü (isim) bulun.

Etki alanı mantığını önceki örnekten genişletiyoruz:

CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);

customer = DATA Customer (Order);

unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);

ilişkisel

SELECT ContactName, ProductName FROM (
SELECT c.ContactName, p.ProductName
, ROW_NUMBER() OVER (
    PARTITION BY c.ContactName
    ORDER BY SUM(od.Quantity * od.UnitPrice * (1 - od.Discount)) DESC
) AS RatingByAmt
FROM Customers c
JOIN Orders o ON o.CustomerID = c.CustomerID
JOIN [Order Details] od ON od.OrderID = o.OrderID
JOIN Products p ON p.ProductID = od.ProductID
WHERE YEAR(o.OrderDate) = 1997
GROUP BY c.ContactName, p.ProductName
) t
WHERE RatingByAmt < 3

İşlevsel

sum (Detail d) = quantity(d) * unitPrice(d) * (1 - discount(d));
bought 'Купил' (Customer c, Product p, INTEGER y) = 
    GROUP SUM sum(Detail d) IF 
        customer(order(d)) = c AND 
        product(d) = p AND 
        extractYear(date(order(d))) = y;
rating 'Рейтинг' (Customer c, Product p, INTEGER y) = 
    PARTITION SUM 1 ORDER DESC bought(c, p, y), p BY c, y;
SELECT contactName(Customer c), name(Product p) WHERE rating(c, p, 1997) < 3;

PARTITION operatörü şu prensibe göre çalışır: SUM'dan (burada 1) sonra belirtilen ifadeyi belirtilen gruplar içinde (burada Müşteri ve Yıl, ancak herhangi bir ifade olabilir) toplar ve gruplar içinde ORDER'de belirtilen ifadelere göre sıralayarak (burada XNUMX) burada satın alındı ​​​​ve eğer eşitse, o zaman dahili ürün koduna göre).

Görev 2.3

Mevcut siparişleri yerine getirmek için tedarikçilerden kaç ürün sipariş edilmesi gerekiyor?

Etki alanı mantığını tekrar genişletelim:

CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);

supplier = DATA Supplier (Product);

unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);

ilişkisel

select s.CompanyName, p.ProductName, sum(od.Quantity) + p.ReorderLevel — p.UnitsInStock as ToOrder
from Orders o
join [Order Details] od on o.OrderID = od.OrderID
join Products p on od.ProductID = p.ProductID
join Suppliers s on p.SupplierID = s.SupplierID
where o.ShippedDate is null
group by s.CompanyName, p.ProductName, p.UnitsInStock, p.ReorderLevel
having p.UnitsInStock < sum(od.Quantity) + p.ReorderLevel

İşlevsel

orderedNotShipped 'Заказано, но не отгружено' (Product p) = 
    GROUP SUM quantity(OrderDetail d) IF product(d) = p;
toOrder 'К заказу' (Product p) = orderedNotShipped(p) + reorderLevel(p) - unitsInStock(p);
SELECT companyName(supplier(Product p)), name(p), toOrder(p) WHERE toOrder(p) > 0;

Yıldız işaretiyle ilgili sorun

Ve son örnek bizzat benden. Sosyal ağın mantığı var. İnsanlar birbirleriyle arkadaş olabilir ve birbirlerinden hoşlanabilirler. İşlevsel bir veritabanı perspektifinden bakıldığında şöyle görünecektir:

CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);

Arkadaşlık için olası adayları bulmak gerekir. Daha resmi olarak, A, B, C'nin tüm insanlarını bulmanız gerekir; öyle ki A, B ile arkadaştır ve B, C ile arkadaştır, A, C'yi sever, ancak A, C ile arkadaş değildir.
İşlevsel bir veritabanı perspektifinden bakıldığında sorgu şöyle görünecektir:

SELECT Person a, Person b, Person c WHERE 
    likes(a, c) AND NOT friends(a, c) AND 
    friends(a, b) AND friends(b, c);

Okuyucunun bu sorunu SQL'de kendi başına çözmesi teşvik edilir. Sevdiğiniz insanlardan çok daha az arkadaşınız olduğu varsayılır. Bu nedenle ayrı tablolarda yer almaktadırlar. Başarılı olursa iki yıldızlı bir görev de vardır. Bunda arkadaşlık simetrik değildir. İşlevsel bir veritabanında şöyle görünecektir:

SELECT 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));

GÜNCELLEME: birinci ve ikinci yıldız işaretiyle sorunun çözümü dss_kalika:

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 

;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 

Sonuç

Verilen dil sözdiziminin, verilen konsepti uygulama seçeneklerinden yalnızca biri olduğuna dikkat edilmelidir. SQL temel alındı ​​ve amaç ona mümkün olduğunca benzer olmasıydı. Elbette bazıları anahtar kelime adlarını, kelime kayıtlarını vb. beğenmeyebilir. Burada asıl önemli olan konseptin kendisidir. İstenirse hem C++ hem de Python'un söz dizimini benzer hale getirebilirsiniz.

Açıklanan veritabanı konsepti bence aşağıdaki avantajlara sahiptir:

  • Kolaylaştırmak. Bu, basit durumlarda açık olmayan nispeten subjektif bir göstergedir. Ancak daha karmaşık durumlara bakarsanız (örneğin, yıldız işaretli sorunlar), o zaman bence bu tür sorgular yazmak çok daha kolaydır.
  • Инкапсуляция. Bazı örneklerde ara işlevler bildirdim (örneğin, satılan, aldım vb.), sonraki işlevlerin oluşturulduğu yer. Bu, gerekirse belirli işlevlerin mantığını, onlara bağlı olanların mantığını değiştirmeden değiştirmenize olanak tanır. Örneğin satış yapabilirsiniz. satılan tamamen farklı nesnelerden hesaplandı, geri kalan mantık ise değişmeyecek. Evet, bu, CREATE VIEW kullanılarak bir RDBMS'de uygulanabilir. Ancak mantığın tamamı bu şekilde yazılırsa pek okunaklı görünmeyecektir.
  • Anlamsal boşluk yok. Böyle bir veritabanı (tablolar ve alanlar yerine) işlevler ve sınıflar üzerinde çalışır. Tıpkı klasik programlamada olduğu gibi (bir metodun ilk parametresi ait olduğu sınıfın formunda olan bir fonksiyon olduğunu varsayarsak). Buna göre evrensel programlama dilleriyle “arkadaş olmak” çok daha kolay olmalı. Ayrıca bu konsept çok daha karmaşık işlevlerin uygulanmasına olanak tanır. Örneğin, aşağıdaki gibi operatörleri gömebilirsiniz:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE  'Что-то Петя продает слишком много одного товара в 2019 году';

  • Kalıtım ve polimorfizm. İşlevsel bir veritabanında, CLASS ClassP: Class1, Class2 yapıları aracılığıyla çoklu kalıtımı tanıtabilir ve çoklu polimorfizmi uygulayabilirsiniz. Muhtemelen gelecekteki makalelerde tam olarak nasıl yazacağım.

Bu sadece bir kavram olmasına rağmen, Java'da tüm işlevsel mantığı ilişkisel mantığa çeviren bazı uygulamalara sahibiz. Ayrıca, temsillerin mantığı ve diğer birçok şey buna çok güzel bir şekilde eklenmiştir, bu sayede bir bütün elde ederiz. platform. Temel olarak RDBMS'yi (şimdilik yalnızca PostgreSQL) bir "sanal makine" olarak kullanıyoruz. RDBMS sorgu iyileştiricisi FDBMS'nin bildiği belirli istatistikleri bilmediğinden bazen bu çeviriyle ilgili sorunlar ortaya çıkar. Teorik olarak, belirli bir yapıyı depolama olarak kullanacak, özellikle işlevsel mantık için uyarlanmış bir veritabanı yönetim sisteminin uygulanması mümkündür.

Kaynak: habr.com

Yorum ekle