Connect to SQL Server with pyodbc.connect(), execute a parameterized SELECT TOP (?) query through a cursor, collect column names from cursor.description, and build a pandas DataFrame with DataFrame.from_records(). Then use head(), tail(), to_numeric(), aggregation methods and value_counts() to inspect furnace behavior.
- The query is read-only and ordered newest-first by date and time.
- Parameter binding supplies the record limit without SQL string interpolation.
- Invalid numeric text becomes
NaN, so it does not corrupt mean, minimum, maximum or standard deviation.
Connect Python to SQL Server and Load Data into pandas
This tutorial answers the common search intent “read SQL Server data with Python.” It uses pyodbc for the connection and SELECT query, converts cursor results into a pandas DataFrame, and then calculates useful process statistics and category counts.
Related Search Topics
connect Python to SQL Server Windows authentication · pyodbc SELECT query example · read SQL Server table into pandas DataFrame · Python SQL Server data analysis
Connect Python to SQL Server and Read Data
This Code 03 exercise reads the latest furnace event records and displays basic process statistics. A SQL SELECT statement retrieves data without modifying the table. The pyodbc driver handles communication with Microsoft SQL Server, and pandas holds the returned records in a DataFrame for inspection and analysis.
The four operating steps are: connect to SQF_DB, select the newest records, display newest and oldest samples, and calculate numeric statistics plus grouped record counts.
2. Requirements and Database Assumptions
- Python 3.10 or later.
pandasandpyodbcinstalled in the same Python environment.- Microsoft ODBC Driver 17 for SQL Server installed on Windows.
- Network and Windows permission to reach
SOFTWELL\WINCC. - Database
SQF_DBwith tabledbo.tblEventand the selected columns. - Code 02 or another source has already inserted furnace-event data.
pip install pandas pyodbc
Trusted_Connection=yes uses the Windows identity running the Python process. Give that identity only the database permissions it needs—normally SELECT permission for this reader.3. Create the SQL Server Connection
SQL_SERVER = r"SOFTWELL\WINCC"
DATABASE = "SQF_DB"
DRIVER = "{ODBC Driver 17 for SQL Server}"
def create_connection():
return pyodbc.connect(
f"Driver={DRIVER};Server={SQL_SERVER};Database={DATABASE};"
"Trusted_Connection=yes;"
)
The raw-string server name preserves the backslash in the named SQL Server instance. The braces around the driver name are part of ODBC connection-string syntax. Keeping connection creation in a function makes testing and later configuration changes easier.
Execute a SQL Server SELECT Query with pyodbc
query = """
SELECT TOP (?)
[DT], [TM], [SQF_No], [ChargeNo], [Event_From], [Event_To],
[Temp_Set], [Temp_Act], [Cp_Set], [Cp_Act],
[Oil_Set], [Oil_Act], [Jacket_Set], [Jacket_Act], [Fan_Status]
FROM [dbo].[tblEvent]
ORDER BY [DT] DESC, [TM] DESC;
"""
cursor = connection.cursor()
cursor.execute(query, top_n)
TOP (?) limits the result and the driver binds top_n as a parameter. The square brackets protect SQL Server identifiers. Ordering both DT and TM descending places the latest timestamp first.
If multiple records can have identical date and time values, add a unique descending key such as Event_ID DESC to make the order deterministic.
Load pyodbc Query Results into a pandas DataFrame
columns = [column[0] for column in cursor.description]
rows = cursor.fetchall()
dataframe = pd.DataFrame.from_records(rows, columns=columns)
cursor.description contains metadata for the returned columns; element zero of each description tuple is the column name. fetchall() returns the selected rows, and DataFrame.from_records() combines the rows and names into a labeled table.
This cursor-based approach avoids adding SQLAlchemy solely for the read operation. For very large result sets, retrieve rows in chunks rather than calling fetchall(); this tutorial deliberately limits the query to 100 rows.
6. Display Newest and Oldest Samples
print("Newest 10 records:")
print(dataframe.head(10).to_string(index=False))
print("Oldest 10 records:")
print(dataframe.tail(10).iloc[::-1].to_string(index=False))
Because SQL returns newest-first, head(10) displays the ten newest rows. tail(10) selects the oldest portion within the retrieved batch, while iloc[::-1] reverses those ten rows so they print oldest-first. These are the oldest among the selected TOP 100, not necessarily the oldest records in the entire table.
7. Calculate Numeric Process Statistics
for column, unit in (
("Temp_Act", "°C"),
("Oil_Act", " bar"),
("Cp_Act", ""),
):
values = pd.to_numeric(dataframe[column], errors="coerce")
print(
f"{column}: count={values.count()}, mean={values.mean():.2f}{unit}, "
f"min={values.min():.2f}{unit}, max={values.max():.2f}{unit}, "
f"std={values.std():.2f}{unit}"
)
| Statistic | Meaning | Important detail |
|---|---|---|
count() | Valid numeric samples | Excludes missing/invalid values converted to NaN |
mean() | Arithmetic average | Useful summary, but sensitive to outliers |
min() / max() | Observed range | Check against engineering and sensor limits |
std() | Sample standard deviation | pandas uses ddof=1 by default |
errors="coerce" converts non-numeric text to NaN. It keeps the program running, but the count tells you how many valid samples were actually analyzed. Production systems should also log or flag invalid source values instead of silently ignoring data-quality problems.
8. Count Furnaces, Process Stages and Fan Status
print(dataframe["SQF_No"].value_counts(dropna=False).sort_index().to_string())
print(dataframe["Event_To"].value_counts(dropna=False).to_string())
print(dataframe["Fan_Status"].value_counts(dropna=False).to_string())
value_counts() produces a frequency table. dropna=False deliberately includes missing values, which helps expose incomplete records. Furnace numbers are sorted by index for easy comparison; process-stage and fan-status results retain frequency order so the most common values appear first.
9. Error Handling and Safe Operation
try:
with create_connection() as connection:
dataframe = read_event_data(connection)
except pyodbc.Error as error:
raise SystemExit(f"Database connection/query failed: {error}") from error
if dataframe.empty:
print("No records found. Run Code 02 first.")
return
The context manager closes the connection when the block finishes, including after an exception. The handler catches ODBC-related failures and exits with a useful message while preserving the original exception chain. The empty check avoids later column/statistics operations when no records were returned.
if **name** == "**main**":. Valid Python is if __name__ == "__main__":.10. Complete Copy-Paste-Ready Python Program
"""Code 03: Read the latest furnace events and display basic statistics."""
import pandas as pd
import pyodbc
SQL_SERVER = r"SOFTWELL\WINCC"
DATABASE = "SQF_DB"
DRIVER = "{ODBC Driver 17 for SQL Server}"
NUMBER_OF_RECORDS = 100
def create_connection():
return pyodbc.connect(
f"Driver={DRIVER};Server={SQL_SERVER};Database={DATABASE};"
"Trusted_Connection=yes;"
)
def read_event_data(connection, top_n=NUMBER_OF_RECORDS):
query = """
SELECT TOP (?)
[DT], [TM], [SQF_No], [ChargeNo], [Event_From], [Event_To],
[Temp_Set], [Temp_Act],
[Cp_Set], [Cp_Act], [Oil_Set], [Oil_Act],
[Jacket_Set], [Jacket_Act], [Fan_Status]
FROM [dbo].[tblEvent]
ORDER BY [DT] DESC, [TM] DESC;
"""
cursor = connection.cursor()
cursor.execute(query, top_n)
columns = [column[0] for column in cursor.description]
return pd.DataFrame.from_records(cursor.fetchall(), columns=columns)
def display_statistics(dataframe):
print("\nTEMPERATURE AND PROCESS STATISTICS")
print("=" * 80)
for column, unit in (
("Temp_Act", "°C"),
("Oil_Act", " bar"),
("Cp_Act", ""),
):
values = pd.to_numeric(dataframe[column], errors="coerce")
print(
f"{column}: count={values.count()}, mean={values.mean():.2f}{unit}, "
f"min={values.min():.2f}{unit}, max={values.max():.2f}{unit}, "
f"std={values.std():.2f}{unit}"
)
print("\nRecords by furnace:")
print(dataframe["SQF_No"].value_counts(dropna=False).sort_index().to_string())
print("\nRecords by process stage:")
print(dataframe["Event_To"].value_counts(dropna=False).to_string())
print("\nFan status:")
print(dataframe["Fan_Status"].value_counts(dropna=False).to_string())
def main():
print("CODE 03: READ AND DISPLAY FURNACE DATA")
try:
with create_connection() as connection:
dataframe = read_event_data(connection)
except pyodbc.Error as error:
raise SystemExit(f"Database connection/query failed: {error}") from error
print(f"\nRetrieved {len(dataframe)} records.")
if dataframe.empty:
print("No records found. Run Code 02 first.")
return
print("\nNewest 10 records:")
print(dataframe.head(10).to_string(index=False))
print("\nOldest 10 records:")
print(dataframe.tail(10).iloc[::-1].to_string(index=False))
display_statistics(dataframe)
if __name__ == "__main__":
main()
11. Expected Output and Troubleshooting
A successful run prints the number of records, the newest ten rows, the oldest ten within the retrieved batch, statistics for Temp_Act, Oil_Act and Cp_Act, followed by category counts.
| Problem | Likely cause | Action |
|---|---|---|
| Data source name not found / driver error | ODBC Driver 17 is missing or driver name differs | Check installed ODBC drivers and update DRIVER |
| Login failed | Windows identity lacks SQL permission | Confirm the executing user and grant only required access |
| Server not found or timeout | Instance name, SQL Browser, firewall or network problem | Test server reachability and SQL Server instance settings |
| Invalid object or column name | Database schema differs from the example | Verify dbo.tblEvent and every selected column |
Statistics show nan | No valid numeric samples | Inspect source types and values; compare valid count with row count |
| Latest row seems inconsistent | Date/time columns are text, null, or tied | Use appropriate SQL date/time types and add a unique tie-breaker |
Hands-On Lab: Read and Verify Furnace Data
Hands-on- Use a test or approved read-only SQL Server environment.
- Verify that the table contains sample data and that the Windows user has SELECT access.
- Install pandas, pyodbc and the matching Microsoft ODBC driver.
- Estimated time: 25 minutes.
Prove the connection
Set the server, database and driver constants, then run only create_connection() inside a with block.
Retrieve a small controlled batch
Call read_event_data(connection, top_n=20) and print the DataFrame columns and length.
Verify ordering
Compare the first and last date/time values with an approved SQL query in SQL Server Management Studio.
Validate statistics and categories
Run display_statistics(), manually verify one mean using known test values, and compare category totals with the DataFrame length.
Related Python and SQL Server Tutorials
Continue through the Softwell Python–SQL Server learning path:
- Python modules, classes and objects
- Python–SQL Server data type mapping
- Create a SQL Server database and table with Python
- Insert data into SQL Server with Python
- Read SQL Server data into pandas
- Complete Python SQL Server CRUD tutorial
- Export SQL Server data to Excel
- Create a Python EXE for WinCC Excel reports
Frequently asked questions
Can pandas read data directly from SQL Server?
Yes. pandas provides SQL-reading APIs. This example uses a pyodbc cursor plus DataFrame.from_records() to remain copy-paste ready without adding SQLAlchemy.
Why use parameter binding for TOP?
It separates the integer limit from the SQL text and lets the driver handle the value safely. Do not insert untrusted text into the query using string formatting.
Why use to_numeric(errors="coerce")?
It converts valid numbers and replaces invalid text with NaN, allowing calculations to continue. Always compare the valid count with the retrieved row count so data-quality problems remain visible.
