DBMS fungsional

Dunia database telah lama didominasi oleh DBMS relasional yang menggunakan bahasa SQL. Sedemikian rupa sehingga varian yang muncul disebut NoSQL. Mereka berhasil mendapatkan tempat tertentu di pasar ini, tetapi DBMS relasional tidak akan mati, dan terus digunakan secara aktif untuk tujuan mereka.

Pada artikel ini saya ingin menjelaskan konsep database fungsional. Untuk pemahaman yang lebih baik, saya akan melakukannya dengan membandingkannya dengan model relasional klasik. Masalah dari berbagai tes SQL yang ditemukan di Internet akan digunakan sebagai contoh.

pengenalan

Basis data relasional beroperasi pada tabel dan bidang. Dalam database fungsional, kelas dan fungsi masing-masing akan digunakan. Bidang dalam tabel dengan N kunci akan direpresentasikan sebagai fungsi dari N parameter. Alih-alih hubungan antar tabel, fungsi akan digunakan yang mengembalikan objek kelas tempat koneksi dibuat. Komposisi fungsi akan digunakan sebagai pengganti GABUNG.

Sebelum langsung ke tugas, saya akan menjelaskan tugas logika domain. Untuk DDL saya akan menggunakan sintaks PostgreSQL. Untuk fungsional, ia memiliki sintaksisnya sendiri.

Tabel dan bidang

Objek Sku sederhana dengan kolom nama dan harga:

Relasional

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

Fungsional

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

Kami mengumumkan dua fungsi, yang mengambil satu parameter Sku sebagai masukan dan mengembalikan tipe primitif.

Diasumsikan bahwa dalam DBMS fungsional, setiap objek akan memiliki beberapa kode internal yang dihasilkan secara otomatis dan dapat diakses jika diperlukan.

Mari kita tetapkan harga produk/toko/pemasok. Ini mungkin berubah seiring waktu, jadi mari tambahkan kolom waktu ke tabel. Saya akan melewatkan mendeklarasikan tabel untuk direktori dalam database relasional untuk mempersingkat kode:

Relasional

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

Fungsional

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

Indeks

Untuk contoh terakhir, kita akan membuat indeks pada semua kunci dan tanggal sehingga kita dapat dengan cepat menemukan harga untuk waktu tertentu.

Relasional

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

Fungsional

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

tugas

Mari kita mulai dengan masalah yang relatif sederhana yang diambil dari masalah terkait Artikel di Habr.

Pertama, mari kita deklarasikan logika domainnya (untuk database relasional, hal ini dilakukan langsung pada artikel di atas).

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

Tantangan 1.1

Menampilkan daftar karyawan yang menerima gaji lebih besar dari atasan langsungnya.

Relasional

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

Fungsional

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

Tantangan 1.2

Buat daftar karyawan yang menerima gaji maksimum di departemennya

Relasional

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

Fungsional

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

Kedua implementasi tersebut setara. Untuk kasus pertama, dalam database relasional Anda dapat menggunakan CREATE VIEW, yang dengan cara yang sama akan menghitung gaji maksimum untuk departemen tertentu di dalamnya terlebih dahulu. Berikut ini, untuk kejelasan, saya akan menggunakan kasus pertama, karena kasus ini lebih mencerminkan solusi.

Tantangan 1.3

Menampilkan daftar ID departemen yang jumlah karyawannya tidak melebihi 3 orang.

Relasional

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

Fungsional

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

Tantangan 1.4

Menampilkan daftar karyawan yang tidak memiliki manajer yang ditunjuk yang bekerja di departemen yang sama.

Relasional

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

Fungsional

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

Tantangan 1.5

Temukan daftar ID departemen dengan total gaji karyawan maksimum.

Relasional

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 )

Fungsional

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

Mari beralih ke tugas yang lebih kompleks dari tugas lain Artikel. Ini berisi analisis rinci tentang bagaimana mengimplementasikan tugas ini di MS SQL.

Tantangan 2.1

Penjual manakah yang menjual lebih dari 1997 unit produk No. 30 pada tahun 1?

Logika domain (seperti sebelumnya di RDBMS kami melewatkan deklarasi):

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

Relasional

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

Fungsional

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;

Tantangan 2.2

Untuk setiap pembeli (nama, nama keluarga), temukan dua barang (nama) yang pembelinya menghabiskan uang paling banyak pada tahun 1997.

Kami memperluas logika domain dari contoh sebelumnya:

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

customer = DATA Customer (Order);

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

Relasional

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

Fungsional

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;

Operator PARTITION bekerja berdasarkan prinsip berikut: ia menjumlahkan ekspresi yang ditentukan setelah SUM (di sini 1), dalam grup yang ditentukan (di sini Pelanggan dan Tahun, tetapi bisa berupa ekspresi apa pun), mengurutkan dalam grup berdasarkan ekspresi yang ditentukan dalam ORDER ( di sini dibeli, dan jika sama, maka sesuai dengan kode produk internal).

Tantangan 2.3

Berapa banyak barang yang perlu dipesan dari pemasok untuk memenuhi pesanan saat ini.

Mari kita perluas lagi logika domainnya:

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

supplier = DATA Supplier (Product);

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

Relasional

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

Fungsional

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;

Masalah dengan tanda bintang

Dan contoh terakhir adalah dari saya pribadi. Ada logika jaringan sosial. Orang bisa berteman satu sama lain dan menyukai satu sama lain. Dari perspektif database fungsional akan terlihat seperti ini:

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

Penting untuk menemukan kandidat yang memungkinkan untuk persahabatan. Lebih formalnya, Anda perlu mencari semua orang A, B, C sedemikian rupa sehingga A berteman dengan B, dan B berteman dengan C, A menyukai C, tetapi A tidak berteman dengan C.
Dari perspektif database fungsional, kuerinya akan terlihat seperti ini:

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

Pembaca didorong untuk menyelesaikan sendiri masalah ini di SQL. Diasumsikan bahwa jumlah teman jauh lebih sedikit daripada orang yang Anda sukai. Oleh karena itu mereka berada di tabel terpisah. Jika berhasil, ada juga tugas dengan dua bintang. Di dalamnya, persahabatan tidak simetris. Pada database fungsional akan terlihat seperti ini:

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

UPD: solusi masalah tanda bintang pertama dan kedua dari 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 

Kesimpulan

Perlu dicatat bahwa sintaksis bahasa yang diberikan hanyalah salah satu opsi untuk mengimplementasikan konsep yang diberikan. SQL diambil sebagai dasar, dan tujuannya adalah agar SQL menjadi semirip mungkin dengannya. Tentu saja, beberapa orang mungkin tidak menyukai nama kata kunci, register kata, dll. Hal utama di sini adalah konsep itu sendiri. Jika diinginkan, Anda dapat membuat sintaks C++ dan Python serupa.

Konsep database yang dijelaskan menurut saya memiliki keunggulan sebagai berikut:

  • Kesederhanaan. Ini adalah indikator yang relatif subyektif yang tidak terlihat jelas dalam kasus-kasus sederhana. Tetapi jika Anda melihat kasus yang lebih kompleks (misalnya, masalah dengan tanda bintang), menurut saya, menulis pertanyaan seperti itu jauh lebih mudah.
  • Инкапсуляция. Dalam beberapa contoh saya mendeklarasikan fungsi perantara (misalnya, terjual, membeli dll.), dari mana fungsi-fungsi selanjutnya dibangun. Hal ini memungkinkan Anda untuk mengubah logika fungsi tertentu, jika perlu, tanpa mengubah logika fungsi yang bergantung padanya. Misalnya, Anda bisa melakukan penjualan terjual dihitung dari objek yang sama sekali berbeda, sedangkan logika lainnya tidak akan berubah. Ya, ini bisa diimplementasikan di RDBMS menggunakan CREATE VIEW. Tetapi jika seluruh logika ditulis seperti ini, maka tidak akan terlihat mudah dibaca.
  • Tidak ada kesenjangan semantik. Basis data seperti itu beroperasi berdasarkan fungsi dan kelas (bukan tabel dan bidang). Sama seperti dalam pemrograman klasik (jika kita berasumsi bahwa suatu metode adalah suatu fungsi dengan parameter pertama berupa kelas yang dimilikinya). Oleh karena itu, akan lebih mudah untuk “berteman” dengan bahasa pemrograman universal. Selain itu, konsep ini memungkinkan penerapan fungsionalitas yang jauh lebih kompleks. Misalnya, Anda dapat menyematkan operator seperti:

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

  • Warisan dan polimorfisme. Dalam database fungsional, Anda dapat memperkenalkan banyak warisan melalui konstruksi CLASS ClassP: Class1, Class2 dan mengimplementasikan banyak polimorfisme. Saya mungkin akan menulis bagaimana tepatnya di artikel mendatang.

Meskipun ini hanya sebuah konsep, kami sudah memiliki beberapa implementasi di Java yang menerjemahkan semua logika fungsional ke dalam logika relasional. Ditambah lagi, logika representasi dan banyak hal lainnya melekat dengan indah padanya, berkat itu kita mendapatkan keseluruhannya peron. Pada dasarnya, kami menggunakan RDBMS (saat ini hanya PostgreSQL) sebagai "mesin virtual". Masalah terkadang muncul dengan terjemahan ini karena pengoptimal kueri RDBMS tidak mengetahui statistik tertentu yang diketahui oleh FDBMS. Secara teori, dimungkinkan untuk mengimplementasikan sistem manajemen basis data yang akan menggunakan struktur tertentu sebagai penyimpanan, yang disesuaikan secara khusus untuk logika fungsional.

Sumber: www.habr.com

Tambah komentar