لطالما تم الاستيلاء على عالم قاعدة البيانات بواسطة أنظمة DBMS العلائقية التي تستخدم لغة SQL. لدرجة أن الأنواع الناشئة تسمى NoSQL. لقد تمكنوا من الفوز بمكان معين لأنفسهم في هذا السوق ، لكن نظم إدارة قواعد البيانات العلائقية لن تموت ، وسيستمر استخدامها بنشاط لأغراضهم الخاصة.
في هذه المقالة ، أريد أن أصف مفهوم قاعدة البيانات الوظيفية. لفهم أفضل ، سأفعل ذلك من خلال المقارنة مع النموذج العلائقي الكلاسيكي. كأمثلة ، سيتم استخدام المهام من اختبارات SQL المختلفة الموجودة على الإنترنت.
مقدمة
تعمل قواعد البيانات العلائقية على الجداول والحقول. في قاعدة البيانات الوظيفية ، سيتم استخدام الفئات والوظائف بدلاً من ذلك ، على التوالي. سيتم تمثيل حقل في جدول يحتوي على مفاتيح N كدالة لمعلمات N. بدلاً من الروابط بين الجداول ، سيتم استخدام الوظائف التي تعيد كائنات الفئة التي ينتقل إليها الرابط. سيتم استخدام تكوين الوظيفة بدلاً من JOIN.
قبل الشروع مباشرة في المهام ، سوف أصف مهمة منطق المجال. بالنسبة إلى DDL ، سأستخدم بناء جملة PostgreSQL. من أجل بناء الجملة الوظيفي الخاص بها.
الجداول والحقول
كائن Sku بسيط مع حقلي الاسم والسعر:
العلائقية
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
وظيفي
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
نعلن اثنين ملامح، والتي تأخذ معلمة Sku واحدة كمدخلات وتعيد نوعًا أوليًا.
من المفترض أنه في نظام DBMS وظيفي ، سيكون لكل كائن بعض التعليمات البرمجية الداخلية التي يتم إنشاؤها تلقائيًا ويمكن الوصول إليها إذا لزم الأمر.
دعنا نحدد سعر المنتج / المتجر / المورد. يمكن أن يتغير بمرور الوقت ، لذلك دعونا نضيف حقلًا زمنيًا إلى الجدول. سأتخطى إعلان جداول الدلائل في قاعدة بيانات علائقية لتقصير الكود:
العلائقية
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)
)
وظيفي
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
مؤشرات
في المثال الأخير ، لنقم ببناء فهرس لجميع المفاتيح والتاريخ حتى نتمكن من العثور بسرعة على السعر لفترة معينة.
العلائقية
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
وظيفي
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
المهام
لنبدأ بمسائل بسيطة نسبيًا مأخوذة من المقابلة
أولاً ، دعنا نعلن عن منطق المجال (بالنسبة لقاعدة البيانات العلائقية ، يتم ذلك مباشرةً في المقالة أعلاه).
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);
تحدي 1.1
اعرض قائمة بالموظفين الذين يتقاضون أجوراً أكبر من تلك الخاصة بالمشرف المباشر.
العلائقية
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
وظيفي
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
تحدي 1.2
اعرض قائمة بالموظفين الحاصلين على أعلى راتب في إدارتهم
العلائقية
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
وظيفي
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));
كلا التطبيقين متكافئان. بالنسبة للحالة الأولى في قاعدة البيانات العلائقية ، يمكنك استخدام CREATE VIEW ، والتي بنفس الطريقة ستحسب أولاً الحد الأقصى للراتب لقسم معين فيه. في المستقبل ، من أجل الوضوح ، سأستخدم الحالة الأولى ، لأنها تعكس الحل بشكل أفضل.
تحدي 1.3
عرض قائمة معرفات القسم ، وعدد الموظفين فيها لا يتجاوز 3 أشخاص.
العلائقية
select department_id
from employee
group by department_id
having count(*) <= 3
وظيفي
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
تحدي 1.4
اعرض قائمة بالموظفين الذين ليس لديهم مدير معين يعمل في نفس القسم.
العلائقية
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
وظيفي
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
تحدي 1.5
ابحث عن قائمة معرفات الأقسام ذات الحد الأقصى لإجمالي الراتب للموظفين.
العلائقية
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 )
وظيفي
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();
دعنا ننتقل إلى مهام أكثر تعقيدًا من أخرى
تحدي 2.1
ما البائعين الذين باعوا أكثر من 1997 قطعة من الصنف رقم 30 في عام 1؟
منطق المجال (كما في السابق ، نتخطى الإعلان عن RDBMS):
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);
العلائقية
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
وظيفي
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;
تحدي 2.2
لكل عميل (الاسم الأول ، الاسم الأخير) ، ابحث عن العنصرين (الاسم) اللذين أنفق عليهما العميل أكبر قدر من المال في عام 1997.
تمديد منطق المجال من المثال السابق:
CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
العلائقية
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
وظيفي
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 وفقًا للمبدأ التالي: يقوم بجمع التعبير المحدد بعد SUM (هنا 1) ضمن المجموعات المحددة (هنا العميل والسنة ، ولكن يمكن أن يكون أي تعبير) ، والفرز داخل المجموعات وفقًا للتعبيرات المحددة في ORDER ( تم شراؤها هنا ، وإذا كانت متساوية ، فحينئذٍ من خلال رمز المنتج الداخلي).
تحدي 2.3
كم عدد السلع التي يجب طلبها من الموردين للوفاء بالطلبات الحالية.
دعنا نوسع منطق المجال مرة أخرى:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
العلائقية
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
وظيفي
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;
مهمة بعلامة النجمة
والمثال الأخير مني شخصيًا. هناك منطق الشبكة الاجتماعية. يمكن للناس أن يكونوا أصدقاء مع بعضهم البعض وأن يحبوا بعضهم البعض. من منظور قاعدة البيانات الوظيفية ، سيبدو هذا كما يلي:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
من الضروري إيجاد مرشحين محتملين للصداقة. بشكل أكثر رسمية ، تحتاج إلى العثور على جميع الأشخاص A و B و C بحيث يكون A صديقًا لـ B و B صديق C و A يحب C ولكن A ليس صديقًا لـ C.
من وجهة نظر قاعدة بيانات وظيفية ، سيبدو الاستعلام كما يلي:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
القارئ مدعو لحل هذه المشكلة بشكل مستقل في SQL. من المفترض أن يكون عدد الأصدقاء أقل بكثير من أولئك الذين يحبونه. لذلك ، هم في جداول منفصلة. في حالة الحل الناجح ، توجد أيضًا مشكلة في علامتين نجميتين. صداقتها ليست متكافئة. في قاعدة البيانات الوظيفية ، ستبدو كما يلي:
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));
محدث: حل المشكلة بعلامة النجمة الأولى والثانية من
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
اختتام
تجدر الإشارة إلى أن بناء الجملة أعلاه للغة هو مجرد أحد الخيارات لتنفيذ المفهوم أعلاه. لقد تم اعتبار SQL كأساس ، وكان الهدف هو جعله مشابهًا له قدر الإمكان. بالطبع ، قد لا يحب شخص ما أسماء الكلمات الرئيسية وحالة الكلمات وما إلى ذلك. الشيء الرئيسي هنا هو المفهوم نفسه. إذا رغبت في ذلك ، يمكنك إنشاء بنية متشابهة لـ C ++ و Python.
مفهوم قاعدة البيانات الموصوفة ، في رأيي ، له المزايا التالية:
- سهولة. هذا مؤشر شخصي نسبيًا وليس واضحًا في الحالات البسيطة. ولكن إذا نظرت إلى الحالات الأكثر تعقيدًا (على سبيل المثال ، المهام ذات العلامات النجمية) ، ففي رأيي ، فإن كتابة مثل هذه الاستفسارات أسهل بكثير.
- Инкапсуляция. في بعض الأمثلة ، أعلنت عن وظائف وسيطة (على سبيل المثال ، باعت, اشترى إلخ) ، والتي بنيت منها الوظائف اللاحقة. يسمح لك هذا بتغيير منطق وظائف معينة ، إذا لزم الأمر ، دون تغيير منطق تلك التي تعتمد عليها. على سبيل المثال ، يمكنك إجراء مبيعات باعت تم حسابها من كائنات مختلفة تمامًا ، بينما لن يتغير المنطق المتبقي. نعم ، في RDBMS يمكن القيام بذلك من خلال إنشاء عرض. ولكن إذا كتبت كل المنطق بهذه الطريقة ، فلن يبدو مقروءًا جدًا.
- لا فجوة دلالية. تعمل قاعدة البيانات هذه مع الوظائف والفئات (بدلاً من الجداول والحقول). بنفس الطريقة كما هو الحال في البرمجة الكلاسيكية (بافتراض أن الطريقة هي وظيفة ذات المعلمة الأولى في شكل فئة تنتمي إليها). وفقًا لذلك ، يجب أن يكون من الأسهل بكثير "تكوين صداقات" مع لغات البرمجة العالمية. بالإضافة إلى ذلك ، يتيح لك هذا المفهوم تنفيذ وظائف أكثر تعقيدًا. على سبيل المثال ، يمكنك تضمين عبارات مثل هذه في قاعدة البيانات:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- الوراثة وتعدد الأشكال. في قاعدة بيانات وظيفية ، يمكنك تقديم وراثة متعددة من خلال CLASS ClassP: Class1 ، Class2 بنيات وتنفيذ تعدد الأشكال. كيف بالضبط ، ربما سأكتب في المقالات التالية.
على الرغم من أن هذا مجرد مفهوم ، فلدينا بالفعل بعض التنفيذ في Java الذي يترجم كل المنطق الوظيفي إلى منطق علائقي. بالإضافة إلى ذلك ، فإن منطق التمثيلات والكثير من الأشياء الأخرى مشدود بشكل جميل إليه ، وبفضل ذلك حصلنا على
المصدر: www.habr.com