Student: Estera
Anul universitar: 2024-2025
Facultatea de Științe Economice
Universitatea "Lucian Blaga" din Sibiu
- Introducere și Tema Aleasă
- Crearea Bazei de Date și Tabelelor
- Crearea Relațiilor între Tabele
- Interogări SQL
- Vederi
- Структuri de Control
- Proceduri Stocate
- Cursoare SQL
- Triggere
- Testare și Validare
- Concluzii
Tema aleasă pentru acest proiect este "Magazin Online" - un sistem de management pentru un magazin de comerț electronic.
Sistemul dezvoltat permite gestionarea:
- Clienților și profilurilor lor
- Produselor și categoriilor
- Comenzilor și detaliilor acestora
- Plăților și metodelor de plată
- Furnizorilor și relațiilor cu aceștia
- Rapoartelor și statisticilor de vânzări
Baza de date conține 8 tabele principale și 2 tabele suplimentare pentru logging și raportare, respectând toate cerințele proiectului.
Prima etapă a constat în crearea bazei de date utilizând comanda DDL:
CREATE DATABASE MagazinOnline;
GO
USE MagazinOnline;
GO
Captură ecran: Se afișează Object Browser cu baza de date "MagazinOnline" creată
CREATE TABLE Clienti (
ID_Client INT PRIMARY KEY IDENTITY(1,1),
Nume NVARCHAR(100) NOT NULL,
Email NVARCHAR(100) UNIQUE NOT NULL,
Data_Inscriere DATE DEFAULT GETDATE(),
Tip_Client NVARCHAR(50) DEFAULT 'Standard',
Telefon VARCHAR(20),
Data_Nasterii DATE,
Activ BIT DEFAULT 1,
Scor_Fidelitate FLOAT DEFAULT 0.0,
Adresa NVARCHAR(255)
);
Tipuri de date utilizate în tabelul Clienti:
INT
- pentru cheia primarăNVARCHAR
- pentru textul UnicodeVARCHAR
- pentru telefonDATE
- pentru date calendaristiceBIT
- pentru valori booleanFLOAT
- pentru numere cu virgulă mobilă
Proprietăți utilizate:
PRIMARY KEY
- definește cheia primarăIDENTITY(1,1)
- numerotare automatăNOT NULL
- câmp obligatoriuUNIQUE
- valoare unică în tabelDEFAULT
- valoare implicită
CREATE TABLE Produse (
ID_Produs INT PRIMARY KEY IDENTITY(1,1),
Denumire NVARCHAR(100) NOT NULL,
Descriere NTEXT,
Pret DECIMAL(10,2) NOT NULL CHECK (Pret > 0),
Stoc INT DEFAULT 0 CHECK (Stoc >= 0),
Categorie NVARCHAR(50) NOT NULL,
Data_Adaugare DATETIME DEFAULT GETDATE(),
Greutate REAL,
Imagine VARBINARY(MAX),
Cod_Produs CHAR(10) UNIQUE
);
Tipuri de date suplimentare:
NTEXT
- pentru text lung UnicodeDECIMAL(10,2)
- pentru prețuri cu 2 zecimaleDATETIME
- pentru data și oraREAL
- pentru numere cu precizie simplăVARBINARY(MAX)
- pentru date binare (imagini)CHAR(10)
- pentru șiruri de lungime fixă
Proprietăți suplimentare:
CHECK
- constrângeri de validare
Tabelele Comenzi, Detalii_Comanda, Plati, Furnizori, Produse_Furnizori și Profile_Clienti au fost create urmând aceleași principii, utilizând diverse tipuri de date și proprietăți SQL Server.
Captură ecran: Object Browser arătând toate tabelele create în secțiunea Tables
Relația Clienti → Comenzi:
FOREIGN KEY (ID_Client) REFERENCES Clienti(ID_Client)
Relația Comenzi → Detalii_Comanda:
FOREIGN KEY (ID_Comanda) REFERENCES Comenzi(ID_Comanda)
Relația Produse → Detalii_Comanda:
FOREIGN KEY (ID_Produs) REFERENCES Produse(ID_Produs)
Relația Clienti → Profile_Clienti:
CREATE TABLE Profile_Clienti (
ID_Client INT PRIMARY KEY,
FOREIGN KEY (ID_Client) REFERENCES Clienti(ID_Client)
);
Tabelul de legătură Produse_Furnizori:
CREATE TABLE Produse_Furnizori (
ID_Produs INT,
ID_Furnizor INT,
PRIMARY KEY (ID_Produs, ID_Furnizor),
FOREIGN KEY (ID_Produs) REFERENCES Produse(ID_Produs),
FOREIGN KEY (ID_Furnizor) REFERENCES Furnizori(ID_Furnizor)
);
Captură ecran: Database Design view arătând relațiile vizuale între tabele
- Integritatea referențială - asigură consistența datelor
- Normalizarea datelor - elimină redundanța
- Eficiența interogărilor - permite JOIN-uri eficiente
Au fost create 25 de interogări care respectă toate cerințele proiectului:
SELECT Nume, Email, Tip_Client, Scor_Fidelitate
FROM Clienti
WHERE Scor_Fidelitate > 100
ORDER BY Scor_Fidelitate DESC;
Captură ecran: Rezultatul interogării arătând clienții cu scor mare de fidelitate
SELECT c.Nume, COUNT(co.ID_Comanda) as Numar_Comenzi, SUM(co.Total) as Total_Cumparaturi
FROM Clienti c
LEFT JOIN Comenzi co ON c.ID_Client = co.ID_Client
GROUP BY c.ID_Client, c.Nume
HAVING COUNT(co.ID_Comanda) > 0
ORDER BY Total_Cumparaturi DESC;
Elemente utilizate:
LEFT JOIN
- pentru a include toți cliențiiGROUP BY
- pentru gruparea rezultatelorHAVING
- filtrarea după grupareCOUNT()
,SUM()
- funcții de agregare
SELECT Nume, Email, Tip_Client
FROM Clienti
WHERE ID_Client IN (
SELECT ID_Client
FROM Comenzi
WHERE Total > (SELECT AVG(Total) FROM Comenzi)
);
SELECT c.Nume, c.Email,
(SELECT COUNT(*)
FROM Comenzi co
WHERE co.ID_Client = c.ID_Client AND co.Status = 'Livrata') as Comenzi_Livrate
FROM Clienti c
WHERE EXISTS (
SELECT 1
FROM Comenzi co2
WHERE co2.ID_Client = c.ID_Client
);
SELECT
p.Denumire,
p.Pret,
p.Stoc,
(p.Pret * p.Stoc) as Valoare_Stoc,
CASE
WHEN p.Stoc < 10 THEN 'Stoc mic'
WHEN p.Stoc BETWEEN 10 AND 25 THEN 'Stoc mediu'
ELSE 'Stoc mare'
END as Nivel_Stoc
FROM Produse p
WHERE p.Stoc > 0
ORDER BY Valoare_Stoc DESC;
SELECT
UPPER(c.Nume) as Nume_Client,
LOWER(p.Denumire) as Produs,
SUBSTRING(co.Status, 1, 3) as Status_Scurt,
LEFT(c.Email, CHARINDEX('@', c.Email) - 1) as Username
FROM Clienti c
JOIN Comenzi co ON c.ID_Client = co.ID_Client
JOIN Detalii_Comanda dc ON co.ID_Comanda = dc.ID_Comanda
JOIN Produse p ON dc.ID_Produs = p.ID_Produs;
Funcții text utilizate:
UPPER()
,LOWER()
- conversie majuscule/minusculeSUBSTRING()
- extragere substringLEFT()
- primele caractereCHARINDEX()
- găsirea poziției unui caracter
SELECT
ID_Comanda,
Data_Comanda,
YEAR(Data_Comanda) as An_Comanda,
MONTH(Data_Comanda) as Luna_Comanda,
DATEDIFF(day, Data_Comanda, GETDATE()) as Zile_De_La_Comanda
FROM Comenzi
WHERE Data_Comanda BETWEEN '2024-05-01' AND '2024-05-31';
Funcții dată utilizate:
YEAR()
,MONTH()
- extragere componente datăDATEDIFF()
- calculul diferenței între dateGETDATE()
- data curentăBETWEEN
- interval de date
SELECT Nume, Email, Telefon
FROM Clienti
WHERE (Nume LIKE '%escu%' OR Email LIKE '%gmail%')
AND Telefon LIKE '07_1%'
ORDER BY Nume;
SELECT p.Denumire, p.Pret, p.Categorie
FROM Produse p
WHERE EXISTS (
SELECT 1
FROM Detalii_Comanda dc
JOIN Comenzi c ON dc.ID_Comanda = c.ID_Comanda
WHERE dc.ID_Produs = p.ID_Produs
AND c.Status = 'Livrata'
);
UPDATE Clienti
SET Scor_Fidelitate = Scor_Fidelitate + 10
WHERE ID_Client IN (
SELECT DISTINCT c.ID_Client
FROM Clienti c
JOIN Comenzi co ON c.ID_Client = co.ID_Client
WHERE co.Status = 'Livrata'
);
DELETE FROM Produse
WHERE Stoc = 0 AND ID_Produs NOT IN (
SELECT DISTINCT ID_Produs
FROM Detalii_Comanda
);
INSERT INTO Profile_Clienti (ID_Client, Preferinte, Newsletter_Subscription)
SELECT ID_Client, 'Preferinte generale', 1
FROM Clienti
WHERE ID_Client NOT IN (SELECT ID_Client FROM Profile_Clienti);
SELECT
p.Categorie,
COUNT(*) as Numar_Produse,
AVG(p.Pret) as Pret_Mediu,
SUM(dc.Cantitate) as Cantitate_Vanduta
INTO Raport_Categorii
FROM Produse p
LEFT JOIN Detalii_Comanda dc ON p.ID_Produs = dc.ID_Produs
GROUP BY p.Categorie;
Captură ecran: Object Browser arătând tabelul "Raport_Categorii" creat automat
Au fost create 3 vederi pentru abstractizarea și simplificarea accesului la date:
CREATE VIEW View_Clienti_Activi AS
SELECT
c.ID_Client,
c.Nume,
c.Email,
c.Tip_Client,
c.Scor_Fidelitate,
COUNT(co.ID_Comanda) as Total_Comenzi,
COALESCE(SUM(co.Total), 0) as Total_Cumparaturi,
MAX(co.Data_Comanda) as Ultima_Comanda
FROM Clienti c
LEFT JOIN Comenzi co ON c.ID_Client = co.ID_Client
WHERE c.Activ = 1
GROUP BY c.ID_Client, c.Nume, c.Email, c.Tip_Client, c.Scor_Fidelitate;
Captură ecran: Rezultatul SELECT * FROM View_Clienti_Activi
CREATE VIEW View_Produse_Populare AS
SELECT
p.ID_Produs,
p.Denumire,
p.Categorie,
p.Pret,
p.Stoc,
COUNT(dc.ID_Detaliu) as Numar_Vanzari,
SUM(dc.Cantitate) as Cantitate_Totala_Vanduta,
AVG(dc.Pret_Unitar) as Pret_Mediu_Vanzare
FROM Produse p
JOIN Detalii_Comanda dc ON p.ID_Produs = dc.ID_Produs
GROUP BY p.ID_Produs, p.Denumire, p.Categorie, p.Pret, p.Stoc
HAVING COUNT(dc.ID_Detaliu) > 0;
CREATE VIEW View_Comenzi_Complete AS
SELECT
co.ID_Comanda,
c.Nume as Nume_Client,
c.Email as Email_Client,
co.Data_Comanda,
co.Status,
co.Total,
COUNT(dc.ID_Detaliu) as Numar_Produse,
pl.Metoda as Metoda_Plata,
pl.Status_Plata
FROM Comenzi co
JOIN Clienti c ON co.ID_Client = c.ID_Client
LEFT JOIN Detalii_Comanda dc ON co.ID_Comanda = dc.ID_Comanda
LEFT JOIN Plati pl ON co.ID_Comanda = pl.ID_Comanda
GROUP BY co.ID_Comanda, c.Nume, c.Email, co.Data_Comanda, co.Status, co.Total, pl.Metoda, pl.Status_Plata;
Avantajele vederilor:
- Simplificarea interogărilor complexe
- Securitate prin restricționarea accesului
- Abstractizarea structurii bazei de date
Au fost implementate 2 structuri de control:
DECLARE @ID_Produs INT = 1;
DECLARE @Cantitate_Vanduta INT = 5;
DECLARE @Stoc_Actual INT;
SELECT @Stoc_Actual = Stoc FROM Produse WHERE ID_Produs = @ID_Produs;
IF @Stoc_Actual >= @Cantitate_Vanduta
BEGIN
UPDATE Produse
SET Stoc = Stoc - @Cantitate_Vanduta
WHERE ID_Produs = @ID_Produs;
PRINT 'Stoc actualizat cu succes';
END
ELSE
BEGIN
PRINT 'Stoc insuficient pentru această tranzacție';
END
Captură ecran: Messages tab arătând rezultatul execuției
DECLARE @ID_Comanda_Process INT;
DECLARE @Total_Comanda DECIMAL(12,2);
DECLARE @Status_Vechi NVARCHAR(50);
DECLARE comenzi_cursor CURSOR FOR
SELECT ID_Comanda, Total, Status
FROM Comenzi
WHERE Status = 'Pending';
OPEN comenzi_cursor;
FETCH NEXT FROM comenzi_cursor INTO @ID_Comanda_Process, @Total_Comanda, @Status_Vechi;
WHILE @@FETCH_STATUS = 0
BEGIN
IF @Total_Comanda > 1000
BEGIN
UPDATE Comenzi
SET Status = 'Confirmata'
WHERE ID_Comanda = @ID_Comanda_Process;
PRINT 'Comanda ' + CAST(@ID_Comanda_Process AS VARCHAR) + ' confirmată automat (valoare mare)';
END
ELSE
BEGIN
UPDATE Comenzi
SET Status = 'In procesare'
WHERE ID_Comanda = @ID_Comanda_Process;
PRINT 'Comanda ' + CAST(@ID_Comanda_Process AS VARCHAR) + ' în procesare';
END
FETCH NEXT FROM comenzi_cursor INTO @ID_Comanda_Process, @Total_Comanda, @Status_Vechi;
END
CLOSE comenzi_cursor;
DEALLOCATE comenzi_cursor;
Au fost create 3 proceduri stocate:
CREATE PROCEDURE sp_AdaugaComanda
@ID_Client INT,
@Produse NVARCHAR(MAX), -- Format: 'ID_Produs:Cantitate,ID_Produs:Cantitate'
@Adresa_Livrare NVARCHAR(255) = NULL,
@Observatii NTEXT = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ID_Comanda INT;
DECLARE @Total DECIMAL(12,2) = 0;
-- [Restul codului procedurii]
END;
Funcționalitate:
- Validarea existenței clientului
- Procesarea listei de produse
- Verificarea stocului disponibil
- Crearea comenzii și detaliilor
- Actualizarea automată a stocului
Captură ecran: Rezultatul execuției EXEC sp_AdaugaComanda
CREATE PROCEDURE sp_RaportVanzari
@Data_Inceput DATE,
@Data_Sfarsit DATE,
@Categorie NVARCHAR(50) = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT
p.Categorie,
p.Denumire,
SUM(dc.Cantitate) as Cantitate_Vanduta,
SUM(dc.Cantitate * dc.Pret_Unitar) as Venituri_Totale,
AVG(dc.Pret_Unitar) as Pret_Mediu,
COUNT(DISTINCT co.ID_Client) as Clienti_Unici
FROM Produse p
JOIN Detalii_Comanda dc ON p.ID_Produs = dc.ID_Produs
JOIN Comenzi co ON dc.ID_Comanda = co.ID_Comanda
WHERE co.Data_Comanda BETWEEN @Data_Inceput AND @Data_Sfarsit
AND (@Categorie IS NULL OR p.Categorie = @Categorie)
AND co.Status IN ('Confirmata', 'Livrata')
GROUP BY p.Categorie, p.Denumire
ORDER BY Venituri_Totale DESC;
END;
CREATE PROCEDURE sp_ActualizeazaPreturi
@Categorie NVARCHAR(50),
@Procent_Modificare DECIMAL(5,2),
@Aplicare_Efective BIT = 0 -- 0 = preview, 1 = aplicare
AS
BEGIN
SET NOCOUNT ON;
IF @Aplicare_Efective = 0
BEGIN
-- Preview modificări
SELECT
ID_Produs,
Denumire,
Pret as Pret_Vechi,
ROUND(Pret * (1 + @Procent_Modificare / 100), 2) as Pret_Nou,
ROUND(Pret * @Procent_Modificare / 100, 2) as Diferenta
FROM Produse
WHERE Categorie = @Categorie;
END
ELSE
BEGIN
-- Aplicare modificări
UPDATE Produse
SET Pret = ROUND(Pret * (1 + @Procent_Modificare / 100), 2)
WHERE Categorie = @Categorie;
END
END;
Captură ecran: Object Browser → Stored Procedures arătând cele 3 proceduri create
Au fost implementate 2 cursoare:
DECLARE @ClientID INT, @TotalComanda DECIMAL(12,2), @StatusComanda NVARCHAR(50);
DECLARE @PuncteBonus DECIMAL(5,2);
DECLARE cursor_fidelitate CURSOR FOR
SELECT co.ID_Client, co.Total, co.Status
FROM Comenzi co
WHERE co.Status = 'Livrata'
AND co.Data_Comanda >= DATEADD(month, -1, GETDATE());
OPEN cursor_fidelitate;
FETCH NEXT FROM cursor_fidelitate INTO @ClientID, @TotalComanda, @StatusComanda;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @PuncteBonus = @TotalComanda / 100;
UPDATE Clienti
SET Scor_Fidelitate = Scor_Fidelitate + @PuncteBonus
WHERE ID_Client = @ClientID;
PRINT CONCAT('Client ID: ', @ClientID, ' - Puncte adăugate: ', @PuncteBonus);
FETCH NEXT FROM cursor_fidelitate INTO @ClientID, @TotalComanda, @StatusComanda;
END
CLOSE cursor_fidelitate;
DEALLOCATE cursor_fidelitate;
DECLARE @ProdusID INT, @StocCurent INT, @DenumireProdus NVARCHAR(100);
DECLARE @CantitateVanduta INT, @PragCritic INT = 5;
DECLARE cursor_stoc CURSOR FOR
SELECT p.ID_Produs, p.Denumire, p.Stoc
FROM Produse p
WHERE p.Stoc <= @PragCritic;
OPEN cursor_stoc;
FETCH NEXT FROM cursor_stoc INTO @ProdusID, @DenumireProdus, @StocCurent;
WHILE @@FETCH_STATUS = 0
BEGIN
SELECT @CantitateVanduta = COALESCE(SUM(dc.Cantitate), 0)
FROM Detalii_Comanda dc
JOIN Comenzi co ON dc.ID_Comanda = co.ID_Comanda
WHERE dc.ID_Produs = @ProdusID
AND co.Data_Comanda >= DATEADD(month, -1, GETDATE());
IF @CantitateVanduta > 0
BEGIN
UPDATE Produse
SET Stoc = Stoc + (@CantitateVanduta * 2)
WHERE ID_Produs = @ProdusID;
PRINT CONCAT('Produs: ', @DenumireProdus, ' - Stoc reaprovizionat cu: ', @CantitateVanduta * 2, ' unități');
END
FETCH NEXT FROM cursor_stoc INTO @ProdusID, @DenumireProdus, @StocCurent;
END
CLOSE cursor_stoc;
DEALLOCATE cursor_stoc;
Captură ecran: Messages tab cu rezultatele procesării cursor-ului
Au fost create 4 triggere:
CREATE TRIGGER tr_ActualizeazaTotalComanda
ON Detalii_Comanda
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
-- Pentru INSERT și UPDATE
IF EXISTS(SELECT * FROM inserted)
BEGIN
UPDATE Comenzi
SET Total = (
SELECT SUM(dc.Cantitate * dc.Pret_Unitar * (1 - dc.Discount/100))
FROM Detalii_Comanda dc
WHERE dc.ID_Comanda = Comenzi.ID_Comanda
)
WHERE ID_Comanda IN (SELECT DISTINCT ID_Comanda FROM inserted);
END
-- Pentru DELETE
IF EXISTS(SELECT * FROM deleted) AND NOT EXISTS(SELECT * FROM inserted)
BEGIN
UPDATE Comenzi
SET Total = COALESCE((
SELECT SUM(dc.Cantitate * dc.Pret_Unitar * (1 - dc.Discount/100))
FROM Detalii_Comanda dc
WHERE dc.ID_Comanda = Comenzi.ID_Comanda
), 0)
WHERE ID_Comanda IN (SELECT DISTINCT ID_Comanda FROM deleted);
END
END;
CREATE TRIGGER tr_VerificaStoc
ON Detalii_Comanda
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ID_Produs INT, @Cantitate INT, @StocDisponibil INT;
DECLARE @DenumireProdus NVARCHAR(100);
DECLARE stoc_cursor CURSOR FOR
SELECT ID_Produs, Cantitate
FROM inserted;
OPEN stoc_cursor;
FETCH NEXT FROM stoc_cursor INTO @ID_Produs, @Cantitate;
WHILE @@FETCH_STATUS = 0
BEGIN
SELECT @StocDisponibil = Stoc, @DenumireProdus = Denumire
FROM Produse
WHERE ID_Produs = @ID_Produs;
IF @StocDisponibil >= @Cantitate
BEGIN
INSERT INTO Detalii_Comanda (ID_Comanda, ID_Produs, Cantitate, Pret_Unitar, Discount)
SELECT ID_Comanda, ID_Produs, Cantitate, Pret_Unitar, Discount
FROM inserted
WHERE ID_Produs = @ID_Produs;
UPDATE Produse
SET Stoc = Stoc - @Cantitate
WHERE ID_Produs = @ID_Produs;
END
ELSE
BEGIN
RAISERROR('Stoc insuficient pentru produsul %s', 16, 1, @DenumireProdus);
END
FETCH NEXT FROM stoc_cursor INTO @ID_Produs, @Cantitate;
END
CLOSE stoc_cursor;
DEALLOCATE stoc_cursor;
END;