क्या एक तालिका में अपडेट होने वाले मूल्य को रखना ठीक है?


31

हम प्रीपेड कार्ड के लिए एक प्लेटफॉर्म विकसित कर रहे हैं, जो मूल रूप से कार्ड और उनके बैलेंस, भुगतान आदि के बारे में डेटा रखता है।

अब तक हमारे पास एक कार्ड इकाई थी जिसमें खाता इकाई का एक संग्रह है, और प्रत्येक खाते में एक राशि है, जो प्रत्येक जमा / निकासी में अपडेट होती है।

टीम में अब एक बहस चल रही है; किसी ने हमें बताया है कि यह कोडेक के 12 नियमों को तोड़ता है और प्रत्येक भुगतान पर इसके मूल्य को अपडेट करना परेशानी है।

क्या यह वास्तव में एक समस्या है?

यदि यह है, तो हम इसे कैसे ठीक कर सकते हैं?


3
DBA.SE पर इस विषय पर एक व्यापक तकनीकी चर्चा है: एक साधारण बैंक स्कीमा लिखना
निक चामास

1
आपकी टीम ने कौन से कोडड नियम का हवाला दिया? नियम एक रिलेशनल सिस्टम को परिभाषित करने का उनका प्रयास था, और सामान्यीकरण का स्पष्ट रूप से उल्लेख नहीं किया था। कोडड ने अपनी पुस्तक द रिलेशनल मॉडल फॉर डेटाबेस मैनेजमेंट में सामान्यीकरण पर चर्चा की ।
इयान सैमुअल मैकलीन एल्डर

जवाबों:


30

हां, यह गैर-सामान्यीकृत है, लेकिन कभी-कभी गैर-सामान्यीकृत डिजाइन प्रदर्शन कारणों से जीतते हैं।

हालाँकि, मैं शायद सुरक्षा कारणों से इसे थोड़ा अलग तरीके से समझूंगा। (अस्वीकरण: मैं वर्तमान में नहीं हूं, और न ही मैंने कभी वित्तीय क्षेत्र में काम किया है। मैं इसे अभी बाहर फेंक रहा हूं।)

कार्ड पर पोस्ट किए गए शेष के लिए एक तालिका रखें। इसमें प्रत्येक खाते के लिए एक पंक्ति सम्मिलित की जाएगी, जो प्रत्येक अवधि (दिन, सप्ताह, महीने, या जो भी उपयुक्त हो) के नज़दीक पोस्ट किए गए शेष को दर्शाती है। इस तालिका को खाता संख्या और दिनांक द्वारा अनुक्रमित करें।

लंबित लेनदेन को पकड़ने के लिए किसी अन्य तालिका का उपयोग करें, जिसे मक्खी पर डाला जाता है। प्रत्येक अवधि के अंत में, एक ऐसी दिनचर्या चलाएं जो नए बैलेंस की गणना करने के लिए खाते के अंतिम समापन संतुलन में अनपोस्टेड लेनदेन को जोड़ता है। या तो लंबित लेनदेन को पोस्ट के रूप में चिह्नित करें, या यह निर्धारित करने के लिए तारीखों को देखें कि क्या अभी भी लंबित है।

इस तरह, आपके पास मांग पर एक कार्ड बैलेंस की गणना करने का एक साधन है, बिना सभी खाता इतिहास के योग करने के लिए, और एक समर्पित पोस्टिंग रूटीन में शेष पुनर्गणना डालकर, आप यह सुनिश्चित कर सकते हैं कि इस पुनर्गणना की लेनदेन सुरक्षा सीमित है एक एकल स्थान (और बैलेंस टेबल पर सुरक्षा को भी सीमित करता है ताकि केवल पोस्टिंग रूटीन ही इसे लिख सके)।

फिर ऑडिटिंग, ग्राहक सेवा और प्रदर्शन आवश्यकताओं के अनुसार केवल उतना ही ऐतिहासिक डेटा रखें जितना आवश्यक हो।


1
सिर्फ दो त्वरित नोट्स। पहले वह लॉग-एग्रीगेट-स्नैपशॉट दृष्टिकोण का बहुत अच्छा विवरण है जो मैं ऊपर सुझा रहा था, और शायद मैं जितना स्पष्ट था। (आपको अपवित्र किया गया)। दूसरे, मुझे संदेह है कि आप "पोस्ट" का उपयोग कुछ अजीब तरीके से कर रहे हैं, जिसका अर्थ है "समापन संतुलन का हिस्सा।" वित्तीय शब्दों में, पोस्ट का मतलब आमतौर पर "वर्तमान खाता बही में संतुलन दिखाना" होता है और इसलिए ऐसा लगता है कि यह समझाने लायक था कि इससे भ्रम पैदा नहीं हुआ।
क्रिस ट्रैवर्स

हाँ, शायद बहुत सारी सूक्ष्मताएँ हैं जो मुझे याद आ रही हैं। मैं सिर्फ इस बात का उल्लेख कर रहा हूं कि लेन-देन व्यवसाय के पास मेरे चेकिंग खाते में "पोस्ट" कैसे किया जाता है, और शेष राशि तदनुसार अपडेट होती है। लेकिन मैं एकाउंटेंट नहीं हूं; मैं उनमें से कई के साथ काम करता हूं।
db2

यह SOX या भविष्य की तरह की आवश्यकता भी हो सकती है , मुझे नहीं पता कि आपको वास्तव में किस प्रकार की माइक्रो-लेन-देन आवश्यकताओं को लॉग इन करना है, लेकिन मैं किसी ऐसे व्यक्ति से पूछूंगा, जो यह जानता हो कि बाद में रिपोर्टिंग की क्या आवश्यकताएं हैं।
jcolebrand

मैं प्रत्येक वर्ष की शुरुआत में उदाहरण के लिए सतत डेटा रखने के लिए इच्छुक हूं, ताकि "योग" स्नैपशॉट कभी भी ओवरराइट न हो - सूची बस के लिए संलग्न हो जाती है (भले ही सिस्टम प्रत्येक खाते के लिए लंबे समय तक उपयोग में रहे। 1,000 वार्षिक योग [ बहुत आशावादी] जमा करें, जो शायद ही असहनीय होगा)। कई वार्षिक योगों को रखने से ऑडिटिंग कोड को यह पुष्टि करने की अनुमति मिलती है कि हाल के वर्षों के बीच लेनदेन में योगों पर उचित प्रभाव पड़ता है [व्यक्तिगत लेनदेन 5 साल बाद शुद्ध हो सकते हैं, लेकिन तब तक अच्छी तरह से हो जाएगा]।
सुपरकैट

17

दूसरी तरफ, एक मुद्दा यह है कि हम अक्सर लेखांकन सॉफ़्टवेयर में चलते हैं। दूसरे शब्दों में बयान:

मैं है वास्तव में पता लगाने के लिए कितना पैसा चेकिंग खाता है डेटा का योग दस साल की जरूरत है?

बेशक आप कोई जवाब नहीं है। यहाँ कुछ दृष्टिकोण हैं। एक परिकलित मान संग्रहीत कर रहा है। मैं इस दृष्टिकोण की अनुशंसा नहीं करता क्योंकि सॉफ़्टवेयर बग जो गलत मानों को ट्रैक करने के लिए बहुत कठिन हैं और इसलिए मैं इस दृष्टिकोण से बचूंगा।

इसका एक बेहतर तरीका यह है कि मैं लॉग-स्नैपशॉट-एग्रीगेट दृष्टिकोण को क्या कहूं। इस दृष्टिकोण में हमारे भुगतान और उपयोग आवेषण हैं और हम कभी भी इन मूल्यों को अपडेट नहीं करते हैं। समय-समय पर हम समय की अवधि में डेटा को एकत्र करते हैं और एक गणना किए गए स्नैपशॉट रिकॉर्ड को सम्मिलित करते हैं जो उस समय डेटा का प्रतिनिधित्व करता है जो स्नैपशॉट मान्य हो गया था (आमतौर पर वर्तमान से पहले की अवधि )।

अब यह कोडड के नियमों को नहीं तोड़ता है क्योंकि समय के साथ स्नैपशॉट सम्मिलित भुगतान / उपयोग डेटा पर पूरी तरह से निर्भर हो सकता है। अगर हमारे पास काम करने वाले स्नैपशॉट हैं तो हम मांग पर वर्तमान शेष राशि की गणना करने की हमारी क्षमता को प्रभावित किए बिना 10 साल पुराने डेटा को शुद्ध करने का निर्णय ले सकते हैं।


2
मैं गणना किए गए योगों को संग्रहीत कर सकता हूं, और मैं पूरी तरह से सुरक्षित हूं - विश्वसनीय बाधाएं यह सुनिश्चित करती हैं कि मेरी संख्या हमेशा सही हो: sqlblog.com/blogs/alexander_kuznetsov/archive/2009/01/23/…
AK

1
मेरे समाधान में कोई किनारे के मामले नहीं हैं - एक विश्वसनीय बाधा आपको कुछ भी भूलने नहीं देगी। मुझे वास्तविक जीवन प्रणाली में NULL मात्राओं के लिए कोई व्यावहारिक आवश्यकता नहीं दिखती है, जो चल रहे योगों को जानने की आवश्यकता है - ये चीजें एक दूसरे के विपरीत हैं। यदि आपको कोई व्यावहारिक आवश्यकता दिखाई देती है, तो कृपया अपना स्कोररियो साझा करें।
एके

1
ठीक है, लेकिन फिर यह db पर काम नहीं कर रहा है, जो अद्वितीयता का उल्लंघन किए बिना कई NULLs की अनुमति देता है, है ना? इसके अलावा अगर आप पिछले डेटा को शुद्ध करते हैं तो आपकी गारंटी खराब हो जाती है?
क्रिस ट्रैवर्स

1
उदाहरण के लिए, अगर मेरे पास PostgreSQL में एक अद्वितीय बाधा (, b) है, तो मेरे पास (a, b) के लिए कई (1, null) मान हो सकते हैं क्योंकि प्रत्येक null को संभावित रूप से अद्वितीय माना जाता है, जो मुझे लगता है कि अज्ञात से सही है मान .....
क्रिस ट्रैवर्स

1
PostgreSQL में "मेरे पास (a, b) पर एक अद्वितीय अवरोध है, मेरे पास कई (1, null) मान हो सकते हैं" - PostgreSql में हमें b (n) पर एक अद्वितीय आंशिक सूचकांक (a) का उपयोग करने की आवश्यकता है।
एके

7

प्रदर्शन के कारणों के लिए, ज्यादातर मामलों में हमें वर्तमान संतुलन को स्टोर करना चाहिए - अन्यथा मक्खी पर गणना करना अंततः निषिद्ध रूप से धीमा हो सकता है।

हम अपने सिस्टम में पहले से चल रहे योगों को संग्रहीत करते हैं। यह सुनिश्चित करने के लिए कि संख्या हमेशा सही होती है, हम बाधाओं का उपयोग करते हैं। निम्नलिखित समाधान को मेरे ब्लॉग से कॉपी किया गया है। यह एक इन्वेंट्री का वर्णन करता है, जो अनिवार्य रूप से एक ही समस्या है:

कुल योग की गणना बहुत धीमी है, चाहे आप इसे कर्सर के साथ करें या त्रिकोणीय जुड़ाव के साथ करें। किसी कॉलम में रनिंग योग को स्टोर करना, विशेष रूप से यदि आप इसे अक्सर चुनते हैं, तो इसे बहुत लुभावना है। हालाँकि, जब आप सामान्य करते हैं, तो आपको अपने असामान्य डेटा की अखंडता की गारंटी देने की आवश्यकता होती है। सौभाग्य से, आप बाधाओं के साथ चल रहे योगों की अखंडता की गारंटी दे सकते हैं - जब तक आपके सभी बाधाओं पर भरोसा किया जाता है, आपके सभी चल रहे योग सही हैं। इस तरह से आप आसानी से यह सुनिश्चित कर सकते हैं कि वर्तमान संतुलन (चल रहा योग) कभी भी नकारात्मक नहीं है - अन्य तरीकों से लागू करना भी बहुत धीमा हो सकता है। निम्नलिखित स्क्रिप्ट तकनीक प्रदर्शित करती है।

CREATE TABLE Data.Inventory(InventoryID INT NOT NULL IDENTITY,
  ItemID INT NOT NULL,
  ChangeDate DATETIME NOT NULL,
  ChangeQty INT NOT NULL,
  TotalQty INT NOT NULL,
  PreviousChangeDate DATETIME NULL,
  PreviousTotalQty INT NULL,
  CONSTRAINT PK_Inventory PRIMARY KEY(ItemID, ChangeDate),
  CONSTRAINT UNQ_Inventory UNIQUE(ItemID, ChangeDate, TotalQty),
  CONSTRAINT UNQ_Inventory_Previous_Columns UNIQUE(ItemID, PreviousChangeDate, PreviousTotalQty),
  CONSTRAINT FK_Inventory_Self FOREIGN KEY(ItemID, PreviousChangeDate, PreviousTotalQty)
    REFERENCES Data.Inventory(ItemID, ChangeDate, TotalQty),
  CONSTRAINT CHK_Inventory_Valid_TotalQty CHECK(TotalQty >= 0 AND (TotalQty = COALESCE(PreviousTotalQty, 0) + ChangeQty)),
  CONSTRAINT CHK_Inventory_Valid_Dates_Sequence CHECK(PreviousChangeDate < ChangeDate),
  CONSTRAINT CHK_Inventory_Valid_Previous_Columns CHECK((PreviousChangeDate IS NULL AND PreviousTotalQty IS NULL)
            OR (PreviousChangeDate IS NOT NULL AND PreviousTotalQty IS NOT NULL))
);
GO
-- beginning of inventory for item 1
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
VALUES(1, '20090101', 10, 10, NULL, NULL);
-- cannot begin the inventory for the second time for the same item 1
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
VALUES(1, '20090102', 10, 10, NULL, NULL);

Msg 2627, Level 14, State 1, Line 10
Violation of UNIQUE KEY constraint 'UNQ_Inventory_Previous_Columns'. Cannot insert duplicate key in object 'Data.Inventory'.
The statement has been terminated.

-- add more
DECLARE @ChangeQty INT;
SET @ChangeQty = 5;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090103', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = 3;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090104', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = -4;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20090105', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

-- try to violate chronological order

SET @ChangeQty = 5;
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
SELECT TOP 1 ItemID, '20081231', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

Msg 547, Level 16, State 0, Line 4
The INSERT statement conflicted with the CHECK constraint "CHK_Inventory_Valid_Dates_Sequence". The conflict occurred in database "Test", table "Data.Inventory".
The statement has been terminated.


SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- -----
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 5           15          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           18          2009-01-03 00:00:00.000 15
2009-01-05 00:00:00.000 -4          14          2009-01-04 00:00:00.000 18


-- try to change a single row, all updates must fail
UPDATE Data.Inventory SET ChangeQty = ChangeQty + 2 WHERE InventoryID = 3;
UPDATE Data.Inventory SET TotalQty = TotalQty + 2 WHERE InventoryID = 3;
-- try to delete not the last row, all deletes must fail
DELETE FROM Data.Inventory WHERE InventoryID = 1;
DELETE FROM Data.Inventory WHERE InventoryID = 3;

-- the right way to update

DECLARE @IncreaseQty INT;
SET @IncreaseQty = 2;
UPDATE Data.Inventory SET ChangeQty = ChangeQty + CASE WHEN ItemID = 1 AND ChangeDate = '20090103' THEN @IncreaseQty ELSE 0 END,
  TotalQty = TotalQty + @IncreaseQty,
  PreviousTotalQty = PreviousTotalQty + CASE WHEN ItemID = 1 AND ChangeDate = '20090103' THEN 0 ELSE @IncreaseQty END
WHERE ItemID = 1 AND ChangeDate >= '20090103';

SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- ----------------
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 7           17          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           20          2009-01-03 00:00:00.000 17
2009-01-05 00:00:00.000 -4          16          2009-01-04 00:00:00.000 20

यह मेरे लिए होता है कि आपके दृष्टिकोण की एक बड़ी सीमा यह है कि किसी विशिष्ट ऐतिहासिक तिथि पर किसी खाते के शेष की गणना करना अभी भी समुच्चय की आवश्यकता है, जब तक कि आप यह भी अनुमान नहीं लगाते हैं कि सभी लेनदेन क्रमिक रूप से तिथि तक दर्ज किए जाते हैं (जो आमतौर पर एक बुरा है कल्पना)।
क्रिस ट्रैवर्स

@ChrisTravers सभी चल रहे योग हमेशा सभी ऐतिहासिक तारीखों के लिए अप-टू-डेट हैं। बाधाओं की गारंटी है। इसलिए किसी भी ऐतिहासिक तिथियों के लिए किसी भी समुच्चय की आवश्यकता नहीं है। यदि हमें कुछ ऐतिहासिक पंक्ति को अपडेट करना है, या कुछ बैक-डेट सम्मिलित करना है, तो हम सभी बाद की पंक्तियों को चलने वाले योगों को अपडेट करते हैं। मुझे लगता है कि यह postgreSql में बहुत आसान है, क्योंकि इसने बाधाओं को टाल दिया है।
एके

6

यह एक बहुत अच्छा सवाल है।

यह मानते हुए कि आपके पास एक लेनदेन तालिका है जो प्रत्येक डेबिट / क्रेडिट को संग्रहीत करती है, आपके डिज़ाइन में कुछ भी गलत नहीं है। वास्तव में, मैंने प्रीपेड टेल्को सिस्टम के साथ काम किया है जो बिल्कुल इसी तरह से काम करता है।

मुख्य बात यह है कि आपको यह सुनिश्चित करना है कि आप डेबिट / क्रेडिट SELECT ... FOR UPDATEकरते समय एक संतुलन बना रहे हैं INSERT। यह सही संतुलन की गारंटी देगा यदि कुछ गलत हो जाता है (क्योंकि पूरे लेनदेन को वापस ले लिया जाएगा)।

जैसा कि अन्य लोगों ने बताया है, आपको यह सत्यापित करने के लिए कि किसी निश्चित अवधि में लेन-देन की अवधि प्रारंभ / समाप्ति शेष राशि के साथ सही ढंग से होने के लिए, विशिष्ट अवधि में शेष राशि की स्नैपशॉट की आवश्यकता होगी। ऐसा करने के लिए अवधि (महीने / सप्ताह / दिन) की आधी रात को चलने वाली बैच की नौकरी लिखें।


4

शेष राशि कुछ व्यावसायिक नियमों के आधार पर एक गणना की गई राशि है, इसलिए हां आप शेष राशि को रखना नहीं चाहते हैं, बल्कि इसे कार्ड और इसलिए खाते से लेन-देन से गणना करते हैं।

आप ऑडिटिंग और स्टेटमेंट रिपोर्टिंग के लिए कार्ड पर सभी लेन-देन पर नज़र रखना चाहते हैं, और यहां तक ​​कि बाद में विभिन्न प्रणालियों के डेटा भी।

लब्बोलुआब यह है कि किसी भी मूल्यों की गणना करें, जिनकी आवश्यकता होने पर गणना की जानी चाहिए


यहां तक ​​कि अगर लेनदेन की अधिकता हो सकती है? इसलिए मुझे इसे हर बार पुनः स्थापित करने की आवश्यकता होगी? क्या यह प्रदर्शन पर थोड़ा कठिन नहीं हो सकता है? क्या आप इस बारे में थोड़ा सा जोड़ सकते हैं कि ऐसी समस्या क्यों है?
मिथिर

2
@ मिथिर क्योंकि यह लेखांकन के अधिकांश नियमों के खिलाफ जाता है, और यह मुद्दों को ट्रेस करना असंभव बनाता है। यदि आप अभी चल रहे कुल को अपडेट करते हैं, तो आपको कैसे पता चलेगा कि कौन सा समायोजन लागू किया गया है? क्या उस चालान को एक या दो बार जमा किया गया था? क्या हमने पहले ही भुगतान राशि काट ली थी? यदि आप लेन-देन को ट्रैक करते हैं तो आप जवाब जानते हैं, यदि आप कुल ट्रैक करते हैं तो आप नहीं करते हैं।
जेएनके

4
कोडड के नियमों का संदर्भ यह है कि यह सामान्य रूप को तोड़ता है। मान लें कि आप लेनदेन को ट्रैक करते हैं (जो आपको मेरे बारे में सोचना होगा), और आपके पास एक अलग चलने वाला कुल है, जो कि वे असहमत हैं तो सही है? आपको सत्य के एकल संस्करण की आवश्यकता है। प्रदर्शन समस्या को तब तक ठीक न करें जब तक कि यह वास्तव में मौजूद न हो।
जेएनके

@ जेएनके अब जैसा है - हम लेन-देन और एक कुल रखते हैं, इसलिए आपके द्वारा उल्लिखित सब कुछ पूरी तरह से ट्रैक किया जा सकता है यदि आवश्यक हो, तो शेष राशि हमें हर कार्रवाई को राशि को पुन: प्राप्त करने से रोकने के लिए है।
मिथिर

2
अब, यह Codd के नियमों को नहीं तोड़ेगा यदि पुराने डेटा को केवल 5 साल के लिए ही सही रखा जा सकता है? उस बिंदु पर संतुलन केवल मौजूदा रिकॉर्ड का योग नहीं है, बल्कि पहले से मौजूद रिकॉर्ड भी शुद्ध है, या मैं कुछ याद कर रहा हूं? मुझे लगता है कि यह केवल कोडेक के नियमों को तोड़ देगा यदि हम अनंत डेटा प्रतिधारण मान लेते हैं, जो कि संभावना नहीं है। यह उन कारणों के लिए कहा जा रहा है जो मैं नीचे कहता हूं, मुझे लगता है कि एक निरंतर अद्यतन मूल्य संग्रहीत करना मुसीबत के लिए पूछ रहा है।
क्रिस ट्रैवर्स
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.