Wie schreibt man Querys? (pyodbc -> MSSQL)

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

Schönen Guten Tag

Ich möchte mein eher bescheidenes Python-Repertoire auf SQL Datenbanken ausweiten bzw. konkret möchte ich Daten direkt aus einer Datenbank ziehen und nicht den Umweg über eine CSV Datei gehen. Nun habe ich unsere IT überredet, dass sie mir einen Benutzer mit Lesezugriff auf eine unserer Datenbanken gewährt. Somit kann ich mir das zumindest mal in Ruhe an einem Realen Beispiel anschauen.

Bei der Datenbank handelt es sich um eine MSSQL Datenbank, mit mehreren Datenloggern die in unterschiedlichen Intervallen Messdaten, wie Temperatur oder Luftfeuchte aufzeichnen. Ich habe erfolgreich eine Verbindung aufgebaut (cnxn) und einen Cursor erstellt (cursor). Ich hab eine Liste aller Tabellen der Datenbank erstellt und in Strings umformatiert. Weiters hab ich eine Möglichkeit gefunden in alle Tabellen rein zu schauen. Anbei der Code den ich hierfür verwendet habe.

Code: Alles auswählen

 #Connection
cnxn = pyodbc.connect('DRIVER={SQL Server};SERVER='+server+';DATABASE='+database+';UID='+username+';PWD='+ password)
#Cursor = ~Zugriff
cursor = cnxn.cursor()

#sql = ~Anfrage; Code der von der Datenbank ausgeführt wird
sql = "SELECT Distinct TABLE_NAME FROM information_schema.TABLES"

#Zugriff auf die Datenbank - Liste aller Tabellen in der Datenbank erstellen
cursor.execute(sql)
tab = cursor.fetchall()

#in tab sind die Einträge 'pyodbc.Row', hiernach 'String'
import re
listen = []
for k in tab:
    x = str(k)
    listen += [re.sub(r'[^a-zA-Z]', '', x)]

#sql bauen um sich in der Datenbank um zu sehen
was = '*'
vonwo = listen[4]
sql = "SELECT "+was+" FROM "+vonwo

#Zugriff mit Pandas
import pandas as pd
test = pd.read_sql(sql, cnxn)
Ich weiß nun:
* dass es in der Datenbank 47 Tabellen gibt, 2 davon Sys und 6 davon cdc, diese 8 konnte ich nicht anschauen.
* dass eine Tabelle "DataPoints" heißt und vermutlich die Datenlogger sind. Hiervon würde ich gerne genau einen auswählen.
* dass eine Tabelle "pointValue" heißt. Diese scheint zu groß für "fetchall()", aber mit fetchone() wurden 5 Werte gefunden, mit fetchval() wurde einer dieser 5 gefunden.

Meine Fragen sind:
* Gehe ich recht in der Annahme, dass die einzelnen Tabellen der Datenbank über eine gemeinsame Spalte verknüpft werden können? Beispielsweise hat "DataPoints" eine Spalte "ID" und ich hoffe nun, dass "pointValue" eine Spalte "DataPointID" hat.
* Wie kann ich mir die Spaltenüberschriften der Tabelle "pointValue" anschauen?
* Wie schreibe ich ein Querry (ein SQL), welches die Tabellen "DataPoints" und "pointValue" so kombiniert, dass ich die Messwerte von einem logger bekomme
* Bonus Frage, dass ganze würde ich dann noch gern auf einen Zeitraum beschränken

Ich hoffe ich konnte mein Anliegen etwas klar darstellen und das jemand die Muse hat mir zu helfen :)

Vielen Dank
Tob

ps.: bitte lasst mich wissen, wenn ich wichtige (oder interessante) Fakten vergessen hab an zu geben :)
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

Der Aufbau der Tabellen sollte von Deiner IT-Abteilung kommen. Aus den Feldnamen irgendwelche Zusammenhänge zu raten, ist nicht der richtige Weg.
Um einen Überblick über eine Datenbank zu bekommen, nimmt man am besten eine GUI, statt da selbst was zu programmieren: https://docs.microsoft.com/en-us/sql/to ... rver-ver16

Dann stückelt man nicht selbst SQL-Strings zusammen, sondern nutzt SQLAlchemy, wenn man dann weiß, wie die Tabellen aufgebaut sind, um auf die Daten zuzugreifen.

Sonstige Anmerkungen zum Code:
Importe stehen am Anfang der Datei, nicht irgendwo zwischendrin.
Benutze sprechende Variablennamen und nicht irgendwelche kryptischen Abkürzungen (cnxn?), oder einbuchstabige Namen wie x oder k, die absolut nichts aussagen.
Statt eine Einelementige Liste zu erzeugen, um sie dann an eine andere Liste anzuhängen, benutzt man `append`. Die String-Repräsentation eines Tuples (oder hier pyodbc.Row) ist nicht dafür gedacht, dass man damit weiterarbeitet. Da was mit regulären Ausdrücken zu machen, grenzt an Körperverletzung für jeden Pythonprogrammierer.
Schau Dir mal die Typen an, die Du da benutzt, und wie man damit umgeht.
`pd.read_sql` erwartet eine Sqlite3-Connection oder eine SQL-Alchemy-Session, alles andere wird nicht offiziell unterstützt.

So könnte man alle Tabellennamen ermitteln:

Code: Alles auswählen

from contextlib import closing
import pyodbc

CONNECTION_STRING = 'DRIVER={SQL Server};SERVER=server;DATABASE=database;UID=username;PWD=password'

def main():
    with closing(pyodbc.connect(CONNECTION_STRING)) as database:
        with closing(database.cursor()) as cursor:
            cursor.execute("SELECT DISTINCT table_name FROM information_schema.tables")
            table_names = [name for name, in cursor]
        print(table_names)

if __name__ == "__main__":
    main()
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

Hallo

Danke für deine Antwort Sirius3.

Natürlich wäre es ein leichtes, wenn ich von unserer IT handwarm alle Infos bekomme und am besten noch Schoki oben drauf, aber das ist leider nicht wie es funktioniert. Entweder ich bekomm das zum Laufen, oder wir brauchen es nicht so dringend.
Um ein graphisches Programm könnte ich prinzipiell ansuchen, ich kann mir aber nicht vorstellen, dass ein Umsehen direkt mit Python nicht möglich sein soll. Prinzipiell kenn ich die Datenbank, ich hab Zugriff auf unser graphisches Auswertetool, wo alle Daten sind und ich hab Zugriff zum Verantwortlichen der Logger, der mir Auskunft geben kann. Ich weiß, wo der Logger physisch hängt, den ich suche und ich weiß welche Seriennummer er hat und welche Werte er gerade aufzeichnet und welche er vor einem Jahr aufgezeichnet hat. Aber ich verstehe natürlich deinen Punkt, sinnvoller und zielführender wäre es, wenn ich mir das alles zeigen lassen kann, anstatt es als Quereinsteiger zu erzwingen.

Ich werde mir SQLAlchemie als nächstes anschauen, wie gesagt ich hab schon ein gutes Gefühl, was ich von den Tabellen brauche.
Leider bleiben meine drei Fragen:
* Gehe ich recht in der Annahme, dass die einzelnen Tabellen der Datenbank über eine gemeinsame Spalte verknüpft werden können? Beispielsweise hat "DataPoints" eine Spalte "ID" und ich hoffe nun, dass "pointValue" eine Spalte "DataPointID" hat.
* Wie kann ich mir die Spaltenüberschriften der Tabelle "pointValue" anschauen?
* Wie schreibe ich ein Querry (ein SQL), welches die Tabellen "DataPoints" und "pointValue" so kombiniert, dass ich die Messwerte von einem Logger bekomme
* Bonus Frage, dass ganze würde ich dann noch gern auf einen Zeitraum beschränken

Danke für die Hilfe, ich komme mit einem Update nachdem ich mit SQLAlchemie genauer angeschaut habe.
Tob

p.s.: Ich geh noch kurz auf deine anderen Antworten ein, weil mir vorkommt du suchst "Fehler" um mich klein zu halten! Bitte hol mal Luft und wenn du dann Lust hast mir zu helfen sehr gerne :)

Bezüglich der Bezeichnung "cnxn", dass ist die Abkürzung die in der Dokumentation von pyodbc und im proof of concept von Microsoft verwenden wird. Da bin ich wohl nicht der Richtige für dein "Das muss conn heißen, nicht cnxn! Niemand kann jemals verstehen was diese Abkürzung soll, auch nicht wenn er sieht das sie mit .connect erzeugt wurde!" (hab ich deinen Ton erwischt oder zu sehr übertrieben?)
https://github.com/mkleehammer/pyodbc/w ... ng-started
https://docs.microsoft.com/en-us/sql/co ... rver-ver16

Zu deiner Verteidigung, die pydobc dokumentation verwendet an anderer Stelle dann "conn" als Abkürzung für die Verbindung.

Auch bin ich eher schmerzbefreit, wenn es darum geht das Wegwerf Variablen in for-schleife sprechend sein sollen. Hier erwarte ich vom Leser, dass er entweder weiß wie for Schleifen funktionieren oder den Code leider nicht versteht.

Mal direkt gefragt, findest du nachfolgenden Code in irgendeiner weiße deutlicher? Bzw. vielleicht hast du mir Beispielabkürzungen, wie ich es besser machen kann?

Code: Alles auswählen

#in tab sind die Einträge 'pyodbc.Row', hiernach 'String'
import re
ListeDerTabellen = tab
listen = []
for Jeder_Eintrag_in_der_Liste_der_Tabelle in ListeDerTabellen:
    StringDesGeradeIteriertenEintrags = str(Jeder_Eintrag_in_der_Liste_der_Tabelle)
    listen += [re.sub(r'[^a-zA-Z]', '', StringDesGeradeIteriertenEintrags)]
pps.: prinzipiell wollte ich Pandas gar nicht verwenden, aber die Tabellen waren leichter zum anschauen. Ich komme auf die gleichen Resultate mit fetchall().

ppps.: Tabellennamen: Ich hab nun Sirius Methode ebenfalls aufgenommen und mein "original" Skript beinhaltet nun folgende 4 Methoden um die Tabellen Namen ab zu rufen. Auf die Schnelle hab ich es nun aber nicht hinbekommen, dass ich mir Sirius3 Methode die Variable tab4 erstelle. Aber daran soll es nicht scheitern.

Code: Alles auswählen

#Connection
cnxn = pyodbc.connect('DRIVER={SQL Server};SERVER='+server+';DATABASE='+database+';UID='+username+';PWD='+ password)

#sql = ~Anfrage; Code der von der Datenbank ausgeführt wird
sql = "SELECT Distinct TABLE_NAME FROM information_schema.TABLES"

#Zugriff auf die Datenbank
#Cursor = ~Zugriff
cursor = cnxn.cursor()
cursor.execute(sql)
tab = cursor.fetchall()

#Zweiter Zugriff, andere Methode
cursor = cnxn.cursor()
tab2 = []
for table_name in cursor.tables(tableType='TABLE'):
    tab2 += [table_name]
    
#Dritter Zugriff mit Pandas
import pandas as pd
tab3 = pd.read_sql(sql, cnxn)

#Vierter Zugriff á la Sirius3
from contextlib import closing
CONNECTION_STRING = 'DRIVER={SQL Server};SERVER='+server+';DATABASE='+database+';UID='+username+';PWD='+ password
def main():
    with closing(pyodbc.connect(CONNECTION_STRING)) as database:
        with closing(database.cursor()) as cursor:
            cursor.execute(sql)
            tab4 = [name for name, in cursor]
        print(tab4)
if __name__ == "__main__":
    main()

#Connection schließen; uU redundant.
cnxn.close()
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OWT: Ja man kann Datensätze aus verschiedenen Tabellen in relationalen Datenbanken in der Regel über Spalten mit Fremdschlüsseln verknüpfen.

Die Spaltennamen stehen in einer anderen Tabelle im `information_schema`. da müsstest Du schauen wie das in SQL Server aufgebaut ist.

Oder Du fragst die bei einer Anfrage vom `Cursor`-Objekt über dessen `description`-Attribut ab.

Aber an der Stelle wäre es wirklich einfacher ein grafisches Werkzeug zu verwenden statt sich das selbst zu basteln oder auch einen Konsolen-Client falls es so etwas für ODBC gibt‽

Bei den beiden letzten Punkten kann man nicht wirklich was sagen ohne die konkreten Tabellen zu kennen. Beides sollte möglich sein. Du musst halt die notwendigen Tabellen verknüpfen entweder per JOIN oder über Bedingungen beim WHERE, und den Zeitraum im WHERE-Teil einschränken.

Am besten schaust Du Dich nach einem SQL-Tutorial um und spielst ein bisschen mit SQLite oder so um das gelernte auszuprobieren.

Zum p.s.: Hol selber mal Luft und nimm technische, konstruktive Kritik nicht persönlich. Hier wird Dein Code angegriffen, nicht Deine Person.

Nein, `cnxn` muss nicht `conn` heissen, sondern `connection`.

Und der Code mit den komischen langen Namen ist natürlich nicht deutlicher weil die Namen jetzt zwar extra lang sind, aber dem Leser nicht vermitteln was die Werte *bedeuten*. Grunddatentypen haben zudem nichts in Namen zu suchen und sollten schon gar nicht den ganzen Namen ausmachen.

`ListeDerTabellen` wäre einfach nur `tabellen`. Ist aber inhaltlich falsch, denn es sind `rows`. Man sollte in aller Regel auch nicht durch einen Zuweisung einen schlechten Namen zu einem guten machen, sondern gleich den guten verwenden. Also `tab` sollte schon von Anfang an `rows` heissen.

`Jeder_Eintrag_in_der_Liste_der_Tabelle` ist inhaltlich falsch, denn der Wert steht gar nicht für jeden Eintrag in der Liste, sondern nur für *einen* Eintrag in der Liste. Die heisst `rows`, also ein Eintrag `row`. Oder man entpackt das eine Element was in `row` steckt, gleich und bindet es an den passenden Namen: `table_name`. Womit man auch diesen `re.sub()`-Horror los geworden ist. Und einen weiteren üblen Namen der zudem anscheinend die Namenschreibweisenvariationen komplettieren sollte‽

Und auch `listen` ist inhaltlich falsch, denn da wird nur *eine* Liste dran gebunden, deren Elemente Zeichenketten sind. Und zwar Tabellennamen, also `table_names`.

Zwischenstand:

Code: Alles auswählen

    table_names = []
    for table_name, in rows:
        table_names += [table_name]
Dann ist ``+=`` das gleiche wie die `extend()`-Methode und dazu da eine Liste mit mehreren Elementen zu erweitern. Extra einelementige Listen erstellen um die dann mit `extent()`/``+=`` zu verwenden ist unsinnig, dafür gibt es die `append()`-Methode:

Code: Alles auswählen

    table_names = []
    for table_name, in rows:
        table_names.append(table_name)
Das wäre jetzt schon deutlich einfacherer, verständlicherer Code, den man noch mal kürzer fassen kann, weil sich hier eine „list comprehension“ anbietet:

Code: Alles auswählen

    table_names = [table_name for table_name, in rows]
Kommt vielleicht aus dem Beitrag von Sirius3 bekannt vor. Falls einem das mit dem Komma bei einelementigen `Row`-Objekten ein bisschen zu leicht zu übersehen ist, könnte man es auch so schreiben:

Code: Alles auswählen

    table_names = [row[0] for row in rows]
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
noisefloor
User
Beiträge: 3843
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,
Aber ich verstehe natürlich deinen Punkt, sinnvoller und zielführender wäre es, wenn ich mir das alles zeigen lassen kann, anstatt es als Quereinsteiger zu erzwingen.
Nee, aber wenn man auf einfachem Weg Infos zu DB Struktur bekommen kann, dann sollte man das dankend annehmen. Natürlich kann man eine DB per reverse engineering durchforsten und versuchen, zu verstehen. Aber a) hast du immer das Restrisiko, dabei Fehlannahmen zu machen und b) verschwendest du im Prinzip deine Zeit, wenn du die Infos anders bekommen kannst. Und die lernst IMHO so gut wie kein Python, wenn du eine DB selber versuchst zu verstehen. Weil dafür brauchst du primär nur SQL-Statements, Python ist in dem Fall nur der Wrapper, um die Statements abzusetzen. Python wird dann interessant, wenn du weißt, wie du an welche Daten kommst und diese dann mit Python weiter verarbeitest.

Wenn du kein SQL kannst -> auf jeden Fall die Grundlagen lernen. Wenn dir so Sachen wie 1:n oder n:m Beziehungen in relationalen Datenbanken (im Moment) nichts sagen, dann wirst du auch schwerlich sinnvolle Queries zur Abfrage der Daten absetzen können. Egal, ob in nacktem SQL oder mit Hilfe von SQLAlchemy.

Gruß, noisefloor
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

Hallo Zusammen

Danke für die Antworten, und den Code.
Sirius3 entschuldige bitte, ich war unnötig unhöflich.

Mein Problem konnte ich auch lösen,
mit .columns konnte ich die spalten von der Tabelle mit den Messwerten sehen (pointValues), damit hatte sich viel erübrigt. Die Zeit verstehe ich noch nicht.

Der Query sieht nun so aus

Code: Alles auswählen

spalten = 'pointValue, ts' #messwert und Zeitstempel?
vonwo = 'pointValues' #tabelle 
bedingung ='dataPointId = 1' #Logger ID (zw. 1 - 1068?)
sql = "SELECT "+spalten+" FROM "+vonwo+" WHERE "+bedingung
Schönen Abend
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OWT: Gewöhn Dir bitte das zusammenstückeln von SQL-Anfragen als Zeichenketten bitte gar nicht erst an.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
noisefloor
User
Beiträge: 3843
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

selbst wenn es ein "normaler" String wäre baut man den nicht mit + zumsammen, sondern mit f-Strings oder der format-Methode von Strings.

SQL-Abfrage baut man so sowieso nicht zusammen, weil das anfällig für SQL Injections ist. Die Python DB-API 2.0 (der quasi alle Python-Module für Datenbankabfragen folgen, kennt Wege für Platzhalter in SQL Statements, die sicher gegen SQL Injections sind.
Die Werte für `select` und `from` zu Parametrieren ist ungewöhnlich und normalerweise nicht notwendig. Das macht man normalerweise nur für die Parameter (wie die `where` Klausel).

Gruß, noisefloor
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Bei den schlechten Namen die offenbar in der Datenbank verwendet werden, würde ich persönlich ja noch eher auf SQLAlchemy setzen, um die im Programm dann auf sinnvolle Namen abzubilden.

Tabellen werden sinnvoller in der Einzahl benannt, also `pointValue`, weil so ein CREATE TABLE ja *einen* Datensatz beschreibt. Wie bei Klassen. Und wie bei Klassen ist es dann komisch wenn ein Attribut, also im SQL-Fall eine Spalte, genau so heisst wie die Klasse/Tabelle. Denn das ist ja in beiden Fällen der Name von etwas aus dem das ”Ding” besteht, und ein `pointValue` besteht aus einem `pointValue` und einem Zeitstempel klingt ja schon extrem komisch. Das müsste eher heissen eine Messung besteht aus einem Wert und einem Zeitstempel. Also als englische Namen dann `Measurement`, `value`, und `timestamp`.

Mit dem ORM könnte es dann so aussehen, ohne irgendwelches Zeichenkettengefummel für die Anfrage (Spaltentypen/-Constraints sind geraten):

Code: Alles auswählen

#!/usr/bin/env python3
from sqlalchemy import TIMESTAMP, Column, Float, Integer, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session

Base = declarative_base()


class Measurement(Base):
    __tablename__ = "pointValues"

    id = Column(Integer, primary_key=True)
    data_point_id = Column("dataPointId", Integer, nullable=False)
    value = Column("pointValue", Float, nullable=False)
    timestamp = Column("ts", TIMESTAMP, nullable=False)


def main():
    engine = create_engine(
        "mssql+pyodbc://username:password@server/database?driver=SQL+Server"
    )
    session = Session(engine)

    query = (
        session.query(Measurement.value, Measurement.timestamp)
        .filter_by(data_point_id=1)
        .order_by(Measurement.timestamp)
    )
    for value, timestamp in query.all():
        print(value, timestamp)


if __name__ == "__main__":
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

Hallo Zusammen

Ich gehe nun lieber drei Schritte zurück und versuche euch genau zuzuhören. Leider tue ich mir schwer den konstruktiven Teil eurer Antworten um zu setzten, hauptsächlich, weil mir die Erfahrung fehlt und ich es nicht verstehe. Leider (oder zum glück) ist mir verstehen aber relativ wichtig. :)

Ich hab nun folgende Probleme mit meinem Query au euren Kommentaren rausgelesen:
1) Das Query ist selbst geschrieben, anstatt generiert zu werden.
2) "Man baut Strings nicht mit '+' sondern mit f-Strings zusammen?
3) Das Query verlangt nur die Spalten Messwert und Zeit anstatt allen Spalten und auch nur von einer Tabelle, anstatt von der gesamten Datenbank
4) Mein Query ist anfällig für SQL Injektions

ad 1) hier war der Vorschlag, dass ich das mit 'SQLAlchemy' mache. Die Doku hab ich mir etwas angesehen und in Essenz hat SQLAlchemy alle Fuktionen von pyodbc und mehr, richtig? Geht es hier drum, dass ich SQL3lite und SQLAlchemy und Pandas verwende, anstatt pyodbc?

ad 2) Warum? Subjektiv kommt mir vor, dass jedes Python Beginner Tutorial erklährt, dass man Strings addieren kann. Hätte vll jemand die Muse mein Beispiel von oben um zu bauen zu f-string?

ad 3) Der Datensatz ist so groß, dass ich ihn als ganzen nicht herunterladen will, daher möchte ich möglichst nur die sinnvollen Daten laden. Warum sollte ich 5 Spalten laden, wenn ich nur zwei davon brauche? Ich glaub ich hab deine Kritik falsch verstanden oder ist es echt üblich alle Spalten (=Select) von allen Tabellen (=from) zu laden?

ad 4) Eine SQL Injektion passieren, wenn ein User eine "böswillige" Angabe macht, die als SQL Code ausgeführt wird, richtig? Aber jeder User der Angaben macht, hat das ganze Scipt in der Hand und kann daher alles machen. Ich gehe aber davon aus , dass es ausreichend ist, dass der Account nur leserechte hat.

Danke für den Input und einen schönen Guten Morgen :)

Nachtrag, den letzten Post von blackjack: das mit pointValue und pointValues hat mich auch kurz kalt erwischt, daran kann ich aber nichts ändern. Dein Code beantwortet sicher ein paar meiner Fragen, dafür muss ich mir aber mehr zeit lassen zum verstehen. Danke
Benutzeravatar
noisefloor
User
Beiträge: 3843
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

zu ad2) "Subjektiv kommt mir vor, dass jedes Python Beginner Tutorial erklährt, dass man Strings addieren kann." - technisch geht das auch, aber es ist halt nicht der pythonische Weg. Und wenn es ein gutes Tutorial ist, dann werden da f-Strings oder die format-Methode verwendet. Letztere haben auch den Vorteil, dass anderen Datentypen direkt umgewandelt werden, so dass die Ausgabe erfolgen kann. Das funktioniert bei + _nicht_.

Beispiel:

Code: Alles auswählen

>>> result = 'Das Ergebnis ist :'
>>> value = 42
>>> print(result + value)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str
>>> print(f'{result} {value}')
Das Ergebnis ist : 42
>>>
Abgesehen davon können f-String bzw. die format-Methode noch viel mehr, siehe Doku

Anfällig für SQL Injections sind Queries, die aus Strings gebaut werden, "nur" dann, wenn du a) externe Benutzereingaben entgegen nimmst, b) dir selber schaden willst (oder schlichtweg einen Programmierfehler machst). Wenn du das Programm alleine benutzt und alle Eingabe selber generierst, dann sollte nichts passieren. Was aber nichts daran ändert, dass es schlechter Stil ist, den man sich am besten gar nicht angewöhnt.

ad 3 verstehe ich nicht. Bzw natürlich lädt man nur die Daten, die man braucht. Darum schreibt man normalerweise ja auch nicht `SELCT * FROM ...` sondern `SELECT foo, bar, spam` FROM ...` und schränkt das ganze ggf. noch mit einer WHERE-Klausel ein.
Die Doku hab ich mir etwas angesehen und in Essenz hat SQLAlchemy alle Fuktionen von pyodbc und mehr
Nein. SQLAlchemy ist DAS Werkzeug für Python um Umgang mit RDBMS. Es macht vieles einfacher und pythonischer. Selbst wenn man nicht das ORM von SQLAlchemy benutzt, sondern nur das "Core" Modul, sind viele Sachen wie Verbindung zu DB, Umgang mit Tabellen etc. einfacher.
BTW: das 2. große ORM für Python ist das Django ORM, dass man normalerweise aber nur in Kombination mit dem Django Webframework benutzt.

Gruß, noisefloor
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Unpopular opinion: ich bin kein Freund von SQLAlchemy (mehr). Es ist eines der besten Frameworks da draussen, und *wenn* ich ein ORM benutzen will, oder eben gegen verschiedene DBs abstrahieren, ist es die Wahl. Aber es erfordert eine eigene DSL jenseits von SQL zu beherrschen, und zumindest fuer jemanden, der damit nicht permanent arbeitet, ist das einfach Ueberhang. Das Netz ist voll von Fragen, wie man etwas, dass die Leute in SQL schon koennen, dann irgendwie in SA ausdruecken wollen, und daran scheitern. Gehen tut's immer, aber der Einsatz will in meinen Augen wohlueberlegt werden, und nicht per default.
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OWT: Ad 2: Weil es unübersichtlich ist das so zusammenzustückeln. Wenn man zwei Zeichenketten ”addiert” okay, aber wenn man da mehrere Teile zusammensetzt, ist das unübersichtlicher, mehr Tipparbeit, und im Gegensatz zu f-Zeichenketten auch langsamer. Bei der `format()`-Methode hat man den Vorteil, dass man Vorlage und Formatierung auch trennen kann.

Ad 3: Sehe ich gerade nicht wo das Thema war‽ Jedenfalls nicht bei mir.

Ad 4: Es ist nicht nur SQL-Injection, sondern auch Probleme das die Anfrage leicht kaputt gehen kann, und bei einigen Datenbanken/Adaptern auch Performance, weil SQL mit Platzhaltern und Daten getrennt übertragen werden können, und sich der „query plan“ für Anfragen die sich nur durch die Daten unterscheiden ja gleich bleibt, also Datenbanken den cachen können und das SQL dafür als Schlüssel verwendet werden kann.

@__deets___: Also für die vorliegende Datenbank würde ich SQLAlchemy schon alleine wegen der Tabellen- und Spaltennamen verwenden wollen. 😇
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

Hallo Zusammen

Abermals Danke für eure Mühe! Ich glaub mit dem Thema / Frage sind wir so weit durch. Gerne höre ich mir weiteres Feedback an, Dümmer wird man ja nicht davon :)
Mal schauen, ob ich den Post als erledigt markiert bekomme :)

@deets, danke dir auch für den Input. Eigentlich wollte ich nur mit einem Modul arbeiten, daher würde ich entweder ganz auf SQLAlchemy wechseln oder eben bei pyodbc bleiben bzw. zu SQLite + Pandas. Ich wollte nur ein paar Messwerte als int oder float und das klappt mit pyodbc. Am Schluss vom Tag wird vermutlich so oder so alles zu numpy arrays.

Habt ihr euch das mit dem F-String so vorgestellt? Zumindest aus meiner Warte kann ich euch versichern, ich mach beim einen gleich viel Fehler wie beim anderen. Wobei ich die Lösung mit '+' intuitiv verständlicher finde, da klar ist, was eine Variable und was ein Fixum im String sein muss / kann.

Code: Alles auswählen

#sql bauen
SELECT = 'SELECT'
FROM = 'FROM'
WHERE = 'WHERE'
spalten = 'pointValue, ts'
vonwo = 'pointValues'
bedingung = 'dataPointId = 1'

sql = "SELECT "+spalten+" FROM "+vonwo+" WHERE "+bedingung
sql_fstring = f'{SELECT} {spalten} {FROM} {vonwo} {WHERE} {bedingung}'

print('sql ist:',sql)
print('sql_fstring ist:',sql_fstring)

ad 3 war auf folgendes Kommentar bezogen, ich häng es an, aber ich verstehe nicht was gemeint war :/
noisefloor hat geschrieben: Mittwoch 22. Juni 2022, 06:02 Die Werte für `select` und `from` zu Parametrieren ist ungewöhnlich und normalerweise nicht notwendig. Das macht man normalerweise nur für die Parameter (wie die `where` Klausel).
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OWT: Nein so haben wir uns das nicht vorgestellt. Warum ziehst Du da jetzt Konstanten raus?

Code: Alles auswählen

    sql = f"SELECT {spalten} FROM {vonwo} WHERE {bedingung}"
Wobei auch weiterhin die Warnung gilt kein SQL manuell zu basteln, insbesondere keine Werte einfach so ”naiv” einzusetzen.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Sirius3
User
Beiträge: 17710
Registriert: Sonntag 21. Oktober 2012, 17:20

Da es sich bei Deinem String fast komplett um Konstanten handelt, ist sowohl + als auch Format-String hier unsinnig.
Das einzig Variable hier ist die ID, und die übergibt man, wie hier schon oft geschrieben, per Platzhalter:

Code: Alles auswählen

statement = 'SELECT pointValue, ts FROM pointValues WHERE dataPointId = ?'
cursor.execute(statement, [1])
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

wenn ich mal fertig bin schon, aber da ich mich aktuell immer noch auf der Datenbank umschaue, ist es fein mal schnell eine andere Tabelle einzutippen.
Auf jeden Fall bin ich begeistert, zum einen das ich dich nicht vergrault habe und zum andern wegen dem Code Vorschlag :)

Code: Alles auswählen

loggerid = 12 #logger Id ist zw 1 und 1068
zeit = '50000' #zeit wurde noch nicht verstanden

sql = "SELECT pointValue, id FROM pointValues WHERE id > ? AND dataPointId = ?"

#Connection und cursor
connect= pyodbc.connect('DRIVER='+treiber+';SERVER='+server+';DATABASE='+database+';UID='+username+';PWD='+ password)
cursor = connect.cursor()

#Zugriff auf die Datenbank
cursor.execute(sql, [zeit, loggerid])
test = cursor.fetchall()
print(test)

Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OWT: Ich glaube zum umsehen wurde schon mal empfohlen ein dafür geeignetes Werkzeug zu verwenden, oder? 😎

Und da würde ich wohl auch als erstes mal probieren ob SQLAlchemy auf den Metadaten ein `reflect()` hinbekommt, und mir diese Daten dann ausgeben lassen. Dann muss man sich nicht selbst mit den Details herumschlagen wie das DBMS die Metadaten für die Tabellen verwaltet.

Man muss schon ein bisschen programmieren bevor man sich das hier ohne SQLAlchemy selbst gebastelt hat:

Code: Alles auswählen

#!/usr/bin/env python3
from sqlalchemy import MetaData, create_engine

URL = "..."


def main():
    engine = create_engine(URL)
    meta_data = MetaData(engine)
    meta_data.reflect()
    for table in meta_data.sorted_tables:
        print("-" * 75)
        print(table.name)
        for column in table.columns:
            print(f"    {column!r}")


if __name__ == "__main__":
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
OWT
User
Beiträge: 29
Registriert: Montag 20. Juni 2022, 10:45

Jo es wurde das ein oder andere mal erwähnt, dass eine graphische Oberfläche nützliche wäre.

ich bekomm den code von dir leider nicht zum laufen, blackjack.
Der Fehler verschwindet, wenn ich die If Bedingung am ende weg lasse. Ich schau mich mal nach einem Treiber update um, das wird aber sicher dauern :/

Fehler: NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:SQL Server
Benutzeravatar
__blackjack__
User
Beiträge: 13003
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@OWT: Dann stimmt die URL nicht. Du musst da eine URL für SQLAlchemy angeben, nicht einfach das was Du `pyodbc.connect()` übergibst. Ich hatte doch weiter oben schon mal ein Beispiel gehabt: Ungetestet: "mssql+pyodbc://username:password@server/database?driver=SQL+Server"
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten