PostgreSQL (SQL) vs pandas vs PySpark.

Read / Inspect

TaskPostgreSQLpandasPySpark
Preview rowsSELECT * FROM t LIMIT 5;df.head(5)df.show(5)
Row countSELECT count(*) FROM t;len(df)df.count()
Columns/schema\d tdf.dtypesdf.printSchema()
DistinctSELECT DISTINCT a FROM t;df.a.drop_duplicates()df.select(“a”).distinct()

Select / Filter

TaskPostgreSQLpandasPySpark
Select colsSELECT a, b FROM t;df[[“a”,”b”]]df.select(“a”,”b”)
FilterWHERE age > 30df[df.age > 30]df.filter(F.col(“age”) > 30)
Multi-conditionWHERE a > 1 AND b < 5df[(df.a>1) & (df.b<5)]df.filter((F.col(“a”)>1) & (F.col(“b”)<5))
In listWHERE c IN (1,2)df[df.c.isin([1,2])]df.filter(F.col(“c”).isin(1,2))

Columns / Expressions

TaskPostgreSQLpandasPySpark
New columnSELECT a + b AS cdf[“c”] = df.a + df.bdf.withColumn(“c”, F.col(“a”)+F.col(“b”))
RenameSELECT a AS xdf.rename(columns={“a”:”x”})df.withColumnRenamed(“a”,”x”)
CastCAST(a AS INT)df.a.astype(“int”)F.col(“a”).cast(“int”)
ConditionalCASE WHEN a>0 THEN ‘pos’ ELSE ‘neg’ ENDnp.where(df.a>0,”pos”,”neg”)F.when(F.col(“a”)>0,”pos”).otherwise(“neg”)

Aggregate / Group

TaskPostgreSQLpandasPySpark
Group + sumSELECT k, sum(v) FROM t GROUP BY k;df.groupby(“k”).v.sum()df.groupBy(“k”).agg(F.sum(“v”))
Count per groupSELECT k, count(*) GROUP BY k;df.groupby(“k”).size()df.groupBy(“k”).count()
MeanSELECT avg(v) FROM t;df.v.mean()df.agg(F.mean(“v”))

Join / Combine

TaskPostgreSQLpandasPySpark
Left joina LEFT JOIN b USING(id)a.merge(b, on=”id”, how=”left”)a.join(b, on=”id”, how=”left”)
Stack rowsSELECT * FROM a UNION ALL SELECT * FROM bpd.concat([a,b])a.union(b)

Sort / Nulls / Output

TaskPostgreSQLpandasPySpark
Sort descORDER BY a DESCdf.sort_values(“a”, ascending=False)df.orderBy(F.desc(“a”))
Drop nullsWHERE a IS NOT NULLdf.dropna()df.na.drop()
Fill nullsCOALESCE(a, 0)df.fillna(0)df.na.fill(0)

PySpark assumes from pyspark.sql import functions as F. Key difference: SQL and pandas execute eagerly; Spark is lazy until an action like .show() or .collect().

Window Functions

TaskPostgreSQLpandasPySpark
Row number per grouprow_number() OVER (PARTITION BY k ORDER BY v DESC)df.sort_values(“v”,ascending=False).groupby(“k”).cumcount()+1F.row_number().over(Window.partitionBy(“k”).orderBy(F.desc(“v”)))
Rankrank() OVER (PARTITION BY k ORDER BY v)df.groupby(“k”).v.rank(method=”min”)
Dense rankdense_rank() OVER (…)df.groupby(“k”).v.rank(method=”dense”)F.dense_rank().over(w)
Laglag(v,1) OVER (ORDER BY t)df.v.shift(1)F.lag(“v”,1).over(Window.orderBy(“t”))
Leadlead(v,1) OVER (ORDER BY t)df.v.shift(-1)F.lead(“v”,1).over(Window.orderBy(“t”))
Running totalsum(v) OVER (ORDER BY t)df.v.cumsum()F.sum(“v”).over(Window.orderBy(“t”).rowsBetween(Window.unboundedPreceding, 0))
Group sum (no collapse)sum(v) OVER (PARTITION BY k)df.groupby(“k”).v.transform(“sum”)F.sum(“v”).over(Window.partitionBy(“k”))

Deduplication

TaskPostgreSQLpandasPySpark
Drop exact dupesSELECT DISTINCT * FROM t;df.drop_duplicates()df.dropDuplicates()
Dupes on subsetDISTINCT ON (a,b)df.drop_duplicates(subset=[“a”,”b”])df.dropDuplicates([“a”,”b”])
Keep latest per keyrow_number() OVER (PARTITION BY k ORDER BY ts DESC) = 1df.sort_values(“ts”).drop_duplicates(“k”, keep=”last”)filter where row_number().over(w)==1
Count dupesGROUP BY a HAVING count(*) > 1df[df.duplicated(“a”, keep=False)]df.groupBy(“a”).count().filter(F.col(“count”)>1)

The “keep latest per key” pattern in Spark, fully written:

python

w = Window.partitionBy("k").orderBy(F.desc("ts"))
df.withColumn("rn", F.row_number().over(w)).filter(F.col("rn")==1).drop("rn")

Reshape (pivot / melt / explode)

TaskPostgreSQLpandasPySpark
Pivotcrosstab(…) (tablefunc ext)df.pivot_table(index=”k”, columns=”c”, values=”v”)df.groupBy(“k”).pivot(“c”).agg(F.sum(“v”))
Melt / unpivotUNNEST / manual UNIONdf.melt(id_vars=”k”)df.select(“k”, F.expr(“stack(…)”))
Explode array → rowsunnest(arr)df.explode(“arr”)df.withColumn(“x”, F.explode(“arr”))
Split string → arraystring_to_array(s, ‘,’)df.s.str.split(“,”)F.split(F.col(“s”), “,”)

String / Date manipulation

TaskPostgreSQLpandasPySpark
Lowercaselower(s)df.s.str.lower()F.lower(“s”)
Substringsubstring(s,1,3)df.s.str[0:3]F.substring(“s”,1,3)
Concata || bdf.a + df.bF.concat(“a”,”b”)
Replacereplace(s,’x’,’y’)df.s.str.replace(“x”,”y”)F.regexp_replace(“s”,”x”,”y”)
Parse dateto_date(s,’YYYY-MM-DD’)pd.to_datetime(df.s)F.to_date(“s”,”yyyy-MM-dd”)
Extract yearextract(year from d)df.d.dt.yearF.year(“d”)
Date diff (days)d2 – d1(df.d2 – df.d1).dt.daysF.datediff(“d2″,”d1”)

Aggregate edge cases

TaskPostgreSQLpandasPySpark
Count distinctcount(DISTINCT a)df.a.nunique()F.countDistinct(“a”)
Conditional countcount(*) FILTER (WHERE a>0)(df.a>0).sum()F.sum(F.when(F.col(“a”)>0,1).otherwise(0))
Collect into listarray_agg(v)df.groupby(“k”).v.apply(list)F.collect_list(“v”)
First/lastfirst_value/last_value OVER (…)df.groupby(“k”).v.first()F.first(“v”) / F.last(“v”)

Creating a schema / defining types

PostgreSQL — types live in the table definition:

CREATE TABLE users (
id INTEGER PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age SMALLINT,
signup DATE,
active BOOLEAN DEFAULT true
);

pandas — schema is a dtype dict, applied at read or after:

dtypes = {“id”:”int32″,”name”:”string”,”age”:”Int16″,”active”:”boolean”}
df = pd.read_csv(“f.csv”, dtype=dtypes, parse_dates=[“signup”])

PySpark — an explicit StructType (preferred over inferSchema for production):

from pyspark.sql.types import (StructType, StructField, IntegerType,
StringType, ShortType, DateType, BooleanType)

schema = StructType([
StructField(“id”, IntegerType(), nullable=False),
StructField(“name”, StringType(), nullable=False),
StructField(“age”, ShortType(), nullable=True),
StructField(“signup”, DateType(), nullable=True),
StructField(“active”, BooleanType(), nullable=True),
])
df = spark.read.csv(“f.csv”, header=True, schema=schema)

Assigning / changing a data type

TaskPostgreSQLpandasPySpark
Cast a columnCAST(a AS INTEGER) or a::intdf.a.astype(“int32”)df.withColumn(“a”, F.col(“a”).cast(“int”))
To stringa::textdf.a.astype(“string”)F.col(“a”).cast(“string”)
To dateto_date(a,’YYYY-MM-DD’)pd.to_datetime(df.a)F.to_date(“a”,”yyyy-MM-dd”)
To numeric (safe)a::numeric (errors if bad)pd.to_numeric(df.a, errors=”coerce”)F.col(“a”).cast(“double”) (bad → null)
Alter stored typeALTER TABLE t ALTER COLUMN a TYPE int;

Data quality checks

CheckPostgreSQLpandasPySpark
Null count per colcount(*) – count(a)df.a.isna().sum()df.filter(F.col(“a”).isNull()).count()
Duplicate keysGROUP BY id HAVING count(*)>1df.duplicated(“id”).sum()df.groupBy(“id”).count().filter(F.col(“count”)>1)
Out-of-rangeWHERE age < 0 OR age > 120`df[(df.age<0)(df.age>120)]`
Distinct countcount(DISTINCT a)df.a.nunique()df.select(“a”).distinct().count()
Pattern / formata ~ ‘^[0-9]+$’df.a.str.match(r”^\d+$”)F.col(“a”).rlike(“^[0-9]+$”)
Cast-failure rowsrows where to_numeric(coerce) is null but original wasn’tsame: compare pre/post-cast nulls
Enforce at writeCHECK, NOT NULL, UNIQUE constraintsmanual assertmanual filter / framework

A compact null-profile across all columns:

pandas

df.isna().sum()

PySpark

df.select([F.sum(F.col(c).isNull().cast(“int”)).alias(c) for c in df.columns])

Renaming a field:

TaskPostgreSQLpandasPySpark
Rename one columnALTER TABLE t RENAME COLUMN a TO x;df.rename(columns={“a”:”x”})df.withColumnRenamed(“a”,”x”)
Rename severalrun multiple RENAME statementsdf.rename(columns={“a”:”x”,”b”:”y”})chain .withColumnRenamed(…) or use select with aliases
Rename in a query (alias only)SELECT a AS x FROM t;n/adf.select(F.col(“a”).alias(“x”))
Rename all at oncedf.columns = [“x”,”y”,”z”]df.toDF(“x”,”y”,”z”)

Rename several in Spark:

python

mapping = {"a":"x", "b":"y"}
for old, new in mapping.items():
    df = df.withColumnRenamed(old, new)

The structural difference: PostgreSQL enforces schema and quality declaratively (constraints reject bad data on insert), while pandas and Spark are permissive — types are assigned but nothing stops bad values, so quality has to be checked explicitly after load.

Key distinction: the Postgres ALTER TABLE permanently changes the stored table, while SELECT a AS x only relabels in the query output. pandas rename returns a new frame (use inplace=True or reassign), and Spark always returns a new immutable DataFrame.

The big conceptual split stays the same: SQL and pandas evaluate eagerly, while Spark builds a lazy plan — window specs and transformations are just descriptions until an action triggers execution.