Feste string Breite

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Antworten
o0omax
User
Beiträge: 3
Registriert: Freitag 24. Dezember 2021, 01:10

Hallo,

wie bekomme ich es hin, dass die Einträge der Abfrage einen festen Abstand haben?

IST:
Fussball 20
Hallenschuhe 200
Schienbeinschoner 30
Tennisbälle 30
Tennischläger 200
Thermounterwäsche 30

SOLL:
Bild

Code: Alles auswählen

import sqlite3, os

db_Pfad = os.getcwd() + '\\Sportgeschäft.db'
db_Verbindung = sqlite3.connect(db_Pfad)
db_Cursor = db_Verbindung.cursor()
sql = 'select a.Artikel_Beschreibung, sum((aab.Menge*a.Artikel_Preis)) as Umsatz from Artikel a inner join ArtikelAufBestellungen aab on a.Artikel_ID = aab.Artikel_ID group by a.Artikel_Beschreibung order by 1;'
db_Cursor.execute(sql)
Abfrage = db_Cursor.fetchall()

for i in range(len(Abfrage)):
    print(Abfrage[i][0], Abfrage[i][1])
Sirius3
User
Beiträge: 17745
Registriert: Sonntag 21. Oktober 2012, 17:20

Pfade setzt man nicht per String+ zusammen, denn Pfade sind keine einfachen Strings; dafür gibt es das pathlib-Modul.
In Deinem Fall hängst Du aber nur das aktuelle Arbeitsverzeichnis an, was ja automatisch passiert bei relativen Pfaden.
Variablennamen schreibt man komplett klein. Ob jede Variable ein db_-Präfiy braucht? Zumindest `verbindung` und `cursor` haben in dem Kontext klaren Bezug zur Datenbank.
`abfrage` ist falsch benannt, da der Inhalt das Ergebnis einer Abfrage ist.
Über den Index einer Liste iteriert man nicht, weil es viel klarer ist gleich über die Elemente zu iterieren.

Code: Alles auswählen

import sqlite3
db_pfad = 'Sportgeschäft.db'
verbindung = sqlite3.connect(db_pfad)
cursor = verbindung.cursor()
sql = '''select a.Artikel_Beschreibung, sum((aab.Menge*a.Artikel_Preis)) as Umsatz
    from Artikel a
    inner join ArtikelAufBestellungen aab
    on a.Artikel_ID = aab.Artikel_ID
    group by a.Artikel_Beschreibung
    order by 1'''
cursor.execute(sql)
ergebnis = cursor.fetchall()
for beschreibung, preis in ergebnis:
    print(beschreibung,  preis)
Zum Problem: erster Schritt ist es, die maximale Länge zu ermitteln und dann diese bei der Ausgabe in einem Formatstring (siehe Doku) zu benutzen.
o0omax
User
Beiträge: 3
Registriert: Freitag 24. Dezember 2021, 01:10

Vielen Dank!!

Vielen Dank auch für die Formattierungshilfen!

Habe es jetzt wie folgt gelöst: das von dir vorgeschlagene str().format hat bei mir auch nach längerem googlen nicht funktioniert.

Code: Alles auswählen

import sqlite3

db_pfad = 'Sportgeschäft.db'
verbindung = sqlite3.connect(db_pfad)
cursor = verbindung.cursor()

sql = '''select a.Artikel_Beschreibung, sum((aab.Menge*a.Artikel_Preis)) as Umsatz
from Artikel a
inner join ArtikelAufBestellungen aab
on a.Artikel_ID = aab.Artikel_ID
group by a.Artikel_Beschreibung
order by 1;'''

cursor.execute(sql)
ergebnis = cursor.fetchall()

print(f'{"Artikel":<20s} {"Umsatz"}')
for beschreibung, umsatz in ergebnis:
    print(f'{beschreibung:<20s} {umsatz}')
Benutzeravatar
__blackjack__
User
Beiträge: 13099
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ergänzende Anmerkungen: Weder Verbindung noch Cursor werden geschlossen. Da bietet sich ``with`` in Verbindung mit `contextlib.closing()` an.

Bei ORDER BY würde man eher keine Spaltennummer sondern den passenden Namen verwenden. Das ist verständlicher und auch robuster bei Programmänderungen da keine magische Zahl stehen zu haben an die man denken muss.

Falls das Datenbankschema noch nicht in Stein gemeisselt ist, würde ich was gegen `artikelaufbestellungen` machen — da gehören Unterstriche zwischen die Worte. IMHO noch schlechter lesbar wird das wenn der Name auf Grossbuchstaben normalisiert wird (`ARTIKELAUFBESTELLUNGEN`). SQLite3 scheint das nicht zu machen, aber das ist ja auch nicht garantiert, das zukünftige Versionen das nicht anders handhaben.

Dann sollte man auch gleich den Tabellennamen aus den Spalten in der jeweiligen Tabelle rausnehmen. Man kann sich dann bei der Abfrage den Aliasnamen sparen und es wird trotzdem ein kleines bisschen weniger Tipparbeit, ohne das man irgendwelche Informationen dadurch verliert. Durch eine Indirektion weniger wird es sogar etwas leichter verständlich.

Noch schöner wird es wenn man den sperrigen Tabellennamen `artikel_auf_bestellungen` auf `bestellung` reduziert, denn das scheint das ja letztlich zu sein. Insgesamt dann eine schön lesbare SQL-Abfrage ohne irgendwelche redundanten Informationen in den Namen oder kryptische Kürzel/Aliasnamen:

Code: Alles auswählen

SELECT artikel.beschreibung,
       SUM((bestellung.menge * artikel.preis)) AS umsatz
FROM artikel
INNER JOIN bestellung ON artikel.id = bestellung.artikel_id
GROUP BY artikel.beschreibung
ORDER BY artikel.beschreibung
Bevor man sich für Tabellenausgabe selbst etwas bastelt: Es gibt da schöne Bibliotheken wie `rich` (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import sqlite3
from contextlib import closing

import rich
from rich.box import ROUNDED
from rich.table import Column, Table
from rich.text import Text

DB_FILENAME = "Sportgeschäft.db"


def main():
    with closing(sqlite3.connect(DB_FILENAME)) as verbindung:
        with closing(verbindung.cursor()) as cursor:
            cursor.execute(
                """
                SELECT artikel.beschreibung,
                       SUM((bestellung.menge * artikel.preis)) AS umsatz
                FROM artikel
                INNER JOIN bestellung
                      ON artikel.id = bestellung.artikel_id
                GROUP BY artikel.beschreibung
                ORDER BY artikel.beschreibung
                """
            )
            ergebnis = cursor.fetchall()

    gesamt_umsatz = sum(umsatz for _, umsatz in ergebnis)

    table = Table("Beschreibung", Column("Umsatz", justify="right"))
    for beschreibung, umsatz in ergebnis:
        table.add_row(beschreibung, f"{umsatz:.2f}")

    table.rows[-1].end_section = True
    table.add_row(
        Text("Gesamt:", "bold", justify="right"),
        Text(f"{gesamt_umsatz:.2f}", "bold underline"),
    )
    rich.print(table)


if __name__ == "__main__":
    main()
Beispielausgabe (im Terminal sind die erste und letzte Zeile fett gedruckt und die Gesamtsumme unterstrichen):

Code: Alles auswählen

╭──────────────────┬─────────╮
│ Artikel          │  Umsatz │
├──────────────────┼─────────┤
│ Tannenbaum       │   42.23 │
│ Schoko-Osterhase │ 4711.00 │
│ (gebraucht)      │         │
├──────────────────┼─────────┤
│          Gesamt: │ 4753.23 │
╰──────────────────┴─────────╯
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
o0omax
User
Beiträge: 3
Registriert: Freitag 24. Dezember 2021, 01:10

Vielen Dank!

Dann muss ich mir die Bibliothek 'rich' nochmal genauer anschauen. Gibt es dafür eine Doku?

Der Grund für den kryptischen Namen ArtikelAufBestellungen (bzw. Artikel_Auf_Bestellungen) ist, dass ich eine Tabelle erstellt habe, bei dem jede Bestellung einem Kunden zugeordnet ist (Tabellenname: Bestellungen). Wieviele Artikel und welche Artikel der Kunde bestellt hat sollte dann in ArtikelAufBestellungen vermerkt sein. Soweit ich weiß, ist das gelebte Praxis, oder?

Hier meine Datenbank:
Bild
Benutzeravatar
__blackjack__
User
Beiträge: 13099
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@o0omax: Ja für `rich` gibt es eine Dokumentation. Und die Module in dem Package, inklusive `rich` selbst, sind auch ausführbar und zeigen Beispiele wo man sich dann auch im Quelltext anschauen kann wie die jeweilige Ausgabe zustande kommt.

Die Datenbankbeziehungen sehen sinnvoll aus. Der Name ist nicht so gängig. Das wäre eher `posten` oder `bestellposten` würde ich sagen.

Ich würde wie gesagt die redundanten Informationen aus den Spaltennamen entfernen und alles klein schreiben. Und die Tabellennamen in Einzahl. Denn wenn man das mal als Entety-Relationship-Diagramm modelliert oder statt SQL selbst zu klöppeln ein ORM verwendet, dann sind die Entitäten- und die ORM-Klassen ja jeweils die Beschreibung *einer* Ausprägung in der Tabelle.

Der `posten`-Tabelle würde ich auch eine eigene ID als Schlüssel verpassen statt einen zusammengesetzten Schlüssel zu verwenden. Ist vielleicht auch ein bisschen Geschmackssache, aber es erleichtert auch den Einsatz von ORM-Bibliotheken.

Womit wir bei SQLAlchemy wären: Ich setze das fast grundsätzlich ein wenn ich an eine SQL-Datenbank muss. Selbst wenn man den ORM-Teil nicht verwendet, ist das besser/sicherer wenn man keine statischen SQL-Abfragen mehr hat, sondern anfängt die dynamisch aufzubauen, als sich da dynamisch Teil-SQL-Abfragen als Zeichenketten und mit Platzhaltern zusammen zu frickeln.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13099
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Das ganze mal mit SQLAlchemy, ohne ORM:

Code: Alles auswählen

#!/usr/bin/env python3
import rich
import sqlalchemy as sa
from rich.box import ROUNDED
from rich.table import Column, Table
from rich.text import Text
from sqlalchemy.sql.functions import sum as sql_sum

DATABASE_URL = "sqlite:///Sportgeschäft.db"


meta_data = sa.MetaData()

article_table = sa.Table(
    "article",
    meta_data,
    sa.Column("id", sa.INTEGER, primary_key=True),
    sa.Column("description", sa.TEXT, nullable=False),
    sa.Column("price", sa.NUMERIC, nullable=False),
)
customer_table = sa.Table(
    "customer",
    meta_data,
    sa.Column("id", sa.INTEGER, primary_key=True),
    sa.Column("name", sa.TEXT, nullable=False),
    sa.Column("surname", sa.TEXT, nullable=False),
    sa.Column("email", sa.TEXT, nullable=False, unique=True),
)
order_table = sa.Table(
    "order",
    meta_data,
    sa.Column("id", sa.INTEGER, primary_key=True),
    sa.Column(
        "customer_id", sa.ForeignKey(customer_table.c.id), nullable=False
    ),
)
order_item_table = sa.Table(
    "order_item",
    meta_data,
    sa.Column("id", sa.INTEGER, primary_key=True),
    sa.Column("order_id", sa.ForeignKey(order_table.c.id), nullable=False),
    sa.Column("article_id", sa.ForeignKey(article_table.c.id), nullable=False),
    sa.Column("amount", sa.INTEGER, nullable=False),
    sa.UniqueConstraint("order_id", "article_id"),
)


def main():
    engine = sa.create_engine(DATABASE_URL)
    meta_data.create_all(engine)

    rows = engine.execute(
        sa.select(
            [
                article_table.c.description,
                sql_sum(
                    order_item_table.c.amount * article_table.c.price
                ).label("sales"),
            ]
        )
        .select_from(sa.join(article_table, order_item_table))
        .group_by(article_table.c.description)
        .order_by(article_table.c.description)
    ).fetchall()

    total_sales = sum(row.sales for row in rows)

    table = Table(
        "Beschreibung", Column("Umsatz", justify="right"), box=ROUNDED
    )
    for row in rows:
        table.add_row(row.description, f"{row.sales:.2f}")

    table.rows[-1].end_section = True
    table.add_row(
        Text("Gesamt:", justify="right"),
        Text(f"{total_sales:.2f}", "underline"),
        style="bold",
    )
    rich.print(table)


if __name__ == "__main__":
    main()
Ausgabe:

Code: Alles auswählen

╭───────────────────┬────────╮
│ Beschreibung      │ Umsatz │
├───────────────────┼────────┤
│ Fussball          │  20.00 │
│ Hallenschuhe      │ 200.00 │
│ Schienbeinschoner │  30.00 │
│ Tennisbälle       │  30.00 │
│ Tennisschläger    │ 100.00 │
│ Thermounterwäsche │  30.00 │
├───────────────────┼────────┤
│           Gesamt: │ 410.00 │
╰───────────────────┴────────╯
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten