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
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
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
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
Sumber: www.habr.com