Warum unterstützen relationale Datenbanken die Rückgabe von Informationen in einem verschachtelten Format nicht?


46

Angenommen, ich erstelle ein Blog, in dem ich Beiträge und Kommentare veröffentlichen möchte. Also erstelle ich zwei Tabellen, eine 'posts'-Tabelle mit einer automatisch inkrementierenden Integer-ID-Spalte und eine' comments'-Tabelle mit dem Fremdschlüssel 'post_id'.

Dann möchte ich ausführen, was wahrscheinlich meine häufigste Abfrage ist, nämlich das Abrufen eines Posts und aller seiner Kommentare. Da relationale Datenbanken noch relativ neu sind, erscheint es mir am offensichtlichsten, eine Abfrage zu schreiben, die ungefähr so ​​aussieht:

SELECT id, content, (SELECT * FROM comments WHERE post_id = 7) AS comments
FROM posts
WHERE id = 7

Das würde mir die ID und den Inhalt des Posts geben, den ich möchte, zusammen mit allen relevanten Kommentarzeilen, die ordentlich in ein Array gepackt sind (eine verschachtelte Darstellung, wie Sie sie in JSON verwenden würden). Natürlich funktionieren SQL-Datenbanken und relationale Datenbanken nicht so, und das Beste, was sie erreichen können, ist eine Verknüpfung zwischen "Posts" und "Kommentaren", die eine Menge unnötiger Duplikate von Daten zurückgibt (wobei dieselben Post-Informationen wiederholt werden in jeder Zeile), was bedeutet, dass die Verarbeitungszeit sowohl für die Datenbank aufgewendet wird, um alles zusammenzusetzen, als auch für meinen ORM, um alles zu analysieren und rückgängig zu machen.

Selbst wenn ich meinen ORM anweise, die Kommentare des Posts eifrig zu laden, ist es am besten, eine Abfrage für den Post und dann eine zweite Abfrage zum Abrufen aller Kommentare zu senden und sie dann clientseitig zusammenzustellen ist auch ineffizient.

Ich verstehe, dass relationale Datenbanken bewährte Technologie sind (zum Teufel, sie sind älter als ich), und dass im Laufe der Jahrzehnte eine Menge Forschung in sie gesteckt wurde, und ich bin mir sicher, dass es einen wirklich guten Grund gibt, warum sie (und die) SQL-Standard) funktionieren so, wie sie es tun, aber ich bin mir nicht sicher, warum der oben beschriebene Ansatz nicht möglich ist. Es scheint mir die einfachste und naheliegendste Möglichkeit zu sein, eine der grundlegendsten Beziehungen zwischen Datensätzen zu implementieren. Warum bieten relationale Datenbanken so etwas nicht an?

(Haftungsausschluss: Ich schreibe hauptsächlich Webapps mit Rails und NoSQL-Datenspeichern, aber kürzlich habe ich Postgres ausprobiert und es gefällt mir sehr gut. Ich will keine relationalen Datenbanken angreifen, ich bin nur ratlos.)

Ich frage nicht, wie ich eine Rails-App optimieren oder mich in einer bestimmten Datenbank um dieses Problem kümmern soll. Ich frage mich, warum der SQL-Standard so funktioniert, wenn er mir nicht intuitiv und verschwenderisch erscheint. Es muss einen historischen Grund geben, warum die ursprünglichen Designer von SQL wollten, dass ihre Ergebnisse so aussehen.


1
Nicht alle Orms funktionieren so. hibernate / nhibernate ermöglicht die Angabe von Joins und das schnelle Laden ganzer Objektbäume aus einer einzigen Abfrage.
Nathan Gonzalez

1
Obwohl dies ein interessanter Diskussionspunkt ist, bin ich mir nicht sicher, ob dies wirklich beantwortet werden kann, ohne ein Treffen mit den ansi sql-Leuten zu haben
Nathan Gonzalez

@ Nathan: Ja, nicht alle. Ich habe Sequel verwendet, mit dem Sie auswählen können, welchen Ansatz Sie für eine bestimmte Abfrage ( Dokumente ) bevorzugen , ermutigen jedoch weiterhin den Ansatz mit mehreren Abfragen (aus Gründen der Leistung, nehme ich an).

5
Da ein RDBMS zum Speichern und Abrufen von Gruppen entwickelt wurde, ist es nicht vorgesehen, Daten zur Anzeige zurückzugeben. Stellen Sie es sich wie MVC vor - warum sollte es versuchen, die Ansicht um den Preis zu implementieren, dass das Modell langsamer oder schwieriger zu bedienen ist? RDBMS bietet Vorteile, die NoSQL-Datenbanken nicht bieten können (und umgekehrt). Wenn Sie es verwenden, weil es das richtige Tool zur Lösung Ihres Problems ist, würden Sie es nicht bitten, Daten zur Anzeige zurückzugeben.

1
Sie sehen für xml
Ian

Antworten:


42

CJ Date geht ausführlich auf dieses Thema in Kapitel 7 und Anhang B der SQL- und relationalen Theorie ein . Sie haben Recht, nichts in der relationalen Theorie verbietet, dass der Datentyp eines Attributs selbst eine Relation ist, solange er in jeder Zeile der gleiche Relationstyp ist. Ihr Beispiel würde sich qualifizieren.

Laut Date sind solche Strukturen "normalerweise - aber nicht immer - kontraindiziert" (dh eine schlechte Idee), da die Hierarchien der Beziehungen asymmetrisch sind . Beispielsweise kann eine Umwandlung von einer verschachtelten Struktur in eine vertraute "flache" Struktur nicht immer rückgängig gemacht werden, um die Verschachtelung wiederherzustellen.

Abfragen, Einschränkungen und Aktualisierungen sind komplexer, schwieriger zu schreiben und für das RDBMS schwieriger zu unterstützen, wenn Sie Attribute mit Bezugswerten (RVA) zulassen.

Außerdem werden die Prinzipien des Datenbankdesigns durcheinandergebracht, da die beste Hierarchie der Beziehungen nicht so klar ist. Sollten wir eine Beziehung von Lieferanten mit einer verschachtelten RVA für Teile entwerfen, die von einem bestimmten Lieferanten geliefert werden? Oder eine Beziehung von Teilen mit einer verschachtelten RVA für Lieferanten, die ein bestimmtes Teil liefern? Oder beide speichern, um die Ausführung verschiedener Abfragetypen zu vereinfachen?

Dies ist das gleiche Dilemma, das sich aus der hierarchischen Datenbank und den dokumentenorientierten Datenbankmodellen ergibt . Aufgrund der Komplexität und der Kosten für den Zugriff auf verschachtelte Datenstrukturen müssen Designer die Daten redundant speichern, um sie bei verschiedenen Abfragen leichter nachschlagen zu können. Das relationale Modell rät von Redundanz ab, sodass RVAs gegen die Ziele der relationalen Modellierung arbeiten können.

Soweit ich weiß (ich habe sie nicht verwendet), sind Rel und Dataphor RDBMS-Projekte, die Attribute mit Bezugswerten unterstützen.


Kommentar von @dportas:

Strukturierte Typen sind Teil von SQL-99, und Oracle unterstützt diese. Sie speichern jedoch nicht mehrere Tupel in der verschachtelten Tabelle pro Zeile der Basistabelle. Das übliche Beispiel ist ein "Adresse" -Attribut, das eine einzelne Spalte der Basistabelle zu sein scheint, aber weitere Unterspalten für Straße, Stadt, Postleitzahl usw. enthält.

Geschachtelte Tabellen werden auch von Oracle unterstützt und ermöglichen mehrere Tupel pro Zeile der Basistabelle. Mir ist jedoch nicht bewusst, dass dies Teil von Standard-SQL ist. Und denken Sie an die Schlussfolgerung eines Blogs: "Ich werde niemals eine verschachtelte Tabelle in einer CREATE TABLE-Anweisung verwenden. Sie verbringen die ganze Zeit damit, sie UN-NESTING zu machen, um sie wieder nützlich zu machen!"


3
Ich würde nicht wirklich eine Beziehung in einer anderen speichern wollen - sie wären in getrennten Tabellen und würden wie üblich denormalisiert. Ich frage nur, warum diese Art der Einbettung von Ergebnissen in Abfragen nicht zulässig ist, wenn sie mir intuitiver erscheinen als das Join-Modell.
PreciousBodilyFluids

Ergebnismengen und Tabellen sind von einer Art. Das Datum nennt sie Relationen bzw. Relvare (entsprechend ist 42 eine Ganzzahl, während eine Variable xden Wert von Ganzzahl 42 haben kann). Dieselben Operationen gelten für Relationen und Relvare, daher muss ihre Struktur kompatibel sein.
Bill Karwin

2
Standard-SQL unterstützt verschachtelte Tabellen. Sie werden als "strukturierte Typen" bezeichnet. Oracle ist ein DBMS mit dieser Funktion.
nvogel

2
Ist es nicht ein bisschen absurd zu behaupten, dass Sie Ihre Abfrage flach und datenvervielfältigend schreiben müssen, um Datenvervielfältigungen zu vermeiden?
Eamon Nerbonne

1
@EamonNerbonne, Symmetrie relationaler Operationen. Zum Beispiel Projektion. Wie kann ich eine umgekehrte Operation auf die Ergebnismenge anwenden, um die ursprüngliche Hierarchie zu reproduzieren, wenn ich einige Unterattribute aus einer RVA auswähle? Ich habe festgestellt, dass Seite 293 von Dates Buch auf Google Books ist, sodass Sie sehen können, was er geschrieben hat: books.google.com/…
Bill Karwin

15

Einige der frühesten Datenbanksysteme basierten auf dem Modell der hierarchischen Datenbank . Dies stellte Daten in einer baumartigen Struktur mit übergeordneten und untergeordneten Elementen dar, wie Sie es hier vorschlagen. HDMS wurden weitgehend von Datenbanken abgelöst, die auf dem relationalen Modell aufbauten. Die Hauptgründe dafür waren, dass RDBMS "viele zu viele" Beziehungen modellieren konnte, die für hierarchische Datenbanken schwierig waren, und dass RDBMS problemlos Abfragen durchführen konnte, die nicht Teil des ursprünglichen Entwurfs waren, während HDBMS Sie dazu zwang, Pfade abzufragen, die zur Entwurfszeit angegeben wurden.

Es gibt immer noch einige Beispiele für hierarchische Datenbanksysteme in freier Wildbahn, insbesondere die Windows-Registrierung und LDAP.

Ausführliche Informationen zu diesem Thema finden Sie im folgenden Artikel


10

Ich nehme an, dass sich Ihre Frage wirklich auf die Tatsache konzentriert, dass Datenbanken zwar auf einer soliden Logik und einer soliden theoretischen Grundlage basieren und sehr gute Arbeit beim Speichern, Bearbeiten und Abrufen von Daten in (zweidimensionalen) Sätzen leisten, während gleichzeitig die referenzielle Integrität und die Parallelität gewährleistet sind und viele andere Dinge, bieten sie keine (zusätzliche) Funktion zum Senden (und Empfangen) von Daten in einem, wie man es nennen könnte, objektorientierten Format oder hierarchischen Format.

Dann behaupten Sie, dass "selbst wenn ich meinen ORM anweise, die Kommentare des Posts eifrig zu laden, das Beste ist, eine Abfrage für den Post zu senden und dann eine zweite Abfrage, um alle Kommentare abzurufen und sie dann zusammenzufügen Client-Seite, die auch ineffizient ist " .

Ich sehe nichts Ineffizientes beim Senden von 2 Abfragen und Empfangen von 2 Ergebnissätzen mit:

--- Query-1-posts
SELECT id, content 
FROM posts
WHERE id = 7


--- Query-2-comments
SELECT * 
FROM comments 
WHERE post_id = 7

Ich würde argumentieren, dass dies (fast) der effizienteste Weg ist (fast, da Sie die posts.idund nicht alle Spalten von nicht wirklich benötigen comments.*).

Wie Todd in seinem Kommentar ausführte, sollten Sie die Datenbank nicht auffordern, Daten zur Anzeige zurückzugeben. Es ist die Aufgabe der Anwendung, dies zu tun. Sie können (eine oder mehrere) Abfragen schreiben, um die Ergebnisse zu erhalten, die Sie für jeden Anzeigevorgang benötigen, damit die Daten, die über die Leitung (oder den Speicherbus) von der Datenbank zur Anwendung gesendet werden, nicht unnötig dupliziert werden.

Ich kann nicht wirklich über ORMs sprechen, aber vielleicht können einige von ihnen einen Teil dieser Arbeit für uns erledigen.

Ähnliche Techniken können bei der Übermittlung von Daten zwischen einem Webserver und einem Client verwendet werden. Andere Techniken (wie das Zwischenspeichern) werden verwendet, damit die Datenbank (oder das Web oder ein anderer Server) nicht mit doppelten Anforderungen überlastet wird.

Ich vermute, dass Standards wie SQL am besten sind, wenn sie auf einen Bereich spezialisiert bleiben und nicht versuchen, alle Bereiche eines Bereichs abzudecken.

Andererseits könnte das Komitee, das den SQL-Standard festlegt, in Zukunft auch anders denken und eine Standardisierung für eine solche zusätzliche Funktion bereitstellen. Aber es ist nicht etwas, das in einer Nacht entworfen werden kann.


1
Ich meinte ineffizient in dem Sinne, dass meine Anwendung den Aufwand und die Verzögerung von zwei Datenbankaufrufen anstelle von nur einem verursachen muss. Abgesehen davon, gibt ein Join nicht auch nur Daten in einem Format zurück, das für die Anzeige bereit ist? Oder mit einer Datenbankansicht? Sie könnten sie auch umgehen, indem Sie einfach mehr kleine Abfragen ausführen und sie in Ihrer App zusammenfügen, wenn Sie möchten, aber sie sind immer noch nützliche Tools. Ich denke nicht, dass das, was ich vorschlage, sich wesentlich von einem Join unterscheidet, abgesehen davon, dass es einfacher zu verwenden und performanter ist.

2
@Precious: Es muss kein erhöhter Overhead für das Ausführen mehrerer Abfragen vorhanden sein. In den meisten Datenbanken können Sie mehrere Abfragen in einem Stapel senden und mehrere Ergebnismengen von einer einzelnen Abfrage erhalten.
Daniel Pryden

@PreciousBodilyFluids - Das SQL-Snippet in der Antwort von ypercube ist eine einzelne Abfrage, die in einem einzelnen Datenbankaufruf gesendet wird und zwei Ergebnismengen in einer einzelnen Antwort zurückgibt.
Carson63000

5

Ich bin nicht in der Lage, mit einer richtigen, argumentierten Antwort zu antworten. Wenn ich falsch liege, können Sie mich in Vergessenheit geraten lassen (aber bitte korrigieren Sie mich, damit wir etwas Neues lernen können). Ich denke, der Grund dafür ist, dass relationale Datenbanken sich auf das relationale Modell konzentrieren, das wiederum auf etwas basiert, von dem ich nichts weiß, und das als "Logik erster Ordnung" bezeichnet wird. Was Sie vielleicht fragen, passt konzeptionell wahrscheinlich nicht in den mathematisch / logischen Rahmen, auf dem relationale Datenbanken aufbauen. Darüber hinaus lässt sich das, wonach Sie fragen, in der Regel leicht durch Diagrammdatenbanken lösen. Dies weist darauf hin, dass die zugrunde liegende Konzeptualisierung der Datenbank im Widerspruch zu dem steht, was Sie erreichen möchten.


5

Ich weiß, dass SQL Server verschachtelte Abfragen unterstützt, wenn Sie FOR XML verwenden.

SELECT id, content, (SELECT * FROM comments WHERE post_id = posts.id FOR XML PATH('comments'), TYPE) AS comments
FROM posts
WHERE id = 7
FOR XML PATH('posts')

Das Problem hierbei ist nicht die mangelnde Unterstützung durch das RDBMS, sondern die mangelnde Unterstützung von verschachtelten Tabellen in Tabellen.

Was hindert Sie außerdem daran, eine innere Verknüpfung zu verwenden?

SELECT id, content, comments.*
FROM posts inner join comments on comments.post_id = posts.id
WHERE id = 7

Sie können den inneren Join tatsächlich als verschachtelte Tabelle betrachten. Es wird immer nur der Inhalt der ersten beiden Felder wiederholt. Ich würde mir keine großen Sorgen um die Leistung des Joins machen. Der einzige langsame Teil einer solchen Abfrage ist das io von der Datenbank zum Client. Dies ist nur dann ein Problem, wenn der Inhalt eine große Datenmenge enthält. In diesem Fall würde ich zwei Abfragen vorschlagen, eine mit select id, contentund eine mit einem inneren Join und select posts.id, comments.*. Dies skaliert sogar mit mehreren Posts, da Sie immer noch nur 2 Abfragen verwenden würden.


Die Fragen sprechen dies an. Entweder müssen Sie zwei Roundtrips durchführen (nicht optimal) oder Sie müssen redundante Daten in den ersten beiden Spalten zurückgeben (ebenfalls nicht optimal). Er will die optimale Lösung (meiner Meinung nach nicht unrealistisch).
Scott Whitlock

Ich weiß, aber es gibt kein Saugen als optimale Lösung. Das Einzige, worüber ich streiten kann, ist, wo der Overhead minimal ist und von wo er abhängt. Wenn Sie die optimale Lösung suchen, können Sie verschiedene Ansätze vergleichen und ausprobieren. Sogar die XML-Lösung könnte je nach Situation langsamer sein, und ich bin mit NoSQL-Datenspeichern nicht vertraut, daher kann ich nicht sagen, ob sie etwas ähnliches wie hat for xml.
Dorus

5

Eigentlich unterstützt Oracle, was Sie wollen, aber Sie müssen die Unterabfrage mit dem Schlüsselwort "cursor" umbrechen. Die Ergebnisse werden über den offenen Cursor abgerufen. In Java werden beispielsweise Kommentare als Ergebnismengen angezeigt. Weitere Informationen hierzu finden Sie in der Oracle-Dokumentation zu "CURSOR Expression".

SELECT id, content, cursor(SELECT * FROM comments WHERE post_id = 7) AS comments
FROM posts
WHERE id = 7

1

Einige unterstützen das Verschachteln (hierarchisch).

Wenn Sie eine Abfrage wünschen, können Sie eine Tabelle haben, die sich selbst referenziert. Einige RDMS unterstützen dieses Konzept. Mit SQL Server können Sie beispielsweise Common Table Expressions (CTEs) für eine hierarchische Abfrage verwenden.

In deinem Fall wären die Posts auf Level 0 und dann wären alle Kommentare auf Level 1.

Die anderen Optionen sind 2 Abfragen oder ein Join mit einigen zusätzlichen Informationen für jeden zurückgegebenen Datensatz (die von anderen erwähnt wurden).

Beispiel für hierarchische:

https://stackoverflow.com/questions/14274942/sql-server-cte-and-recursion-example

In dem obigen Link zeigt EmpLevel die Ebene der Verschachtelung (oder Hierarchie) an.


Ich kann keine Dokumentation zu Unterergebnismengen in SQL Server finden. Auch bei Verwendung eines CTE. Mit Resultset meine ich Datenzeilen mit gerade genug stark typisierten Spalten. Können Sie Ihrer Antwort Verweise hinzufügen?
SandRock

@SandRock - Eine Datenbank sendet eine einzelne Ergebnismenge aus einer SQL-Abfrage zurück. Indem Sie Ebenen in der Abfrage selbst identifizieren, können Sie eine hierarchische oder verschachtelte Ergebnismenge erstellen, die verarbeitet werden muss. Ich denke, derzeit ist es am nächsten, dass wir Daten zurückgeben, die verschachtelt sind.
Jon Raynor

0

Es tut mir leid, ich bin nicht sicher, ob ich Ihr Problem genau verstehe.

In MSSQL können Sie nur 2 SQL-Anweisungen ausführen.

SELECT id, content
FROM posts
WHERE id = 7

SELECT * FROM comments WHERE post_id = 7

Und es werden gleichzeitig Ihre 2 Ergebnismengen zurückgegeben.


Die Person, die die Frage stellt, gibt an, dass dies weniger effizient ist, da es zu zwei Roundtrips in der Datenbank kommt. In der Regel versuchen wir, Roundtrips aufgrund des Overheads zu minimieren. Er möchte eine Rundreise machen und beide Tische zurückbekommen.
Scott Whitlock

Aber es wird eine Rundreise sein. stackoverflow.com/questions/2336362/…
Biff MaGriff

0

RDBMs basieren auf Theorie und halten sich an diese Theorie. Dies ermöglicht eine gute Konsistenz und mathematisch nachgewiesene Zuverlässigkeit.

Da das Modell einfach ist und wieder auf der Theorie basiert, ist es für die Menschen einfach, Optimierungen und viele Implementierungen vorzunehmen. Dies ist anders als bei NoSQL, wo jeder etwas anders vorgeht.

Es hat in der Vergangenheit Versuche gegeben, hierarchische Datenbanken zu erstellen, aber IIRC (kann es nicht googeln) gab es Probleme (Zyklen und Gleichheit kommen in den Sinn).


0

Sie haben ein spezifisches Bedürfnis. Es wäre vorzuziehen, Daten aus einer Datenbank in dem gewünschten Format zu extrahieren, damit Sie damit machen können, was Sie wollen.

Einige Dinge, die Datenbanken nicht so gut können, sind jedoch nicht unmöglich, sie dafür zu erstellen. Die Formatierung anderen Anwendungen zu überlassen, ist die aktuelle Empfehlung, rechtfertigt jedoch nicht, warum dies nicht möglich ist.

Das einzige Argument, das ich gegen Ihren Vorschlag habe, ist, dass ich mit dieser Ergebnismenge auf eine "SQL" -Weise umgehen kann. Es wäre eine schlechte Idee, ein Ergebnis in der Datenbank zu erstellen und nicht in der Lage zu sein, damit zu arbeiten oder es zu manipulieren. Angenommen, ich habe eine Ansicht erstellt, die so aufgebaut ist, wie Sie es vorgeschlagen haben. Wie kann ich sie in eine andere select-Anweisung einfügen? Datenbanken mögen es, Ergebnisse zu erfassen und Dinge damit zu tun. Wie würde ich es mit einem anderen Tisch verbinden? Wie würde ich Ihre Ergebnismenge mit einer anderen vergleichen?

Dann ist der Vorteil von RDMS die Flexibilität von SQL. Die Syntax zum Auswählen von Daten aus einer Tabelle kommt einer Liste von Benutzern oder anderen Objekten im System ziemlich nahe (zumindest das ist das Ziel.). Ich bin mir nicht sicher, ob es Sinn macht, etwas völlig anderes zu tun. Sie haben sie noch nicht einmal dazu gebracht, Prozedurcode / Cursor oder BLOBS von Daten sehr effizient zu handhaben.


0

Meiner Meinung nach liegt dies hauptsächlich an SQL und der Art und Weise, wie aggregierte Abfragen ausgeführt werden. Aggregatfunktionen und Gruppierungen werden für große zweidimensionale Rowsets ausgeführt, um Ergebnisse zurückzugeben. So war es von Anfang an und es ist sehr schnell (die meisten NoSQL-Lösungen sind bei der Aggregation recht langsam und stützen sich auf denormalisierte Schemata anstelle komplexer Abfragen).

Natürlich hat PostgreSQL einige Funktionen aus der objektorientierten Datenbank. Entsprechend dieser E-Mails ( Nachricht ) können Sie das erreichen, was Sie benötigen, indem Sie ein benutzerdefiniertes Aggregat erstellen.

Persönlich verwende ich Frameworks wie Doctrine ORM (PHP), die die Aggregation auf der Anwendungsseite durchführen und Funktionen wie das verzögerte Laden unterstützen, um die Leistung zu steigern.


0

PostgreSQL unterstützt eine Vielzahl strukturierter Datentypen, einschließlich Arrays und JSON . Mit SQL oder einer der eingebetteten prozeduralen Sprachen können Sie Werte mit beliebig komplexer Struktur erstellen und an Ihre Anwendung zurückgeben. Sie können auch Tabellen mit Spalten eines beliebigen strukturierten Typs erstellen. Sie sollten jedoch sorgfältig prüfen, ob Sie Ihr Design unnötig denormalisieren.


1
Dies scheint nichts Wesentliches zu bieten über Punkte, die in den vorherigen 13 Antworten gemacht und erklärt wurden
Mücke

Die Frage erwähnt speziell JSON und diese Antwort ist die einzige, die darauf hinweist, dass JSON in Abfragen von mindestens einem RDBMS zurückgegeben werden kann. Ich hätte die Frage lieber kommentiert, um zu sagen, dass sie auf einer falschen Prämisse beruht und daher keine endgültige Antwort erwarten kann. StackExchange lässt mich das jedoch nicht zu.
Jonathan Rogers
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.